Skip to main content

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}