#
LD31D
Хешування паролів

Хешування паролів

Останнім часом аналізував застосунки та побачив, що багато розробників взагалі не знають як правильно зберігати паролі користувачів. Хтось зберігає їх у відкритому вигляді, хтось хешує функціями загального призначення, хтось їх шифрує.

Загалом кожен з цих методів має право на життя, з точки зору працездатності, але ж з точки зору безпеки - ні.

Сьогодні ми розберемо підходи до збергання паролів та зрозуміємо як все ж таки краще зберігати паролі.

Що ми маємо зараз?

Припустимо що в нас банківський застосунок, щоденно ним користуються сотні користувачів і відправляють один одному гроші.

Для нього в нас є база даних з таблицею users, в якій є 2 поля:

  • username
  • password

Пароль зараз зберігається у відкритому вигляді.

Це наша точка A, далі ми поступово будемо покращувати це і прийдемо до дійсно безпечних методів. Мені здалось так буде зрозуміліше ніж я б одразу сказав використовуйте технологію X.

Чому зберігання у відкритому вигляді це погано?

Зараз в нас є таблиця де паролі користувачів, зберігаються у відкритому вигляді.

Це відкриває одразу декілька небезпек.

Сценарій 1

Про наш сервіс дізнався зловмисник Карл.

Він потратив вечір на те щоб отримати доступ до нашої БД і тепер в нього є паролі усіх наших користувачів.

Користуючись цими даними, Карл переказав усі гроші на свій рахунок.

І тепер до нас приходять злі користувачі.

Сценарій 2

Вчора Клара, яка працює менеджером в нашому банку, вирішила купити собі нову хустку, але грощей їй не висточало, начальник відмовився давати її аванс.

Клара була дуже засмучена і згадавши, що вона працює менеджером в банку і має доступ до бази даних, скористалась паролем одного з користувачів щоб “позичити” з його рахунку грошей на нову хустку.


Цих би проблем не було як би ми не зберігали паролі у відкритому вигляді.

Ми починаємо думати над вирішенням цієї проблеми і знаходимо майже магічне слово “шифрування”.

Чому шифрувати паролі зазвичай погано?

Тепер ми впевнені, що саме шифрування вирішить всі наші проблеми.

Ми дізнаємося що для шифрування нам потрібен ключ, створюємо великий випадковий ключ, та ховаємо його так що його тепер знає тільки наш застосунок.

Тепер при регистрації користувач вигадує пароль, наш застосунок його шифрує та зберігає в БД.

Паролі захищені?

Ні, Карл знаходить можливість отримати ключ і тепер без проблем може розшифрувати паролі.

Ми знову сідаємо за вирішення цього питання і приходемо до розуміння, що ні Карл, не хтось інший, не повинен мати можливості розшифровувати паролі.

Потрібно зробити якось так щоб в нас було одностороннє перетворення, паролі зберігалися не в відкритому вигляді і не могли бути відновлені. Що ж ну тепер ми точно знаємо що робити - хешувати.

Хешування паролів функціями загального використання

Хешування - це перетворення даних будь-якої довжини у вихідний хеш фіксованої довжини без можливості відновлення початкових даних.

Тож ми вирішуємо використати надійну хеш функцію загального призначення - SHA-256.

І тепер ніхто окрім користувачів не знає їх паролі, але ж вони в нас не дуже обізнані в сфері безпеки і більшість з них ставить пароль - 123456, який є найрозповсюджуваним паролем у світі.

Карл знову отримує доступ до нашої БД і бачить що він тепер не може відновити пароль, але він знає що таке хеш-функція. І він знає основну її властивість однакові дані дають однаковий хеш.

І він бачить, що найчастіше зустрічається такий хеш:

8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92

Карл має список найпопулярніших паролів і починає самостійно їх хешувати.

Хешуючи значення 123456 він отримує такий самий хеш:

Тепер він знає, що всі хто має такий хеш використовує пароль 123456.

Більшість користувачів використовують однакові паролі: password, qwerty, 12345678. Тому підбір одного такого паролю - відкриває доступ до всіх користувачів які його використовують.

