Engee documentation
Notebook

Text data encryption and authentication

Introduction

In the modern digital world, the protection of information when transmitted through open channels is one of the key tasks. This example shows symmetric encryption of a text message with simultaneous verification of its integrity. The solution is based on the AES-256 algorithm in CBC mode combined with the HMAC-SHA256 authentication code.

What is AES?

The Advanced Encryption Standard (AES) is a symmetric block cipher adopted as a standard by the US National Institute of Standards and Technology in 2001. It is called symmetric because the same key is used for encryption and decryption. It is blocky because it operates with fixed-size blocks. In the case of AES, the block size is 128 bits (16 bytes), and the key length can be 128, 192, or 256 bits. In this example, a maximum length of 256 bits is used, providing the highest level of durability. AES replaced the outdated DES and is now used in TLS, Wi-Fi, disk encryption systems and many other applications.

The problem of simple Block encryption

If you encrypt each block independently, this mode is called Electronic Codebook (ECB). It has a fundamental flaw: identical blocks of plaintext turn into identical blocks of ciphertext. This allows the observer to draw conclusions about the data structure without knowing the key. A classic example: an image encrypted in ECB is still distinguishable by its contours. Therefore, real-world tasks use modes that eliminate this problem.

CBC

Cipher Block Chaining (CBC) is a mode in which each block of plaintext is XORED with the previous block of ciphertext before encryption. For the first block, the initialization vector (IV) is used instead of the previous one. Due to concatenation, identical plaintext blocks produce different ciphertext blocks. However, the CBC itself only provides confidentiality and does not protect against data modification.

HMAC

Hash-based Message Authentication Code (HMAC) is a mechanism for verifying integrity and authenticity. It accepts a key and data, and outputs a short, fixed-length value called a tag. Only the owner of the key can calculate the correct tag. Any data change results in a completely different tag. If the intruder changes at least one bit, but does not know the HMAC key, they will not be able to forge the tag.

Authenticated Encryption

The scheme combining encryption and authentication is called Authenticated Encryption. It provides two properties at once: confidentiality and integrity. Simple encryption without authentication does not provide a second guarantee. Moreover, CBC without authentication is vulnerable to attacks that gradually decrypt a message without a key. Adding HMAC and checking the tag before decryption completely closes this vulnerability. In this example, the encrypt-then-MAC scheme is used: first, the data is encrypted, then the tag is calculated using the ciphertext. Upon receipt, the tag is checked first, and only if it matches, decryption is performed.

Joining libraries

Adding the necessary libraries:

  • Nettle is a wrapper over the GNU Nettle library and provides an implementation of AES, CBC mode, HMAC, and auxiliary block addition operations.;
  • Base64 is responsible for encoding and decoding to the Base64 format. Since the ciphertext is arbitrary bytes, it cannot be directly transmitted over text channels. Base64 converts bytes into a sequence of printable characters suitable for email, instant messengers, JSON, and other text protocols.;
  • Random is used to generate cryptographically random values, namely salt and initialization vector.
In [ ]:
using Nettle, Base64, Random

Encryption function

Function зашифровать accepts text and password, returns a Base64 string with all the data to decrypt.

соль these are random 8 bytes added to the password so that the same passwords give different keys. It is not secret, it is transmitted openly.

The key is generated by the function gen_key32_iv16 from the password and the salt. Its length is 32 bytes (256 bits) for AES-256. The function is deterministic, so the recipient recovers the key himself.

iv16 these are random 16 bytes for the first CBC block. It must be unique for each message, so it is generated randomly. It is transmitted openly.

The HMAC key is a separate secret key for the tag, obtained by hashing the salt and password via SHA-256. The encryption and authentication keys must not match.

SHA-256 is a hash function that converts data to 256 bits. The slightest change in the input gives a completely different result.

Encryption is performed via encrypt with AES-256 and CBC mode. Since AES works in blocks of 16 bytes, the addition is applied. add_padding_PKCS5(): bytes equal to their number are added to the end. The result is called a ciphertext.

The tag is HMAC-SHA256 based on a 32-byte ciphertext. It proves that the ciphertext has not been changed. Upon acceptance, the tag is calculated anew; if there is a mismatch, the decryption is rejected.

All components are combined into a package: соль, iv16, шифр, тег. The order allows you to divide the package unambiguously, since the sizes are fixed. The package is encoded in Base64.

In [ ]:
function зашифровать(текст, пароль) 
    соль = rand(UInt8, 8)
    (ключ, _) = gen_key32_iv16(Vector{UInt8}(пароль), соль)
    iv16 = rand(UInt8, 16)
    hmac_ключ = hex2bytes(hexdigest("sha256", соль, Vector{UInt8}(пароль)))
    шифр = encrypt(Encryptor("AES256", ключ), :CBC, iv16, add_padding_PKCS5(Vector{UInt8}(текст), 16))
    тег = hex2bytes(hexdigest("sha256", hmac_ключ, шифр))
return base64encode(vcat(соль, iv16, шифр, тег))
end
Out[0]:
зашифровать (generic function with 1 method)

Decryption function

Function расшифровать accepts a Base64 string and password, returns the source text, or throws an error.:

  • Base64 decoding and parsing into four parts is performed. соль takes bytes 1-8, iv16 9–24, тег the last 32 bytes, everything in between is шифр;
  • the encryption key and the HMAC key are newly generated from the salt and password. If the password is correct, they will match those used during encryption.;
  • the cipher and the recovered HMAC key are used to calculate the tag and compare it with the transmitted one. If there is a mismatch, the function immediately throws an error, and decryption is not performed. This is a key stage in terms of security.
