Files
michael 268b986bb9 Rust port of OmniVox, wire-compatible with the C++ original
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
2026-09-30 06:35:24 -04:00

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()
}