Skip to main content

rose_squared_sdk/crypto/
kdf.rs

1// src/crypto/kdf.rs
2//
3// Key Derivation: password → MasterKeySet
4//
5// Design decisions (locked):
6//   • Argon2id (RFC 9106) for password hardening — resists both side-channel
7//     and GPU attacks.  Parameters: m=64 MiB, t=3, p=1 (browser-safe cost).
8//   • HKDF-SHA-256 (RFC 5869) to fan out one root key into four domain-
9//     separated sub-keys, one per protocol role.
10//   • Each sub-key uses a distinct info label so a break in one role cannot
11//     be leveraged to recover another.
12
13use hkdf::Hkdf;
14use sha2::Sha256;
15use argon2::{Argon2, Algorithm, Version, Params};
16use zeroize::Zeroizing;
17
18use crate::crypto::primitives::{SecretKey, LAMBDA};
19use crate::error::VaultError;
20
21// ── Argon2id parameters ────────────────────────────────────────────────────────
22// Tuned for a browser main thread: ~200 ms on a mid-range device.
23// Operators may increase m_cost for higher-security deployments (native).
24const ARGON2_M_COST: u32 = 65_536; // 64 MiB
25const ARGON2_T_COST: u32 = 3;      // iterations
26const ARGON2_P_COST: u32 = 1;      // parallelism (single-threaded WASM)
27const ARGON2_OUTPUT: usize = 32;   // 256-bit root key
28
29// ── HKDF domain labels (must never change — changing breaks existing state) ───
30const INFO_TAG:   &[u8] = b"rose2:k_tag:v1";
31const INFO_VAL:   &[u8] = b"rose2:k_val:v1";
32const INFO_STATE: &[u8] = b"rose2:k_state:v1";
33const INFO_SRCH:  &[u8] = b"rose2:k_srch:v1";
34
35// ── MasterKeySet ──────────────────────────────────────────────────────────────
36
37/// The four secret keys that drive the entire RO(SE)² protocol.
38/// All four are derived deterministically from a user password + salt,
39/// so they can be re-derived on any device that knows the password.
40pub struct MasterKeySet {
41    /// K_tag — derives EDB entry addresses (tags).
42    /// `tag = HMAC-SHA256(k_tag, "tag:" || keyword || index || epoch)`
43    pub k_tag:   SecretKey,
44
45    /// K_val — encrypts document payloads stored in EDB entries.
46    /// `val_key = HMAC-SHA256(k_val, "val:" || keyword || index || epoch)`
47    pub k_val:   SecretKey,
48
49    /// K_state — encrypts the local ClientStateTable before persistence.
50    pub k_state: SecretKey,
51
52    /// K_srch — derives per-search blinding factors (used in SWiSSSE phase).
53    pub k_srch:  SecretKey,
54}
55
56impl MasterKeySet {
57    /// Derive a full `MasterKeySet` from a UTF-8 password and a 16-byte salt.
58    ///
59    /// The salt must be unique per vault and stored alongside the encrypted
60    /// state.  It does NOT need to be secret.
61    ///
62    /// # Errors
63    /// Returns `VaultError::Kdf` if Argon2 parameters are rejected (internal).
64    pub fn derive(password: &str, salt: &[u8; 16]) -> Result<Self, VaultError> {
65        // Step 1: Argon2id — stretch the password into a 256-bit root key.
66        let params = Params::new(ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(ARGON2_OUTPUT))
67            .map_err(|e| VaultError::Kdf(e.to_string()))?;
68        let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
69
70        // Zeroizing<Vec> ensures the root key is wiped even if we return early.
71        let mut root_key = Zeroizing::new([0u8; 32]);
72        argon2
73            .hash_password_into(password.as_bytes(), salt, root_key.as_mut())
74            .map_err(|e| VaultError::Kdf(e.to_string()))?;
75
76        // Step 2: HKDF-SHA256 — fan out the root key into four sub-keys.
77        // The salt argument to HKDF is the vault salt (public), giving each
78        // derivation a unique PRK even if two vaults share a password.
79        let hk = Hkdf::<Sha256>::new(Some(salt), root_key.as_ref());
80
81        Ok(Self {
82            k_tag:   derive_subkey(&hk, INFO_TAG)?,
83            k_val:   derive_subkey(&hk, INFO_VAL)?,
84            k_state: derive_subkey(&hk, INFO_STATE)?,
85            k_srch:  derive_subkey(&hk, INFO_SRCH)?,
86        })
87    }
88}
89
90/// Expand one HKDF output block into a `SecretKey`.
91fn derive_subkey(hk: &Hkdf<Sha256>, info: &[u8]) -> Result<SecretKey, VaultError> {
92    let mut out = [0u8; LAMBDA];
93    hk.expand(info, &mut out)
94        .map_err(|_| VaultError::Kdf("HKDF expand failed".into()))?;
95    Ok(SecretKey::from_bytes(out))
96}