Шифрование файлов на диске

Работа с файловой системой в Node.js в связке с криптографическими алгоритмами чаще всего сводится к задаче преобразования бинарных данных в зашифрованный формат и последующего восстановления исходного содержимого при наличии ключа. Библиотека CryptoJS предоставляет реализацию симметричных алгоритмов шифрования, включая AES, а также инструменты для хеширования и генерации ключей.

Архитектура процесса шифрования файлов

Шифрование файлов на диске строится вокруг последовательности операций:

  • чтение файла в память
  • преобразование данных в формат, пригодный для шифрования
  • генерация ключа шифрования
  • применение алгоритма симметричного шифрования
  • сохранение результата вместе с параметрами, необходимыми для расшифровки

При расшифровке выполняется обратная последовательность операций.

Ключевой особенностью CryptoJS является работа с данными в формате WordArray, поэтому любые бинарные данные должны быть корректно преобразованы.


Установка и подготовка окружения

Для использования CryptoJS в Node.js достаточно установить пакет:

npm install crypto-js

Дополнительно используется встроенный модуль файловой системы:

const fs = require('fs');
const CryptoJS = require('crypto-js');

Представление файловых данных в CryptoJS

Файлы в Node.js представлены Buffer. CryptoJS работает с WordArray, поэтому требуется преобразование:

function bufferToWordArray(buffer) {
    return CryptoJS.lib.WordArray.create(buffer);
}

function wordArrayToBuffer(wordArray) {
    const words = wordArray.words;
    const sigBytes = wordArray.sigBytes;

    const buffer = Buffer.alloc(sigBytes);
    let offset = 0;

    for (let i = 0; i < words.length; i++) {
        const word = words[i];

        buffer[offset++] = (word >> 24) & 0xff;
        buffer[offset++] = (word >> 16) & 0xff;
        buffer[offset++] = (word >> 8) & 0xff;
        buffer[offset++] = word & 0xff;
    }

    return buffer;
}

Генерация ключа на основе пароля

Использование прямого ключа считается небезопасным, поэтому применяется PBKDF2:

function deriveKey(password, salt) {
    return CryptoJS.PBKDF2(password, salt, {
        keySize: 256 / 32,
        iterations: 10000
    });
}

Salt должен быть случайным:

function generateSalt() {
    return CryptoJS.lib.WordArray.random(128 / 8);
}

Алгоритм AES для файлов

AES используется в режиме CBC с случайным IV:

function generateIv() {
    return CryptoJS.lib.WordArray.random(128 / 8);
}

Шифрование выполняется так:

function encryptData(wordArray, key, iv) {
    return CryptoJS.AES.encrypt(wordArray, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
}

Формат хранения зашифрованного файла

Для корректного восстановления данных необходимо сохранять:

  • salt (для восстановления ключа)
  • iv (инициализационный вектор)
  • ciphertext

Удобный формат — объединение всех частей в base64:

function serializeEncrypted(salt, iv, ciphertext) {
    return JSON.stringify({
        salt: CryptoJS.enc.Base64.stringify(salt),
        iv: CryptoJS.enc.Base64.stringify(iv),
        data: ciphertext.toString()
    });
}

Шифрование файла

Полный процесс шифрования:

function encryptFile(inputPath, outputPath, password) {
    const fileBuffer = fs.readFileSync(inputPath);

    const wordArray = bufferToWordArray(fileBuffer);

    const salt = generateSalt();
    const iv = generateIv();

    const key = deriveKey(password, salt);

    const encrypted = encryptData(wordArray, key, iv);

    const payload = serializeEncrypted(salt, iv, encrypted);

    fs.writeFileSync(outputPath, payload, 'utf8');
}

Дешифрование файла

Обратный процесс требует восстановления всех параметров:

function decryptFile(inputPath, outputPath, password) {
    const raw = fs.readFileSync(inputPath, 'utf8');
    const payload = JSON.parse(raw);

    const salt = CryptoJS.enc.Base64.parse(payload.salt);
    const iv = CryptoJS.enc.Base64.parse(payload.iv);
    const ciphertext = payload.data;

    const key = deriveKey(password, salt);

    const decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });

    const buffer = wordArrayToBuffer(decrypted);

    fs.writeFileSync(outputPath, buffer);
}

Обработка больших файлов

CryptoJS не поддерживает потоковое шифрование, поэтому при работе с большими файлами возникает ограничение памяти.

Используются следующие подходы:

  • разбиение файла на блоки фиксированного размера
  • шифрование каждого блока отдельно
  • хранение метаданных для восстановления порядка

Простейшая реализация блочного подхода:

const CHUNK_SIZE = 1024 * 1024; // 1MB

function encryptLargeFile(inputPath, outputPath, password) {
    const buffer = fs.readFileSync(inputPath);
    const salt = generateSalt();
    const iv = generateIv();
    const key = deriveKey(password, salt);

    const chunks = [];

    for (let offset = 0; offset < buffer.length; offset += CHUNK_SIZE) {
        const chunk = buffer.slice(offset, offset + CHUNK_SIZE);
        const wordArray = bufferToWordArray(chunk);

        const encrypted = CryptoJS.AES.encrypt(wordArray, key, {
            iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });

        chunks.push(encrypted.toString());
    }

    const payload = {
        salt: CryptoJS.enc.Base64.stringify(salt),
        iv: CryptoJS.enc.Base64.stringify(iv),
        chunks
    };

    fs.writeFileSync(outputPath, JSON.stringify(payload));
}

Дешифрование блочных данных

function decryptLargeFile(inputPath, outputPath, password) {
    const raw = JSON.parse(fs.readFileSync(inputPath, 'utf8'));

    const salt = CryptoJS.enc.Base64.parse(raw.salt);
    const iv = CryptoJS.enc.Base64.parse(raw.iv);
    const key = deriveKey(password, salt);

    const buffers = [];

    for (const chunk of raw.chunks) {
        const decrypted = CryptoJS.AES.decrypt(chunk, key, {
            iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });

        buffers.push(wordArrayToBuffer(decrypted));
    }

    fs.writeFileSync(outputPath, Buffer.concat(buffers));
}

Управление безопасностью ключей

Критически важные моменты при работе с файловым шифрованием:

  • пароль не должен храниться в коде
  • salt должен быть уникальным для каждого файла
  • IV не должен повторяться при одинаковом ключе
  • количество итераций PBKDF2 должно быть достаточным для замедления brute-force атак

Типичные ошибки реализации

Часто возникающие проблемы:

  • потеря IV приводит к невозможности расшифровки
  • некорректное преобразование Buffer → WordArray
  • использование одинакового salt для разных файлов
  • попытка шифрования потоковых данных без буферизации
  • несоответствие кодировок при сериализации

Полный пример использования

encryptFile('input.txt', 'encrypted.json', 'strong-password');

decryptFile('encrypted.json', 'output.txt', 'strong-password');

Ограничения CryptoJS в контексте файлового шифрования

CryptoJS реализован как библиотека для работы в памяти, поэтому:

  • отсутствует нативная потоковая модель
  • большие файлы требуют дополнительной буферизации
  • производительность ниже, чем у нативного Node.js crypto
  • увеличенное потребление памяти при обработке больших объектов

При необходимости обработки больших объемов данных рациональнее использовать модуль crypto из Node.js, однако CryptoJS остается удобным в задачах совместимости и учебных сценариях.