#![allow(clippy::disallowed_methods)]
use buffa::Message as _;
use wacore::appstate::hash::HashState;
use wacore::store::device::{CachedNoiseCert, CachedServerCertChain};
use wacore::store::error::StoreError;
use wacore::store::traits::AppStateSyncKey;
mod proto {
#![allow(
unused,
non_camel_case_types,
non_snake_case,
unreachable_patterns,
clippy::all,
clippy::pedantic,
clippy::nursery
)]
buffa::include_proto!("wire");
}
use proto::{AppStateSyncKeyWire, HashStateWire, NoiseCert, ServerCertChain};
const NOISE_KEY_LEN: usize = 32;
const HASH_STATE_LEN: usize = 128;
const APP_STATE_KEY_LEN: usize = 32;
fn bad_len(field: &str, expected: usize, got: usize) -> StoreError {
StoreError::Serialization(format!("{field}: expected {expected} bytes, got {got}").into())
}
fn decode_err(e: buffa::DecodeError) -> StoreError {
StoreError::Serialization(Box::new(e))
}
impl From<&CachedNoiseCert> for NoiseCert {
fn from(c: &CachedNoiseCert) -> Self {
Self {
key: c.key.to_vec(),
not_before: c.not_before,
not_after: c.not_after,
}
}
}
fn noise_cert_from_wire(w: NoiseCert) -> Result<CachedNoiseCert, StoreError> {
let got = w.key.len();
let key: [u8; NOISE_KEY_LEN] = w
.key
.try_into()
.map_err(|_| bad_len("noise_cert.key", NOISE_KEY_LEN, got))?;
Ok(CachedNoiseCert {
key,
not_before: w.not_before,
not_after: w.not_after,
})
}
pub(crate) fn encode_server_cert_chain(c: &CachedServerCertChain) -> Vec<u8> {
ServerCertChain {
intermediate: buffa::MessageField::some(NoiseCert::from(&c.intermediate)),
leaf: buffa::MessageField::some(NoiseCert::from(&c.leaf)),
}
.encode_to_vec()
}
pub(crate) fn decode_server_cert_chain(bytes: &[u8]) -> Result<CachedServerCertChain, StoreError> {
let w = ServerCertChain::decode_from_slice(bytes).map_err(decode_err)?;
let intermediate = w.intermediate.ok_or_else(|| {
StoreError::Serialization("server_cert_chain.intermediate missing".into())
})?;
let leaf = w
.leaf
.ok_or_else(|| StoreError::Serialization("server_cert_chain.leaf missing".into()))?;
Ok(CachedServerCertChain {
intermediate: noise_cert_from_wire(intermediate)?,
leaf: noise_cert_from_wire(leaf)?,
})
}
pub(crate) fn encode_app_state_sync_key(k: &AppStateSyncKey) -> Vec<u8> {
AppStateSyncKeyWire {
key_data: k.key_data.clone(),
fingerprint: k.fingerprint.clone(),
timestamp: k.timestamp,
}
.encode_to_vec()
}
pub(crate) fn decode_app_state_sync_key(bytes: &[u8]) -> Result<AppStateSyncKey, StoreError> {
let w = AppStateSyncKeyWire::decode_from_slice(bytes).map_err(decode_err)?;
if w.key_data.len() != APP_STATE_KEY_LEN {
return Err(bad_len(
"app_state_sync_key.key_data",
APP_STATE_KEY_LEN,
w.key_data.len(),
));
}
Ok(AppStateSyncKey {
key_data: w.key_data,
fingerprint: w.fingerprint,
timestamp: w.timestamp,
})
}
pub(crate) fn encode_hash_state(s: &HashState) -> Vec<u8> {
HashStateWire {
version: s.version,
hash: s.hash.to_vec(),
index_value_map: s
.index_value_map
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
mac_mismatch_fatal: s.mac_mismatch_fatal,
}
.encode_to_vec()
}
pub(crate) fn decode_hash_state(bytes: &[u8]) -> Result<HashState, StoreError> {
let w = HashStateWire::decode_from_slice(bytes).map_err(decode_err)?;
let got = w.hash.len();
let hash: [u8; HASH_STATE_LEN] = w
.hash
.try_into()
.map_err(|_| bad_len("hash_state.hash", HASH_STATE_LEN, got))?;
Ok(HashState {
version: w.version,
hash,
index_value_map: w.index_value_map.into_iter().collect(),
mac_mismatch_fatal: w.mac_mismatch_fatal,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn server_cert_chain_roundtrips() {
let chain = CachedServerCertChain {
intermediate: CachedNoiseCert {
key: [0xAB; 32],
not_before: 1_700_000_000,
not_after: 1_900_000_000,
},
leaf: CachedNoiseCert {
key: [0xCD; 32],
not_before: 1_700_000_500,
not_after: 1_899_999_500,
},
};
let decoded = decode_server_cert_chain(&encode_server_cert_chain(&chain)).unwrap();
assert_eq!(decoded, chain);
}
#[test]
fn server_cert_chain_rejects_wrong_key_len() {
let bytes = ServerCertChain {
intermediate: buffa::MessageField::some(NoiseCert {
key: vec![0u8; 5],
not_before: 1,
not_after: 2,
}),
leaf: buffa::MessageField::some(NoiseCert {
key: vec![0u8; 32],
not_before: 1,
not_after: 2,
}),
}
.encode_to_vec();
assert!(decode_server_cert_chain(&bytes).is_err());
}
#[test]
fn app_state_sync_key_roundtrips() {
let key = AppStateSyncKey {
key_data: vec![7u8; 32],
fingerprint: vec![9, 8, 7],
timestamp: 1_700_000_123,
};
let decoded = decode_app_state_sync_key(&encode_app_state_sync_key(&key)).unwrap();
assert_eq!(decoded.key_data, key.key_data);
assert_eq!(decoded.fingerprint, key.fingerprint);
assert_eq!(decoded.timestamp, key.timestamp);
}
#[test]
fn app_state_sync_key_rejects_wrong_key_len() {
let bytes = AppStateSyncKeyWire {
key_data: vec![0u8; 16],
fingerprint: vec![1, 2, 3],
timestamp: 1,
}
.encode_to_vec();
assert!(decode_app_state_sync_key(&bytes).is_err());
}
#[test]
fn hash_state_roundtrips() {
let mut index_value_map = HashMap::new();
index_value_map.insert("idx-a".to_string(), vec![1, 2, 3]);
index_value_map.insert("idx-b".to_string(), vec![]);
let mut hash = [0u8; 128];
hash[0] = 0xFF;
hash[127] = 0x11;
let state = HashState {
version: 42,
hash,
index_value_map: index_value_map.clone(),
mac_mismatch_fatal: true,
};
let decoded = decode_hash_state(&encode_hash_state(&state)).unwrap();
assert_eq!(decoded.version, 42);
assert_eq!(decoded.hash, hash);
assert_eq!(decoded.index_value_map, index_value_map);
assert!(
decoded.mac_mismatch_fatal,
"a latched collection must stay latched across restarts, or the \
divergence is re-detected on every patch forever"
);
}
#[test]
fn hash_state_default_roundtrips() {
let state = HashState::default();
let decoded = decode_hash_state(&encode_hash_state(&state)).unwrap();
assert_eq!(decoded.version, 0);
assert_eq!(decoded.hash, [0u8; 128]);
assert!(decoded.index_value_map.is_empty());
assert!(!decoded.mac_mismatch_fatal);
}
#[test]
fn hash_state_row_without_the_latch_field_decodes_as_healthy() {
let bytes = HashStateWire {
version: 9,
hash: vec![0u8; 128],
index_value_map: Default::default(),
mac_mismatch_fatal: false,
}
.encode_to_vec();
let decoded = decode_hash_state(&bytes).expect("an old row must still decode");
assert_eq!(decoded.version, 9);
assert!(!decoded.mac_mismatch_fatal);
}
#[test]
fn hash_state_rejects_wrong_hash_len() {
let bytes = HashStateWire {
version: 1,
hash: vec![0u8; 64],
index_value_map: Default::default(),
mac_mismatch_fatal: false,
}
.encode_to_vec();
assert!(decode_hash_state(&bytes).is_err());
}
}