In [ ]:
function расшифровать(сообщение, пароль)
    принято = base64decode(сообщение)
    соль   = принято[1:8]
    iv16  = принято[9:24]
    шифр   = принято[25:end-32]
    тег    = принято[end-31:end]
    (ключ, _) = gen_key32_iv16(Vector{UInt8}(пароль), соль)
    hmac_ключ  = hex2bytes(hexdigest("sha256", соль, Vector{UInt8}(пароль)))

    if hex2bytes(hexdigest("sha256", hmac_ключ, шифр)) != тег
        error("ОШИБКА: Данные повреждены или подделаны!")
    end
return String(trim_padding_PKCS5(decrypt(Decryptor("AES256", ключ), :CBC, iv16, шифр)))
end
Out[0]:
расшифровать (generic function with 1 method)

Initial data

We will determine the password on the basis of which the encryption key will be generated and set the source text of the secret encrypted message.

In [ ]:
пароль = "qwerty123";
текст  = "Это секретное сообщение. Cтрого конфиденциально."
Out[0]:
"Это секретное сообщение. Cтрого конфиденциально."

Encryption

Referring to the function зашифровать(). let's encrypt the source text data.

In [ ]:
сообщение = зашифровать(текст, пароль);

Sending a message

Imagine that the data is transmitted over an open text channel. The recipient will only see encrypted data in base64 format. He must know the password to decrypt it.

In [ ]:
пароль = nothing
текст = nothing
display(сообщение)
"OVLUQ86U2ook0hIns1PSIKCpQK4HHIgprkSwyAJZ+SBA9W/xnjO3ziCcoN2FX8uCuTDGlKgyKQFobwfXXa6ijLRbOFy+83mFPTvm+ElL1fc0BZUzAnzAwRnVjkuTrFofFxk3RqWNykPCOqLDsbSrhErCrtIerlSP7u8GUpkSBuxalf7o3CDqwwCDzKyB5Bn3Q07m1kZFDvI="

Receiving and decrypting the message

Let's consider possible further scenarios:

  • incorrect password entered;
  • the correct password has been entered, and one bit of data has been substituted;
  • The correct password has been entered, and the true message has been transmitted.

Invalid password

If you enter an incorrect password, an error message will be displayed.

In [ ]:
неверный_пароль = "qwerty12345"
текст = расшифровать(сообщение, неверный_пароль);
display(text)
ОШИБКА: Данные повреждены или подделаны!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] расшифровать(сообщение::String, пароль::String)
   @ Main ./In[9]:12
 [3] top-level scope
   @ In[39]:2
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

Message substitution

Let's simulate the situation of data distortion during transmission. Let's display the contents of the bits in the 25th byte.

In [ ]:
байты = base64decode(сообщение)
bitstring(байты[25])
Out[0]:
"10101110"

Let's replace one bit in it.

In [ ]:
байты[25] ⊻= 0x01
bitstring(байты[25])
Out[0]:
"10101111"

And it will do a message substitution.

In [ ]:
подмена = base64encode(байты)
Out[0]:
"OVLUQ86U2ook0hIns1PSIKCpQK4HHIgpr0SwyAJZ+SBA9W/xnjO3ziCcoN2FX8uCuTDGlKgyKQFobwfXXa6ijLRbOFy+83mFPTvm+ElL1fc0BZUzAnzAwRnVjkuTrFofFxk3RqWNykPCOqLDsbSrhErCrtIerlSP7u8GUpkSBuxalf7o3CDqwwCDzKyB5Bn3Q07m1kZFDvI="

Let's compare the characters in the transmitted and encrypted message.

In [ ]:
for i in 1:length(сообщение)
    if сообщение[i] != подмена[i]
        println("Position ", i, ": '", сообщение[i], "' → '", подмена[i], "'")
    end
end
Позиция 34: 'k' → '0'

In the message, the symbol is in the 34th position. replaced by . When trying to decrypt, even with the correct password, an error message will be displayed.

In [ ]:
пароль = "qwerty123"
расшифровать(подмена, пароль)
ОШИБКА: Данные повреждены или подделаны!

Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:44
 [2] расшифровать(сообщение::String, пароль::String)
   @ Main ./In[9]:12
 [3] top-level scope
   @ In[42]:3
 [4] eval(m::Module, e::Any)
   @ Core ./boot.jl:489

A true message and a valid password

When the password is correct and the transmitted data is intact, the message is decrypted.

In [ ]:
пароль = "qwerty123"
текст = расшифровать(сообщение, пароль)
display(текст)
"Это секретное сообщение. Cтрого конфиденциально."

Conclusion

This example demonstrates the complete scheme of secure transmission of a text message. It is shown that encryption without authentication does not ensure data integrity, and the addition of HMAC makes it possible to detect even a single bit change.

These principles underlie many real-world systems. The TLS protocol, which provides HTTPS security, uses similar schemes. Secure messengers use symmetric encryption with authentication for each message. Disk encryption systems, such as LUKS or BitLocker, use block ciphers with authentication. Secure email protocols, VPN tunnels, and electronic document management systems also rely on these principles.

Obviously, you should never use passwords like "qwerty123" In reality, you need a password with as many case-sensitive characters, numbers, and special characters as possible, preferably a long passphrase or a random 256-bit key. The password cannot be transmitted over the same channel, stored openly, or reused.

Reliable message protection consists of several elements: a strong encryption algorithm, a correctly selected mode, an authentication mechanism, correct key generation, and password management discipline.