Хранение паролей в базе данных: лучший способ сделать это

Хранение паролей в базе данных: лучший способ сделать это

25 апреля 2024 г.

Как бы вы сохранили пароль пользователя?

Как бы вы сохранили пароль, если бы вас попросили создать систему аутентификации? Самым простым решением было бы хранить простой текст, введенный пользователями. Например, если пользователи вводят свой адрес электронной почты как codecurated@codecurated.com и пароль как weakpassword, мы можем вставить его в нашу таблицу users. , да?

Когда пользователь пытается войти в систему, мы можем запросить таблицу с помощью SELECT * FROMusers WHERE email = {email} AND пароль = {password}. Если запрос возвращает результат, мы можем аутентифицировать пользователя. Задача выполнена? Ну, не совсем.

Проблема с сохранением пароля в виде обычного текста

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

Утечка данных — это еще один вопрос, почему пароли в виде простого текста ужасны. Кто-то, кому вы не собираетесь доверять, может завладеть вашей базой данных, и это происходит постоянно, даже в крупных компаниях. . Используя простой текстовый пароль, злоумышленник может получить все учетные данные вашего пользователя, запросив таблицу users.

Лучший способ хранения пароля пользователя?

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

Одной из самых популярных и безопасных функций хеширования является SHA256. Если мы попытаемся хэшировать weakpassword с помощью SHA256, результат будет таким:

9b5705878182ccecf493b6c5ef3d2c723082141d0af33432c997b52dcc9f3e71

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

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

Атака по таблице Rainbow — это атака с использованием таблицы заранее вычисленного хеша общих паролей. С помощью атаки радужной таблицы злоумышленник сможет получить учетные данные пользователей со слабыми паролями в вашей базе данных. Чтобы бороться с этим, при хешировании пароля обычно используется salt. salt — это случайно сгенерированное значение, которое вы можете объединить с паролем перед его хешированием.

Например, если мы сгенерировали jvFJ4 как соль, объединили его с weakpassword и хешировали (sha256("jvFJ4weakpassword")), то получится :

b104c5bf49e2e4937ac2419e94864f7209014a96cae582302f6e5f891e426e22

Это совершенно другой результат, чем хеширование простого weakpassword.

Теперь с salt наша таблица будет выглядеть так:

Что нам нужно делать, когда пользователь входит в систему?

  • Пользователь предоставит свой адрес электронной почты и простой пароль.

* Системе необходимо запросить таблицу users по электронной почте, например, SELECT * FROMusers WHERE email='codecurated@codecurated.com', чтобы получить хешированный пароль и соль.

* Вычислить SHA-256(соль + введенный пароль).

* Сравните результат с хешированным паролем. Если он тот же, это означает, что пользователь ввел правильный пароль, и система может аутентифицировать пользователя.

С помощью этой конструкции мы смягчили многие атаки, но достаточно ли этого? Ну...

Подходы, которые мы создали ранее, могут смягчить многие атаки. Но это не атака по словарю. Что, если злоумышленник получит соль, объединит ее с общим паролем, хеширует комбинацию и сравнит ее с паролем в базе данных? Только время убережет злоумышленника от получения пароля вашего пользователя. На самом деле время – это одна из переменных, которую мы можем настроить.

SHA256 не подходит для хеширования паролей, поскольку он предназначен для хеширования достаточно сложных входных данных (чего часто не делает пароль) и быстрого их вычисления. Я пытался выполнить хеширование weakpassword 10 миллионов раз с помощью моего процессора AMD Ryzen 5 3600 (6 ядер, 12 потоков @ 3,6 ГГц). Я могу закончить это очень быстро.

h := sha256.New()  
start := time.Now()  
for i := 0; i < 10000000; i++ {  
   h.Write([]byte("weakpassword"))  
}  
elapsed := time.Since(start)  
log.Printf("SHA256 took %s n", elapsed)

Результат:

2022/07/28 16:57:00 SHA256 took 337.2232ms 

С помощью SHA-256 злоумышленник сможет быстро хешировать и сравнивать с ним множество паролей. Нам нужна более медленная функция хеширования.

Bcrypt

И здесь на помощь приходит Bcrypt. Bcrypt — это функция хеширования паролей, основанная на Blowfish, в котором вы можете определить стоимость его запуска. Эта особенность, в частности, идеально подходит для хеширования паролей, поскольку она обеспечит надежность функции хеширования в будущем, когда появится более быстрая машина.

