Goal

Today's goal is to implement at least some chunk encryption. I'll be continuing the branch I started a week ago.

  • Start of encryption: obnam chunk encrypt and obnam chunk decrypt that are like encode and decode, but with an extra required option --key KEY.
  • Change obnam chunk inspect to support encrypted chunks: shows what is in the encrypted chunk before encrypting, and if given a key, what is in a decrypted chunk as well.

Acceptance criteria is that an encryption round trip works and this is verified by a Subplot scenario.

Plan

  • Add a "null encryption" encryption mode.
  • Using the aes-gcm-siv crate, implement a real encryption mode. (I chose that over aes-gcm because it claims to be harder to misuse.)

Notes

  • Made a new branch from the existing one, so that I have a known good place to retreat to, if I mess everything up today.
  • I will first implement the stupidest encryption method I can think of that isn't must a null cipher. I'll encrypt successive bytes of cleartext with successive bytes of key, rotating around the key when I reach the end. I don't want a null cipher, where ciphertext is identical to cleartext, because that makes it impossible to verify that anything happened.
  • The aes-gcm-siv crate is not the easiest to use and I'm not entirely sure I understand all the intricacies. So I'll do a separate small toy project with just that crate.
  • Based on example from the docs:
use aes_gcm_siv::{
    aead::{Aead, KeyInit, OsRng},
    Aes256GcmSiv,
    Nonce, // Or `Aes128GcmSiv`
};

fn main() -> Result<(), aes_gcm_siv::Error> {
    let msg = b"hello, world";
    let key = Aes256GcmSiv::generate_key(&mut OsRng);
    let cipher = Aes256GcmSiv::new(&key);
    let nonce = Nonce::from_slice(b"unique nonce"); // 96-bits; unique per message
    let ciphertext = cipher.encrypt(nonce, msg.as_ref())?;
    let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())?;
    assert_eq!(&plaintext, msg);

    println!("original plaintext  : {msg:?}");
    println!("de-crypted plaintext: {plaintext:?}");

    Ok(())
}
  • However, this uses a fixed nonce, which is a big no-no.
  • I can create a random nonce every time using the operating system random number generator via aes_gcm_siv::aead::OsRng. For this encryption method, the nonce MUST be exactly 96 bits. I can't find the length documented in a constant, only in the comment in the example. I'll make my own constant.
fn nonce() -> Vec<u8> {
    let mut out: Vec<u8> = vec![0; NONCE_BYTES];
    let mut rng = OsRng::default();
    rng.fill_bytes(&mut out);
    out
}
  • However, then I need to create a GenericArray from that vector:
use aes_gcm_siv::aead::generic_array::GenericArray;
let nonce = GenericArray::from_slice(&nonce);
  • It is notable that the decrypt operation needs the same nonce. It can transmitted in cleartext, as it's not sensitive. Only the key is sensitive.
  • For the real chunk encryption I'll need associated data as well as the nonce. The encrypted message is, therefore:
struct EncryptedMessage {
    ciphertext: Vec<u8>,
    nonce: Vec<u8>,
    ad: Vec<u8>,
}
  • The whole toy program is:
use aes_gcm_siv::{
    aead::{
        generic_array::GenericArray,
        rand_core::{OsRng, RngCore},
        Aead, KeyInit,
    },
    Aes256GcmSiv,
};

const NONCE_BYTES: usize = 12; // 96 bits

fn main() -> Result<(), aes_gcm_siv::Error> {
    let msg = b"hello, world";
    let ad = b"greeting";

    let key = Aes256GcmSiv::generate_key(&mut OsRng);
    let cipher = Aes256GcmSiv::new(&key);
    let nonce = nonce();

    let encrypted = EncryptedMessage {
        ciphertext: cipher.encrypt(GenericArray::from_slice(&nonce), msg.as_ref())?,
        ad: ad.to_vec(),
        nonce,
    };

    let plaintext = cipher.decrypt(
        GenericArray::from_slice(&encrypted.nonce),
        encrypted.ciphertext.as_ref(),
    )?;
    assert_eq!(&plaintext, msg);

    println!("original plaintext  : {msg:?}");
    println!("de-crypted plaintext: {plaintext:?}");

    Ok(())
}

fn nonce() -> Vec<u8> {
    let mut out: Vec<u8> = vec![0; NONCE_BYTES];

    let mut rng = OsRng::default();
    rng.fill_bytes(&mut out);

    out
}

struct EncryptedMessage {
    ciphertext: Vec<u8>,
    nonce: Vec<u8>,
    ad: Vec<u8>,
}
  • I can now go back to the Obnam3 repository.
  • The ad field in EncryptedMessage in the toy program above isn't actually needed. The caller will need to provide it anyway.

  • An hour or two passes. Not sure how long, was deep in debugging and forgot to take notes.

  • This branch has become too much of a mess to want to continue. I will stop here, and think what the concepts and abstractions and data types should be, for the next session.

Summary

When you don't have a clear plan, and you try to do too much in one session, you end up with a mess. I keep re-learning this lesson over and over again. But version control helps.

Comments?

If you have feedback on this development session, please use the following fediverse thread: https://toot.liw.fi/@liw/114331793560371903.