Skip to main content

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}