Шифрование файлов
Шифрование и расшифровка файлов с произвольными данными
Введение
Современный обмен данными часто происходит по открытым каналам: электронная почта, мессенджеры, облачные хранилища, публичные сети Wi-Fi. В этих условиях любой файл – документ, изображение, архив - по пути к получателю может быть перехвачен, скопирован или изменён. Задача защиты сводится к двум независимым требованиям: сохранить конфиденциальность и обеспечить целостность.
В данной работе представлена компактная схема шифрования файла произвольного типа: содержимое вместе с именем файла шифруется симметричным алгоритмом AES-256 в режиме CBC, а подлинность упакованного контейнера подтверждается кодом аутентификации HMAC-SHA256. В данной схеме расшифровка выполняется только после успешной проверки тега. На приёмной стороне восстанавливается исходный файл, а при совпадении имён подбирается свободное имя с индексом.
Присоединение библиотек
В данной работе используются две библиотеки:
-
Nettle – реализация AES, режима CBC, HMAC-SHA256 и вспомогательных операций дополнения блоков;
-
Random – генератор криптографически случайных значений, используемый для соли и вектора инициализации.
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-файл, имя которого подбирается с учётом уже существующих (при их наличии в текущей директории), таким образом, прежние контейнеры не перезаписываются.
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
Функция расшифровки
Функция dec_file(binf, pswd) принимает имя .bin-контейнера и пароль, восстанавливает исходный файл и возвращает имя восстановленного файла.
Контейнер разделяется на тело и тег. Из тела контейнера извлекаются соль, вектор инициализации и шифротекст. По соли и паролю заново формируются оба ключа, при верном пароле они совпадут с использованными при шифровании.
Далее идёт проверка целостности: по телу контейнера заново вычисляется HMAC-тег и сравнивается с переданным. При несовпадении функция немедленно выбрасывает ошибку, и данные не расшифровываются. Такой порядок закрывает класс атак на режим CBC, при которых атакующий пытается расшифровать сообщение, наблюдая за реакцией получателя на повреждённые пакеты.
При успешной проверке снимается дополнение PKCS#5 и расшифровывается полезная нагрузка. Первые 256 байт – буфер имени, остальное – содержимое файла. Нулевое дополнение имени отсекается, имя восстанавливается, а при коллизии подбирается свободное имя с индексом. Файл записывается в текущую директорию, его имя возвращается как результат.
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
Шифрование
Для шифрования выберем файл Данные.pdf, содержащий данный пример в формате pdf. Зададим имя файла, пароль, и обратимся к функции шифрования.
имя_файла = "Данные.pdf"
пароль = "qwerty12345678";
enc_file(имя_файла, пароль)
В результате шифрования создан бинарный файл Данные.bin.
Передача данных
Удалим исходный файл Данные.pdf; удалим переменные, содержащие его имя и пароль для функции шифрования. Представим, что зашифрованный файл передан получателю по открытому каналу связи.
!isnothing(имя_файла) && isfile(имя_файла) && rm(имя_файла)
имя_файла = nothing
пароль = nothing
Расшифровка
Представим, что теперь мы находимся на стороне получателя. Мы приняли зашифрованный файл Данные.bin, нам заранее известен пароль. Зададим имя файла и пароль и обратимся к функции расшифровки.
имя_файла = "Данные.bin"
пароль = "qwerty12345678";
dec_file(имя_файла, пароль)
В результате расшифровки, исходный файл Данные.pdfвосстановлен.
Заключение
В данном примере реализована полная схема защищённой передачи файла произвольного типа по открытому каналу передачи данных. Симметричное шифрование AES-256-CBC обеспечивает конфиденциальность содержимого и имени файла. Схема encrypt-then-MAC с проверкой тега до расшифровки исключает атаки, которым подвержен режим CBC, и гарантирует, что ни один изменённый байт не пройдёт незамеченным. Автоматический подбор имён при коллизии делает восстановление безопасным и предсказуемым.
Такие схемы лежат в основе множества реальных систем. Протокол TLS, обеспечивающий работу HTTPS, применяет аналогичный подход. Защищённые мессенджеры шифруют каждое сообщение отдельным ключом с обязательной аутентификацией. Системы шифрования дисков (LUKS, BitLocker, FileVault) используют блочные шифры с проверкой целостности секторов. Защищённая электронная почта, VPN-туннели, системы электронного документооборота и резервного копирования – всё это опирается на те же принципы.
Практическая ценность данной работы также заключается в том, что весь код доступен для проверки: видно, какие именно алгоритмы используются, как формируются ключи, как проверяется целостность и что происходит с данными на каждом шаге. Когда применение сторонних приложений для шифрования по каким-либо причинам нежелательно, например: из-за закрытого исходного кода, неясной модели угроз, избыточного функционала или требований к среде исполнения – скрипт может применяться напрямую либо служить основой для собственного решения.