Храним пароли безопасно. Интерактивное руководство по bcrypt в Python
Краткое предисловие
Тема болезненная, обширная и важная и касается буквально каждого из нас, хотелось бы нам этого или нет.
В современном мире трудно представить человека, который не имеет, например, электронной почты и не зарегистрирован ни в каком сервисе. Как известно, при регистрации мы обычно указываем логин и пароль и на этом наша сторона ответственности заканчивается (а, нет, еще в нашей ответственности создать себе надежный пароль из примерно 16 символов, который содержит и строчные и заглавные буквы, и спецсимволы и желательно вообще нечитабельный, а еще - уникальный для каждого сервиса :)). Способ хранения паролей же лежит на стороне разработчика сервиса и обычно мы об этом никогда не узнаем, так как исходные коды обычно закрыты.
Чем же плохо хранение паролей в открытом виде?
Это однозначно небезопасно и может привести к серьезным последствиям.
Как минимум, если злоумышленник вдруг получит доступ к базе данных сервиса, он получит сразу и пары логин-пароль всех пользователей и сможет делать многие действия от имени взломанных бедолаг.
Кроме того, как мы обычно поступаем? Мы придумываем 1 пароль для многих сервисов для удобства. А если злоумышленник уже получил логин-пароль от одного сервиса, то можно считать, что он уже получил ключики от всех дверей. Осталось их только перебрать :).
Погружаемся в тему
Итак, давайте в рамках статьи представим, что у нас есть вымышленный сервис доставки dostavallo, который хранит пароли в открытом виде, примерно так:
------------------------------------------
| email | password |
| ------------------ |-------------------- |
| user1@gmail.com | my_best_password |
| user2@yandex.ru | secured_password |
------------------------------------------
Поможем ему обезопаситься и не потерять пользователей!
База
Без теории никак, поэтому немного ознакомимся с основными терминами.
Криптографическая хэш-функция, упрощенно говоря - это математическая функция, превращающая последовательность входных данных в строку фиксированной длины, состоящей из букв и цифр.
Главная особенность полученных хэшей в том, что они односторонние, то есть расшифровать их и получить исходный пароль уже не получится.
Часто в контексте безопасного хранения паролей можно услышать фразу "Надо посолить пароль" :) Что же это значит?
Соль - это случайная строка данных, которая хэшируется вместе с паролем, чтобы результат хэширования был всегда уникальным. Позволяет скрыть факт использования одинаковых паролей при использовании для них разной соли.
Соль в нашем случае нужна для защиты от атак с использованием радужных таблиц.
Радужные таблицы - это специальный вариант таблиц поиска для обращения криптографических хеш-функций, использующий механизм разумного компромисса между временем поиска по таблице и занимаемой памятью.
Проще говоря, это таблицы, которые позволяют быстро найти соответствие между хэшем и исходным паролем. Если пароли защищены солью, радужные таблицы становятся бесполезными.
К слову, почему хэширование, а не шифрование? Потому что это односторонний процесс и восстановить исходный пароль уже вряд ли получится.
Шифрование подходит для данных, которые нужно вернуть в исходный вид, например, для передачи сообщений. В случае с паролями такой необходимости нет, поэтому хэширование предпочтительнее.
За дело берется bcrypt
bcrypt - адаптивная криптографическая хеш-функция формирования ключа, используемая для защищенного хранения паролей.
В Python уже есть готовая библиотека, которую мы и используем.
Реализация
Давайте напишем небольшой скрипт, который будет хэшировать пароли.
Для начала устанавливаем библиотеку к себе в проект:
pip install bcrypt
Попробуем посолить и хэшировать пароль:
# Наш исходный пароль: 123 password = "123" # Генерируем соль, хэшируем пароль с солью и предоставляем его # в виде строки из байткода с помощью функции decode() hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() # Теперь наш пароль выглядит примерно так: print(hashed_password) >>> '$2b$12$G3gSjjkBe1Bq3zwOGbmIXOUQ9C48kYcB6lJndABNOiGxJdip1.EES'
Отлично! Теперь мы представили наш пароль в виде, который нельзя расшифровать, но как же теперь проверить, что наш исходный пароль соответствует хэшированному?
Все довольно просто, надо лишь использовать функцию checkpw, которая возвращает результат типа bool:
password = "123" hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() # Проверяем, что указанный нами пароль соответствует хэшу print(bcrypt.checkpw(password.encode(), hashed_password.encode())) >>> True # Попробуем указать неправильный пароль и получим False wrong_password = "1234" print(bcrypt.checkpw(wrong_password.encode(), hashed_password.encode())) >>> False
Теперь мы убедились, что можно хранить пароль в хэшированном виде, который абсолютно не читаем и устойчив к атакам. Даже если он утечет, с ним мало что можно сделать. При этом, всегда можно проверить, корректен ли введенный пароль хэшированному, чтобы принять решение, пускать ли пользователя в систему.
Набираем обороты и помогаем сервису dostavallo с безопасностью
Давайте теперь рассмотрим пример посложнее, реализуем небольшое приложение командной строки, которое будет реализовывать функционал регистрации и входа в систему, а пары логин-пароль мы будем просто хранить в json-файлике.
Обратите внимание, что в реальных приложениях пароли и другие данные пользователей хранятся в защищенных базах данных, а не в файлах. :)
Пример будет рассмотрен одним блоком кода с комментариями / документацией. Погнали!
Реализация программы
import bcrypt
import json
# Путь до файла, в котором будут храниться данные о пользователях
USERS_FILEPATH = "users.json"
def hash_password(password: str) -> str:
"""Функция для хэширования пароля"""
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def is_password_correct(password: str, hashed_password: str) -> bool:
"""Функция для проверки пароля. Если ок - возвращает True"""
return bcrypt.checkpw(password.encode(), hashed_password.encode())
def load_users(file_path=USERS_FILEPATH) -> dict:
"""Функция для загрузки пользователей из файла"""
try:
with open(file_path, "r") as file:
return json.load(file)
except FileNotFoundError:
return {}
def save_user(user: dict, file_path=USERS_FILEPATH) -> None:
"""Функция для сохранения регистрационных данных пользователя (login-password) в файл"""
with open(file_path, "w") as file:
json.dump(user, file, indent=4)
def register_user(users: dict) -> dict:
"""
Функция регистрации пользователя.
Внутри происходит хэширование пароля и сохранение данных пользователя в файл.
"""
username = input("Введите имя пользователя: ").strip()
if username in users:
print(f"Пользователь {username} уже существует.")
return users
password = input("Введите пароль: ").strip()
hashed_password = hash_password(password)
users[username] = hashed_password
save_user(users)
print(f"Пользователь {username} успешно зарегистрирован!")
return users
def login_user(users: dict) -> None:
"""
Функция для логина пользователя в систему.
При удачном входе, выполнение программы завершается.
"""
username = input("Введите имя пользователя: ").strip()
if username not in users:
print(f"Пользователь {username} не найден.")
return
while True:
password = input("Введите пароль (или нажмите 'q' для возврата в главное меню): ").strip()
if password.lower() == 'q':
print("Возврат в главное меню.")
return
if is_password_correct(password=password, hashed_password=users[username]):
print(f"\nВведен Правильный пароль, вход выполнен. \nДобро пожаловать, {username}!")
exit()
else:
print("Неверный пароль! Попробуйте еще раз.")
# Основной цикл программы
def main():
users = load_users()
print("Добро пожаловать в сервис доставки 'dostavallo'!")
while True:
print("\nВыберите действие:")
print("1. Зарегистрироваться")
print("2. Войти в систему")
print("3. Выход")
choice = input("Ваш выбор: ").strip()
match choice:
case "1":
users = register_user(users)
case "2":
login_user(users)
case "3":
print("Выход из программы. До свидания!")
break
case _:
print("Неверный выбор. Попробуйте еще раз.")
if __name__ == "__main__":
main()Запускаем
Давайте залогиним моего кота в систему, чтобы он мог заказать себе вкусных рыбов.
python3 bcrypt_test.py
В зависимости от системы, здесь может быть просто python bcrypt_test.py. Также программу можно запустить через IDE.
После запуска программа дружелюбно приветствует нас:
Добро пожаловать в сервис доставки 'dostavallo'! Выберите действие: 1. Зарегистрироваться 2. Войти в систему 3. Выход Ваш выбор:
Выберем регистрацию, для этого достаточно ввести цифру 1.
Вводим имя пользователя и пароль:
Ваш выбор: 1 Введите имя пользователя: Bublik Введите пароль: Fi$h
Видим сообщение об успешной регистрации и переход в главное меню:
Пользователь Bublik успешно зарегистрирован! Выберите действие: 1. Зарегистрироваться 2. Войти в систему 3. Выход Ваш выбор:
Выберем вход, для этого введем цифру 2.
Далее вводим логин и пароль, которые мы только что создали:
Ваш выбор: 2 Введите имя пользователя: Bublik Введите пароль (или нажмите 'q' для возврата в главное меню): Fi$h Введен Правильный пароль, вход выполнен. Добро пожаловать, Bublik!
Удача буквально преследует нас, все получилось!
А что с паролями для dostavallo?
Давайте откроем файлик users.json и посмотрим, как хранятся данные наших пользователей. Внутри мы увидим примерно такое:
{
"Bublik": "$2b$12$VgDqlE5Xcwm7ID20Aw9isuA.qQp1o.hnMN1./8xxrqS1qXdRpuCW2"
}Теперь мы можем быть спокойны. Никто просто так не получит пароль нашего пользователя, даже при условии, что у него был задан довольно простой пароль.
Злоумышленники не смогут поднять историю заказов и пошантажировать кота тем, что недавно он заказывал руководство "Как заставить хозяев играть с тобой по ночам".
Послесловие
Теперь мы знаем, что соль бывает не только поваренной, а радужные таблицы - это вовсе не про закрашенные ячейки в MS Excel :)
Познакомились поближе с хэш-функциями и спасли сервис доставки от беды, а кота - от шантажа.
Кстати, если хотите проверить, не утекал ли ваш пароль куда-нибудь, можете сделать это с помощью сайта https://haveibeenpwned.com. Просто вводите туда свою почту и получите ответ :).
Если увидели там что-то, поступаем просто - меняем пароль для указанного сервиса.
Я вот себя нашел. По информации сайта, мои данные подвергались утечке 3 раза.
- https://www.securitylab.ru/blog/personal/xiaomite-journal/353801.php
- https://en.wikipedia.org/wiki/Have_I_Been_Pwned%3F
Берегите себя во всех смыслах.
Мой telegram-канал, присоединяйтесь :)
⬇⬇⬇
https://t.me/python3_with_love