Encrypting and decrypting files with arbitrary data
Introduction
Modern data exchange often takes place through open channels: e-mail, instant messengers, cloud storage, public Wi-Fi networks. Under these conditions, any file – document, image, archive - can be intercepted, copied, or modified on its way to the recipient. The task of protection boils down to two independent requirements: maintain confidentiality and ensure integrity.
This paper presents a compact encryption scheme for a file of any type: the contents along with the file name are encrypted using the symmetric AES-256 algorithm in CBC mode, and the authenticity of the packaged container is confirmed by the HMAC-SHA256 authentication code. In this scheme, decryption is performed only after successful verification of the tag. On the receiving side, the source file is restored, and if the names match, a free name with an index is selected.
Joining libraries
Two libraries are used in this work:
-
Nettle – implementation of AES, CBC mode, HMAC-SHA256 and auxiliary block addition operations;
-
Random is a cryptographically random value generator used for salt and initialization vector.
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
Encryption function
Function enc_file(fnam, pswd) accepts the name of the source file in the current directory and the password, creates a binary data container and returns its name.
The file name is converted to bytes and packed into a fixed-length buffer (256 bytes), the free space is padded with zeros. The buffer is supplemented with the contents of the file, thus obtaining a single payload. This makes it possible to decipher unambiguously where the name ends and the content begins: the boundary is set by the structure itself.
Then the salt and initialization vector are generated. Two independent keys are derived from the password and the salt: a 256-bit AES key and a separate key for HMAC. The payload is supplemented according to the PKCS#5 scheme and encrypted with the AES-256 algorithm in CBC mode. The HMAC-SHA256 tag is calculated from the container body (salt, vector, ciphertext), proving that the contents have not been changed. The container body and tag are written to .bin-a file whose name is selected based on existing ones (if any in the current directory), so that the old containers are not overwritten.
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
Decryption function
Function dec_file(binf, pswd) accepts a name .bin-The container and password, restores the original file and returns the name of the recovered file.
The container is divided into a body and a tag. The salt, initialization vector, and ciphertext are extracted from the container body. Both keys are generated using the salt and password. If the password is correct, they will match those used during encryption.
Next comes the integrity check: the HMAC tag is recalculated over the container body and compared with the transmitted one. If there is a mismatch, the function immediately throws an error, and the data is not decrypted. This procedure closes the class of attacks on CBC mode, in which the attacker tries to decrypt the message by observing the recipient's reaction to corrupted packets.
Upon successful verification, the PKCS#5 supplement is removed and the payload is decrypted. The first 256 bytes are the buffer of the name, the rest are the contents of the file. The zero complement of the name is cut off, the name is restored, and in case of a collision, a free name with an index is selected. The file is written to the current directory, and its name is returned as the result.
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
Encryption
Select a file for encryption Данные.pdf, containing this example in pdf format. Setting the file name, пароль, and let's turn to the encryption function.
имя_файла = "Данные.pdf"
пароль = "qwerty12345678";
enc_file(имя_файла, пароль)
As a result of the encryption, a binary file was created. Данные.bin.
Data transmission
Delete the source file Данные.pdf; delete the variables containing its name and пароль for the encryption function. Imagine that an encrypted file is transmitted to the recipient via an open communication channel.
!isnothing(имя_файла) && isfile(имя_файла) && rm(имя_файла)
имя_файла = nothing
пароль = nothing
Decoding
Let's imagine that we are now on the recipient's side. We have accepted an encrypted file. Данные.bin, is known to us in advance пароль. Let's set the file name and password and turn to the decryption function.
имя_файла = "Данные.bin"
пароль = "qwerty12345678";
dec_file(имя_файла, пароль)
As a result of decryption, the source file Данные.pdfrestored.
Conclusion
This example implements a complete scheme for secure file transfer of any type over an open data channel. Symmetric AES-256-CBC encryption ensures confidentiality of the file contents and name. The encrypt-then-MAC scheme with tag verification before decryption eliminates attacks to which CBC mode is susceptible and ensures that not a single modified byte goes unnoticed. Automatic naming in case of a collision makes recovery safe and predictable.
Such schemes underlie many real systems. The TLS protocol, which makes HTTPS work, uses a similar approach. Secure messengers encrypt each message with a separate key with mandatory authentication. Disk encryption systems (LUKS, BitLocker, FileVault) use block ciphers with sector integrity verification. Secure email, VPN tunnels, electronic document management and backup systems are all based on the same principles.
The practical value of this work also lies in the fact that all the code is available for verification: you can see exactly which algorithms are used, how keys are generated, how integrity is checked, and what happens to the data at each step. When using third-party encryption applications is undesirable for any reason, for example: due to closed source code, an unclear threat model, excessive functionality, or requirements for the execution environment, the script can be applied directly or serve as the basis for its own solution.