Шифрование и криптография в веб-приложениях: пароли, данные, ключи и токены
Как правильно применять криптографию в веб-приложении: хеширование паролей Argon2id и bcrypt, AES-GCM для данных, управление ключами и секретами, сессии и JWT, хранение токенов в браузере, российские требования и чек-лист.
Криптография в веб-приложении редко ломается из-за того, что кто-то взломал алгоритм. Она ломается из-за того, как алгоритм применён: пароли хешируются быстрым хешем без соли, ключ шифрования лежит в репозитории рядом с зашифрованными данными, токен авторизации хранится там, откуда его может прочитать внедрённый скрипт, а собственный «надёжный» способ шифрования придуман разработчиком за вечер. Эта статья — практическое руководство для разработчиков и технических руководителей: какие криптографические задачи возникают в обычном веб-приложении, какие решения считаются правильными сегодня, как хранить пароли, шифровать данные, работать с токенами и ключами и какие ошибки встречаются чаще всего. Главный принцип, который пройдёт через весь текст: не изобретать криптографию, а правильно применять проверенные библиотеки и протоколы. Четыре задачи, которые решает криптография Конфиденциальность — данные может прочитать только тот, у кого есть ключ. Шифрование. Целостность — данные не изменены. Хеши и коды аутентификации сообщений. Аутентичность — данные созданы тем, кем заявлено. Подписи и коды аутентификации. Безопасное хранение секретов , которые нельзя восстановить, — пароли. Специальные функции хеширования паролей. Путаница между этими задачами — источник многих ошибок: например, «шифрование» паролей, которые на самом деле нужно не шифровать, а хешировать, или хеш без ключа там, где нужна проверка подлинности. Основные понятия Симметричное шифрование — один ключ для шифрования и расшифровки. Быстрое, используется для шифрования данных. Современный стандартный выбор — AES в режиме GCM или ChaCha20-Poly1305. Асимметричное шифрование — пара ключей: открытый и закрытый. Используется для обмена ключами, цифровых подписей и в TLS. Медленнее симметричного, поэтому данные шифруют симметрично, а асимметрично — обмениваются ключом. Хеш-функция — однонаправленное преобразование данных в значение фиксированной длины. SHA-256 подходит для проверки целостности, но не для паролей. HMAC — код аутентификации сообщения на основе хеша и секретного ключа. Проверяет, что сообщение создано обладателем ключа и не изменено. Аутентифицированное шифрование — шифрование, которое одновременно обеспечивает конфиденциальность и целостность. AES-GCM относится к таким режимам; режимы без аутентификации позволяют злоумышленнику незаметно менять зашифрованные данные. Хранение паролей Почему нельзя обычный хеш Пароли никогда не хранятся открытым текстом и не шифруются обратимо. Но и быстрые хеши вроде MD5 или SHA-256 для паролей не подходят: они созданы, чтобы вычисляться быстро, а значит, при утечке базы злоумышленник перебирает миллиарды вариантов на обычных видеокартах. Для паролей нужны функции, которые специально сделаны медленными и требовательными к памяти. Что использовать Argon2id — рекомендуемый выбор для новых систем, в том числе в рекомендациях OWASP по хранению паролей. Требует настраиваемого объёма памяти, что затрудняет перебор на специализированном оборудовании. bcrypt — проверенный многолетней практикой вариант, широко поддерживается. Учтите ограничение: bcrypt использует только первые 72 байта пароля. scrypt — тоже требователен к памяти, приемлемая альтернатива. import argon2 from 'argon2'; export async function hashPassword(password) { return argon2.hash(password, { type: argon2.argon2id }); } export async function verifyPassword(hash, password) { return argon2.verify(hash, password); } Библиотеки сами генерируют уникальную соль для каждого пароля и сохраняют её вместе с параметрами в итоговой строке. Параметры стоимости подбирают так, чтобы проверка занимала заметную для злоумышленника, но приемлемую для пользователя долю секунды на вашем оборудовании, и периодически пересматривают. Что ещё важно ограничение числа попыток входа и задержки после неудачных попыток; проверка паролей по спискам утёкших паролей при регистрации и смене; двухфакторная аутентификация для административных и чувствительных учётных записей; безопасный сброс пароля: одноразовые токены с коротким сроком жизни, хранящиеся в базе в виде хеша; одинаковый ответ и время ответа для существующих и несуществующих учётных записей, чтобы нельзя было перебирать email. Шифрование данных Шифрование при передаче HTTPS на всех страницах и во всех API — обязательный минимум. Сертификаты выпускаются бесплатно и обновляются автоматически, поэтому оправданий для HTTP не осталось. Настройки сервера: современные версии TLS, отключение устаревших протоколов и шифров, заголовок Strict-Transport-Security , запрещающий браузеру обращаться к сайту по HTTP. Внутренние соединения между сервисами и с базой данных тоже стоит шифровать, особенно если они проходят через общую сеть. Шифрование при хранении Шифрование дисков и управляемых баз данных у облачного провайдера защищает от физического доступа к носителям, но не от атаки через приложение: приложение видит данные расшифрованными. Для особо чувствительных полей — паспортных данных, медицинской информации, токенов доступа к сторонним сервисам — применяют шифрование на уровне приложения. import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; const KEY = Buffer.from(process.env.FIELD_KEY_B64, 'base64'); // 32 байта export function encrypt(plaintext) { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', KEY, iv); const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return Buffer.concat([iv, tag, data]).toString('base64'); } export function decrypt(payload) { const buf = Buffer.from(payload, 'base64'); const iv = buf.subarray(0, 12); const tag = buf.subarray(12, 28); const data = buf.subarray(28); const decipher = createDecipheriv('aes-256-gcm', KEY, iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8'); } Ключевые детали: новый случайный вектор инициализации для каждой операции, проверка тега аутентификации при расшифровке и хранение ключа отдельно от данных. Повтор вектора инициализации с тем же ключом в режиме GCM разрушает защиту. Поиск по зашифрованным полям Зашифрованное поле нельзя найти обычным запросом. Если нужен точный поиск — например, по номеру документа, — рядом хранят HMAC значения с отдельным ключом и ищут по нему. Полнотекстовый поиск по зашифрованным данным требует отдельных решений и почти всегда — компромиссов. Управление ключами и секретами Шифрование настолько надёжно, насколько защищён ключ. ключи и секреты не хранятся в репозитории, в образах контейнеров и в конфигурационных файлах рядом с кодом; используется хранилище секретов — облачный сервис управления ключами или специализированное решение вроде HashiCorp Vault, — либо как минимум переменные окружения, заполняемые при развёртывании из защищённого источника; доступ к ключам — только у сервисов, которым они нужны, с журналированием; предусмотрена ротация ключей: новые данные шифруются новым ключом, старые перешифровываются или расшифровываются старым по идентификатору версии ключа; для особо чувствительных данных применяют конвертное шифрование: данные шифруются ключом данных, а он — главным ключом в сервисе управления ключами. Если секрет попал в репозиторий, удаления коммита недостаточно: история уже скопирована. Секрет нужно немедленно отозвать и выпустить новый. Токены авторизации Сессии или JWT Классические сессии хранят состояние на сервере, а клиент получает непрозрачный идентификатор в cookie. JWT содержит данные пользователя и подпись, и сервер проверяет его без обращения к хранилищу. JWT не лучше и не хуже сессий — он решает другую задачу: удобен для взаимодействия между сервисами и для API, но усложняет отзыв токенов. Для обычного веб-приложения с одним сервером сессии в cookie часто проще и безопаснее. Правила работы с JWT явно задавать допустимый алгоритм подписи при проверке и отклонять токены с алгоритмом none ; проверять срок действия, издателя и получателя токена; короткий срок жизни токена доступа и отдельный токен обновления с возможностью отзыва; не класть в токен конфиденциальные данные: содержимое JWT закодировано, но не зашифровано и читается кем угодно; для подписи использовать достаточно длинный случайный секрет или асимметричные ключи. Где хранить токен в браузере Токен в localStorage доступен любому скрипту на странице, а значит, при уязвимости XSS его можно украсть. Более безопасный вариант для веб-приложений — cookie с флагами HttpOnly , Secure и SameSite : скрипт не может прочитать такую cookie, а атрибут SameSite вместе с защитой от CSRF снижает риск подделки запросов. res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 15 * 60 * 1000, }); Смежные защиты, без которых криптография бесполезна XSS. Внедрённый скрипт действует от имени пользователя, и никакое шифрование его не остановит. Защита: экранирование вывода, отказ от вставки непроверенного HTML, политика безопасности контента. CSRF. Для cookie-авторизации — атрибут SameSite и токены против подделки запросов для изменяющих операций. Ограничение частоты запросов на вход, сброс пароля и дорогие операции. Сравнение секретов за постоянное время — функции вроде timingSafeEqual вместо обычного сравнения строк при проверке подписей и токенов. Криптографически стойкие случайные числа — crypto.randomBytes или crypto.getRandomValues , а не Math.random , для токенов, кодов и идентификаторов, которые нельзя угадать. Подробно о защите API — в статье об API Security , о защите мобильных клиентов — в материале о кибербезопасности мобильных приложений . Российские требования Для большинства коммерческих веб-приложений достаточно международных стандартных алгоритмов в TLS и при хранении данных. Но в ряде случаев применимы отечественные требования: государственные информационные системы, отдельные классы защищаемой информации, взаимодействие с государственными сервисами. Там может потребоваться использование сертифицированных средств криптографической защиты и отечественных алгоритмов ГОСТ. Применимость требований определяется категорией системы и данных и должна проверяться со специалистами по информационной безопасности. Персональные данные в любом случае требуют мер защиты по 152-ФЗ — подробно в статье о персональных данных на сайте . Частые ошибки MD5, SHA-1 или SHA-256 без специальной функции для паролей; собственный алгоритм шифрования или «усиление» стандартного алгоритма самодельными преобразованиями; режим шифрования без аутентификации и повтор вектора инициализации; ключ шифрования в коде, в репозитории или рядом с зашифрованными данными; JWT с конфиденциальными данными внутри, без проверки алгоритма и с долгим сроком жизни; токены авторизации в localStorage в приложении с пользовательским контентом; Math.random для токенов сброса пароля; отключённая проверка сертификатов в запросах к внешним сервисам «чтобы заработало». Чек-лист HTTPS везде, HSTS, современные версии TLS. Пароли хешируются Argon2id или bcrypt с уникальной солью. Ограничение попыток входа, двухфакторная аутентификация для администраторов. Чувствительные поля шифруются AES-GCM на уровне приложения. Ключи и секреты в хранилище секретов, с разграничением доступа и ротацией. Токены авторизации в cookie с HttpOnly, Secure и SameSite; короткий срок жизни. JWT проверяется с явным алгоритмом, сроком действия и получателем. Защита от XSS и CSRF, ограничение частоты запросов. Криптографически стойкие случайные значения для всех токенов. Зависимости с криптографическими функциями регулярно обновляются. Безопасную архитектуру аутентификации, хранения данных и интеграций мы закладываем в проектах разработки веб-проектов и мобильных приложений . Частые вопросы Можно ли шифровать пароли, чтобы при необходимости их восстановить? Нет. Пароли должны храниться так, чтобы их нельзя было получить обратно даже администратору системы. Если пользователь забыл пароль, он задаёт новый через безопасную процедуру сброса. Что лучше: bcrypt или Argon2? Для новых систем предпочтительнее Argon2id. bcrypt остаётся прием