Давайте дружить в Телеграме: рассказываем про новые фичи, общаемся в комментах, прислушиваемся к вашим идеям Подписаться

Как безопасно хранить пароли с помощью PostgreSQL

Миша Курушин
Миша Курушин
Технический писатель
22 сентября 2023 г.
2532
12 минут чтения
Средний рейтинг статьи: 4.5

PostgreSQL — это бесплатная объектно-реляционная база данных с открытым исходным кодом.

Объектно-реляционные БД отличаются от просто реляционных. Поэтому PostgreSQL — не то же самое.

Данные все также хранятся в таблицах, столбцы которых связаны друг с другом. Однако PostgreSQL работает согласная стандартам ACID (Atomicity, Consistency, Isolation and Durability), которые гарантируют достоверность данных за счет консистентности и атомарности операций внутри таблиц, — изменения вносятся последовательно, что позволяет отлавливать сбои сразу по ходу записи значений.

PostgreSQL поддерживает мультиверсионность (Multiversion concurrency control, MVCC) — особенность БД, за счет которой создаются копии записей во время изменения, защищающие от потерей и конфликтов одновременного чтения или записи.

Система индексации PostgreSQL устроена сложнее и работает быстрее — используются деревья и разные типы индексации: частичная, хеш, выражения.

Синтаксис PostgreSQL аналогичен синтаксису MySQL, однако первая поддерживает дополнительные подзапросы, такие как «LIMIT» или «ALL».

К тому же PostgreSQL совместима с большим количество языков программирования. Самые основные:

  • C/C++
  • Delphi
  • Erlang
  • Go
  • Java
  • Javascript
  • JSON (native since version 9.2)
  • Lisp
  • .NET
  • Python
  • R
  • Tcl

Проекты, подразумевающие регистрацию и авторизацию пользователей, должны хранить пароли на стороне сервера — как правило, в зашифрованном виде. Базы данных — наиболее подходящий инструмент для этого.

В этой статье мы разберемся, как правильно хранить пароли в базе данных (записывать и читать) с помощью PostgreSQL, соблюдая все меры безопасности.

Зачем защищать учетные данные

Начать разговор о хранении паролей в БД стоит с напоминания, что не один нормальный проект не будет хранить учетные данные в виде открытого текста, то есть незашифрованного человеко-читаемого текста. Данные всегда шифруются. Всегда.

Вот короткие причины почему.

Взлом разработчика

Сервера разработчика-держателя приложения могут быть взломаны. Например, с помощью какой нибудь 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 на практике?

  1. Создание таблицы для паролей

В реальном проекте учетные данные хранятся в таблицах и по необходимости записываются или читаются оттуда.

Поэтому мы создадим таблицу 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. Дополнительно указывается логин — обычно это почта или номер телефона.

  1. Обновление пароля в таблице

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

UPDATE accounts
SET password = crypt('new_password', gen_salt('md5'))
WHERE login= 'login_1' ;
  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, так и проверенные временем внешние библиотечные решения — все они написаны опытными людьми, выполнившими множество итераций по исправлению ошибок и уязвимостей.

Нет никакого смысла создавать собственные криптографические «нагромождения», наивно полагая, что это окажется более лучшим решением. Скорее всего это приведет ко множеству внутренних проблем и увеличит вероятность взломов.

Заключение

Как видно, хранение паролей на стороне сервере требует ответственности от разработчика и системного администратора приложения. Поэтому оно несет в себе определенный репутационные издержки в случае различных сбоев или хакерских атак.

Авторизация — отдельное IT-направление. Чтобы сделать надежную систему аутентификации, нужен опыт и время. Поэтому последнее время преобладает тренд на «аутсорсинг» авторизации. Все больше сервисов опираются на внешние системы аутентификации, разработчики которых специализированы преимущественно на безопасности, а не бизнес-логике. Это своего рода разделение труда.

Например, существуют протоколы (стандарты) OpenID и OAuth 2.0. Последний используется в Google API для авторизации пользователей, поэтому любой может подключить аутентификацию через Google в свое приложение или онлайн-сервис.

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

Однако, аутентификация по паролю все еще надежный (хотя и консервативный) метод. Поэтому, безопасное хранение пользовательских паролей в базе данных — неотъемлемая часть такой реализации.

Зарегистрируйтесь и начните пользоваться
сервисами Timeweb Cloud прямо сейчас

15 лет опыта
Сосредоточьтесь на своей работе: об остальном позаботимся мы
165 000 клиентов
Нам доверяют частные лица и компании, от небольших фирм до корпораций
Поддержка 24/7
100+ специалистов поддержки, готовых помочь в чате, тикете и по телефону