//! Golden vectors produced by the C++ implementation (CryptoPP + cereal) //! in `omnivox-ref/cppgen.cpp`. The Rust leaves must reproduce these bytes. use omnivox::{AeadPacket, Cereal, decrypt, derive_encrypt_key, encrypt_with_nonce}; fn vectors() -> Vec<(String, String)> { include_str!("vectors.txt") .lines() .filter_map(|l| l.split_once('=')) .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect() } fn get<'a>(vs: &'a [(String, String)], key: &str) -> &'a str { vs.iter() .find(|(k, _)| k == key) .unwrap_or_else(|| panic!("missing vector {key}")) .1 .as_str() } fn unhex(s: &str) -> Vec { (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) .collect() } #[test] fn scrypt_key_matches_cpp_default_params() { let vs = vectors(); let pw = b"SecurePassword123"; let salt: Vec = (1u8..=8).collect(); let key = derive_encrypt_key(pw, &salt, 17, 8, 1).expect("scrypt"); assert_eq!(hex(key), get(&vs, "scrypt_key")); } #[test] fn scrypt_key_matches_cpp_short_inputs() { let vs = vectors(); let key = derive_encrypt_key(b"pass", b"salt", 17, 8, 1).expect("scrypt"); assert_eq!(hex(key), get(&vs, "scrypt_key2")); } #[test] fn empty_inputs_rejected_like_cpp_invalid_argument() { assert!(derive_encrypt_key(b"", b"salt", 17, 8, 1).is_err()); assert!(derive_encrypt_key(b"pw", b"", 17, 8, 1).is_err()); } #[test] fn datagram_payload_layout_matches_cereal() { struct DatagramPayload { id: u16, v1: u64, payload: Vec, } impl Cereal for DatagramPayload { fn encode(&self, out: &mut Vec) { out.extend_from_slice(&self.id.to_le_bytes()); out.extend_from_slice(&self.v1.to_le_bytes()); omnivox::write_byte_vec(out, &self.payload); } fn decode(bytes: &[u8]) -> Result { let mut r = omnivox::Reader::new(bytes); Ok(Self { id: r.u16()?, v1: r.u64()?, payload: r.byte_vec()?, }) } } let vs = vectors(); let dp = DatagramPayload { id: 0x1234, v1: 0x1122334455667788, payload: b"Hello".to_vec(), }; let mut out = Vec::new(); dp.encode(&mut out); assert_eq!(hex(&out), get(&vs, "cereal_datagram")); let back = DatagramPayload::decode(&out).unwrap(); assert_eq!( (back.id, back.v1, back.payload), (0x1234, 0x1122334455667788, b"Hello".to_vec()) ); let mut zeroed = Vec::new(); DatagramPayload { id: 0, v1: 0, payload: vec![], } .encode(&mut zeroed); assert_eq!(hex(zeroed), get(&vs, "cereal_datagram_default")); } #[test] fn aead_packet_layout_matches_cereal() { let vs = vectors(); let pkt = AeadPacket { nonce: std::array::from_fn(|i| i as u8), tag: std::array::from_fn(|i| 0xA0 + i as u8), payload: vec![0xDE, 0xAD, 0xBE, 0xEF], }; let mut out = Vec::new(); pkt.encode(&mut out); assert_eq!(hex(&out), get(&vs, "cereal_aead")); assert_eq!(AeadPacket::decode(&out).unwrap(), pkt); let empty = AeadPacket { nonce: [0; 12], tag: [0; 16], payload: vec![], }; let mut out = Vec::new(); empty.encode(&mut out); assert_eq!(hex(out), get(&vs, "cereal_aead_empty")); } #[test] fn aead_matches_cryptopp_output() { let vs = vectors(); let key: [u8; 32] = unhex(get(&vs, "scrypt_key")).try_into().unwrap(); let nonce: [u8; 12] = std::array::from_fn(|i| i as u8); let pkt = encrypt_with_nonce(&key, &nonce, b"Hello"); assert_eq!(hex(&pkt.payload), get(&vs, "aead_ct")); assert_eq!(hex(pkt.tag), get(&vs, "aead_tag")); assert_eq!(decrypt(&key, &pkt).unwrap(), b"Hello"); let empty = encrypt_with_nonce(&key, &nonce, b""); assert_eq!(hex(empty.tag), get(&vs, "aead_empty_tag")); assert_eq!(decrypt(&key, &empty).unwrap(), b""); } #[test] fn tampered_packet_fails_authentication() { let key = [3u8; 32]; let mut pkt = omnivox::encrypt(&key, b"important"); pkt.payload[0] ^= 0x01; let err = decrypt(&key, &pkt).unwrap_err(); assert!(err.to_string().contains("tampered")); } fn hex(bytes: impl AsRef<[u8]>) -> String { bytes.as_ref().iter().map(|b| format!("{b:02x}")).collect() }