use aes::Aes128;
use ctr::Ctr128BE;
use ctr::cipher::{KeyIvInit, StreamCipher};
use crate::voip::hkdf_sha256;
pub use crate::voip::warp::{
WARP_MI_TAG_LEN, append_warp_mi_tag, compute_warp_mi_tag, verify_warp_mi_tag,
};
type AesCtr = Ctr128BE<Aes128>;
#[doc(hidden)]
#[derive(Clone)]
pub struct E2eSrtpKeys {
pub cipher_key: [u8; 16],
pub salt: [u8; 14],
pub auth_key: [u8; 20],
}
impl core::fmt::Debug for E2eSrtpKeys {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("E2eSrtpKeys([redacted])")
}
}
fn aes_cm_kdf(master_key: &[u8], master_salt: &[u8], label: u8, len: usize) -> Vec<u8> {
let mut iv = [0u8; 16];
iv[..14].copy_from_slice(&master_salt[..14]);
iv[7] ^= label;
let mut out = vec![0u8; len];
let mut cipher = AesCtr::new_from_slices(master_key, &iv).expect("16-byte key/iv");
cipher.apply_keystream(&mut out);
out
}
fn derive_session_keys_from_master(master: &[u8]) -> E2eSrtpKeys {
let master_key = &master[0..16];
let master_salt = &master[16..30];
let mut keys = E2eSrtpKeys {
cipher_key: [0u8; 16],
salt: [0u8; 14],
auth_key: [0u8; 20],
};
keys.cipher_key
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x00, 16));
keys.auth_key
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x01, 20));
keys.salt
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x02, 14));
keys
}
#[doc(hidden)]
pub fn derive_e2e_keys(call_key: &[u8], participant_lid: &str) -> Option<E2eSrtpKeys> {
if call_key.len() < 32 {
return None;
}
let master = hkdf_sha256(&[0u8; 32], &call_key[..32], participant_lid.as_bytes(), 46);
Some(derive_session_keys_from_master(&master))
}
#[doc(hidden)]
pub fn derive_e2e_keys_from_raw(raw_epoch: &[u8], participant_lid: &str) -> Option<E2eSrtpKeys> {
derive_e2e_keys(raw_epoch, participant_lid)
}
pub fn build_e2e_rtp_iv(salt: &[u8], ssrc: u32, roc: u32, seq: u16) -> [u8; 16] {
let mut iv = [0u8; 16];
let n = salt.len().min(14);
let off = 14 - n;
iv[off..off + n].copy_from_slice(&salt[..n]);
iv[4] ^= (ssrc >> 24) as u8;
iv[5] ^= (ssrc >> 16) as u8;
iv[6] ^= (ssrc >> 8) as u8;
iv[7] ^= ssrc as u8;
let packet_index = (roc as u64) * 0x1_0000 + (seq as u64);
let hi16 = ((packet_index >> 32) & 0xffff) as u16;
let lo32 = (packet_index & 0xffff_ffff) as u32;
iv[8] ^= (hi16 >> 8) as u8;
iv[9] ^= hi16 as u8;
iv[10] ^= (lo32 >> 24) as u8;
iv[11] ^= (lo32 >> 16) as u8;
iv[12] ^= (lo32 >> 8) as u8;
iv[13] ^= lo32 as u8;
iv
}
#[doc(hidden)]
pub fn crypt_payload(keys: &E2eSrtpKeys, ssrc: u32, seq: u16, roc: u32, payload: &[u8]) -> Vec<u8> {
let iv = build_e2e_rtp_iv(&keys.salt, ssrc, roc, seq);
let mut out = payload.to_vec();
let mut cipher = AesCtr::new_from_slices(&keys.cipher_key, &iv).expect("16-byte key/iv");
cipher.apply_keystream(&mut out);
out
}
pub const SRTCP_AUTH_TAG_LEN: usize = 10;
const RTCP_HEADER_LEN: usize = 8;
fn hmac_sha1_20(key: &[u8], data: &[u8]) -> [u8; 20] {
use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
let mut mac = Hmac::<Sha1>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
let mut tag = [0u8; 20];
tag.copy_from_slice(&mac.finalize().into_bytes());
tag
}
#[doc(hidden)]
pub fn derive_srtcp_keys(call_key: &[u8], participant_lid: &str) -> Option<E2eSrtpKeys> {
if call_key.len() < 32 {
return None;
}
let master = hkdf_sha256(&[0u8; 32], &call_key[..32], participant_lid.as_bytes(), 46);
let master_key = &master[0..16];
let master_salt = &master[16..30];
let mut keys = E2eSrtpKeys {
cipher_key: [0u8; 16],
salt: [0u8; 14],
auth_key: [0u8; 20],
};
keys.cipher_key
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x03, 16));
keys.auth_key
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x04, 20));
keys.salt
.copy_from_slice(&aes_cm_kdf(master_key, master_salt, 0x05, 14));
Some(keys)
}
#[doc(hidden)]
pub fn derive_srtcp_keys_from_raw(raw_epoch: &[u8], participant_lid: &str) -> Option<E2eSrtpKeys> {
derive_srtcp_keys(raw_epoch, participant_lid)
}
pub fn protect_srtcp(keys: &E2eSrtpKeys, sender_ssrc: u32, index: u32, rtcp: &[u8]) -> Vec<u8> {
let split = rtcp.len().min(RTCP_HEADER_LEN);
let iv = build_e2e_rtp_iv(
&keys.salt,
sender_ssrc,
index >> 16,
(index & 0xffff) as u16,
);
let mut out = Vec::with_capacity(rtcp.len() + 4 + SRTCP_AUTH_TAG_LEN);
out.extend_from_slice(&rtcp[..split]);
let mut body = rtcp[split..].to_vec();
let mut cipher = AesCtr::new_from_slices(&keys.cipher_key, &iv).expect("16-byte key/iv");
cipher.apply_keystream(&mut body);
out.extend_from_slice(&body);
out.extend_from_slice(&(0x8000_0000u32 | (index & 0x7fff_ffff)).to_be_bytes());
let tag = hmac_sha1_20(&keys.auth_key, &out);
out.extend_from_slice(&tag[..SRTCP_AUTH_TAG_LEN]);
out
}
pub fn unprotect_srtcp(
keys: &E2eSrtpKeys,
sender_ssrc: u32,
packet: &[u8],
) -> Option<(Vec<u8>, u32)> {
if packet.len() < RTCP_HEADER_LEN + 4 + SRTCP_AUTH_TAG_LEN {
return None;
}
let tag_start = packet.len() - SRTCP_AUTH_TAG_LEN;
let expected = hmac_sha1_20(&keys.auth_key, &packet[..tag_start]);
if !bool::from(subtle::ConstantTimeEq::ct_eq(
&packet[tag_start..],
&expected[..SRTCP_AUTH_TAG_LEN],
)) {
return None;
}
let idx_start = tag_start - 4;
let index = u32::from_be_bytes(packet[idx_start..tag_start].try_into().ok()?) & 0x7fff_ffff;
let iv = build_e2e_rtp_iv(
&keys.salt,
sender_ssrc,
index >> 16,
(index & 0xffff) as u16,
);
let mut out = packet[..RTCP_HEADER_LEN].to_vec();
let mut body = packet[RTCP_HEADER_LEN..idx_start].to_vec();
let mut cipher = AesCtr::new_from_slices(&keys.cipher_key, &iv).expect("16-byte key/iv");
cipher.apply_keystream(&mut body);
out.extend_from_slice(&body);
Some((out, index))
}
#[derive(Default)]
pub(crate) struct RocTracker {
roc: u32,
last_seq: u16,
initialized: bool,
}
impl RocTracker {
pub fn advance(&mut self, seq: u16) -> u32 {
if !self.initialized {
self.last_seq = seq;
self.initialized = true;
return self.roc;
}
if (seq as i32 - self.last_seq as i32) < -32768 {
self.roc = self.roc.wrapping_add(1);
}
self.last_seq = seq;
self.roc
}
}
#[derive(Default)]
pub(crate) struct RecvRocTracker {
roc: u32,
s_l: u16,
initialized: bool,
}
impl RecvRocTracker {
pub fn estimate_roc(&self, seq: u16) -> u32 {
if !self.initialized {
return self.roc;
}
if self.s_l < 0x8000 {
if (seq as i32 - self.s_l as i32) > 0x8000 {
self.roc.wrapping_sub(1) } else {
self.roc
}
} else if (self.s_l as i32 - seq as i32) > 0x8000 {
self.roc.wrapping_add(1) } else {
self.roc
}
}
pub fn commit_roc(&mut self, v: u32, seq: u16) {
if !self.initialized {
self.s_l = seq;
self.initialized = true;
return;
}
if v == self.roc {
if seq > self.s_l {
self.s_l = seq;
}
} else if v == self.roc.wrapping_add(1) {
self.roc = v;
self.s_l = seq;
}
}
#[cfg(test)]
pub fn guess_roc(&mut self, seq: u16) -> u32 {
let v = self.estimate_roc(seq);
self.commit_roc(v, seq);
v
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::voip::testkat::{hexd, kats};
fn keys_from(k: &serde_json::Value, who: &str) -> E2eSrtpKeys {
let mut keys = E2eSrtpKeys {
cipher_key: [0u8; 16],
salt: [0u8; 14],
auth_key: [0u8; 20],
};
keys.cipher_key
.copy_from_slice(&hexd(k, &["e2e_srtp", &format!("{who}_cipherKey")]));
keys.salt
.copy_from_slice(&hexd(k, &["e2e_srtp", &format!("{who}_salt")]));
keys.auth_key
.copy_from_slice(&hexd(k, &["e2e_srtp", &format!("{who}_authKey")]));
keys
}
#[test]
fn srtcp_round_trips_and_authenticates() {
let call_key: Vec<u8> = (0u8..40).collect();
let lid = "12345:0@lid";
let srtp = derive_e2e_keys(&call_key, lid).unwrap();
let srtcp = derive_srtcp_keys(&call_key, lid).unwrap();
assert_ne!(
srtp.cipher_key, srtcp.cipher_key,
"SRTCP must use its own key"
);
assert_ne!(srtp.salt, srtcp.salt);
let ssrc: u32 = 0x0a0b_0c0d;
let mut sr = vec![0x80, 200, 0x00, 0x06];
sr.extend_from_slice(&ssrc.to_be_bytes());
sr.extend_from_slice(&[0x11; 20]);
let protected = protect_srtcp(&srtcp, ssrc, 0, &sr);
assert_eq!(protected.len(), sr.len() + 4 + SRTCP_AUTH_TAG_LEN);
assert_eq!(&protected[..8], &sr[..8], "header/SSRC left in the clear");
assert_ne!(&protected[8..28], &sr[8..28], "body must be encrypted");
assert_eq!(
protected[protected.len() - SRTCP_AUTH_TAG_LEN - 4] & 0x80,
0x80
);
let (plain, index) = unprotect_srtcp(&srtcp, ssrc, &protected).unwrap();
assert_eq!(plain, sr);
assert_eq!(index, 0);
let mut forged = protected.clone();
*forged.last_mut().unwrap() ^= 1;
assert_eq!(unprotect_srtcp(&srtcp, ssrc, &forged), None);
let p1 = protect_srtcp(&srtcp, ssrc, 1, &sr);
assert_ne!(
protected[8..28],
p1[8..28],
"a new index must change the ciphertext"
);
let (plain, index) = unprotect_srtcp(&srtcp, ssrc, &p1).unwrap();
assert_eq!(plain, sr);
assert_eq!(index, 1);
}
#[test]
fn derive_e2e_keys_matches_kat() {
let k = kats();
let call_key = hexd(&k, &["inputs", "callKey"]);
let peer = derive_e2e_keys(&call_key, k["inputs"]["peerLid"].as_str().unwrap()).unwrap();
let expect = keys_from(&k, "peer");
assert_eq!(peer.cipher_key, expect.cipher_key, "peer cipher_key");
assert_eq!(peer.salt, expect.salt, "peer salt");
assert_eq!(peer.auth_key, expect.auth_key, "peer auth_key");
let self_keys =
derive_e2e_keys(&call_key, k["inputs"]["selfLid"].as_str().unwrap()).unwrap();
let expect_self = keys_from(&k, "self");
assert_eq!(
self_keys.cipher_key, expect_self.cipher_key,
"self cipher_key"
);
assert_eq!(self_keys.auth_key, expect_self.auth_key, "self auth_key");
}
#[test]
fn rtp_iv_matches_kat() {
let k = kats();
let peer = keys_from(&k, "peer");
let ssrc = k["inputs"]["ssrc"].as_u64().unwrap() as u32;
let seq = k["inputs"]["seq"].as_u64().unwrap() as u16;
let roc = k["inputs"]["roc"].as_u64().unwrap() as u32;
let iv = build_e2e_rtp_iv(&peer.salt, ssrc, roc, seq);
assert_eq!(hex::encode(iv), k["e2e_srtp"]["rtpIv"].as_str().unwrap());
}
#[test]
fn roc_tracker_wraps() {
let mut tx = RocTracker::default();
assert_eq!(tx.advance(0xFFFE), 0); assert_eq!(tx.advance(0xFFFF), 0);
assert_eq!(tx.advance(0x0000), 1, "0xFFFF→0x0000 bumps ROC");
assert_eq!(tx.advance(0x0001), 1);
assert_eq!(tx.advance(0x0000), 1, "a backward dip does not bump ROC");
assert_eq!(tx.advance(0x0001), 1);
for s in [0x7000u16, 0xE000, 0xFFFF] {
tx.advance(s);
}
assert_eq!(tx.advance(0x0000), 2, "second wrap gives ROC=2");
let mut rx = RecvRocTracker::default();
assert_eq!(rx.guess_roc(0xFFFE), 0); assert_eq!(rx.guess_roc(0xFFFF), 0);
assert_eq!(rx.guess_roc(0x0000), 1, "0xFFFF→0x0000 bumps ROC");
assert_eq!(rx.guess_roc(0x0001), 1);
assert_eq!(
rx.guess_roc(0x0000),
1,
"a reordered dip stays in the same ROC"
);
assert_eq!(rx.guess_roc(0x0002), 1, "state intact after the dip");
for s in [0x7000u16, 0xE000, 0xFFFF] {
assert_eq!(rx.guess_roc(s), 1);
}
assert_eq!(rx.guess_roc(0x0000), 2, "second wrap gives ROC=2");
assert_eq!(
rx.guess_roc(0xFFF0),
1,
"reordered late packet returns the lower ROC"
);
assert_eq!(
rx.guess_roc(0x0001),
2,
"state not corrupted by the late packet"
);
}
#[test]
fn unauthenticated_staircase_cannot_advance_roc_without_commit() {
let mut rx = RecvRocTracker::default();
rx.guess_roc(0x7FFE); assert_eq!(rx.roc, 0);
let _ = rx.estimate_roc(0xFFFE);
let _ = rx.estimate_roc(0x7FFD);
assert_eq!(rx.roc, 0, "estimate alone must not advance the ROC");
assert_eq!(
rx.estimate_roc(0x7FFF),
0,
"a legit in-window seq still maps to roc=0"
);
rx.commit_roc(rx.estimate_roc(0xFFFE), 0xFFFE);
rx.commit_roc(rx.estimate_roc(0x7FFD), 0x7FFD);
assert_eq!(
rx.roc, 1,
"committing the staircase advances the ROC (the pre-fix desync)"
);
}
#[test]
fn crypt_payload_matches_kat() {
let k = kats();
let peer = keys_from(&k, "peer");
let ssrc = k["inputs"]["ssrc"].as_u64().unwrap() as u32;
let seq = k["inputs"]["seq"].as_u64().unwrap() as u16;
let roc = k["inputs"]["roc"].as_u64().unwrap() as u32;
let payload = hexd(&k, &["inputs", "payload"]);
let ct = crypt_payload(&peer, ssrc, seq, roc, &payload);
assert_eq!(
hex::encode(&ct),
k["e2e_srtp"]["cipher_out"].as_str().unwrap()
);
let pt = crypt_payload(&peer, ssrc, seq, roc, &ct);
assert_eq!(pt, payload);
}
#[test]
fn build_iv_tolerates_oversized_salt() {
let _ = build_e2e_rtp_iv(&[0u8; 16], 0x0102_0304, 0, 0);
let _ = build_e2e_rtp_iv(&[0xABu8; 32], 0xdead_beef, 7, 0xFFFF);
let iv = build_e2e_rtp_iv(&[0x11u8; 14], 0, 0, 0);
assert_eq!(&iv[0..14], &[0x11u8; 14]);
assert_eq!(&iv[14..16], &[0u8; 2]);
}
#[test]
fn crypt_payload_roundtrips_across_seq_wrap() {
let keys = E2eSrtpKeys {
cipher_key: [7u8; 16],
salt: [9u8; 14],
auth_key: [0u8; 20],
};
let ssrc = 0x5741_0001u32;
let seqs = [0xFFFEu16, 0xFFFF, 0x0000, 0x0001];
let plaintexts: Vec<Vec<u8>> = (0..4u8).map(|i| vec![i.wrapping_mul(37); 40]).collect();
let mut send_roc = RocTracker::default();
let sent: Vec<(u16, Vec<u8>)> = seqs
.iter()
.zip(&plaintexts)
.map(|(&seq, pt)| {
let roc = send_roc.advance(seq);
(seq, crypt_payload(&keys, ssrc, seq, roc, pt))
})
.collect();
let mut recv_roc = RecvRocTracker::default();
for &i in &[0usize, 1, 3, 2] {
let (seq, ct) = &sent[i];
let roc = recv_roc.guess_roc(*seq);
let recovered = crypt_payload(&keys, ssrc, *seq, roc, ct);
assert_eq!(
&recovered, &plaintexts[i],
"seq {seq:#06x} must decrypt across the wrap"
);
}
}
}