Пошук уроків, статей та іншого контенту
Освоїте хешування, випадкові байти та базові криптографічні операції модуля crypto.
cryptoВбудований модуль crypto надає Node.js криптографічні операції без встановлення додаткових пакетів. За його допомогою можна:
обчислювати хеші;
генерувати криптографічно стійкі випадкові байти;
створювати випадкові числа та UUID;
обчислювати HMAC;
порівнювати секретні значення без простих атак за часом виконання.
Підключити модуль можна так:
const crypto = require('node:crypto');Хеш-функція перетворює вхідні дані на рядок або набір байтів фіксованої довжини.
Властивості криптографічного хешу:
однакові вхідні дані дають однаковий результат;
навіть невелика зміна вхідних даних суттєво змінює хеш;
за хешем практично неможливо відновити початкові дані;
для однакового алгоритму довжина результату є сталою.
У Node.js хеш створюють через createHash().
const { createHash } = require('node:crypto');
const hash = createHash('sha256')
.update('Hello, Node.js!')
.digest('hex');
console.log(hash);Метод update() додає дані до хешу, а digest() завершує обчислення та повертає результат.
Метод digest() може повернути результат у різних форматах:
'hex' — шістнадцятковий рядок;
'base64' — рядок Base64;
'base64url' — URL-безпечний варіант Base64;
без аргументу — об’єкт Buffer.
const { createHash } = require('node:crypto');
const data = 'важливі дані';
const hexHash = createHash('sha256')
.update(data, 'utf8')
.digest('hex');
const base64Hash = createHash('sha256')
.update(data, 'utf8')
.digest('base64');
const hashBuffer = createHash('sha256')
.update(data, 'utf8')
.digest();
console.log(hexHash);
console.log(base64Hash);
console.log(hashBuffer);utf8 є типовим кодуванням для тексту, тому його можна не вказувати явно. Явне зазначення кодування часто робить код зрозумілішим.
Метод update() можна викликати кілька разів. Результат буде таким самим, як для об’єднаного рядка.
const { createHash } = require('node:crypto');
const hash = createHash('sha256')
.update('частина ')
.update('перша')
.digest('hex');
const expectedHash = createHash('sha256')
.update('частина перша')
.digest('hex');
console.log(hash === expectedHash); // trueПісля виклику digest() об’єкт хешу завершений. Повторно використати його для нових даних не можна.
Хеші часто використовують для перевірки цілісності файлів. Для великих файлів не обов’язково завантажувати весь файл у пам’ять — його можна читати частинами через потік.
const fs = require('node:fs');
const { createHash } = require('node:crypto');
const hash = createHash('sha256');
const stream = fs.createReadStream('./package.json');
stream.on('data', (chunk) => {
hash.update(chunk);
});
stream.on('end', () => {
console.log('SHA-256:', hash.digest('hex'));
});
stream.on('error', (error) => {
console.error('Не вдалося прочитати файл:', error.message);
});Такий підхід підходить для файлів будь-якого розміру, оскільки в пам’яті перебуває лише поточна частина даних.
Звичайний SHA-256 не призначений для безпечного зберігання паролів. Він працює надто швидко, тому зловмисник може перевірити дуже велику кількість можливих паролів.
Для паролів потрібні спеціальні повільні алгоритми, наприклад scrypt. Node.js має вбудовані scrypt() і scryptSync().
Під час зберігання пароля потрібно використовувати випадкову сіль — додаткові випадкові дані, унікальні для кожного пароля.
const {
randomBytes,
scryptSync,
timingSafeEqual,
} = require('node:crypto');
function hashPassword(password) {
const salt = randomBytes(16);
const derivedKey = scryptSync(password, salt, 64);
return {
salt: salt.toString('hex'),
hash: derivedKey.toString('hex'),
};
}
function verifyPassword(password, stored) {
const salt = Buffer.from(stored.salt, 'hex');
const expectedHash = Buffer.from(stored.hash, 'hex');
const actualHash = scryptSync(password, salt, expectedHash.length);
return (
actualHash.length === expectedHash.length &&
timingSafeEqual(actualHash, expectedHash)
);
}
const storedPassword = hashPassword('correct horse battery staple');
console.log(storedPassword);
console.log(verifyPassword('correct horse battery staple', storedPassword)); // true
console.log(verifyPassword('wrong password', storedPassword)); // falseУ реальному серверному коді краще використовувати асинхронну версію scrypt(), щоб повільне обчислення не блокувало основний потік Node.js.
Важливо зберігати і сіль, і результат похідного ключа. Сіль не є секретом, тому її можна зберігати поруч із хешем.
Для токенів, солей та інших секретних значень не можна використовувати Math.random(). Це генератор для звичайних випадкових чисел, а не для криптографічного захисту.
Модуль crypto надає randomBytes():
const { randomBytes } = require('node:crypto');
const token = randomBytes(32).toString('hex');
console.log(token);
console.log(token.length); // 64 символиОдин байт у hex-представленні займає два символи, тому 32 випадкові байти перетворюються на рядок довжиною 64 символи.
Асинхронний варіант:
const { randomBytes } = require('node:crypto');
randomBytes(32, (error, buffer) => {
if (error) {
console.error('Не вдалося створити випадкові дані:', error);
return;
}
console.log(buffer.toString('base64url'));
});Для коротких операцій синхронний варіант зручний, але в обробниках великої кількості запитів варто враховувати блокування потоку.
Для токенів, які передаються в URL або HTTP-заголовках, зручно використовувати base64url:
const { randomBytes } = require('node:crypto');
const resetToken = randomBytes(32).toString('base64url');
console.log(resetToken);На відміну від звичайного Base64, base64url не використовує символи, які можуть вимагати спеціального кодування в URL.
Метод randomInt() генерує криптографічно стійке випадкове ціле число в заданому діапазоні. Верхня межа не входить у діапазон.
const { randomInt } = require('node:crypto');
const diceRoll = randomInt(1, 7);
console.log(diceRoll); // число від 1 до 6Асинхронний варіант приймає callback:
const { randomInt } = require('node:crypto');
randomInt(1000, 10000, (error, number) => {
if (error) {
console.error(error);
return;
}
console.log('Код:', number);
});Для випадкового ідентифікатора також можна використати randomUUID():
const { randomUUID } = require('node:crypto');
console.log(randomUUID());HMAC поєднує хеш-функцію із секретним ключем. Він дає змогу перевірити:
що дані не були змінені;
що підпис створив той, хто знає секретний ключ.
const { createHmac } = require('node:crypto');
const secret = 'server-secret';
const message = 'userId=42&role=user';
const signature = createHmac('sha256', secret)
.update(message)
.digest('hex');
console.log(signature);Сторона, яка перевіряє підпис, повинна самостійно обчислити HMAC для того самого повідомлення та порівняти результати.
Для порівняння секретних значень використовуйте timingSafeEqual(), а не просте ===.
const {
createHmac,
timingSafeEqual,
} = require('node:crypto');
function createSignature(message, secret) {
return createHmac('sha256', secret)
.update(message)
.digest();
}
function isValidSignature(message, secret, receivedSignature) {
const expectedSignature = createSignature(message, secret);
if (expectedSignature.length !== receivedSignature.length) {
return false;
}
return timingSafeEqual(expectedSignature, receivedSignature);
}
const secret = 'server-secret';
const message = 'userId=42&role=user';
const signature = createSignature(message, secret);
console.log(isValidSignature(message, secret, signature)); // true
console.log(
isValidSignature('userId=42&role=admin', secret, signature),
); // falsetimingSafeEqual() приймає два Buffer однакової довжини. Якщо довжини різні, метод викидає помилку, тому перед викликом потрібно перевірити їх.
Наступна програма створює хеш повідомлення, генерує токен і перевіряє HMAC-підпис.
const {
createHash,
createHmac,
randomBytes,
timingSafeEqual,
} = require('node:crypto');
const message = 'Замовлення №123';
const secret = randomBytes(32);
// Обчислення хешу повідомлення
const messageHash = createHash('sha256')
.update(message, 'utf8')
.digest('hex');
// Створення випадкового токена
const token = randomBytes(24).toString('base64url');
// Створення HMAC-підпису
const signature = createHmac('sha256', secret)
.update(message, 'utf8')
.digest();
// Перевірка HMAC-підпису
const repeatedSignature = createHmac('sha256', secret)
.update(message, 'utf8')
.digest();
const isValid =
signature.length === repeatedSignature.length &&
timingSafeEqual(signature, repeatedSignature);
console.log('Хеш:', messageHash);
console.log('Токен:', token);
console.log('Підпис коректний:', isValid);Запустіть приклад у файлі crypto-demo.js командою:
node crypto-demo.jsMath.random() для секретів// Неправильно для токенів
const token = Math.random().toString(36);Math.random() не призначений для створення паролів, токенів, солей або ключів.
Для цього використовуйте randomBytes(), randomInt() або randomUUID().
Хешування є одностороннім перетворенням. Хеш не розшифровують. Щоб перевірити значення, обчислюють новий хеш і порівнюють його з відомим результатом.
SHA-256 добре підходить для перевірки цілісності даних, але не для зберігання паролів. Для паролів використовуйте scrypt або інший спеціалізований алгоритм, призначений для повільного виведення ключа.
Однакові байти, представлені як hex, base64 і звичайний текст, є різними рядками. Під час перевірки потрібно використовувати однакове кодування на обох етапах.
timingSafeEqual() для буферів різної довжиниПеред порівнянням перевіряйте довжину буферів. Інакше метод викине виняток.
createHash() обчислює односторонній хеш даних.
digest('hex') і digest('base64url') перетворюють результат на зручний рядок.
Для великих файлів хеш можна обчислювати потоково.
randomBytes() створює криптографічно стійкі випадкові байти.
randomInt() і randomUUID() призначені для безпечних випадкових значень відповідних типів.
Для паролів потрібні спеціальні повільні алгоритми, зокрема scrypt.
createHmac() створює підпис на основі секретного ключа.
Секретні значення слід порівнювати через timingSafeEqual(), перевіривши однакову довжину буферів.