Хеширование паролей в Python: туториал по Bcrypt с примерами

Создание сервера с нуля или использование легковесного фреймворка наделяет огромной властью. С этой властью приходит и ответственность, в частности, ответственность за безопасное хранение паролей пользователей.

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

Могу ли я хранить пароли в виде простого текста?

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

  • user.one@gmail.com – somePa$$wordHere
  • user.two@hotmail.com – otherPlainTextPass

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

Наилучшее решение — хеширование

Хеш-функция (или, более конкретно, в нашем случае, функция получения ключа) детерминистически создает надежный ключ из пароля. Поскольку хеши являются односторонними, злоумышленник не может воссоздать незашифрованный пароль из хеша. Теперь злоумышленник увидит что-то подобное в базе данных:

  • user.one@gmail.com – cab864878af008fbc550087940ffacdb79a7f82201725e3350e25d6cfbdd4255
  • user.two@hotmail.com – 42a7fd2b639d18b3aba5db8504d4530f1f1ab58ab9615414b7629d6ec5c157b8

Они не смогут использовать хеш для входа в другие системы, потому что у них нет доступа к исходному паролю.

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

import bcrypt
bcrypt.hashpw('userPlainTextPassword'.encode(), bcrypt.gensalt())

Радужные таблицы и соли

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

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

Например, допустим, что LoveMatchingToday обновился и начал хешировать пароли, но не содержал уникальных солей. Злоумышленник может иметь предварительно вычисленную таблицу хэшей:

  • aab864878af008fbc550087940ffacdb79a7f82201725e3350e25d6cfbdd425f = password123
  • afg3683232297323f2f0087940ffacdb79a7f8284723732350e25d6cfbdd4cccc = shadowTheHedgehog1234

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

По этой причине нам нужно «посолить» пароли. К счастью, Bcrypt обрабатывает засолку автоматически. Однако, ради обучения давайте предположим, что этого не случилось. Если бы такого не произошло, наш псевдокод выглядел бы примерно так:

# Save new password
salt = creatRandomSalt()
hashedPassword = hash(newPassword.concat(salt))
database.save(hashedPassword, salt)

# Check password
hashedPassword, salt = database.GetUserCredentals()
passwordInput = userInput
if hash(passwordInput.concat(salt)) == hashedPassword:
  login()
else:
  failure()

Однако, поскольку Bcrypt автоматически сохраняет соль с хэшированным результатом в формате «{соль}{хэш}», мы можем просто использовать следующий код:

import bcrypt

# password = userInput
hashAndSalt = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# save "hashAndSalt" in data base

# To check:
# password = userInput
valid = bcrypt.checkpw(password.encode(), hashAndSalt)

Спасибо за прочтение!