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
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
//! 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()
|
||||
}
|
||||
Reference in New Issue
Block a user