srt_runtime/crypto.rs
1//! SRT payload encryption — `draft-sharabayko-srt-01` §6 ("Encryption").
2//!
3//! Spec grounding: `specs/rules/srt-crypto.md` (curated §6, external-algorithm
4//! references grep-verified against RFC 3394 / RFC 8018 / RFC 2104 / FIPS 197
5//! / NIST SP 800-38A). Cross-refs: `specs/rules/srt-rules.md` §"Key Material
6//! message — §3.2.2" (wire layout of `Salt`/`ICV`/`xSEK`/`oSEK`/`KLen`, all
7//! carried opaquely by [`crate::packet::KeyMaterial`]) and §"Data packet —
8//! §3.1" (`KK` field, [`crate::packet::EncryptionKeyField`]).
9//!
10//! This module adds the crypto *primitives* §6 names but does not restate:
11//!
12//! - **AES-CTR** payload encrypt/decrypt ([`aes_ctr_apply`]) — §6.2.2/§6.3.2.
13//! - **RFC 3394 AES key wrap/unwrap** of the SEK ([`wrap_sek`]/[`unwrap_sek`])
14//! — §6.1.5/§6.2.1/§6.3.1.
15//! - **PBKDF2 (HMAC-SHA1) KEK derivation** from a passphrase
16//! ([`derive_kek`]) — §6.1.4/§6.2.1/§6.3.1.
17//!
18//! Gated behind the `crypto` feature; the default/no_std packet-codec core
19//! (this crate's `--no-default-features` build) pulls none of these
20//! dependencies.
21//!
22//! # ⚠ The draft's two IV formulas (srt-crypto.md, "Conflicting IV formula")
23//!
24//! `draft-sharabayko-srt-01` gives **two different, unreconciled** formulas
25//! for the AES-CTR IV:
26//!
27//! - §6.1.2 (Overview): build a 128-bit `{80 zero bits | 32-bit packet index
28//! | 16-bit block counter}` word and XOR its upper 112 bits with `IV =
29//! MSB(112, Salt)`.
30//! - §6.2.2 (Encryption Process) / §6.3.2 (Decryption Process), verbatim and
31//! identical in both places: `IV = (MSB(112, Salt) << 2) XOR (PktSeqNo)`.
32//!
33//! These are not algebraically the same construction, and the draft never
34//! reconciles them. No sample ciphertext exists anywhere in the draft to
35//! disambiguate by reproduction (`srt-crypto.md`, "No test vectors" note), so
36//! this module resolves it against the **reference implementation** instead:
37//! `libsrt` (`haivision/srt`) `haicrypt/hcrypt.h` `hcrypt_SetCtrIV` builds the
38//! counter as §6.1.2 describes — `{80 zero bits | 32-bit packet index | 16-bit
39//! block counter}` with `MSB(112, Salt)` XORed across the top 112 bits — and
40//! performs **no** `<< 2` shift. The §6.2.2/§6.3.2 `<< 2` is a draft-text
41//! artifact; implementing it literally yields a keystream no real SRT peer can
42//! decrypt. [`packet_counter`] therefore follows `hcrypt_SetCtrIV` (≡ the
43//! §6.1.2 layout), verified byte-for-byte against that macro in the tests.
44
45use aes::cipher::{KeyIvInit, StreamCipher};
46use aes::{Aes128, Aes192, Aes256};
47use aes_kw::{KekAes128, KekAes192, KekAes256};
48use alloc::vec;
49use alloc::vec::Vec;
50use ctr::Ctr128BE;
51use hmac::Hmac;
52use pbkdf2::pbkdf2;
53use sha1::Sha1;
54
55use crate::error::{Error, Result};
56use crate::packet::EncryptionKeyField;
57
58/// PBKDF2 iteration count mandated by the draft (`Iter = 2048`,
59/// §6.1.4/§6.2.1/§6.3.1 — identical value at every citation).
60pub const PBKDF2_ITERATIONS: u32 = 2048;
61
62/// Length of the 128-bit Key Material `Salt` field in bytes (`SLen/4 = 4`
63/// words — the only salt length the draft defines, `srt-rules.md` §3.2.2).
64pub const SALT_LEN: usize = 16;
65
66/// Number of least-significant bytes of the 128-bit `Salt` fed to PBKDF2 as
67/// its salt argument (`LSB(64,Salt)`, §6.2.1/§6.3.1: 64 bits = 8 bytes). The
68/// remaining (most-significant) bytes of `Salt` feed the AES-CTR IV instead
69/// (§6.1.2/§6.2.2/§6.3.2).
70const PBKDF2_SALT_LEN: usize = 8;
71
72/// Number of most-significant bytes of the 128-bit `Salt` used to build the
73/// AES-CTR IV (`MSB(112, Salt)`, §6.1.2/§6.2.2/§6.3.2: 112 bits = 14 bytes).
74const IV_SALT_LEN: usize = 14;
75
76/// AES block size in bytes (FIPS 197) — also the AES-CTR counter width
77/// (§6.1.2: "The counter for AES-CTR is the size of the cipher's block, i.e.
78/// 128 bits").
79const AES_BLOCK_LEN: usize = 16;
80
81/// A key length not one of AES-128/192/256's 16/24/32 bytes (`KLen/4` ∈
82/// `{4,6,8}`, `srt-rules.md` §3.2.2).
83fn invalid_key_length(what: &'static str) -> Error {
84 Error::InvalidField {
85 what,
86 reason: "length must be 16, 24, or 32 bytes (AES-128/192/256)",
87 }
88}
89
90// ---------------------------------------------------------------------------
91// §6.1.4/§6.2.1/§6.3.1 — KEK derivation (passphrase path).
92// ---------------------------------------------------------------------------
93
94/// Derive the Key Encrypting Key (KEK) from the pre-shared passphrase
95/// (`draft-sharabayko-srt-01` §6.1.4, §6.2.1 sender / §6.3.1 receiver —
96/// identical formula both sides):
97///
98/// ```text
99/// KEK = PBKDF2(passphrase, LSB(64,Salt), Iter=2048, KLen)
100/// ```
101///
102/// `salt` is the Key Material message's 128-bit `Salt` field; `klen` is the
103/// desired KEK length in bytes (16/24/32, matching the handshake's
104/// Encryption Field / the Key Material message's `KLen/4` — "the KEK has to
105/// be at least as long as the SEK", §6.1.4).
106///
107/// # Errors
108/// [`Error::InvalidField`] if `klen` is not 16, 24, or 32.
109pub fn derive_kek(passphrase: &[u8], salt: &[u8; SALT_LEN], klen: usize) -> Result<Vec<u8>> {
110 if !matches!(klen, 16 | 24 | 32) {
111 return Err(invalid_key_length("KLen"));
112 }
113 // LSB(64, Salt): the low/least-significant 8 bytes of the 128-bit,
114 // big-endian-wire Salt.
115 let pbkdf2_salt = &salt[SALT_LEN - PBKDF2_SALT_LEN..];
116 let mut kek = vec![0u8; klen];
117 pbkdf2::<Hmac<Sha1>>(passphrase, pbkdf2_salt, PBKDF2_ITERATIONS, &mut kek)
118 .expect("HMAC-SHA1 accepts any key length, so PBKDF2 cannot fail here");
119 Ok(kek)
120}
121
122// ---------------------------------------------------------------------------
123// §6.1.5/§6.2.1/§6.3.1 — RFC 3394 AES key wrap/unwrap of the SEK.
124// ---------------------------------------------------------------------------
125
126/// Wrap one or two SEKs with the KEK (RFC 3394 AES key wrap, external
127/// algorithm — `draft-sharabayko-srt-01` §6.1.5/§6.2.1: `Wrap = AESkw(KEK,
128/// SEK)`).
129///
130/// `plaintext_keys` is the concatenation of the SEK(s) being wrapped — one
131/// SEK's worth of bytes (Key Material `KK` = even/odd) or two concatenated
132/// SEKs (`KK` = both), per the Wrap-field length formula `n*KLen + 8` in
133/// `srt-rules.md` §"Key Material message — §3.2.2". Returns `(icv, wrapped)`
134/// to match [`crate::packet::KeyMaterial`]'s `icv`/`x_sek`/`o_sek` fields —
135/// RFC 3394's first 8-byte output block *is* the wrap's Integrity Check
136/// Vector (the encrypted default IV, RFC 3394 §2.2.3.1), and the remaining
137/// bytes are the wrapped key material, the same length as the input.
138///
139/// # Errors
140/// [`Error::InvalidField`] if `kek.len()` is not 16, 24, or 32, or if
141/// `plaintext_keys.len()` is not a multiple of 8 bytes (RFC 3394 operates on
142/// 64-bit semiblocks).
143pub fn wrap_sek(kek: &[u8], plaintext_keys: &[u8]) -> Result<([u8; 8], Vec<u8>)> {
144 let mut out = vec![0u8; plaintext_keys.len() + 8];
145 match kek.len() {
146 16 => KekAes128::try_from(kek)
147 .map_err(|_| invalid_key_length("KEK"))?
148 .wrap(plaintext_keys, &mut out),
149 24 => KekAes192::try_from(kek)
150 .map_err(|_| invalid_key_length("KEK"))?
151 .wrap(plaintext_keys, &mut out),
152 32 => KekAes256::try_from(kek)
153 .map_err(|_| invalid_key_length("KEK"))?
154 .wrap(plaintext_keys, &mut out),
155 _ => return Err(invalid_key_length("KEK")),
156 }
157 .map_err(|_| Error::InvalidField {
158 what: "SEK",
159 reason: "length must be a multiple of 8 bytes (RFC 3394 semiblocks)",
160 })?;
161 let mut icv = [0u8; 8];
162 icv.copy_from_slice(&out[..8]);
163 Ok((icv, out[8..].to_vec()))
164}
165
166/// Unwrap the SEK(s) with the KEK (inverse RFC 3394 AES key wrap —
167/// `draft-sharabayko-srt-01` §6.1.5/§6.3.1: `SEK = AESkuw(KEK, Wrap)`).
168///
169/// `icv`/`wrapped` are [`crate::packet::KeyMaterial`]'s `icv`/`x_sek` (or
170/// `o_sek`) fields. A wrap-integrity failure — wrong KEK (wrong passphrase)
171/// or corrupt wire data — is the spec's "it does not have the SEK" case
172/// (§6.1.5, L3799-3803/L3820-3823): a structured error, never a panic or
173/// silently-wrong plaintext.
174///
175/// # Errors
176/// [`Error::InvalidField`] if `kek.len()` is not 16, 24, or 32, if
177/// `wrapped.len()` is not a multiple of 8 bytes, or if the RFC 3394
178/// integrity check fails.
179pub fn unwrap_sek(kek: &[u8], icv: &[u8; 8], wrapped: &[u8]) -> Result<Vec<u8>> {
180 let mut input = Vec::with_capacity(8 + wrapped.len());
181 input.extend_from_slice(icv);
182 input.extend_from_slice(wrapped);
183 let mut out = vec![0u8; wrapped.len()];
184 let bad_wrap = || Error::InvalidField {
185 what: "AES key wrap",
186 reason: "integrity check failed (wrong KEK / passphrase, or corrupt wire data)",
187 };
188 match kek.len() {
189 16 => KekAes128::try_from(kek)
190 .map_err(|_| invalid_key_length("KEK"))?
191 .unwrap(&input, &mut out),
192 24 => KekAes192::try_from(kek)
193 .map_err(|_| invalid_key_length("KEK"))?
194 .unwrap(&input, &mut out),
195 32 => KekAes256::try_from(kek)
196 .map_err(|_| invalid_key_length("KEK"))?
197 .unwrap(&input, &mut out),
198 _ => return Err(invalid_key_length("KEK")),
199 }
200 .map_err(|_| bad_wrap())?;
201 Ok(out)
202}
203
204// ---------------------------------------------------------------------------
205// §6.1.2/§6.2.2/§6.3.2 — AES-CTR payload encrypt/decrypt.
206// ---------------------------------------------------------------------------
207
208/// Compute the 128-bit AES-CTR initial counter for one data packet.
209///
210/// This follows the **reference SRT construction** (`libsrt` `haicrypt/hcrypt.h`,
211/// `hcrypt_SetCtrIV`), which is what real SRT peers use on the wire:
212///
213/// ```text
214/// memset(iv, 0, 16); // all 16 bytes zeroed
215/// memcpy(&iv[10], &pki, 4); // 32-bit packet index at bytes [10..14]
216/// iv[0..14] ^= salt[0..14]; // XOR MSB(112, Salt) across the top 14 bytes
217/// // iv[14..16] = per-packet AES-block counter, starts 0
218/// //
219/// // byte: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
220/// // +-----------------------+-----------+-----+
221/// // | 0s | pki | ctr |
222/// // +-----------------------+-----------+-----+
223/// ```
224///
225/// **On the draft's `IV = (MSB(112, Salt) << 2) XOR PktSeqNo` (§6.2.2/§6.3.2):**
226/// the `<< 2` is a known artifact/typo of the draft text — the reference
227/// implementation does **no** left shift. Implementing the `<< 2` literally
228/// produces a keystream no real SRT peer can decrypt, so this function follows
229/// `hcrypt_SetCtrIV` (which is equivalent to §6.1.2's field layout: packet index
230/// in bits `[47:16]`, block counter in `[15:0]`). See the module doc's
231/// "conflicting IV formula" note.
232///
233/// The low 16 bits (`counter[14..16]`) are the per-packet AES-block counter,
234/// left at `0` here — a standard 128-bit big-endian CTR increments the whole
235/// counter per block, which only touches these low bits as long as a single
236/// packet's payload stays under `2^16` AES blocks (1 MiB); every SRT payload
237/// (bounded by a UDP datagram) is far smaller.
238///
239/// `pkt_seq_no` is the data packet's 31-bit Packet Sequence Number
240/// (`draft-sharabayko-srt-01` §3.1); bit 31 (the `F` header bit) is never part
241/// of it.
242pub fn packet_counter(salt: &[u8; SALT_LEN], pkt_seq_no: u32) -> [u8; AES_BLOCK_LEN] {
243 let mut counter = [0u8; AES_BLOCK_LEN];
244 // 32-bit packet index at bytes [10..14] (hcrypt.h: `memcpy(&iv[10], pki, 4)`).
245 counter[10..14].copy_from_slice(&pkt_seq_no.to_be_bytes());
246 // XOR MSB(112, Salt) — the top 14 bytes of the Salt — across bytes [0..14]
247 // (hcrypt.h: `hcrypt_XorStream(&iv[0], nonce, 112/8)`). No left shift.
248 for i in 0..IV_SALT_LEN {
249 counter[i] ^= salt[i];
250 }
251 // counter[14..16] stays 0 — the block counter starts at 0 for this packet.
252 counter
253}
254
255/// AES-CTR encrypt/decrypt one data packet's payload **in place**
256/// (`draft-sharabayko-srt-01` §6.2.2/§6.3.2). CTR mode is its own inverse —
257/// `EncryptedPayload = AES_CTR_Encrypt(SEK, IV, UnencryptedPayload)` and
258/// `DecryptedPayload = AES_CTR_Encrypt(SEK, IV, EncryptedPayload)` are the
259/// same operation (XOR with the same keystream), so this one function serves
260/// both directions.
261///
262/// `sek` selects AES-128/192/256 by its length (16/24/32 bytes); `salt` and
263/// `pkt_seq_no` feed [`packet_counter`]. No padding is applied or expected —
264/// CTR is a stream cipher (§6.1.1).
265///
266/// # Errors
267/// [`Error::InvalidField`] if `sek.len()` is not 16, 24, or 32.
268pub fn aes_ctr_apply(
269 sek: &[u8],
270 salt: &[u8; SALT_LEN],
271 pkt_seq_no: u32,
272 data: &mut [u8],
273) -> Result<()> {
274 let counter = packet_counter(salt, pkt_seq_no);
275 match sek.len() {
276 16 => {
277 let mut cipher =
278 Ctr128BE::<Aes128>::new_from_slices(sek, &counter).map_err(|_| bad_sek())?;
279 cipher.apply_keystream(data);
280 }
281 24 => {
282 let mut cipher =
283 Ctr128BE::<Aes192>::new_from_slices(sek, &counter).map_err(|_| bad_sek())?;
284 cipher.apply_keystream(data);
285 }
286 32 => {
287 let mut cipher =
288 Ctr128BE::<Aes256>::new_from_slices(sek, &counter).map_err(|_| bad_sek())?;
289 cipher.apply_keystream(data);
290 }
291 _ => return Err(bad_sek()),
292 }
293 Ok(())
294}
295
296fn bad_sek() -> Error {
297 invalid_key_length("SEK")
298}
299
300// ---------------------------------------------------------------------------
301// §3.1 `KK` — select the active SEK by odd/even parity.
302// ---------------------------------------------------------------------------
303
304/// Select the SEK to use for a data packet from its `KK` field
305/// (`draft-sharabayko-srt-01` §3.1/§6.1.6): `even`/`odd` are the two SEKs
306/// currently held (both may be live during the `±`KM-Pre-Announcement-Period`
307/// rekey transition window, §6.1.6).
308///
309/// # Errors
310/// [`Error::InvalidField`] if the packet is unencrypted (`KK = NotEncrypted`)
311/// or carries the control-packet-only reserved value, or if the selected
312/// parity's SEK is not currently held (e.g. not yet unwrapped, or already
313/// decommissioned — §6.3, step 11: such packets "must be dropped").
314pub fn select_sek<'a>(
315 key_flag: EncryptionKeyField,
316 even: Option<&'a [u8]>,
317 odd: Option<&'a [u8]>,
318) -> Result<&'a [u8]> {
319 let no_sek = |parity: &'static str| Error::InvalidField {
320 what: "SEK",
321 reason: match parity {
322 "even" => "even key not currently held",
323 _ => "odd key not currently held",
324 },
325 };
326 match key_flag {
327 EncryptionKeyField::Even => even.ok_or_else(|| no_sek("even")),
328 EncryptionKeyField::Odd => odd.ok_or_else(|| no_sek("odd")),
329 EncryptionKeyField::NotEncrypted => Err(Error::InvalidField {
330 what: "KK",
331 reason: "packet is not encrypted (KK=00b)",
332 }),
333 EncryptionKeyField::Reserved(_) => Err(Error::InvalidField {
334 what: "KK",
335 reason: "reserved value (11b) is control-packet-only, not valid on a data packet",
336 }),
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 // -----------------------------------------------------------------
345 // RFC 3394 §4.1 — "Wrap 128 bits of Key Data with a 128-bit KEK".
346 // <https://datatracker.ietf.org/doc/html/rfc3394#section-4.1>
347 // -----------------------------------------------------------------
348 const RFC3394_KEK_128: [u8; 16] = [
349 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
350 0x0F,
351 ];
352 const RFC3394_KEY_DATA_128: [u8; 16] = [
353 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
354 0xFF,
355 ];
356 const RFC3394_WRAPPED_128: [u8; 24] = [
357 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, 0x7B,
358 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5,
359 ];
360
361 #[test]
362 fn rfc3394_wrap_matches_worked_vector() {
363 let (icv, wrapped) = wrap_sek(&RFC3394_KEK_128, &RFC3394_KEY_DATA_128).unwrap();
364 assert_eq!(&icv[..], &RFC3394_WRAPPED_128[..8]);
365 assert_eq!(wrapped.as_slice(), &RFC3394_WRAPPED_128[8..]);
366 }
367
368 // -----------------------------------------------------------------
369 // packet_counter known-answer, hand-derived from libsrt
370 // haicrypt/hcrypt.h `hcrypt_SetCtrIV`:
371 // memset(iv,0,16); memcpy(&iv[10], pki, 4); iv[0..14] ^= salt[0..14];
372 // For salt = 01 02 … 10 and pki = 0x11223344:
373 // [0..10] = salt[0..10] = 01 02 03 04 05 06 07 08 09 0A
374 // [10] = salt[10]^0x11 = 0x0B^0x11 = 1A
375 // [11] = salt[11]^0x22 = 0x0C^0x22 = 2E
376 // [12] = salt[12]^0x33 = 0x0D^0x33 = 3E
377 // [13] = salt[13]^0x44 = 0x0E^0x44 = 4A
378 // [14..16] = 00 00 (block counter, salt[14..] unused)
379 // The old `MSB(112,Salt) << 2` construction gives byte[0]=0x04 (not 0x01)
380 // and different [10..14] — this test BITES against that regression.
381 // -----------------------------------------------------------------
382 #[test]
383 fn packet_counter_matches_libsrt_hcrypt_setctriv() {
384 let salt: [u8; SALT_LEN] = [
385 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
386 0x0F, 0x10,
387 ];
388 let expected: [u8; AES_BLOCK_LEN] = [
389 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x1A, 0x2E, 0x3E, 0x4A,
390 0x00, 0x00,
391 ];
392 let got = packet_counter(&salt, 0x1122_3344);
393 assert_eq!(got, expected, "counter must match hcrypt_SetCtrIV (no <<2)");
394 // Explicit guard against the buggy left-shift construction.
395 assert_eq!(got[0], 0x01, "byte 0 is pure salt, not the <<2 value 0x04");
396 }
397
398 #[test]
399 fn rfc3394_unwrap_matches_worked_vector() {
400 let mut icv = [0u8; 8];
401 icv.copy_from_slice(&RFC3394_WRAPPED_128[..8]);
402 let recovered = unwrap_sek(&RFC3394_KEK_128, &icv, &RFC3394_WRAPPED_128[8..]).unwrap();
403 assert_eq!(recovered.as_slice(), &RFC3394_KEY_DATA_128[..]);
404 }
405
406 #[test]
407 fn rfc3394_unwrap_rejects_wrong_kek() {
408 let mut icv = [0u8; 8];
409 icv.copy_from_slice(&RFC3394_WRAPPED_128[..8]);
410 let mut wrong_kek = RFC3394_KEK_128;
411 wrong_kek[0] ^= 0xFF;
412 assert!(unwrap_sek(&wrong_kek, &icv, &RFC3394_WRAPPED_128[8..]).is_err());
413 }
414
415 // -----------------------------------------------------------------
416 // NIST SP 800-38A Appendix F.5.1 — "CTR-AES128 (Encrypt)".
417 // <https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf>
418 // -----------------------------------------------------------------
419 const NIST_F5_1_KEY: [u8; 16] = [
420 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F,
421 0x3C,
422 ];
423 const NIST_F5_1_INIT_COUNTER: [u8; 16] = [
424 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE,
425 0xFF,
426 ];
427 const NIST_F5_1_PLAINTEXT: [u8; 64] = [
428 0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93, 0x17,
429 0x2A, 0xAE, 0x2D, 0x8A, 0x57, 0x1E, 0x03, 0xAC, 0x9C, 0x9E, 0xB7, 0x6F, 0xAC, 0x45, 0xAF,
430 0x8E, 0x51, 0x30, 0xC8, 0x1C, 0x46, 0xA3, 0x5C, 0xE4, 0x11, 0xE5, 0xFB, 0xC1, 0x19, 0x1A,
431 0x0A, 0x52, 0xEF, 0xF6, 0x9F, 0x24, 0x45, 0xDF, 0x4F, 0x9B, 0x17, 0xAD, 0x2B, 0x41, 0x7B,
432 0xE6, 0x6C, 0x37, 0x10,
433 ];
434 const NIST_F5_1_CIPHERTEXT: [u8; 64] = [
435 0x87, 0x4D, 0x61, 0x91, 0xB6, 0x20, 0xE3, 0x26, 0x1B, 0xEF, 0x68, 0x64, 0x99, 0x0D, 0xB6,
436 0xCE, 0x98, 0x06, 0xF6, 0x6B, 0x79, 0x70, 0xFD, 0xFF, 0x86, 0x17, 0x18, 0x7B, 0xB9, 0xFF,
437 0xFD, 0xFF, 0x5A, 0xE4, 0xDF, 0x3E, 0xDB, 0xD5, 0xD3, 0x5E, 0x5B, 0x4F, 0x09, 0x02, 0x0D,
438 0xB0, 0x3E, 0xAB, 0x1E, 0x03, 0x1D, 0xDA, 0x2F, 0xBE, 0x03, 0xD1, 0x79, 0x21, 0x70, 0xA0,
439 0xF3, 0x00, 0x9C, 0xEE,
440 ];
441
442 /// The NIST vector's counter is a raw 128-bit CTR seed, not this crate's
443 /// packet-counter construction — drive `Ctr128BE` directly to validate
444 /// the underlying AES-CTR primitive [`aes_ctr_apply`] wraps.
445 #[test]
446 fn nist_sp800_38a_f5_1_ctr_aes128_encrypt() {
447 let mut buf = NIST_F5_1_PLAINTEXT;
448 let mut cipher =
449 Ctr128BE::<Aes128>::new_from_slices(&NIST_F5_1_KEY, &NIST_F5_1_INIT_COUNTER).unwrap();
450 cipher.apply_keystream(&mut buf);
451 assert_eq!(buf, NIST_F5_1_CIPHERTEXT);
452 }
453
454 #[test]
455 fn nist_sp800_38a_f5_1_ctr_aes128_decrypt() {
456 // CTR is self-inverse: re-applying the keystream to the ciphertext
457 // recovers the plaintext.
458 let mut buf = NIST_F5_1_CIPHERTEXT;
459 let mut cipher =
460 Ctr128BE::<Aes128>::new_from_slices(&NIST_F5_1_KEY, &NIST_F5_1_INIT_COUNTER).unwrap();
461 cipher.apply_keystream(&mut buf);
462 assert_eq!(buf, NIST_F5_1_PLAINTEXT);
463 }
464
465 // -----------------------------------------------------------------
466 // SRT §6 payload round-trip (no spec test vectors exist for this —
467 // srt-crypto.md's "No test vectors" note — so this exercises the crate's
468 // own packet_counter + aes_ctr_apply against each other, not an
469 // external ground truth).
470 // -----------------------------------------------------------------
471
472 #[test]
473 fn srt_payload_round_trips_and_wrong_sek_does_not_recover() {
474 let sek = [0x42u8; 16];
475 let salt = [0x99u8; SALT_LEN];
476 let pkt_seq_no = 0x0123_4567u32;
477 let plaintext = b"SRT payload encryption round trip test vector.".to_vec();
478
479 let mut encrypted = plaintext.clone();
480 aes_ctr_apply(&sek, &salt, pkt_seq_no, &mut encrypted).unwrap();
481 assert_ne!(encrypted, plaintext, "encryption must change the bytes");
482
483 let mut decrypted = encrypted.clone();
484 aes_ctr_apply(&sek, &salt, pkt_seq_no, &mut decrypted).unwrap();
485 assert_eq!(
486 decrypted, plaintext,
487 "correct SEK must recover the plaintext"
488 );
489
490 let wrong_sek = [0x43u8; 16];
491 let mut wrongly_decrypted = encrypted;
492 aes_ctr_apply(&wrong_sek, &salt, pkt_seq_no, &mut wrongly_decrypted).unwrap();
493 assert_ne!(
494 wrongly_decrypted, plaintext,
495 "wrong SEK must not recover the plaintext"
496 );
497 }
498
499 #[test]
500 fn different_seq_no_gives_different_keystream() {
501 let sek = [0x11u8; 24];
502 let salt = [0x22u8; SALT_LEN];
503 let plaintext = [0u8; 32];
504
505 let mut a = plaintext;
506 aes_ctr_apply(&sek, &salt, 1, &mut a).unwrap();
507 let mut b = plaintext;
508 aes_ctr_apply(&sek, &salt, 2, &mut b).unwrap();
509 assert_ne!(a, b);
510 }
511
512 #[test]
513 fn kek_derivation_all_sizes_and_deterministic() {
514 for klen in [16usize, 24, 32] {
515 let salt = [0xABu8; SALT_LEN];
516 let kek1 = derive_kek(b"correct horse battery staple", &salt, klen).unwrap();
517 let kek2 = derive_kek(b"correct horse battery staple", &salt, klen).unwrap();
518 assert_eq!(kek1.len(), klen);
519 assert_eq!(kek1, kek2, "PBKDF2 is deterministic for the same inputs");
520
521 let different_salt = [0xACu8; SALT_LEN];
522 let kek3 = derive_kek(b"correct horse battery staple", &different_salt, klen).unwrap();
523 assert_ne!(kek1, kek3, "different salt must give a different KEK");
524 }
525 }
526
527 #[test]
528 fn invalid_klen_errs_without_panic() {
529 let salt = [0u8; SALT_LEN];
530 assert!(derive_kek(b"pw", &salt, 20).is_err());
531 assert!(wrap_sek(&[0u8; 20], &[0u8; 16]).is_err());
532 assert!(aes_ctr_apply(&[0u8; 20], &salt, 0, &mut [0u8; 4]).is_err());
533 }
534
535 #[test]
536 fn select_sek_picks_correct_parity_and_rejects_bad_flags() {
537 let even = [1u8; 16];
538 let odd = [2u8; 16];
539 assert_eq!(
540 select_sek(EncryptionKeyField::Even, Some(&even), Some(&odd)).unwrap(),
541 &even[..]
542 );
543 assert_eq!(
544 select_sek(EncryptionKeyField::Odd, Some(&even), Some(&odd)).unwrap(),
545 &odd[..]
546 );
547 assert!(select_sek(EncryptionKeyField::Even, None, Some(&odd)).is_err());
548 assert!(select_sek(EncryptionKeyField::NotEncrypted, Some(&even), Some(&odd)).is_err());
549 assert!(select_sek(EncryptionKeyField::Reserved(0b11), Some(&even), Some(&odd)).is_err());
550 }
551
552 #[test]
553 fn both_seks_wrap_unwrap_round_trip() {
554 // KK=Both: two concatenated SEKs wrapped under one KEK/ICV, matching
555 // the Wrap-field length formula `n*KLen + 8` (`srt-rules.md` §3.2.2).
556 let kek = [0x77u8; 16];
557 let even_sek = [0xAAu8; 16];
558 let odd_sek = [0xBBu8; 16];
559 let mut plaintext = Vec::new();
560 plaintext.extend_from_slice(&even_sek);
561 plaintext.extend_from_slice(&odd_sek);
562
563 let (icv, wrapped) = wrap_sek(&kek, &plaintext).unwrap();
564 assert_eq!(wrapped.len(), 32);
565 let recovered = unwrap_sek(&kek, &icv, &wrapped).unwrap();
566 assert_eq!(recovered, plaintext);
567 assert_eq!(&recovered[..16], &even_sek[..]);
568 assert_eq!(&recovered[16..], &odd_sek[..]);
569 }
570}