Хэширование, соление и проверка паролей в NodeJS, Python, Golang и Java

Хэширование, соление и проверка паролей в NodeJS, Python, Golang и Java

30 апреля 2022 г.

Хранение паролей может быть нюансом из-за ответственности за их компрометацию. Что еще хуже, пользователи, как правило, повторно используют пароли в разных службах, что делает их безопасное хранение еще более важным.


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


Использование шифрования может показаться хорошим выбором, поскольку злоумышленник не будет знать фактических паролей (поскольку они зашифрованы). Однако, если база данных скомпрометирована, ключи шифрования, вероятно, также будут скомпрометированы. Используя эти ключи, злоумышленник сможет расшифровать зашифрованные пароли, что делает этот метод хранения слабым.


Здесь в игру вступают хеширование или хеш-функции.


Что такое хеш-функции?


Это функции, обладающие следующими свойствами:


  1. Сопротивление прообразу: учитывая выходные данные хэш-функции «Out», должно быть трудно найти какие-либо входные данные «In», которые при хэшировании дают тот же результат («хеш (In) = Out». ). Например, если я возьму случайный вывод хеш-функции SHA256 (тип данных string), такой как "401357cf18542b4117ca59800657b64cce2a36d8ad4c56b6102a1e0b03049e97", то будет очень сложно понять, какие входные данные для хеш-функции привели к этому выводу. Попробуйте поискать его в Google!

  1. Сопротивление второму прообразу: если входные данные для хеш-функции известны, должно быть трудно найти другой входной сигнал с таким же хэшированным выходным значением.

  1. Сопротивление столкновению: это говорит о том, что трудно найти какие-либо два входных данных, для которых их хэшированные выходные данные будут одинаковыми. Это немного отличается от (2), так как в (2) вам дается один ввод, и в этом случае вы можете приготовить любой ввод.

  1. Предсказуемость: хэш-функция всегда должна возвращать один и тот же результат при одних и тех же входных данных.

  1. Вывод фиксированной длины: вывод хеш-функции всегда имеет одинаковую длину (количество символов), независимо от длины ввода.

  1. Входная чувствительность: Небольшое изменение ввода (даже всего один символ) должно иметь большое изменение в выходной строке. Например, хэш SHA256 «hello» — это «2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824». Но хэш «hella» — это «70de66401b1399d79b843521ee726dcec1e9a8cb5708ec1520f1f3bb4b1dd984». Как видите, результаты проверяются по-разному.

В общем, хеш-функции — это «односторонние функции». Если вы знаете только вывод, невозможно/трудно узнать его ввод. Это отличается от шифрования, при котором, зная выходные данные и ключ шифрования, вы можете знать входные данные.


Из-за этого одностороннего свойства сохранение хэшированного значения пароля является хорошей идеей, поскольку, если их хэш будет скомпрометирован (через утечку базы данных), злоумышленник не узнает исходный пароль (который является входом для хеш-функции). . На самом деле, единственным «субъектом», который мог бы знать входные данные для хеш-функции, был бы конечный пользователь, сгенерировавший пароль в первую очередь. Это именно то, что мы хотим с точки зрения безопасности.


Из-за свойства номер (5) существует бесконечное количество входных данных, которые могут дать один и тот же результат хеширования. Но также очень сложно найти какой-либо из этих бесконечных входных данных с заданным выходным сигналом!


Что такое соление и почему одного только хеширования недостаточно - Проблемы с людьми


Многие люди склонны использовать общие пароли, такие как «пароль», «12345» и т. д. Поскольку хэш одного и того же ввода никогда не меняется (см. свойство ( 4) выше), мы можем предварительно вычислить хеш общих паролей, а затем сверить просочившиеся данные базы данных с этими предварительно вычисленными хэшами.


