Skip to main content

gbp_sframe/
kdf.rs

1use gbp_mls::MlsContext;
2use hkdf::Hkdf;
3use sha2::Sha256;
4
5use crate::error::SFrameError;
6
7/// SFrame ciphersuite selection.
8///
9/// `Aes128Gcm` is the standard choice; `Aes256Gcm` is available for
10/// high-assurance deployments.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum CipherSuite {
13    /// AES-128-GCM: 16-byte key, 12-byte nonce.
14    Aes128Gcm,
15    /// AES-256-GCM: 32-byte key, 12-byte nonce.
16    Aes256Gcm,
17}
18
19impl CipherSuite {
20    /// Key length in bytes.
21    pub(crate) fn key_len(self) -> usize {
22        match self {
23            Self::Aes128Gcm => 16,
24            Self::Aes256Gcm => 32,
25        }
26    }
27
28    /// Numeric discriminant used in the FFI (`0` = AES-128, `1` = AES-256).
29    pub fn from_u8(v: u8) -> Option<Self> {
30        match v {
31            0 => Some(Self::Aes128Gcm),
32            1 => Some(Self::Aes256Gcm),
33            _ => None,
34        }
35    }
36
37    /// Numeric discriminant.
38    pub fn as_u8(self) -> u8 {
39        match self {
40            Self::Aes128Gcm => 0,
41            Self::Aes256Gcm => 1,
42        }
43    }
44}
45
46/// Derived key material for one participant in one epoch.
47pub(crate) struct ParticipantKeys {
48    /// AES key: 16 bytes for AES-128-GCM, 32 bytes for AES-256-GCM.
49    pub key: Vec<u8>,
50    /// 12-byte base nonce (XOR'd with the counter to produce each frame nonce).
51    pub salt: [u8; 12],
52}
53
54/// Derives the 32-byte SFrame base key from the MLS `ExportSecret`.
55///
56/// `label` is the application-defined export label
57/// (e.g. `"gbp/sframe v1"`).
58/// `epoch` is passed as an 8-byte big-endian context to bind the key to the
59/// current MLS epoch.
60pub fn derive_base_key(
61    mls: &MlsContext,
62    label: &str,
63    epoch: u64,
64) -> Result<[u8; 32], SFrameError> {
65    let context = epoch.to_be_bytes();
66    let raw = mls
67        .export_raw(label, &context, 32)
68        .map_err(|e| SFrameError::MlsExport(e.to_string()))?;
69    let mut out = [0u8; 32];
70    out.copy_from_slice(&raw);
71    Ok(out)
72}
73
74/// Derives the encryption key and base salt for participant `leaf_index`.
75///
76/// Uses HKDF-Expand (SHA-256) over the epoch's `base_key` with
77/// deterministic info strings so every member can reproduce any sender's
78/// key material.
79pub(crate) fn derive_participant(
80    base_key: &[u8; 32],
81    leaf_index: u32,
82    suite: CipherSuite,
83) -> ParticipantKeys {
84    // SAFETY: base_key is 32 bytes = SHA-256 HashLen, so from_prk never panics.
85    let hk = Hkdf::<Sha256>::from_prk(base_key)
86        .expect("base_key is exactly SHA-256 HashLen (32 bytes)");
87
88    let leaf_be = leaf_index.to_be_bytes();
89
90    let mut key_info = b"gbp sframe key ".to_vec();
91    key_info.extend_from_slice(&leaf_be);
92    let mut key = vec![0u8; suite.key_len()];
93    hk.expand(&key_info, &mut key)
94        .expect("key length is well within 255 * HashLen");
95
96    let mut salt_info = b"gbp sframe salt ".to_vec();
97    salt_info.extend_from_slice(&leaf_be);
98    let mut salt = [0u8; 12];
99    hk.expand(&salt_info, &mut salt)
100        .expect("salt length (12) is well within 255 * HashLen");
101
102    ParticipantKeys { key, salt }
103}
104
105/// Constructs the 12-byte per-frame nonce:
106/// `participant_salt XOR (CTR_LE64 || 0x00_00_00_00)`.
107pub(crate) fn make_nonce(salt: &[u8; 12], ctr: u64) -> [u8; 12] {
108    let mut nonce = *salt;
109    let ctr_le = ctr.to_le_bytes(); // 8 bytes little-endian
110    for i in 0..8 {
111        nonce[i] ^= ctr_le[i];
112    }
113    nonce
114}