Документация Engee
Notebook

Шифрование и расшифровка файлов с произвольными данными

Введение

Современный обмен данными часто происходит по открытым каналам: электронная почта, мессенджеры, облачные хранилища, публичные сети Wi-Fi. В этих условиях любой файл – документ, изображение, архив - по пути к получателю может быть перехвачен, скопирован или изменён. Задача защиты сводится к двум независимым требованиям: сохранить конфиденциальность и обеспечить целостность.

В данной работе представлена компактная схема шифрования файла произвольного типа: содержимое вместе с именем файла шифруется симметричным алгоритмом AES-256 в режиме CBC, а подлинность упакованного контейнера подтверждается кодом аутентификации HMAC-SHA256. В данной схеме расшифровка выполняется только после успешной проверки тега. На приёмной стороне восстанавливается исходный файл, а при совпадении имён подбирается свободное имя с индексом.

Присоединение библиотек

В данной работе используются две библиотеки:

  • Nettle – реализация AES, режима CBC, HMAC-SHA256 и вспомогательных операций дополнения блоков;

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

In [ ]:
for lib in ["Nettle", "Random"]
    try
        eval(Meta.parse("using $lib"))
    catch ex
        import Pkg
        Pkg.add(lib)
        eval(Meta.parse("using $lib"))
    end
end

Функция шифрования

Функция enc_file(fnam, pswd) принимает имя исходного файла в текущей директории и пароль, создаёт бинарный контейнер данных и возвращает его имя.

Имя файла переводится в байты и упаковывается в буфер фиксированной длины (256 байт), свободное место дополняется нулями. Буфер дополняется содержимым файла, таким образом получается единая полезная нагрузка. Это позволяет при расшифровке однозначно понять, где заканчивается имя и начинается содержимое: граница задана самой структурой.

Затем генерируются соль и вектор инициализации. Из пароля и соли выводятся два независимых ключа: 256-битный ключ AES и отдельный ключ для HMAC. Полезная нагрузка дополняется по схеме PKCS#5 и шифруется алгоритмом AES-256 в режиме CBC. По телу контейнера (соль, вектор, шифротекст) вычисляется тег HMAC-SHA256, доказывающий, что содержимое не изменено. Тело контейнера и тег записываются в .bin-файл, имя которого подбирается с учётом уже существующих (при их наличии в текущей директории), таким образом, прежние контейнеры не перезаписываются.

In [ ]:
function enc_file(fnam, pswd)
    size = 256   
    namb = Vector{UInt8}(basename(fnam))
    if length(namb) > size
        error("Имя файла слишком длинное: ", length(namb), " байт (максимум ", size, ")")
    end
    nbuf = zeros(UInt8, size)
    nbuf[1:length(namb)] .= namb
    salt = rand(UInt8, 8)
    iv   = rand(UInt8, 16)
    (key, _) = gen_key32_iv16(Vector{UInt8}(pswd), salt)
    hmac = HMACState("sha256", salt)
    Nettle.update!(hmac, Vector{UInt8}(pswd))
    body = vcat(salt, iv, encrypt(Encryptor("AES256", key), :CBC, iv, add_padding_PKCS5(vcat(nbuf, read(fnam)), 16)))
    hmac = HMACState("sha256", Nettle.digest!(hmac))
    Nettle.update!(hmac, body)
    base, _ = splitext(fnam)
    cand = string(base, ".bin")
    indx = 1
    while isfile(joinpath(pwd(), cand))
        cand = string(base, "(", indx, ").bin")
        indx += 1
    end

    write(cand, vcat(body, Nettle.digest!(hmac)))
    return cand
end
Out[0]:
enc_file (generic function with 1 method)

Функция расшифровки

Функция dec_file(binf, pswd) принимает имя .bin-контейнера и пароль, восстанавливает исходный файл и возвращает имя восстановленного файла.

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

Далее идёт проверка целостности: по телу контейнера заново вычисляется HMAC-тег и сравнивается с переданным. При несовпадении функция немедленно выбрасывает ошибку, и данные не расшифровываются. Такой порядок закрывает класс атак на режим CBC, при которых атакующий пытается расшифровать сообщение, наблюдая за реакцией получателя на повреждённые пакеты.

