Сериализация данных в криптографических библиотеках определяет способ
преобразования структурированных объектов в последовательности байтов,
пригодные для шифрования, передачи и последующего восстановления. В
CryptoJS этот процесс тесно связан с типом WordArray,
кодировками и механизмом Formatter, который управляет тем,
как зашифрованные данные представляются в виде строки и обратно
преобразуются в структуру, пригодную для дешифрования.
Внутренним представлением бинарных данных в библиотеке выступает
CryptoJS.lib.WordArray. Этот формат хранит данные в виде
массива 32-битных слов, что позволяет эффективно выполнять
криптографические операции.
При сериализации происходит переход между:
WordArray ↔︎ строковые представления (Base64, Hex,
Latin1, Utf8)Ключевая особенность заключается в том, что сериализация не ограничивается кодировкой. Она включает также упаковку метаданных: соль, IV, алгоритм, параметры режима.
CryptoJS предоставляет несколько базовых кодировок:
CryptoJS.enc.HexCryptoJS.enc.Base64CryptoJS.enc.Utf8CryptoJS.enc.Latin1Каждая из них реализует два метода:
stringify(wordArray)parse(string)Пример преобразования:
const wordArray = CryptoJS.enc.Utf8.parse("test data");
const base64 = CryptoJS.enc.Base64.stringify(wordArray);
const restored = CryptoJS.enc.Base64.parse(base64);
Ограничение такого подхода заключается в отсутствии структурной информации. Кодировки работают только с «сырыми» байтами, не сохраняя контекст криптографических параметров.
При шифровании данные оборачиваются в объект
CipherParams, содержащий:
ciphertext — зашифрованные данные (WordArray)key — ключ (опционально)iv — вектор инициализацииsalt — сольalgorithm — алгоритм шифрованияmode и padding — параметры режимаПрямое преобразование этого объекта в строку невозможно без форматера, поскольку требуется определить структуру итогового представления.
Formatter определяет два ключевых метода:
stringify(cipherParams) — преобразование в строкуparse(str) — восстановление объекта
CipherParamsСтандартный форматер OpenSSL реализует схему:
Salted__ + salt + ciphertext (Base64)
Пример реализации:
const OpenSSLFormatter = {
stringify(cipherParams) {
const wordArray = cipherParams.ciphertext;
const salt = cipherParams.salt;
if (salt) {
const salted = CryptoJS.lib.WordArray.create(
[0x53616c74, 0x65645f5f] // "Salted__"
).concat(salt).concat(wordArray);
return CryptoJS.enc.Base64.stringify(salted);
}
return CryptoJS.enc.Base64.stringify(wordArray);
},
parse(str) {
const words = CryptoJS.enc.Base64.parse(str);
const sig = CryptoJS.lib.WordArray.create(words.words.slice(0, 2));
const salted = CryptoJS.enc.Utf8.stringify(sig);
let cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: words
});
if (salted.startsWith("Salted__")) {
cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.lib.WordArray.create(words.words.slice(2)),
salt: CryptoJS.lib.WordArray.create(words.words.slice(2, 4))
});
}
return cipherParams;
}
};
Создание собственного формата требуется при необходимости:
Минимально расширяемый формат должен включать:
Пример бинарной структуры:
[MAGIC][VERSION][IV_LEN][IV][SALT_LEN][SALT][DATA_LEN][DATA][HMAC]
Работа с WordArray позволяет собирать собственные
структуры без промежуточного JSON.
const CustomFormatter = {
stringify(cipherParams) {
const iv = cipherParams.iv;
const salt = cipherParams.salt;
const ciphertext = cipherParams.ciphertext;
const magic = CryptoJS.enc.Utf8.parse("CSTM");
const version = CryptoJS.enc.Utf8.parse("\x01");
const ivLen = CryptoJS.enc.Utf8.parse(String.fromCharCode(iv.sigBytes));
const saltLen = salt ? CryptoJS.enc.Utf8.parse(String.fromCharCode(salt.sigBytes)) : CryptoJS.enc.Utf8.parse("\x00");
const header = magic
.concat(version)
.concat(iv)
.concat(salt || CryptoJS.lib.WordArray.create());
const payload = header.concat(ciphertext);
const base64 = CryptoJS.enc.Base64.stringify(payload);
return base64;
}
};
Обратный процесс требует строгого контроля структуры:
const CustomFormatter = {
parse(str) {
const data = CryptoJS.enc.Base64.parse(str);
const words = data.words;
const magic = CryptoJS.lib.WordArray.create(words.slice(0, 1));
const version = CryptoJS.lib.WordArray.create(words.slice(1, 2));
const iv = CryptoJS.lib.WordArray.create(words.slice(2, 6));
const ciphertext = CryptoJS.lib.WordArray.create(words.slice(6));
return CryptoJS.lib.CipherParams.create({
ciphertext,
iv
});
}
};
При использовании PBKDF2 или EvpKDF соль
становится обязательной частью формата.
const key = CryptoJS.PBKDF2(password, salt, {
keySize: 256 / 32
});
Сериализация должна сохранять:
Для защиты от модификации данных формат расширяется:
const hmac = CryptoJS.HmacSHA256(ciphertext, key);
И включается в сериализацию:
DATA || HMAC
При десериализации происходит проверка:
const recalculated = CryptoJS.HmacSHA256(ciphertext, key);
if (!CryptoJS.enc.Hex.stringify(recalculated).equals(hmac)) {
throw new Error("Invalid integrity check");
}
Версия формата должна быть первым изменяемым параметром, так как позволяет:
Пример:
const FORMAT_VERSION = 2;
При парсинге:
if (version !== expectedVersion) {
throw new Error("Unsupported format version");
}
Для уменьшения размера применяются:
Пример упаковки флагов:
bit 0 — наличие salt
bit 1 — наличие IV
bit 2 — наличие HMAC
На практике часто возникают критические проблемы:
Собственный формат подключается через параметр
format:
const encrypted = CryptoJS.AES.encrypt("data", key, {
format: CustomFormatter
});
И обратное восстановление:
const decrypted = CryptoJS.AES.decrypt(encrypted.toString(), key, {
format: CustomFormatter
});
При проектировании формата критично учитывать:
Собственные форматы сериализации применяются при:
В таких сценариях формат становится частью протокола, а не просто способом представления данных.