gbp_sframe/cipher.rs
1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use sframe::frame::{EncryptedFrameView, MediaFrameView, MonotonicCounter, ReplayAttackProtection};
5use sframe::header::KeyId;
6use sframe::key::{DecryptionKey, EncryptionKey};
7
8use crate::error::SFrameError;
9use crate::header::SFrameHeader;
10use crate::kdf::CipherSuite;
11
12/// Replay-window tolerance in frames (gbp's historical fixed 1024-frame window).
13const REPLAY_WINDOW: u64 = 1024;
14
15// ─── SFrameEncryptor ─────────────────────────────────────────────────────────
16
17/// The key + counter for one `(epoch, leaf_index)` KID.
18///
19/// Shared behind an [`Arc<Mutex<_>>`] by every [`SFrameEncryptor`] handle for
20/// that KID, so cloning a handle - or obtaining a new one from
21/// [`crate::SFrameSession::encryptor`] - can never produce a second,
22/// independent counter that would reuse a `(key, KID, CTR)` nonce.
23struct EncryptorState {
24 key: EncryptionKey,
25 counter: MonotonicCounter,
26}
27
28/// Stateful per-sender SFrame encryptor handle.
29///
30/// Cloning a handle, or requesting another one for the same `(epoch,
31/// leaf_index)` via [`crate::SFrameSession::encryptor`], shares the same
32/// underlying counter - it does **not** create an independent one. This
33/// makes it safe to hold multiple handles (e.g. one per thread) for the same
34/// sender without risking AEAD nonce reuse.
35#[derive(Clone)]
36pub struct SFrameEncryptor {
37 state: Arc<Mutex<EncryptorState>>,
38 kid: KeyId,
39}
40
41impl SFrameEncryptor {
42 pub(crate) fn new(base_key: &[u8; 32], kid: u64, suite: CipherSuite) -> Self {
43 let key = EncryptionKey::derive_from(suite.to_sframe(), kid, base_key)
44 .expect("key derivation from a 32-byte base key never fails");
45 Self {
46 state: Arc::new(Mutex::new(EncryptorState {
47 key,
48 counter: MonotonicCounter::default(),
49 })),
50 kid,
51 }
52 }
53
54 /// Encrypts `plaintext` and returns the complete SFrame payload:
55 /// `header ‖ ciphertext ‖ GCM-tag`.
56 ///
57 /// `extra_aad` is bound into the AEAD tag (e.g. an RTP header) but is **not**
58 /// carried in the returned payload; the receiver supplies the same slice to
59 /// [`SFrameDecryptor::decrypt`].
60 ///
61 /// Safe to call concurrently from multiple handles sharing this sender's
62 /// state: the counter is allocated under a lock, so concurrent calls
63 /// never allocate the same value twice.
64 pub fn encrypt(&mut self, plaintext: &[u8], extra_aad: &[u8]) -> Result<Vec<u8>, SFrameError> {
65 let mut state = self
66 .state
67 .lock()
68 .expect("sframe encryptor state mutex poisoned");
69 let frame = MediaFrameView::with_meta_data(&mut state.counter, plaintext, extra_aad);
70 let encrypted = frame
71 .encrypt(&state.key)
72 .map_err(|_| SFrameError::Encrypt)?;
73
74 // sframe serialises `meta_data ‖ header ‖ ciphertext`; strip the
75 // metadata prefix so `extra_aad` stays off the wire.
76 Ok(encrypted.as_ref()[extra_aad.len()..].to_vec())
77 }
78
79 /// Current counter value (number of frames encrypted so far).
80 pub fn counter(&self) -> u64 {
81 self.state
82 .lock()
83 .expect("sframe encryptor state mutex poisoned")
84 .counter
85 .current()
86 }
87
88 /// KID this encryptor was created for.
89 pub fn kid(&self) -> u64 {
90 self.kid
91 }
92}
93
94// ─── SFrameDecryptor ─────────────────────────────────────────────────────────
95
96/// Per-sender decryption state maintained inside [`SFrameDecryptor`].
97///
98/// One [`ReplayAttackProtection`] per KID: sframe's validator keys off the
99/// counter only (it ignores the KID), so a shared validator would mix
100/// interleaved senders' counters and raise false rejections.
101struct SenderState {
102 key: DecryptionKey,
103 replay: ReplayAttackProtection,
104}
105
106/// Multi-sender SFrame decryptor for one epoch.
107///
108/// Lazily derives per-sender key material and replay state from the epoch's
109/// base key as new `KID`s are encountered.
110///
111/// Obtain via [`crate::SFrameSession::decryptor`].
112pub struct SFrameDecryptor {
113 base_key: [u8; 32],
114 epoch: u64,
115 suite: CipherSuite,
116 /// Keyed by KID.
117 senders: HashMap<KeyId, SenderState>,
118}
119
120impl SFrameDecryptor {
121 pub(crate) fn new(base_key: [u8; 32], epoch: u64, suite: CipherSuite) -> Self {
122 Self {
123 base_key,
124 epoch,
125 suite,
126 senders: HashMap::new(),
127 }
128 }
129
130 /// Decrypts an SFrame `payload` and returns `(plaintext, sender_leaf)`.
131 ///
132 /// `extra_aad` must be the same slice passed on the encrypting side.
133 pub fn decrypt(
134 &mut self,
135 payload: &[u8],
136 extra_aad: &[u8],
137 ) -> Result<(Vec<u8>, u32), SFrameError> {
138 let view = EncryptedFrameView::try_with_meta_data(&payload, &extra_aad)
139 .map_err(|e| SFrameError::Header(e.to_string()))?;
140
141 let kid = view.header().key_id();
142 let ctr = view.header().counter();
143
144 if SFrameHeader::epoch_from_kid(kid) != SFrameHeader::epoch_lsb(self.epoch) {
145 return Err(SFrameError::UnknownKid(kid));
146 }
147 let leaf = SFrameHeader::leaf_from_kid(kid);
148
149 let suite = self.suite;
150 let base_key = self.base_key;
151 let state = self.senders.entry(kid).or_insert_with(|| SenderState {
152 key: DecryptionKey::derive_from(suite.to_sframe(), kid, base_key)
153 .expect("key derivation from a 32-byte base key never fails"),
154 replay: ReplayAttackProtection::with_tolerance(REPLAY_WINDOW),
155 });
156
157 // Replay check before decryption (matches sframe's example flow).
158 let view = view
159 .validate(&state.replay)
160 .map_err(|_| SFrameError::Replay { kid, ctr })?;
161
162 let media = view.decrypt(&state.key).map_err(|_| SFrameError::Decrypt)?;
163 Ok((media.payload().to_vec(), leaf))
164 }
165
166 /// Drops all per-sender key + replay state (call on epoch change).
167 pub fn reset(&mut self) {
168 self.senders.clear();
169 }
170}