Skip to main content

gbp_sframe/
cipher.rs

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