Щоб від цього захиститися потрібно внести якусь рандомізацію і зробити так щоб навіть якщо користувачі мали однакові паролі - в них був разний хеш.

Hashing with salt

Для таких випадків при хешуванні зазвичай використовують salt (якесь випадкове значення, яке перед хешуванням додаеться до пароля).

Значення salt потрібно обирати випадково для кожного пароля. Його можна зберігати в БД разом з вихідним хешем:

salt.hash

І тепер для перевірки пароля потрібно:

  1. Отримати salt та хеш
  2. Захешувати значення паролю з salt
  3. Перевірити чи хеш паролю відповідає тому що зберігається у БД

Тепер наші хеші наших паролів різні і зловмисник вже не може знайти найпопулярніший пароль, тепер зламавши один пароль він не отримує доступ до інших.

Тепер паролі в повній безпеці? Не зовсім.

Це вже значно краще, ніж було раніше, але зловмисник усе ще може підібрати пароль.

Карл знає hash, знає salt.
Все, що йому потрібно - знайти такий пароль, який разом із salt дає потрібний hash.

І тут ми підходимо до головної проблеми.

Ми обрали хеш-функцію загального призначення - SHA-256. Вона криптографічно стійка, але при цьому дуже швидка.

І це не недолік алгоритму - це його особливість. Але для паролів швидкість - це ворог.

Карл не вводить паролі вручну. Він не сидить і не пробує: password, password1, password123.

Він використовує відеокарту.

Сучасні GPU можуть обчислювати мільйони або навіть мільярди SHA-256 хешів за секунду.

Отже, ми приходимо до простого висновку: для зберігання паролів нам не підходять швидкі хеш-функції.

Нам потрібен алгоритм, який навмисно працює повільно. Такий, щоб підбір одного пароля займав секунди або навіть хвилини, а масовий перебір ставав економічно невигідним.

Саме для цього і були створені спеціалізовані алгоритми для хешування паролів.

Password Based Key Derivation Function (PBKDF)

Цю задачу виконуют PBKDF. Вони навмисно зроблені повільними, а також мають параметри для налаштування, які ми можемо самостійно коригувати, щоб добитися потрібної нам швидкості.

PBKDF багаторазово застосовує криптографічні перетворення, використовуючи salt та додаткові параметри, щоб значно уповільнити перебір.

Таким чином кожна спроба підібрати пароль стає дорогою.

Ми більше не намагаємось зробити пароль “незламним”. Ми робимо так, щоб його злам було економічно невигідним.

Існує декілька популярних реалізацій PBKDF:

  • bcrypt
  • scrypt
  • Argon2

Розглянемо кожен з них.

bcrypt

Майже в будь якій статті про хешування паролів використовують bcrypt і не пояснюють його переваги та недоліки, ми же розгляне це.

bcrypt має лише один параметр rounds - це кількість раундів які буде проходити наш хеш перед тим як повернутися до нас. Чим більше значення цього параметру тим більше часу займе генерація хешу.

Самостійно генерувати salt нам не потрібно, а також тут немає параметрів які виглядають лячно, тому багато хто обирає саме його.

Але з ним є декілька важливих особливостей, по переше цей шифр був розроблений в 1999 році, коли не було потужних відеокарт, тому цей алгоритм гірше масштабується проти сучасних GPU атак.

Також в цього алгоритму є особливість про яку слід пам’ятати. bcrypt працює з даними розміром 72 байти, якщо ми будемо передавати дані більшого розміру, все після 72 байтів буде ігноруватися.

Якщо ми додамо ще байти то хеш все одно буде співпадати:

Це важливо розуміти, бо іноді при хешуванні пароля до нього додаяють якісь метадані.

Також якщо користувач вигадає пароль довший за 72 байти, частина паролю не буде впливати.

scrypt

scrypt був створений як відповідь на одну з головних проблем bcrypt - його слабку стійкість до перебору за допомогою GPU.

На відміну від bcrypt, scrypt робить акцент не лише на час виконання, а й на використання оперативної памʼяті.

Щоб обчислити хеш, алгоритму потрібно багато памʼяті.

