Шифрование данных с использованием пароля пользователя в Web Crypto API строится на связке двух криптографических примитивов:
Пароль напрямую не используется как ключ шифрования. Он преобразуется в криптографический ключ через устойчивую функцию вывода ключа, которая включает соль и большое число итераций.
Типовая схема:
Web Crypto API доступна через:
crypto.subtle
Основные используемые методы:
deriveKey — получение ключа из пароляencrypt — шифрованиеdecrypt — расшифрованиеgenerateKey — генерация ключей AESimportKey — импорт сырого пароля как материалаWeb Crypto работает с ArrayBuffer, поэтому текст
необходимо преобразовывать:
const enc = new TextEncoder();
const dec = new TextDecoder();
const data = enc.encode("секретное сообщение");
Соль и вектор инициализации должны быть случайными:
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
PBKDF2 применяется через deriveKey.
async function deriveKey(password, salt) {
const baseKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-256"
},
baseKey,
{
name: "AES-GCM",
length: 256
},
false,
["encrypt", "decrypt"]
);
}
Ключевые параметры:
async function encryptData(password, plaintext) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const encoded = new TextEncoder().encode(plaintext);
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
return {
ciphertext: new Uint8Array(ciphertext),
salt,
iv
};
}
Для хранения обычно используется сериализация:
function toBase64(buffer) {
return btoa(String.fromCharCode(...buffer));
}
function fromBase64(str) {
return Uint8Array.from(atob(str), c => c.charCodeAt(0));
}
Структура объекта:
{
"ciphertext": "...base64...",
"salt": "...base64...",
"iv": "...base64..."
}
Процесс обратный:
async function decryptData(password, payload) {
const salt = payload.salt;
const iv = payload.iv;
const key = await deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
key,
payload.ciphertext
);
return new TextDecoder().decode(decrypted);
}
const password = "strong-password";
const message = "конфиденциальные данные";
const encrypted = await encryptData(password, message);
const decrypted = await decryptData(password, encrypted);
AES-GCM выполняет одновременно:
Это означает, что при изменении зашифрованного текста расшифрование завершится ошибкой.
Использование одинакового IV с одним ключом критически нарушает безопасность AES-GCM.
Без соли пароль становится уязвим к предвычисленным атакам.
Низкие значения (например, <10 000) позволяют ускорить перебор пароля.
Пароль должен использоваться только для вывода ключа и не сохраняться.
PBKDF2 намеренно медленный. При 100 000 итераций:
Практическая структура данных:
salt — 16 байтiv — 12 байтciphertext — переменной длиныРекомендуемое объединение:
{
v: 1,
alg: "AES-GCM",
kdf: "PBKDF2",
salt: "...",
iv: "...",
data: "..."
}
const record = {
ciphertext: encrypted.ciphertext,
salt: encrypted.salt,
iv: encrypted.iv
};
Далее сохраняется как бинарный Blob или base64-строки.
AES-GCM не различает:
Обе ситуации приводят к исключению при decrypt.
Комбинация PBKDF2 + AES-GCM в Web Crypto API формирует стандартный подход клиентского шифрования, где безопасность определяется: