Храним пароли безопасно. Интерактивное руководство по 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