GPU може рахувати багато хешів одночасно, але кожному потоку потрібна памʼять. А памʼять - це дорого.

scrypt має 3 параметри:

  • N - кількість ітерацій (степінь 2)
  • r - розмір блоку
  • p - рівень паралелізму

Вимога памʼяті у scrypt розраховується як:

128Nrp байт128 * N * r * p \ \text{байт}

Вибір параметрів залежить від того, скільки ви хочете чекати і який рівень безпеки, ви хочете досягти.

Мінемально рекомендовані параметри:

  • N = 16 384 (2^14)
  • r = 8
  • p = 1

Це мінімальні історичні рекомендації, для нових систем краще розглядати вищі значення.

Вимоги пам’яті для цих значень:

12816 28481=16 674 816 байт16 674 816 байт16 MB128 * 16\ 284 * 8 * 1 = 16 674 816 \ \text{байт} \\[4pt] 16 674 816 \ \text{байт} \approx 16 \ \text{MB}

Тобто для генерації одного хешу потрібно 16 MB пам’яті.

Ви можете коригувати ці параметри, під свої потреби і дивитися як швидко обчислюється хеш у вашіс системі.

На відміну від bcrypt, scrypt не реалізує якийсь стандартний метод збереження параметрів та солі, нам самим потрібно вигадувати структуру для збереження цих даних.

Argon2

Argon2 - це сучасний стандарт хешування паролів.

Він був створений спеціально для цієї задачі та переміг у Password Hashing Competition у 2015 році.

Параметри Argon2

  1. time_cost (t) — кількість ітерацій (скільки разів буде пройдений алгоритм).
    • Збільшує час виконання, робить перебір дорожчим.
  2. memory_cost (m) — скільки пам’яті використовується (в KB).
  3. parallelism (p) — паралельність (кількість потоків/ядер).
    • Зазвичай = числу доступних CPU-ядер.
  4. type — режим Argon2:
    • Argon2id — рекомендується (гібрид: захищає і від GPU, і від side-channel атак).
    • Argon2i — проти side-channel атак (але слабший проти GPU).
    • Argon2d — проти GPU (але вразливий до side-channel атак).

Вибір параметрів Argon2

type=Argon2id - це дає максимальний ступінь захисту.

Вибір основних параметрів залежить від потужностей нашої машини. Грунтуючись на рекомендаціях розробників Argon2, ми можемо поставити такі параметри:

  • Time cost: 3
  • Memory cost: 64 MB
  • Parallelism: 1

Це початкові значення, які ми можемо коригувати.

Всі тести я запускав на своєму ПК, де я адаптував параметри під свою систему:

  • Time cost: 4
  • Memory cost: 128 MB
  • Parallelism: 8

При таких значеннях обчислення одного хешу займає 55 ms, що є дуже непоганим результатом.

Якщо виділити 8 GB памʼяті (50% від загальної) для обчислення, то ми зможемо генерувати 8 * 1024 / 128 = 64 хеші за раз.

За секунду ми зможемо запустити десь 1000 / 55 = 18 таких запусків.

Тобто за секунду ми можемо обчислити 18 * 64 = 1152 хеші. Теоретичний максимум - 1152 хеші/сек, а на практиці буде трохи менше.

Додаткові ресурси

Резюме

Зберігання паролів — це не про те, щоб їх неможливо зламати.

Це про те, щоб навіть у разі витоку бази даних, зловмисник не зміг ефективно атакувати систему.

Можливі підходи до зберігання паролів:

  • відкритий вигляд - небезпечно
  • шифрування - для деяких випадків шифрування має сенс, але в більшості випадків це не потрібно
  • SHA-256 навіть з salt - недостатньо для повноцінної безпеки
  • PBKDF - кращий вибір:
    • bcrypt - мінімально допустимий рівень, але слід враховувати його особливості
    • scrypt - кращий захист від GPU, але немає стандарту зберігання
    • Argon2 - сучасний і рекомендований стандарт

Навіть кращі PBKDF не гарантують 100% безпеки пароля.

Якщо користувач використовує слабкий пароль його все ще легко буде зламати.

Тому також потрібно впровадити валідацію від слабких паролів.