Введение: Почему не VeraCrypt?

Всё началось с простой задачи: нужно было безопасно передавать файлы на обычных USB-флешках. Существующие решения либо создавали контейнеры (VeraCrypt), что неудобно для быстрого доступа к отдельным файлам на разных ОС, либо работали слишком сложно для конечного пользователя.

Мне нужно было решение уровня «вставил флешку -> ввел пароль -> файлы зашифрованы». Но главное требование — безопасность данных даже при сбое питания. Если выдернуть флешку посередине шифрования, данные не должны превратиться в кашу.

Так появился crypto_engine. Это не попытка изобрести свою криптографию (мы используем стандартные AES-GCM и ChaCha20), а инженерная работа над тем, как безопасно управлять ключами в памяти, обрабатывать гигабайтные файлы без переполнения RAM и гарантировать целостность данных.

1. Проблема памяти в Python и класс SecureBytes

Самая большая уязвимость криптографических утилит на управляемых языках (Python, Java) — это работа с памятью. Когда вы храните пароль или ключ в переменной bytes, сборщик мусора (GC) может скопировать эти данные в другое место памяти при сборке мусора, оставив исходные копии «висеть» в RAM до неопределенного времени.

В моем движке я реализовал класс SecureBytes, который решает эту проблему:

class SecureBytes:
    def __init__(self, data: Union[bytes, bytearray, int]):
        if isinstance(data, int):
            self._buffer = bytearray(data)
        else:
            self._buffer = bytearray(data)
        self._finalized = False
        # Регистрируем слабый финализатор
        self._weak_ref = weakref.ref(self, self._cleanup_callback)

    def wipe(self, passes: int = 3):
        if self._finalized or len(self._buffer) == 0:
            return
        # Проход 1: случайные данные
        self._buffer[:] = secrets.token_bytes(len(self._buffer))
        # Проход 2: нули
        self._buffer[:] = b'\x00' * len(self._buffer)
        self._finalized = True
        gc.collect()

    def __del__(self):
        if not self._finalized:
            self.wipe()

Что здесь важно:

  1. Использование bytearray: В отличие от неизменяемых bytes, bytearray позволяет перезаписывать данные по тому же адресу памяти.

  2. Многопроходная очистка: Перед освобождением памяти буфер перезаписывается случайными данными, затем нулями (согласно рекомендациям NIST SP 800-88).

  3. Контекстный менеджер: Ключи используются только внутри блока with secure_key(...):, что гарантирует очистку даже при возникновении исключений.

2. Потоковое шифрование больших файлов

Шифрование файла размером 10 ГБ на флешке с 4 ГБ оперативной памяти — нетривиальная задача. Загружать файл целиком в RAM нельзя.

Я реализовал MemorySensitiveReader, который автоматически переключается между режимами работы в зависимости от размера файла и доступной памяти:

class MemorySensitiveReader:
    def __init__(self, file_path: str, memory_threshold: int = 100 * 1024 * 1024):
        self.file_size = os.path.getsize(file_path)
        # Порог переключения на потоковый режим
        self.use_streaming = self.file_size > memory_threshold 

    def iter_chunks(self, chunk_size: int = 8192):
        # Чтение и шифрование блоками
        ...

Проблема Nonce при потоковом шифровании:
В режимах AES-GCM и ChaCha20 нельзя использовать один и тот же nonce (номер однократного использования) для разных блоков с одним ключом. Это критическая уязвимость. Решение в моем коде — деривация уникального nonce для каждого блока на основе базового nonce и индекса блока:

def _derive_block_nonce_12bit(base_nonce: bytes, block_index: int) -> bytes:
    # Первые 8 байт — префикс, последние 4 — счётчик блока
    prefix = base_nonce[:8]
    block_counter = block_index.to_bytes(4, byteorder='big')
    return prefix + block_counter

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

3. Отказоустойчивость: что если выдернуть флешку?

Самый страшный сценарий для пользователя — потеря данных из-за сбоя во время шифрования. Стандартный подход «зашифровать -> удалить оригинал» здесь не работает.

Я внедрил систему блокировок и отката (rollback):

  1. Lock-файл (.encryption_lock.json): Перед началом операции создается файл, где записывается статус in_progress и список уже обработанных файлов.

  2. Временные файлы: Шифрование идет в .tmp файл. Только после успешной проверки целостности оригинал удаляется, а временный файл переименовывается.

  3. Проверка целостности: Перед удалением оригинала я расшифровываю блок данных обратно и сравниваю HMAC и SHA-256 хеши. Если не совпадает — оригинал не трогается.

  4. Восстановление: Если процесс прервался, утилита видит lock-файл и предлагает откатить операцию (rollback_operation), расшифровав уже обработанные файлы обратно.

4. Алгоритмы и производительность

Движок поддерживает три алгоритма:

  • AES-256-GCM: Стандарт индустрии, аппаратное ускорение на большинстве CPU.

  • ChaCha20-Poly1305: Быстрее на устройствах без AES-NI (например, некоторые ARM-процессоры).

  • XChaCha20-Poly1305: Увеличенный nonce (24 байта), что снижает риск коллизий при очень больших объемах данных.

Для ускорения работы с множеством мелких файлов реализована параллельная обработка через ThreadPoolExecutor. Однако из-за GIL в Python прирост заметен скорее на операциях I/O, чем на чистом шифровании.

5. Интерфейс и использование

Хотя ядро написано на Python, для пользователей доступен GUI, чтобы не запускать скрипты через консоль.

6. Ограничения и Threat Model

Важно понимать, для чего этот инструмент подходит, а для чего — нет.

  1. Метаданные не скрыты: Имена файлов и структура папок сохраняются в .usb_crypt_meta.json. Злоумышленник с доступом к флешке увидит список файлов, но не сможет их открыть. Скрыть имена файлов без создания контейнера технически сложно и неудобно для навигации.

  2. Защита от физической потери: Инструмент защищает данные, если вы потеряли флешку. Он не защищает от кейлоггеров на компьютере, где вы вводите пароль.

  3. Парольная политика: Встроенная валидация требует минимум 12 символов, цифры и спецсимволы. Слабые пароли блокируются на уровне кода.

Заключение

Написание собственного крипто-движка — это всегда баланс между безопасностью и удобством. Я постарался сделать акцент на безопасности управления памятью (что редко встречается в Python-скриптах) и отказоустойчивости операций.

Проект открыт, код доступен для аудита. Если вы найдете уязвимости или способы оптимизировать SecureBytes — добро пожаловать в Issues.

GitHub: https://github.com/slimeopus/SparkLock/releases