Переход от Node.js crypto к Web Crypto API
KODВведение
Web Crypto API - это новый инструмент Javascript для криптографии. Он совместим с современными браузерами и ведущими платформами, включая Cloudflare Workers и Vercel, а также Node.js. Такая взаимозаменяемость подразумевает мечту разработчика - напишите свой криптографический код один раз и выполните его на множестве платформ с использованием Web Crypto API.
Однако у Node.js уже есть модуль "crypto" с долгой историей, оставляющий значительное количество устаревшего кода, нуждающегося в миграции. В этой статье мы подробно рассмотрим опыт перехода, предоставляя всестороннее руководство, сосредоточенное на 3 общих сценариях. Мы стремимся осветить путь к успешной миграции.
Что такое Web Crypto API
Web Crypto API, открытый стандарт W3C для JavaScript, - это коллекция стандартизированных криптографических примитивов, определенных в спецификации Web Cryptography API. Он был создан после того, как несколько браузеров и платформ начали добавлять свои собственные несовместимые криптографические функции.
API предлагает примитивы для генерации ключей, шифрования и дешифрования, цифровых подписей, ключевого и битового вывода и криптографического дайджеста. Он основывается на интерфейсе, называемом SubtleCrypto, вы можете найти больше деталей и руководств в документации Mozilla MDN.
Но у Node.js уже есть модуль крипто?
Разработчики Node.js обычно знакомы с крипто модулем. Он предлагает полный набор криптографических примитивов. Этот модуль не только предоставляет механизмы для тех же криптографических операций, определенных в Web Crypto API, но часто включает более широкий диапазон криптографических алгоритмов.
Так зачем нам все еще нужен Web Crypto API? Поскольку Javascript становится популярным на многих платформах и средах, от клиентской стороны до серверов и особенно на периферии, важно иметь кросс-платформенный криптографический инструмент для упрощения процессов.
Кроме того, функции в стандарте Web Crypto API все возвращают обещания и поддерживают синтаксис async / await. Это значительное преимущество перед модулем crypto, который синхронен и может блокировать цикл событий.
А есть интересная вещь: Node.js добавляет его поддержку для Web Crypto API, что означает, что в большинстве случаев Web Crypto API идеален для всех известных платформ.
Использование Web Crypto API
На большинстве платформ коллекция API Web Crypto доступна через глобальный объект crypto, который включает 3 основных утилиты: getRandomValues, randomUUID и subtle.
Есть много отличий по сравнению с традиционным модулем крипто. Они могут быть суммированы в 3 наиболее распространенных части. Давайте пройдемся по ним и посмотрим, как перейти от существующего кода.
#1 Генерация случайных значений
В крипто модуле вы можете генерировать случайные значения, вызывая randomBytes
import { randomBytes } from 'crypto';
const generateRandomString = (length = 64) => randomBytes(length).toString('hex');
В Web Crypto есть похожая функция под названием getRandomValues, но возвращаемое значение - это ArrayBuffer, поэтому нам нужен еще один шаг для его преобразования в строку.
const generateRandomString = (length = 64) => {
const array = new Uint8Array(10);
crypto.getRandomValues(array);
return Array.from(array)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
#2 Хеширование (или дайджест)
createHash легко использовать в крипто модуле:
export const sha256 = (text: string): string => {
return createHash('sha256').update(text).digest('hex');
};
В Web Crypto мы можем использовать subtle.createHash
export const sha256 = async (text: string): Promise<string> => {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
Как видите, также требуется преобразование из ArrayBuffer в строку hex.
#3 Шифрование и дешифрование
В крипто модуле AES-шифрование и дешифрование можно реализовать так:
export const encrypt = (text: string, password: string) => {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', password, iv);
const encrypted = cipher.update(text, 'utf8', 'base64');
return {
ciphertext: `${encrypted}${cipher.final('base64')}`,
iv: iv.toString('base64'),
};
};
export const decrypt = (ciphertext: string, iv: string, password: string) => {
const decipher = createDecipheriv('aes-256-gcm', password, iv);
const decrypted = decipher.update(encrypted, 'base64', 'utf8');
return `${decrypted}${decipher.final('utf8')}`;
};
В Web Crypto нам нужно сначала создать ключ с помощью importKey:
async function encrypt(text: string, password: string) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encodedPlaintext = new TextEncoder().encode(text);
const secretKey = await crypto.subtle.importKey(
'raw',
Buffer.from(await getKeyFromPassword(password, crypto), 'hex'),
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt', 'decrypt']
);
const ciphertext = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
secretKey,
encodedPlaintext
);
return {
ciphertext: Buffer.from(ciphertext).toString('base64'),
iv: Buffer.from(iv).toString('base64'),
};
}
async function decrypt(ciphertext: string, iv: string, password: string) {
const secretKey = await crypto.subtle.importKey(
'raw',
Buffer.from(await getKeyFromPassword(password, crypto), 'hex'),
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt', 'decrypt']
);
const cleartext = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: Buffer.from(iv, 'base64'),
},
secretKey,
Buffer.from(ciphertext, 'base64')
);
return new TextDecoder().decode(cleartext);
}
Вывод
Как вы можете видеть, миграция не сложна, главное - изменить синтаксис, чтобы соответствовать новому API и обработать ArrayBuffer с помощью TextEncoder.
Как продукт идентификации, Logto использует криптографию во многих местах. Мы перешли от модуля крипто к Web Crypto API. Этот переход позволяет нам лучше адаптироваться к окружениям на периферии и делает возможным безопасное выполнение кода SDK в браузере.