Menu
CoddyTech

Никогда не храните пароли

Часть раздела Аутентификация и безопасность путешествия по Node.js на Coddy. Урок 4 из 24.

Люди повторно используют пароли, поэтому утечка пароля в открытом виде может привести к раскрытию учетных записей в других сервисах. Храните хеш пароля с солью: производное значение, которое проверяют, вычисляя его заново, а не расшифровывая.

const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(inviteCode, salt, 32);
const saved = salt + '.' + key.toString('hex');

В этом примере соль и производный ключ объединяются для кода приглашения. Разделитель позволяет при проверке извлечь соль и получить другой ключ с теми же параметрами. В вашем задании с паролями используются собственные разделитель и длина ключа. Для каждого нового пароля генерируйте соль, получайте ключ, а затем храните их оба. Хеши с солью всё равно требуют надёжных паролей, поскольку злоумышленник может перебирать варианты.

challenge icon

Задание

Легко

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

  1. В passwords.js измените hashPassword(password), чтобы она возвращала текст, который сохраняется при регистрации, в формате: соль, двоеточие, хеш:
    • Соль — это 16 случайных байт, генерируемых заново при каждом вызове и записываемых в шестнадцатеричном виде: 32 символа от 0 до 9 и от a до f.
    • Хеш — это ключ длиной 64 байта, который scrypt создаёт из пароля и соли и тоже записывает в шестнадцатеричном виде: 128 символов. Третий аргумент scryptAsync задаёт длину ключа в байтах.
    • Объедините их двоеточием. Сохранённое значение выглядит так: f2bba0669d9fa2d203afc7e61bf8f065:c3f7cae352e023a8d4766da9a316b5a2c844100efff22c4079f89a8ab8512ac91c33227481c3afa88c3ba0e88347f9e3e0c22dad9d55f19688a350f71be8dbd2
  2. Изменять server.js не нужно. В нём POST /signup уже вызывает hashPassword и сохраняет возвращённое ею значение. Там также есть GET /debug/users/:username, который показывает, что было сохранено для одного пользователя. Этот маршрут существует только для того, чтобы вы могли посмотреть; в настоящем приложении его никогда не бывает.

Попробуйте: Нажмите «Запустить». Отправьте POST /signup с {"username": "ada", "password": "correct horse"}. Вы получите статус 201 и {"username": "ada"}. Отправьте то же тело запроса с "username": "bob". Теперь отправьте GET /debug/users/ada, а затем GET /debug/users/bob. Каждое значение passwordHash состоит из 32 символов, двоеточия и ещё 128 символов. Эти два значения различаются, хотя ada и bob выбрали один и тот же пароль.

Попробуйте сами

import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

// Преобразует пароль в текст, который сохраняет signup.
// Пока пароль сохраняется в том виде, в котором он был введён.
export async function hashPassword(password) {
    return password;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Аутентификация и безопасность

Потренируйтесь самостоятельно: Песочница Express