Давайте посмотрим, как работает Bcrypt:

for i := 10; i < 21; i++ {  
   start := time.Now()  
   bcrypt.GenerateFromPassword([]byte("weakpassword"), i)  
   elapsed := time.Since(start)  
   fmt.Printf("cost: %d Elapsed time: %sn", i, elapsed)  
}

Результат:

cost: 10 Elapsed time: 56.233ms
cost: 11 Elapsed time: 113.1521ms
cost: 12 Elapsed time: 213.0455ms
cost: 13 Elapsed time: 447.572ms
cost: 14 Elapsed time: 877.3284ms
cost: 15 Elapsed time: 1.8126554s
cost: 16 Elapsed time: 3.375513s
cost: 17 Elapsed time: 6.5935858s
cost: 18 Elapsed time: 13.3655301s
cost: 19 Elapsed time: 27.0033831s
cost: 20 Elapsed time: 53.8954938s

Мы видим, что время по сравнению с затратами стало параболическим. Более высокая стоимость означает лучшую безопасность, но худший пользовательский опыт. Представьте себе, что если вы установите стоимость 20, пользователю придется ждать 53 секунды при входе в систему. Но если вы установите слишком низкое значение, злоумышленнику будет легче украсть учетные данные вашего пользователя.

:::информация Давайте займемся математикой. Предположим, в вашей базе данных десять миллионов пользователей, а у злоумышленника есть словарь из 1000 наиболее распространенных паролей. Сколько времени понадобится злоумышленнику, чтобы вычислить хэш пароля с помощью SHA256 по сравнению с Bcrypt со стоимостью 12? Во-первых, нам нужно будет подсчитать, сколько хеш-операций необходимо выполнить злоумышленнику. Это можно получить, умножив количество имеющихся у нас пользователей на количество общих паролей, которые использует злоумышленник.

Итак, 10.000.000 * 1000 = 10.000.000.00 Теперь мы вычисляем время, необходимое злоумышленнику для вычисления хеша десять миллиардов раз. Для SHA256 мы выполнили миллион вычислений за 337,2232 мс. Итак, мы можем вычислить весь хэш: 10.000.000.000 / 1.000.000 * 337.2232 мс = 3372232 м, что составляет чуть меньше 1 часа. Далее попробуем использовать Bcrypt со стоимостью 12: 10.000.000.000 * 213.0455ms = 2,130455 e+12m, что равно 4053377 годам.

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

:::

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

hashedPassword,_ := bcrypt.GenerateFromPassword([]byte("weakpassword"), 10)  
fmt.Printf("hashed password: %s", hashedPassword)
hashed password: $2a$10$.krQtTcne8xlhG2rJONbKu9KZepUpwl8tyC/fFIB6lRmNufvPfge2

Если разбить результат, мы получим:

Bcrypt breakdown

Расшифровка Bcrypt

  1. alg: идентификатор алгоритма has, $2a означает Bcrypt
  2. 2. cost: стоимость Bcrypt; помните, мы установили это значение в коде как 10

    3. salt (22 символа): случайная соль для хеширования пароля, созданная функцией хеширования Bcrypt.

    4. хешированный пароль (31 символ): результат хеш-функции.

    Наконец, давайте посмотрим, как проверить пароль, хешированный с помощью Bcrypt:

    hashedPassword,_ := bcrypt.GenerateFromPassword([]byte("weakpassword"), 10)  
    err := bcrypt.CompareHashAndPassword(hashedPassword, []byte("weakpassword"))  
    
    if err != nil {  
       fmt.Print(err)  
    } else {  
       fmt.Printf("Password true")  
    }
    

    Как мы видим, нам не нужно отправлять стоимость, alg и соль при сравнении хеша и пароля, поскольку все необходимые входные данные были добавлены к самому результату хеширования Bcrypt.< /п>

    Давайте подведем итог: есть две важные особенности Bcrypt, которые делают его подходящим для хеширования паролей:

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

      Следующий шаг

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

      Также существует вероятность того, что злоумышленник сможет провести атаку «человек посередине», если ваш сайт не использует HTTPS.

      Если вы хотите больше узнать о том, как защитить процесс аутентификации, я настоятельно рекомендую вам прочитать:

      1. MFA
      2. Вход без пароля (вход по OTP, Magic Link и WebAuthn)
      3. Протокол TLS

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