use efema_proto::Epoch;
use lacodda_seal::{Key, SEALED_OVERHEAD};
use crate::DeviceId;
pub(crate) const KEY_CONTEXT: &[u8] = b"efema/stream-key/v1";
const ENTRY_DOMAIN: &[u8] = b"efema/entry/v1";
const FORMAT: u8 = 1;
const INNER_HEADER: usize = 1 + 16;
pub const ENTRY_OVERHEAD: usize = SEALED_OVERHEAD + INNER_HEADER;
fn context(epoch: Epoch) -> Vec<u8> {
[ENTRY_DOMAIN, &epoch.0.to_be_bytes()].concat()
}
pub(crate) fn seal(key: &Key, epoch: Epoch, device: DeviceId, data: &[u8]) -> Result<Vec<u8>, lacodda_seal::Error> {
let mut plaintext = Vec::with_capacity(INNER_HEADER + data.len());
plaintext.push(FORMAT);
plaintext.extend_from_slice(device.as_bytes());
plaintext.extend_from_slice(data);
key.seal(&context(epoch), &plaintext)
}
pub(crate) enum Opened {
Entry { device: DeviceId, data: Vec<u8> },
Newer(u8),
}
pub(crate) fn open(key: &Key, epoch: Epoch, sealed: &[u8]) -> Result<Opened, lacodda_seal::Error> {
let plaintext = key.open(&context(epoch), sealed)?;
match plaintext.first() {
Some(&FORMAT) if plaintext.len() >= INNER_HEADER => {
let device = DeviceId::from_bytes(plaintext[1..INNER_HEADER].try_into().expect("sixteen bytes"));
Ok(Opened::Entry { device, data: plaintext[INNER_HEADER..].to_vec() })
}
Some(&version) if version > FORMAT => Ok(Opened::Newer(version)),
_ => Err(lacodda_seal::Error::Inauthentic),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_entry_opens_in_its_epoch_only() {
let key = Key::from_bytes([1; 32]);
let device = DeviceId::from_bytes([2; 16]);
let sealed = seal(&key, Epoch(3), device, b"data").unwrap();
assert_eq!(sealed.len(), b"data".len() + ENTRY_OVERHEAD);
match open(&key, Epoch(3), &sealed).unwrap() {
Opened::Entry { device: from, data } => assert_eq!((from, data.as_slice()), (device, &b"data"[..])),
Opened::Newer(_) => panic!("written in this format"),
}
assert!(open(&key, Epoch(4), &sealed).is_err(), "an epoch relabelled on the relay must not open");
}
#[test]
fn a_newer_format_is_named() {
let key = Key::from_bytes([1; 32]);
let mut plaintext = vec![FORMAT + 1];
plaintext.extend_from_slice(&[0; 16]);
let sealed = key.seal(&context(Epoch(1)), &plaintext).unwrap();
assert!(matches!(open(&key, Epoch(1), &sealed), Ok(Opened::Newer(v)) if v == FORMAT + 1));
let sealed = key.seal(&context(Epoch(1)), &[]).unwrap();
assert!(open(&key, Epoch(1), &sealed).is_err(), "an entry with no header");
}
}