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}