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
andobnam chunk decrypt
that are likeencode
anddecode
, 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 overaes-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 inEncryptedMessage
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.