SubtleCrypto (Web Crypto API) предоставляет нативный криптографический слой браузера, реализованный на уровне платформы и значительно более быстрый и безопасный по сравнению с чисто JavaScript-реализациями. SJCL (Stanford JavaScript Crypto Library), в свою очередь, представляет собой полностью JS-библиотеку, работающую поверх стандартных типов JavaScript и использующую собственное представление бинарных данных.
Совместное использование этих двух систем требует понимания ключевого
различия: SubtleCrypto работает с
ArrayBuffer/TypedArray, тогда как SJCL использует
собственный тип sjcl.bitArray.
Основной тип данных:
ArrayBufferUint8ArrayCryptoKeyПример:
const enc = new TextEncoder();
const data = enc.encode("hello"); // Uint8Array
Основной тип данных:
sjcl.bitArrayПример:
const bits = sjcl.codec.utf8String.toBits("hello");
Любая интеграция требует преобразования:
Без этого невозможно смешивать:
function arrayBufferToBitArray(buffer) {
const uint8 = new Uint8Array(buffer);
const words = [];
for (let i = 0; i < uint8.length; i += 4) {
words.push(
(uint8[i] << 24) |
(uint8[i + 1] << 16) |
(uint8[i + 2] << 8) |
(uint8[i + 3] || 0)
);
}
return sjcl.codec.hex.toBits(
Array.from(uint8)
.map(b => b.toString(16).padStart(2, "0"))
.join("")
);
}
Более корректный вариант через hex-кодек SJCL:
function arrayBufferToBitArray(buffer) {
const bytes = new Uint8Array(buffer);
let hex = "";
for (let i = 0; i < bytes.length; i++) {
hex += bytes[i].toString(16).padStart(2, "0");
}
return sjcl.codec.hex.toBits(hex);
}
function bitArrayToArrayBuffer(bitArray) {
const hex = sjcl.codec.hex.fromBits(bitArray);
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes.buffer;
}
async function encryptWithSubtle(key, plaintext) {
const enc = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
enc.encode(plaintext)
);
return { ciphertext, iv };
}
function convertEncryptedToSJCL(ciphertext) {
return arrayBufferToBitArray(ciphertext);
}
SJCL-данные могут быть использованы в SubtleCrypto только после конвертации:
async function importSJCLKeyForSubtle(sjclKeyBits) {
const raw = bitArrayToArrayBuffer(sjclKeyBits);
return crypto.subtle.importKey(
"raw",
raw,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
}
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: enc.encode(salt),
iterations: 100000,
hash: "SHA-256"
},
baseKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
const keyBits = sjcl.misc.pbkdf2("password", "salt", 100000, 256);
SubtleCrypto → SJCL:
const keyBuffer = await crypto.subtle.exportKey("raw", cryptoKey);
const sjclKey = arrayBufferToBitArray(keyBuffer);
SJCL → SubtleCrypto:
const buffer = bitArrayToArrayBuffer(sjclKey);
const cryptoKey = await crypto.subtle.importKey(
"raw",
buffer,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
SubtleCrypto всегда использует бинарные буферы, тогда как SJCL ожидает bitArray.
Ошибка:
sjcl.encrypt(key, arrayBuffer); // некорректно
SubtleCrypto требует явного хранения IV:
{ ciphertext, iv }
SJCL часто инкапсулирует IV внутри структуры.
SJCL AES ≠ SubtleCrypto AES-GCM:
const CryptoBridge = {
toSJCL(buffer) {
return arrayBufferToBitArray(buffer);
},
toSubtle(bits) {
return bitArrayToArrayBuffer(bits);
}
};
async function hybridEncrypt(cryptoKey, message) {
const enc = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
cryptoKey,
enc.encode(message)
);
return {
iv: CryptoBridge.toSJCL(iv.buffer),
data: CryptoBridge.toSJCL(encrypted)
};
}
SubtleCrypto выполняет операции:
SJCL:
Оптимальная стратегия:
Такая архитектура позволяет использовать преимущества обеих систем без потери совместимости и без нарушения криптографической корректности.