При успешной проверке снимается дополнение PKCS#5 и расшифровывается полезная нагрузка. Первые 256 байт – буфер имени, остальное – содержимое файла. Нулевое дополнение имени отсекается, имя восстанавливается, а при коллизии подбирается свободное имя с индексом. Файл записывается в текущую директорию, его имя возвращается как результат.

In [ ]:
function dec_file(binf, pswd)
    size = 256
    allb = read(binf)
    body = allb[1:end-32]
    salt = body[1:8]
    (key, _) = gen_key32_iv16(Vector{UInt8}(pswd), salt)
    hmac = HMACState("sha256", salt)
    Nettle.update!(hmac, Vector{UInt8}(pswd))
    hmac = HMACState("sha256", Nettle.digest!(hmac))
    Nettle.update!(hmac, body)
    if Nettle.digest!(hmac) != allb[end-31:end]
        error("ОШИБКА: Данные повреждены или подделаны.")
    end
    payl = trim_padding_PKCS5(decrypt(Decryptor("AES256", key), :CBC, body[9:24], body[25:end]))
    if length(payl) < size
        error("ОШИБКА: Контейнер повреждён.")
    end
    bufr = payl[1:size]
    lend = findlast(!iszero, bufr)
    if lend === nothing
        error("ОШИБКА: Контейнер повреждён.")
    end
    fnam = String(bufr[1:lend])
    base, ext = splitext(fnam)
    cand = fnam
    indx = 1
    while isfile(joinpath(pwd(), cand))
        cand = string(base, "(", indx, ")", ext)
        indx += 1
    end
    write(cand, payl[size+1:end])
    return cand
end
Out[0]:
dec_file (generic function with 1 method)

Шифрование

Для шифрования выберем файл Данные.pdf, содержащий данный пример в формате pdf. Зададим имя файла, пароль, и обратимся к функции шифрования.

In [ ]:
имя_файла = "Данные.pdf"
пароль = "qwerty12345678";
enc_file(имя_файла, пароль)
Out[0]:
"Данные.bin"

В результате шифрования создан бинарный файл Данные.bin.

Передача данных

Удалим исходный файл Данные.pdf; удалим переменные, содержащие его имя и пароль для функции шифрования. Представим, что зашифрованный файл передан получателю по открытому каналу связи.

In [ ]:
!isnothing(имя_файла) && isfile(имя_файла) && rm(имя_файла)
имя_файла = nothing
пароль = nothing

Расшифровка

Представим, что теперь мы находимся на стороне получателя. Мы приняли зашифрованный файл Данные.bin, нам заранее известен пароль. Зададим имя файла и пароль и обратимся к функции расшифровки.

In [ ]:
имя_файла = "Данные.bin"
пароль = "qwerty12345678";
dec_file(имя_файла, пароль)
Out[0]:
"Данные.pdf"

В результате расшифровки, исходный файл Данные.pdfвосстановлен.

Заключение

В данном примере реализована полная схема защищённой передачи файла произвольного типа по открытому каналу передачи данных. Симметричное шифрование AES-256-CBC обеспечивает конфиденциальность содержимого и имени файла. Схема encrypt-then-MAC с проверкой тега до расшифровки исключает атаки, которым подвержен режим CBC, и гарантирует, что ни один изменённый байт не пройдёт незамеченным. Автоматический подбор имён при коллизии делает восстановление безопасным и предсказуемым.

Такие схемы лежат в основе множества реальных систем. Протокол TLS, обеспечивающий работу HTTPS, применяет аналогичный подход. Защищённые мессенджеры шифруют каждое сообщение отдельным ключом с обязательной аутентификацией. Системы шифрования дисков (LUKS, BitLocker, FileVault) используют блочные шифры с проверкой целостности секторов. Защищённая электронная почта, VPN-туннели, системы электронного документооборота и резервного копирования – всё это опирается на те же принципы.

Практическая ценность данной работы также заключается в том, что весь код доступен для проверки: видно, какие именно алгоритмы используются, как формируются ключи, как проверяется целостность и что происходит с данными на каждом шаге. Когда применение сторонних приложений для шифрования по каким-либо причинам нежелательно, например: из-за закрытого исходного кода, неясной модели угроз, избыточного функционала или требований к среде исполнения – скрипт может применяться напрямую либо служить основой для собственного решения.