use super::crypto::{aes_key_unwrap, aes_key_wrap, pbkdf2_hmac_sha1, Aes};
const KM_VERSION: u8 = 1;
const KM_PT_KEYMAT: u8 = 2;
const KM_SIGN: u16 = 0x2029; const CIPHER_AES_CTR: u8 = 2;
const SE_SRT: u8 = 2;
const KK_EVEN: u8 = 1;
const PBKDF2_ITERS: u32 = 2048;
const SALT_LEN: usize = 16;
const SALT_PBKDF_LEN: usize = 8;
#[derive(Clone)]
pub(crate) struct KeyMaterial {
sek: Vec<u8>,
salt: [u8; SALT_LEN],
}
impl KeyMaterial {
pub(crate) fn generate(key_len: usize) -> KeyMaterial {
let mut sek = vec![0u8; key_len];
random_bytes(&mut sek);
let mut salt = [0u8; SALT_LEN];
random_bytes(&mut salt);
KeyMaterial { sek, salt }
}
fn kek(&self, passphrase: &[u8]) -> Vec<u8> {
pbkdf2_hmac_sha1(
passphrase,
&self.salt[SALT_LEN - SALT_PBKDF_LEN..],
PBKDF2_ITERS,
self.sek.len(),
)
}
pub(crate) fn to_message(&self, passphrase: &[u8]) -> Vec<u8> {
let kek = self.kek(passphrase);
let wrapped = aes_key_wrap(&kek, &self.sek);
let mut m = Vec::with_capacity(16 + SALT_LEN + wrapped.len());
m.push((KM_VERSION << 4) | KM_PT_KEYMAT); m.extend_from_slice(&KM_SIGN.to_be_bytes());
m.push(KK_EVEN); m.extend_from_slice(&0u32.to_be_bytes()); m.push(CIPHER_AES_CTR);
m.push(0); m.push(SE_SRT);
m.push(0); m.extend_from_slice(&0u16.to_be_bytes()); m.push((SALT_LEN / 4) as u8); m.push((self.sek.len() / 4) as u8); m.extend_from_slice(&self.salt);
m.extend_from_slice(&wrapped);
m
}
pub(crate) fn parse(passphrase: &[u8], m: &[u8]) -> Option<KeyMaterial> {
if m.len() < 16 + SALT_LEN {
return None;
}
if m[0] != (KM_VERSION << 4) | KM_PT_KEYMAT {
return None;
}
if u16::from_be_bytes([m[1], m[2]]) != KM_SIGN {
return None;
}
if m[8] != CIPHER_AES_CTR {
return None;
}
let slen = (m[14] as usize) * 4;
let klen = (m[15] as usize) * 4;
if slen != SALT_LEN || !(klen == 16 || klen == 24 || klen == 32) {
return None;
}
let salt: [u8; SALT_LEN] = m.get(16..16 + SALT_LEN)?.try_into().ok()?;
let wrapped = m.get(16 + SALT_LEN..)?;
if wrapped.len() != klen + 8 {
return None;
}
let probe = KeyMaterial {
sek: vec![0u8; klen],
salt,
};
let kek = probe.kek(passphrase);
let sek = aes_key_unwrap(&kek, wrapped)?;
Some(KeyMaterial { sek, salt })
}
pub(crate) fn transform(&self, seq: u32, payload: &mut [u8]) {
let mut iv = [0u8; 16];
iv[10..14].copy_from_slice(&seq.to_be_bytes());
for (b, s) in iv.iter_mut().zip(self.salt.iter()).take(14) {
*b ^= *s;
}
Aes::new(&self.sek).ctr_xor(&iv, payload);
}
}
fn random_bytes(out: &mut [u8]) {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut i = 0;
while i < out.len() {
let mut h = RandomState::new().build_hasher();
h.write_usize(i);
h.write_u128(now);
let v = h.finish().to_le_bytes();
let n = (out.len() - i).min(8);
out[i..i + n].copy_from_slice(&v[..n]);
i += n;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_key_material_with_correct_passphrase() {
let pass = b"correcthorsebatterystaple";
let km = KeyMaterial::generate(16);
let msg = km.to_message(pass);
let recovered = KeyMaterial::parse(pass, &msg).expect("unwrap with right passphrase");
assert_eq!(recovered.sek, km.sek);
assert_eq!(recovered.salt, km.salt);
}
#[test]
fn rejects_wrong_passphrase() {
let km = KeyMaterial::generate(32);
let msg = km.to_message(b"the-real-secret");
assert!(KeyMaterial::parse(b"a-different-secret", &msg).is_none());
}
#[test]
fn ctr_round_trips_and_is_seq_bound() {
let km = KeyMaterial::generate(16);
let plain = b"MPEG-TS payload riding inside an SRT data packet".to_vec();
let mut enc = plain.clone();
km.transform(42, &mut enc);
assert_ne!(enc, plain, "payload is actually encrypted");
let mut dec = enc.clone();
km.transform(42, &mut dec);
assert_eq!(dec, plain, "same seq decrypts");
let mut wrong = enc.clone();
km.transform(43, &mut wrong);
assert_ne!(wrong, plain);
}
#[test]
fn generate_is_random() {
let a = KeyMaterial::generate(16);
let b = KeyMaterial::generate(16);
assert_ne!(a.sek, b.sek, "SEKs differ across sessions");
assert_ne!(a.salt, b.salt, "salts differ across sessions");
}
#[test]
fn parses_at_all_key_lengths() {
for &klen in &[16usize, 24, 32] {
let pass = b"klen-test";
let km = KeyMaterial::generate(klen);
let msg = km.to_message(pass);
let got = KeyMaterial::parse(pass, &msg).unwrap();
assert_eq!(got.sek.len(), klen);
}
}
}