Byte-exact cereal binary layout, RFC 8439 ChaCha20-Poly1305, RFC 7914 scrypt defaults; tokio transport preserving the original's observable behavior (contract B1-B3, B5 quirks included). Parity evidence: golden C++ vectors in tests/vectors.rs and loopback behavior tests; both cross-directions verified against the unmodified C++ binaries. Source: https://git.0xfeedc0de.com/michael/OmniVox
150 lines
4.3 KiB
Rust
150 lines
4.3 KiB
Rust
//! 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<u8> {
|
|
(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<u8> = (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<u8>,
|
|
}
|
|
impl Cereal for DatagramPayload {
|
|
fn encode(&self, out: &mut Vec<u8>) {
|
|
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<Self, omnivox::CodecError> {
|
|
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()
|
|
}
|