Материал подготовлен командой Simple-Server для администраторов VPS и выделенных серверов. Команды и пути проверяйте на тестовой машине перед production.
Зачем защищать учетные данные
Начать разговор о хранении паролей в БД стоит с напоминания, что не один нормальный проект не будет хранить учетные данные в виде открытого текста, то есть незашифрованного человеко-читаемого текста. Данные всегда шифруются. Всегда.
Вот короткие причины почему.
Сервера разработчика-держателя приложения могут быть взломаны. Например, с помощью какой нибудь SQL-инъекции, которая позволит выудить из БД строку с паролем. Незашифрованные данные сразу попадут к хакерам и будут скомпрометированы. Дальше можно только догадываться, что с ними случится и к чему это приведет
Пользователи почти всегда игнорируют рекомендации безопасности — не используют менеджеры паролей либо игнорируют правила нейминга паролей для разных сервисов. Раскрытие пароля пользователя в одном приложении может привести к компрометации учётной записи в других.
Пользователи легко обвинят поставщика услуг (разработчика приложения) в недобросовестности, если поставщик сможет читать пароли. Причем даже в том случае, если никаких противоправных действий со стороны сотрудников не было. Это будет удар по репутации компании или проекта.
Сперва отметим, что в случае с паролями выполняется не шифрование в прямом виде, а скорее хэширование.
Важно понимать, что если что-то шифруется, то это всегда можно расшифровать. Зашифрованная информация является той же самой информацией, но в другом представлении.
Однако хэширование работает иначе. Хэш — это совершенно другая, новая и уникальная информация, полученная из неких исходных данных. В нашем случае из пароля.
Главное, что из хэша нельзя получить (теоретически можно, но на практике пока что еще нет) исходные данные. Короче, хэширование — это одностороннее вычисление.
Вот еще несколько неочевидных минусов шифрования, благодаря которым эволюционно прижилось именно хэширование:
- В отличие от хэша, шифр имеет переменную длину, что не очень хорошо в контексте хранения внутри БД и отправки серверных (или клиентских) пакетов.
- Генерация шифра занимает больше вычислительного времени, нежели генерация хэша.
- При использовании шифрования придется заниматься менеджментов ключей. То есть их придется где-то хранить и молиться, чтобы их никто не нашел.
Как выглядит хэш? По сути, это строка случайного вида — набор символов, лишенный смысла. Алгоритм, который генерирует такую строку, называют хэш-функцией.
Хеширование на текущий момент можно взломать только с помощью перебора. Причем способ довольно топорный и работает только на изначально слабых паролях. Хакеры просто пытаются перебрать длинный список наиболее распространенных паролей на основе словаря. Каждый пароль хешируется, после чего отправляется на атакуемый сервер для попытки авторизации. И так до тех пор, пока не будет найдено совпадение. В общем, никаких чудес.
При этом само хэширование — вычислительно сложная задача. Получение из исходных данных (пароля) производных так или иначе занимает время. Некоторые хэш-функции генерируют ключи повышенной длины (например, через многократное хэширование) только для того, чтобы увеличить время генерации. В таком случае перебор по словарю через брутфорс занимает больше времени, давая фору для службы безопасности или самого пользователя.
Хэширование непосредственно в PostgreSQL
Встроенное расширение** pgcrypto_**
В PostgreSQL есть встроенное расширение, предназначенное специально для хэширования паролей, — его не нужно загружать отдельно. Для его активации нужно выполнить команду-запрос:
CREATE EXTENSION pgcrypto;Эта команда загрузит доступное расширение в вашу текущую базу данных — в том случае, если его в ней еще нет. По факту будет запущен сценарий расширения, добавляющий новые SQL-объекты — функции, типы данных, операторы и методы индексации.
Добавление соли через** gen_salt()**
Чтобы сделать хэш еще надежнее, во время операции хэширования добавляется так называемая соль.
Дело в том, что хэш-функция всегда генерирует одно и то же значения для конкретных входных данных. Из этой особенности следует несколько проблем:
- Два одинаковых пароля у двух разных пользователей имеют один и тот же хэш. А лучше бы им быть разными.
- Чтобы не выполнять лишние вычисления по хэшированию словаря во время брутфорса, хакеры используют так называемые радужные таблицы — предварительно хэшированные словари с часто используемыми паролями.
Решение тривиально. Использовать в качестве входных данных при хэшировании не только пароль, но и некий дополнительный текст — соль.
Соль представляет собой псевдослучайную строку, которая и обеспечивает уникальность хэша на выходе.
В PostgreSQL для этого есть специальная функция — gen_salt(), в качестве аргумента которой передается тип криптографического алгоритма:
- md5 (MD5)
- des (DES)
- xdes (Extended DES)
- bf (Blowfish)
Например, вот так можно получить соль, используя довольно популярные MD5:
SELECT gen_salt('md5');На самом деле, многие разработчики не рекомендуют более использовать MD5, так как считают его не надежным.
Итак, с солью мы определились. Теперь рассмотри варианты самого хэширования.
Хэширование пароля с помощью функции** crypt()**
Каждый раз, когда пользователь создает новый или меняет существующий пароль, PostgreSQL должна сохранить его хэш.
Сама генерация выполняется с помощью встроенной функции crypt(). У нее есть 2 аргумента:
- строка пароля
- строка соли
Соответственно, вместе с функцией генерацией соли получение хэша выглядит следующим образом:
SELECT crypt('password', gen_salt('md5'));Кстати, если по каким-то причинам вы не хотите делать хэш уникальным, достаточно просто подставлять константное значение во второй аргумент:
SELECT crypt('password’, 'nothing');Что интересно, проверка пароля выполняется той же самой функцией хэширования. Отличаются только аргументы.
Например, чтобы проверить пароль «password» на соответствие его же хэшу, выполняется команда:
SELECT crypt('password’, хэш);Обратите внимание, здесь не указывается соль — только пароль и хэш. На выходе этой команды получается тот же самый хэш, что был создан ранее — и который был указан вторым аргументом.
Однако, если пароль неверный, хэш будет отличаться:
SELECT crypt('another_password’, хэш);Еще раз напомним. Вызов crypt с паролем password и хэшем этого пароля hash выведет на выходе хэш hash пароля password. В любом другом случае вывод будет отличаться.
Как использовать хэширование PostgreSQL на практике?
- Создание таблицы для паролей
В реальном проекте учетные данные хранятся в таблицах и по необходимости записываются или читаются оттуда.
Поэтому мы создадим таблицу account с тремя столбцами: идентификатор, логин и хэш пароля:
CREATE table accounts (identifier SERIAL, login VARCHAR(10), password VARCHAR(100));Теперь заполним созданную таблицу импровизированными учетными данными:
INSERT INTO accounts (login, password)
VALUES ('login_1', crypt('some_password', gen_salt('md5'))) ;Примерно так можно сохранить пароль в базе данных PostgreSQL. Дополнительно указывается логин — обычно это почта или номер телефона.
- Обновление пароля в таблице
Каждый раз, когда пользователь меняет пароль, данные в таблице должны быть обновлены. В самом тривиальном случае запрос на запись хэша нового пароля выглядит следующим образом:
UPDATE accounts
SET password = crypt('new_password', gen_salt('md5'))
WHERE login= 'login_1' ;- Проверка введенного пароля с ранее сохраненным
При авторизации пользователя его учетные данные извлекаются из базы данных, после чего хэши паролей сопоставляются:
SELECT (password = crypt(введенный_пароль, password))
AS password_match
FROM accounts
WHERE login= 'login_1' ;Если password_match окажется равно t (true), то пароли совпадают. Если f (false) — пароли разные.
Кстати, у функции gen_salt есть дополнительный аргумент — количество итераций. Работает он только с алгоритмами xdes и bf :
- Число итераций xdes может быть нечетным числом от 1 до 16777215. По умолчанию — 725
- Число итераций bf может быть целым числом от 4 до 31. По умолчанию — 6
Например, вот так можно задать количество итераций для Extended DES:
SELECT crypt('password', gen_salt('xdes', 963));Разумеется, число итераций влияет на время хэширования и увеличивает безопасность пароля. Выбор нужного числа — вопрос баланса между скоростью и безопасностью. Высокое значение может уменьшить удобство как для пользователям, так и для разработчика. Низкое значение снижает стойкость. Предполагается, что серверное оборудование должно вычислять в среднем от 15 до 100 хэшей в секунду.
Хэширование на стороне клиентского или серверного приложения
Один из подходов, предотвращающий отправку пароля (от клиента к серверу) в открытом виде, — хэширование пароля на стороне приложения. На самом деле, это усложняет механизм реализации клиент-серверного общения, однако в некоторых случаях к этому прибегают. Тем не менее, большинство веб-ресурсов используют https-шифрование, что позволяет передавать многие конфиденциальные данные в «открытом» виде.
Возможен и другой вариант — хэширования пароля на уровне серверного (не клиентского) приложения, а не самой БД. В этом случае уже готовый хэш помещается в таблицу база данных, как и любое другое значение.
Многие популярные языки программирования предоставляют готовые модули или библиотеки, созданные специально для хэширования паролей. Это не просто хэш-функции, а более высокоуровневые конструкции, позволяющие работать непосредственно с парольным хэшем и аутентификацией.
Одни из таких модулей — пакет bcrypt на основе алгоритма Blowfish. От языка к языку интерфейс может отличаться, но функционал один и тот же.
Вот простой пример использования bcrypt в Python:
import bcrypt
# создание хэша перед тем, как отправить его в БД
def generate_hash(password):
# конвертация из строки в набор байтов
password_bytes = password.encode("utf-8")
# генерация соли
password_salt = bcrypt.gensalt()
# генерация хэша
hash_bytes = bcrypt.hashpw(password_bytes, password_salt)
# конвертация байтов обратно в строку
hash_str = hash_bytes.decode("utf-8")
return hash_str
# выполняет проверку пароля и хэша, после чего возвращает булевый результат
def authenticate(password, hash):
# конвертируем все из строки в байты
password_bytes = password.encode("utf-8")
hash_bytes = hash.encode("utf-8")
# соль автоматически (криптографически) «обнаруживается» в хэше, поэтому во время проверки ее не надо указывать отдельно
result = bcrypt.checkpw(password_bytes, hash_bytes)
return resultВпоследствии к подобному коду добавляются вызовы API-функций, отправляющих созданный хэш в базу данных, либо читающих его из БД во время авторизации.
Так или иначе, в каждом языке программирования есть свои специальные библиотеки для работы с хэшами. Поэтому, в зависимости от того, какой язык вы используете, изучайте соответствующие документации, стандарты или open-source библиотеки, упрощающие работу с паролями.
И самое главное — не изобретайте велосипед. Как встроенные функции (расширения) PostgreSQL, так и проверенные временем внешние библиотечные решения — все они написаны опытными людьми, выполнившими множество итераций по исправлению ошибок и уязвимостей.
Нет никакого смысла создавать собственные криптографические «нагромождения», наивно полагая, что это окажется более лучшим решением. Скорее всего это приведет ко множеству внутренних проблем и увеличит вероятность взломов.
Нужен сервер для практики? Арендуйте VPS/VDS в России — root-доступ, NVMe, DDoS-защита и поддержка 24/7.