satoshi.ke

peer to peer systems


Symmetric Encryption

Introduction

Since the ancient days, a concern for groups of people has been how to securely transmit a secret message securely from one place to another without interception or modification, for example for military purposes. These led to schemes such as the Caesar Cipher being developed.

In modern computer systems, computer security relies on confidentiality, i.e. being able to store or transmit information that can only be understood by authorized parties but not adversaries.

This is where encryption comes into play. Encryption is the process of encoding secret information into a concealed form that only the intended recipient(s) can decode back to the original.

The original message is known as plaintext. It is encoded into ciphertext which is random gibberish to anyone but the authorized parties. A piece of information known as a secret key that is known only by the originator of the message and the intended recipient(s) is used to both encrypt and decrypt the message i.e turning it from plaintext to ciphertext, and back to plaintext.

Symmetric Encryption

There are two types of encryption, symmetric and asymmetric. In this article we’ll focus on symmetric encryption.

(Update: See article on asymmetric encryption here)

In symmetric encryption, a single key is used to both encrypt and decrypt the message as illustrated below.

illustration of symmetric encryption [dot file]

This means that both the originator and the recipient of the message have to have the same key, and it has to have been shared securely between them, because anyone with the key can decrypt the ciphertext.

This needs to happen in a separate secure channel different from the one transmitting the ciphertext, otherwise anyone monitoring it can get the secret key as well as the ciphertext and decrypt it.

However, this is less of an issue where it is being used to encrypt stored information if only one person needs to both encrypt and decrypt it. The only consideration here is keeping the key safe, because if the key is lost, the encrypted information can be considered lost as well.

Examples in code

The following code examples demonstrate symmetric encryption in Python and JavaScript. Note that these are simple examples just to illustrate the concept. For production code, additional considerations need to be taken into account for robustness.

Python

from cryptography.fernet import Fernet


plaintext_in = "The quick brown fox jumps over the lazy dog"
secret_key = Fernet.generate_key()


def encrypt(plaintext_bytes, key):
    encrypter = Fernet(key)
    return encrypter.encrypt(plaintext_bytes)


input_bytes = bytes(plaintext_in, encoding="utf-8")
cipher_text = encrypt(input_bytes, secret_key)


def decrypt(cipher_text, key):
    decrypter = Fernet(key)
    return decrypter.decrypt(cipher_text)


output_bytes = decrypt(cipher_text, secret_key)
plaintext_out = str(output_bytes, encoding="utf-8")

assert plaintext_in == plaintext_out  # should be equal

[source file]

In python, we use the third party library cryptography that can be installed via:

pip install cryptography

It provides a class Fernet that conveniently hides all the lower level details involved in symmetric encryption. Using it, we can generate a secret key, and use the key to create objects for both encrypting and decrypting the plaintext.

The plaintext in this case is a simple string, but it can be any kind of information e.g. sound, images and the like since the encryption process works on bytes.

Fernet will additionally encode the generated key and the ciphertext bytes into a strings of printable characters that can be conveniently transmitted over the network across different types of systems. Base64 encoding is used for this.

The Fernet class uses the algorithm AES 128 in CBC mode for symmetric encryption. The technical details can be found in its spec

JavaScript

const { createCipheriv, createDecipheriv, randomBytes } = await import('node:crypto');
const { Buffer } = await import ("node:buffer");

const plaintextIn = "The quick brown fox jumps over the lazy dog";
const secretKey = randomBytes(16);
const initializationVector = randomBytes(16);

const encrypt = (plaintextBytes, secretKey, initializationVector) => {
    let cipher = createCipheriv('aes-128-cbc', secretKey, initializationVector);
    return Buffer.concat([
	cipher.update(plaintextBytes),
	cipher.final()
    ]).toString("base64");
};

let bytesIn = Buffer.from(plaintextIn);
let cipherText = encrypt(bytesIn, secretKey, initializationVector);

const decrypt = (cipherText, secretKey, initializationVector) => {
    let decipher = createDecipheriv('aes-128-cbc', secretKey, initializationVector);
    return Buffer.concat([decipher.update(cipherText, "base64"), decipher.final()]);
};

let bytesOut = decrypt(cipherText, secretKey, initializationVector);
let plaintextOut = bytesOut.toString('utf8');

console.assert(plaintextIn == plaintextOut);  // should be equal

[source file]

In JavaScript, we use the crypto module that is distributed by default with NodeJS. It is a bit lower level that the Fernet class we saw in Python as we have to implement a few things by hand that Fernet did automatically.

We generate the secret key using randomBytes from the crypto module. Similar the the python example, we’ll use the AES 128 algorithm in CBC mode, so the key is 128 bits (16 bytes * 8bits) long.

An additional detail that was implicit in the python example but is explicit here is the initialization vector. This is a random piece of information that is generated and used in each encryption operation. It is required both for encryption and decryption.

Its purpose is to randomize each ciphertext message transmitted across the network even if they have the same plaintext message. This prevents an adversary monitoring the network and collecting a large number of ciphertext messages from being able use this data to derive the secret key.

In the python example, the initialization vector is generated and attached to the ciphertext automatically, but in the JS example, we generate it separately.

NodeJs also uses separate functions for creating encryption and decryption objects. We need to pass in the same algorithm, secret key and intialization vector to both for encryption and decryption to work. On each of the objects, we call method update any number of times to add plaintext or ciphertext, and then call final to mark the end of input. Using Buffer.concatenate, we combine the byte objects output of each of the method calls into one.

Conclusion

This article discusses the basics of symmetric encryption and gives examples in Python and JavaScript. For a more in-depth exploration, checkout the CRYPTO101 course and the Cryptopals challenge.