Например, хэш SHA256 «12345» — это «5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5». Если этот хэш встречается в базе данных, мы знаем, что пароль пользователя — «12345». На самом деле существует целая база данных предварительно вычисленных хэшей, с которыми можно сверяться. Они называются [радужные таблицы] (https://www.geeksforgeeks.org/understanding-rainbow-table-attack/).


Способ решения этой проблемы состоит в том, чтобы добавить некоторую случайную строку, известную как «соль», к паролю перед его хэшированием (во время процесса регистрации), а затем мы добавляем эту случайную строку к вычисленному хешу перед сохранением его в базе данных. Возьмем пример:


  • Пароль Алисы: "12345"

  • Пароль Боба: "12345"

  • Случайная строка Алисы (соль): "ab$45"

  • Случайная строка Боба (соль): "ih&g3"

  • Измененный пароль Алисы: "12345ab$45"

  • Измененный пароль Боба: "12345ih&g3"

  • Хэш SHA256 Алисы: "2bb12bb768eb669f0e4b9df29e22a00467eb513c275ccfff1013288facac7889"

  • Хэш SHA256 Боба: "b63400702c6f012aeaa57b5dc7eefaaaf3207cc6b68917911c410015ac0659b2"

Как видите, их вычисленные хэши совершенно разные, хотя пароли у них одинаковые. Самое главное, этих хэшей не будет ни в одной радужной таблице, поскольку они генерируются из довольно случайных строк («12345ab$45» и «12345ih&g3»).


Прежде чем мы сохраним эти хэши в базе данных, мы должны добавить к ним соль:


  • Хэш Алисы + соль: "2bb12bb768eb669f0e4b9df29e22a00467eb513c275ccfff1013288facac7889.ab$45"

  • Хэш Боба + соль: "b63400702c6f012aeaa57b5dc7eefaaaf3207cc6b68917911c410015ac0659b2.ih&g3"

Причина, по которой мы добавляем соль к хешу, заключается в том, что в процессе проверки мы должны использовать ту же соль, что и изначально. Поэтому мы должны хранить это где-то. Даже если соль скомпрометирована, это не проблема безопасности, поскольку злоумышленнику все равно нужно знать/угадывать пароль пользователя, чтобы иметь возможность сгенерировать тот же хэш.


Давайте посмотрим, как происходит процесс проверки:


  • Пароль Алисы: "abcdef" (неверный пароль)

  • Соль Алисы: "ab$45" (взято из БД)

  • Измененный пароль Алисы: "abcdefab$45"

  • Хэш SHA256 Алисы: "c5110931a3ae4762c1c0334d8eeba8c9c555962cf7d2750fdd732936319a058c"

  • Хэш Алисы + соль: "c5110931a3ae4762c1c0334d8eeba8c9c555962cf7d2750fdd732936319a058c.ab$45"

Поскольку вычисленный хэш + соль не соответствует тому, что есть в базе данных, мы отклоняем этот пароль. Если бы Алиса ввела свой правильный пароль («12345»), он действительно сгенерировал бы тот же хэш + соль, что и в базе данных, подтверждая ее личность.


Какую хэш-функцию выбрать?


Даже после засолки проблема атак методом перебора остается. Злоумышленник может неоднократно угадывать разные пароли (очень быстро), чтобы увидеть, какой из них соответствует утекшему хэшу. Есть два параметра, которые определяют, насколько быстро злоумышленник может найти совпадение:


  1. Случайность и длина пароля пользователя.

  1. Время, необходимое хэш-функции для вычисления хэша

Если пользователь использует случайный и достаточно длинный пароль, шансы злоумышленника угадать именно эту строку уменьшаются. Это означает, что им придется обдумывать больше догадок, что займет больше времени. Вот действительно классный инструмент, который оценивает, сколько времени потребуется, чтобы угадать заданный пароль.


Чем медленнее и сложнее в вычислительном отношении хэш-функция, тем больше времени потребуется для проверки каждого предположения. На момент написания этой статьи (2 марта 2022 г.) рекомендуемым методом хеширования является использование Argon2id с минимальной конфигурацией 15 МБ памяти, количество итераций 2 и 1 степень параллелизма.


По мере увеличения вычислительной мощности меняются и рекомендуемые методы хеширования. Даже если алгоритм останется прежним, рекомендуемое количество «раундов»/количество «работы», которое должно быть выполнено на один хэш пароля, может увеличиться.


Пример кода


NodeJS


```js


импортировать * как аргон2 из "аргон2";


импортировать * как крипто из "крипто";


const hashingConfig = { // на основе рекомендаций OWASP (по состоянию на март 2022 г.)


параллелизм: 1,


memoryCost: 64000, // 64 мб


timeCost: 3 // количество итераций


асинхронная функция hashPassword (пароль: строка) {


пусть соль = crypto.randomBytes (16);


return await argon2.hash (пароль, {


...конфигурация хеширования,


соль,


асинхронная функция verifyPasswordWithHash (пароль: строка, хэш: строка) {


return await argon2.verify(хэш, пароль, конфигурация хеширования);


hashPassword("somePassword").then(async (хэш) => {


console.log("Хэш + соль пароля:", хэш)


console.log("Успешная проверка пароля:", await verifyPasswordWithHash("somePassword", хеш));


Вышеизложенное производит следующий вывод:


``` ударить


Хэш + соль пароля: $argon2i$v=19$m=15000,t=3,p=1$tgSmiYOCjQ0im5U6NXEvPg$xKC4V31JqIK2XO91fnMCfevATq1rVDjIRX0cf/dnbKY


Успешная проверка пароля: правда


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


Голанг


```иди


основной пакет


импорт (


"крипто/ранд"


"крипто/тонкий"


"кодировка/base64"


"ошибки"


"ФМТ"


"журнал"


"струны"


"golang.org/x/crypto/argon2"


введите структуру параметров {


память uint32


итерации uint32


параллелизм uint8


сольДлина uint32


длина ключа uint32


основная функция () {


р := &параметры{


память: 64*1024, // 64 МБ


итераций: 3,


параллелизм: 1,


сольДлина: 16,


длина ключа: 32,


encodedHash, err := generateHashFromPassword("somePassword", p)


если ошибка != ноль {


log.Fatal(ошибка)


fmt.Println("Хэш + соль пароля:")


fmt.Println (закодированный хэш)


совпадение, ошибка := verifyPassword ("somePassword", encodedHash)


если ошибка != ноль {


log.Fatal(ошибка)


fmt.Printf("
Успешная проверка пароля: %v
", совпадение)


func generateHashFromPassword (строка пароля, p * params) (строка encodedHash, ошибка ошибки) {


соль, ошибка: = generateRandomBytes (p.saltLength)


если ошибка != ноль {


вернуть "", ошибиться


hash := argon2.IDKey([]байт(пароль), соль, p.iterations, p.memory, p.parallelism, p.keyLength)


// Base64 кодирует соль и хешированный пароль.


b64Salt: = base64.RawStdEncoding.EncodeToString (соль)


b64Hash: = base64.RawStdEncoding.EncodeToString (хэш)


// Возвращаем строку, используя стандартное закодированное хэш-представление.


encodedHash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p .параллелизм, b64Salt, b64Hash)


вернуть encodedHash, ноль


func generateRandomBytes(n uint32) ([]байт, ошибка) {


б := сделать ([] байт, n)


_, ошибка := rand.Read(b)


если ошибка != ноль {


вернуть ноль, ошибиться


вернуть б, ноль


func verifyPassword (пароль, строка encodedHash) (соответствие bool, ошибка ошибки) {


// Извлекаем параметры, соль и производный ключ из зашифрованного пароля


// хеш.


p, соль, хэш, ошибка: = decodeHash (encodedHash)


если ошибка != ноль {


вернуть ложь, ошибиться


// Получить ключ из другого пароля, используя те же параметры.


otherHash := argon2.IDKey([]байт(пароль), соль, p.iterations, p.memory, p.parallelism, p.keyLength)


// Проверяем, что содержимое хешированных паролей идентично. Примечание


// что мы используем для этого функцию thin.ConstantTimeCompare()


// для предотвращения атак по времени.


если thin.ConstantTimeCompare(хеш, др.хэш) == 1 {


вернуть истину, ноль


вернуть ложь, ноль


func decodeHash (строка encodedHash) (p *params, salt, hash []byte, err error) {


vals := strings.Split(encodedHash, "$")


если len(vals) != 6 {


вернуть nil, nil, nil, errors.New ("закодированный хэш имеет неправильный формат")


переменная версия целое


_, err = fmt.Sscanf(vals[2], "v=%d", &версия)


если ошибка != ноль {


вернуть ноль, ноль, ноль, ошибиться


если версия != argon2.Version {


вернуть nil, nil, nil, errors.New("несовместимая версия argon2")


р = &параметры{}


_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)


если ошибка != ноль {


вернуть ноль, ноль, ноль, ошибиться


соль, ошибка = base64.RawStdEncoding.Strict().DecodeString(vals[4])


если ошибка != ноль {


вернуть ноль, ноль, ноль, ошибиться


p.saltLength = uint32 (длина (соль))


хэш, ошибка = base64.RawStdEncoding.Strict().DecodeString(vals[5])


если ошибка != ноль {


вернуть ноль, ноль, ноль, ошибиться


p.keyLength = uint32 (длина (хэш))


вернуть p, соль, хэш, ноль


Питон


```питон


импорт аргона2


argon2Hasher = argon2.PasswordHasher(


time_cost=3, # количество итераций


memory_cost=64 * 1024, # 64мб


parallelism=1, # сколько параллельных потоков использовать


hash_len=32, # размер полученного ключа


salt_len=16 # размер случайно сгенерированной соли в байтах


пароль = "некоторый пароль"


hash = argon2Hasher.hash(пароль)


print("Хэш + соль пароля", хэш)


VerifyValid = argon2Hasher.verify(хэш, пароль)


print("Успешная проверка пароля:", verifyValid)


Джава


```java


импортировать de.mkammerer.argon2.Argon2;


импортировать de.mkammerer.argon2.Argon2Factory;


открытый класс PasswordHashing {


public static void main(String[] args) {


// соль 32 байта


// длина хеша 64 байта


Аргон2 аргон2 = Аргон2Фабрика.создать(


Argon2Factory.Argon2Types.ARGON2id,


16,


32);


char[] пароль = "somePassword".toCharArray();


Строка hash = argon2.hash(3, // Количество итераций


64*1024, // 64мб


1, // сколько параллельных потоков использовать


пароль);


System.out.println("Хэш + соль пароля: "+хеш);


System.out.println("Успешная проверка пароля: "+ argon2.verify(хэш, пароль));


Сноски:


  1. Если только они не хранятся в «безопасном хранилище», подобном этому. Но тогда также все еще возможно, что они просочились.

  1. Технически, пароль пользователя может быть любым, что при хешировании дает "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5". Но если мы попытаемся войти в систему с паролем "12345", это сработает, поскольку алгоритм просто сопоставляет вычисленный хэш с хешем в базе данных.

  1. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html

Написано людьми на SuperTokens — надеюсь, вам понравилось! Мы всегда доступны на нашем сервере Discord. Присоединяйтесь к нам, если у вас есть вопросы или вам нужна помощь.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE