PostgreSQL — это бесплатная объектно-реляционная база данных с открытым исходным кодом.
Объектно-реляционные БД отличаются от просто реляционных. Поэтому PostgreSQL — не то же самое.
Данные все также хранятся в таблицах, столбцы которых связаны друг с другом. Однако PostgreSQL работает согласная стандартам ACID (Atomicity, Consistency, Isolation and Durability), которые гарантируют достоверность данных за счет консистентности и атомарности операций внутри таблиц, — изменения вносятся последовательно, что позволяет отлавливать сбои сразу по ходу записи значений.
PostgreSQL поддерживает мультиверсионность (Multiversion concurrency control, MVCC) — особенность БД, за счет которой создаются копии записей во время изменения, защищающие от потерей и конфликтов одновременного чтения или записи.
Система индексации PostgreSQL устроена сложнее и работает быстрее — используются деревья и разные типы индексации: частичная, хеш, выражения.
Синтаксис PostgreSQL аналогичен синтаксису MySQL, однако первая поддерживает дополнительные подзапросы, такие как «LIMIT» или «ALL».
К тому же PostgreSQL совместима с большим количество языков программирования. Самые основные:
Проекты, подразумевающие регистрацию и авторизацию пользователей, должны хранить пароли на стороне сервера — как правило, в зашифрованном виде. Базы данных — наиболее подходящий инструмент для этого.
В этой статье мы разберемся, как правильно хранить пароли в базе данных (записывать и читать) с помощью PostgreSQL, соблюдая все меры безопасности.
dbaas
Начать разговор о хранении паролей в БД стоит с напоминания, что не один нормальный проект не будет хранить учетные данные в виде открытого текста, то есть незашифрованного человеко-читаемого текста. Данные всегда шифруются. Всегда.
Вот короткие причины почему.
Взлом разработчика
Сервера разработчика-держателя приложения могут быть взломаны. Например, с помощью какой нибудь SQL-инъекции, которая позволит выудить из БД строку с паролем. Незашифрованные данные сразу попадут к хакерам и будут скомпрометированы. Дальше можно только догадываться, что с ними случится и к чему это приведет
Легкомысленность пользователей
Пользователи почти всегда игнорируют рекомендации безопасности — не используют менеджеры паролей либо игнорируют правила нейминга паролей для разных сервисов. Раскрытие пароля пользователя в одном приложении может привести к компрометации учетной записи в других.
Репутация и доверие
Пользователи легко обвинят поставщика услуг (разработчика приложения) в недобросовестности, если поставщик сможет читать пароли. Причем даже в том случае, если никаких противоправных действий со стороны сотрудников не было. Это будет удар по репутации компании или проекта.
Сперва отметим, что в случае с паролями выполняется не шифрование в прямом виде, а скорее хэширование.
Важно понимать, что если что-то шифруется, то это всегда можно расшифровать. Зашифрованная информация является той же самой информацией, но в другом представлении.
Однако хэширование работает иначе. Хэш — это совершенно другая, новая и уникальная информация, полученная из неких исходных данных. В нашем случае из пароля.
Главное, что из хэша нельзя получить (теоретически можно, но на практике пока что еще нет) исходные данные. Короче, хэширование — это одностороннее вычисление.
Вот еще несколько неочевидных минусов шифрования, благодаря которым эволюционно прижилось именно хэширование:
Как выглядит хэш? По сути, это строка случайного вида — набор символов, лишенный смысла. Алгоритм, который генерирует такую строку, называют хэш-функцией.
Хеширование на текущий момент можно взломать только с помощью перебора. Причем способ довольно топорный и работает только на изначально слабых паролях. Хакеры просто пытаются перебрать длинный список наиболее распространенных паролей на основе словаря. Каждый пароль хешируется, после чего отправляется на атакуемый сервер для попытки авторизации. И так до тех пор, пока не будет найдено совпадение. В общем, никаких чудес.
При этом само хэширование — вычислительно сложная задача. Получение из исходных данных (пароля) производных так или иначе занимает время. Некоторые хэш-функции генерируют ключи повышенной длины (например, через многократное хэширование) только для того, чтобы увеличить время генерации. В таком случае перебор по словарю через брутфорс занимает больше времени, давая фору для службы безопасности или самого пользователя.
Встроенное расширение pgcrypto_
В PostgreSQL есть встроенное расширение, предназначенное специально для хэширования паролей, — его не нужно загружать отдельно. Для его активации нужно выполнить команду-запрос:
CREATE EXTENSION pgcrypto;
Эта команда загрузит доступное расширение в вашу текущую базу данных — в том случае, если его в ней еще нет. По факту будет запущен сценарий расширения, добавляющий новые SQL-объекты — функции, типы данных, операторы и методы индексации.
Добавление соли через gen_salt()
Чтобы сделать хэш еще надежнее, во время операции хэширования добавляется так называемая соль.
Дело в том, что хэш-функция всегда генерирует одно и то же значения для конкретных входных данных. Из этой особенности следует несколько проблем:
Решение тривиально. Использовать в качестве входных данных при хэшировании не только пароль, но и некий дополнительный текст — соль.
Соль представляет собой псевдослучайную строку, которая и обеспечивает уникальность хэша на выходе.
В PostgreSQL для этого есть специальная функция — gen_salt()
, в качестве аргумента которой передается тип криптографического алгоритма:
Например, вот так можно получить соль, используя довольно популярные 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. В любом другом случае вывод будет отличаться.
В реальном проекте учетные данные хранятся в таблицах и по необходимости записываются или читаются оттуда.
Поэтому мы создадим таблицу 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:
Например, вот так можно задать количество итераций для 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, так и проверенные временем внешние библиотечные решения — все они написаны опытными людьми, выполнившими множество итераций по исправлению ошибок и уязвимостей.
Нет никакого смысла создавать собственные криптографические «нагромождения», наивно полагая, что это окажется более лучшим решением. Скорее всего это приведет ко множеству внутренних проблем и увеличит вероятность взломов.
Подготовили для вас выгодные тарифы на DBaaS
Как видно, хранение паролей на стороне сервере требует ответственности от разработчика и системного администратора приложения. Поэтому оно несет в себе определенный репутационные издержки в случае различных сбоев или хакерских атак.
Авторизация — отдельное IT-направление. Чтобы сделать надежную систему аутентификации, нужен опыт и время. Поэтому последнее время преобладает тренд на «аутсорсинг» авторизации. Все больше сервисов опираются на внешние системы аутентификации, разработчики которых специализированы преимущественно на безопасности, а не бизнес-логике. Это своего рода разделение труда.
Например, существуют протоколы (стандарты) OpenID и OAuth 2.0. Последний используется в Google API для авторизации пользователей, поэтому любой может подключить аутентификацию через Google в свое приложение или онлайн-сервис.
Это выгодно в том числе и для пользователей, т.к. они смогут авторизовываться через привычную им почту, не создавая огромное количество учетных данных, которые всегда есть риск утерять.
Однако, аутентификация по паролю все еще надежный (хотя и консервативный) метод. Поэтому, безопасное хранение пользовательских паролей в базе данных — неотъемлемая часть такой реализации.