use alloc::vec::Vec;
use chacha20poly1305::aead::{Aead, Payload};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use crate::error::Error;
pub const ENCRYPTED_FRAME: u8 = 0xE0;
const HEADER_LEN: usize = 1 + 8;
const TAG_LEN: usize = 16;
const MIN_FRAME_LEN: usize = HEADER_LEN + TAG_LEN;
fn make_nonce(prefix: &[u8; 4], seq: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[..4].copy_from_slice(prefix);
nonce[4..].copy_from_slice(&seq.to_be_bytes());
nonce
}
fn make_header(seq: u64) -> [u8; HEADER_LEN] {
let mut header = [0u8; HEADER_LEN];
header[0] = ENCRYPTED_FRAME;
header[1..].copy_from_slice(&seq.to_be_bytes());
header
}
pub struct RecordSealer {
cipher: ChaCha20Poly1305,
nonce_prefix: [u8; 4],
next_seq: u64,
}
impl RecordSealer {
pub(crate) fn new(cipher: ChaCha20Poly1305, nonce_prefix: [u8; 4]) -> Self {
Self {
cipher,
nonce_prefix,
next_seq: 0,
}
}
#[cfg(test)]
pub(crate) fn with_seq(cipher: ChaCha20Poly1305, nonce_prefix: [u8; 4], next_seq: u64) -> Self {
Self {
cipher,
nonce_prefix,
next_seq,
}
}
pub fn seal(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
let seq = self.next_seq;
let header = make_header(seq);
let nonce = make_nonce(&self.nonce_prefix, seq);
let ciphertext = self
.cipher
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: plaintext,
aad: &header,
},
)
.map_err(|_| Error::KeyDerivation)?;
self.next_seq = self
.next_seq
.checked_add(1)
.ok_or(Error::SequenceExhausted)?;
let mut frame = Vec::with_capacity(HEADER_LEN + ciphertext.len());
frame.extend_from_slice(&header);
frame.extend_from_slice(&ciphertext);
Ok(frame)
}
}
pub struct RecordOpener {
cipher: ChaCha20Poly1305,
nonce_prefix: [u8; 4],
next_seq: u64,
}
impl RecordOpener {
pub(crate) fn new(cipher: ChaCha20Poly1305, nonce_prefix: [u8; 4]) -> Self {
Self {
cipher,
nonce_prefix,
next_seq: 0,
}
}
#[cfg(test)]
pub(crate) fn with_seq(cipher: ChaCha20Poly1305, nonce_prefix: [u8; 4], next_seq: u64) -> Self {
Self {
cipher,
nonce_prefix,
next_seq,
}
}
pub fn open(&mut self, frame: &[u8]) -> Result<Vec<u8>, Error> {
if frame.len() < MIN_FRAME_LEN || frame[0] != ENCRYPTED_FRAME {
return Err(Error::MalformedFrame);
}
let header = &frame[..HEADER_LEN];
let mut seq_bytes = [0u8; 8];
seq_bytes.copy_from_slice(&header[1..HEADER_LEN]);
let seq = u64::from_be_bytes(seq_bytes);
if seq != self.next_seq {
return Err(Error::OutOfOrder);
}
let nonce = make_nonce(&self.nonce_prefix, seq);
let ciphertext = &frame[HEADER_LEN..];
let plaintext = self
.cipher
.decrypt(
Nonce::from_slice(&nonce),
Payload {
msg: ciphertext,
aad: header,
},
)
.map_err(|_| Error::Decrypt)?;
self.next_seq = self
.next_seq
.checked_add(1)
.ok_or(Error::SequenceExhausted)?;
Ok(plaintext)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chacha20poly1305::KeyInit;
fn cipher() -> ChaCha20Poly1305 {
ChaCha20Poly1305::new((&[9u8; 32]).into())
}
#[test]
fn seal_at_max_seq_fails_closed() {
let mut sealer = RecordSealer::with_seq(cipher(), [1, 2, 3, 4], u64::MAX);
let err = sealer.seal(b"payload").unwrap_err();
assert_eq!(err, Error::SequenceExhausted);
}
#[test]
fn open_at_max_seq_fails_closed() {
use chacha20poly1305::aead::{Aead, Payload};
use chacha20poly1305::Nonce;
let prefix = [1u8, 2, 3, 4];
let seq = u64::MAX;
let header = make_header(seq);
let nonce = make_nonce(&prefix, seq);
let ct = cipher()
.encrypt(
Nonce::from_slice(&nonce),
Payload {
msg: b"payload",
aad: &header,
},
)
.unwrap();
let mut frame = Vec::new();
frame.extend_from_slice(&header);
frame.extend_from_slice(&ct);
let mut opener = RecordOpener::with_seq(cipher(), prefix, seq);
let err = opener.open(&frame).unwrap_err();
assert_eq!(err, Error::SequenceExhausted);
}
}