rose-squared-sdk 0.1.0

Privacy-preserving encrypted search SDK implementing the SWiSSSE protocol with forward/backward security and volume-hiding, compilable to WebAssembly
Documentation
// src/crypto/kdf.rs
//
// Key Derivation: password → MasterKeySet
//
// Design decisions (locked):
//   • Argon2id (RFC 9106) for password hardening — resists both side-channel
//     and GPU attacks.  Parameters: m=64 MiB, t=3, p=1 (browser-safe cost).
//   • HKDF-SHA-256 (RFC 5869) to fan out one root key into four domain-
//     separated sub-keys, one per protocol role.
//   • Each sub-key uses a distinct info label so a break in one role cannot
//     be leveraged to recover another.

use hkdf::Hkdf;
use sha2::Sha256;
use argon2::{Argon2, Algorithm, Version, Params};
use zeroize::Zeroizing;

use crate::crypto::primitives::{SecretKey, LAMBDA};
use crate::error::VaultError;

// ── Argon2id parameters ────────────────────────────────────────────────────────
// Tuned for a browser main thread: ~200 ms on a mid-range device.
// Operators may increase m_cost for higher-security deployments (native).
const ARGON2_M_COST: u32 = 65_536; // 64 MiB
const ARGON2_T_COST: u32 = 3;      // iterations
const ARGON2_P_COST: u32 = 1;      // parallelism (single-threaded WASM)
const ARGON2_OUTPUT: usize = 32;   // 256-bit root key

// ── HKDF domain labels (must never change — changing breaks existing state) ───
const INFO_TAG:   &[u8] = b"rose2:k_tag:v1";
const INFO_VAL:   &[u8] = b"rose2:k_val:v1";
const INFO_STATE: &[u8] = b"rose2:k_state:v1";
const INFO_SRCH:  &[u8] = b"rose2:k_srch:v1";

// ── MasterKeySet ──────────────────────────────────────────────────────────────

/// The four secret keys that drive the entire RO(SE)² protocol.
/// All four are derived deterministically from a user password + salt,
/// so they can be re-derived on any device that knows the password.
pub struct MasterKeySet {
    /// K_tag — derives EDB entry addresses (tags).
    /// `tag = HMAC-SHA256(k_tag, "tag:" || keyword || index || epoch)`
    pub k_tag:   SecretKey,

    /// K_val — encrypts document payloads stored in EDB entries.
    /// `val_key = HMAC-SHA256(k_val, "val:" || keyword || index || epoch)`
    pub k_val:   SecretKey,

    /// K_state — encrypts the local ClientStateTable before persistence.
    pub k_state: SecretKey,

    /// K_srch — derives per-search blinding factors (used in SWiSSSE phase).
    pub k_srch:  SecretKey,
}

impl MasterKeySet {
    /// Derive a full `MasterKeySet` from a UTF-8 password and a 16-byte salt.
    ///
    /// The salt must be unique per vault and stored alongside the encrypted
    /// state.  It does NOT need to be secret.
    ///
    /// # Errors
    /// Returns `VaultError::Kdf` if Argon2 parameters are rejected (internal).
    pub fn derive(password: &str, salt: &[u8; 16]) -> Result<Self, VaultError> {
        // Step 1: Argon2id — stretch the password into a 256-bit root key.
        let params = Params::new(ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(ARGON2_OUTPUT))
            .map_err(|e| VaultError::Kdf(e.to_string()))?;
        let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

        // Zeroizing<Vec> ensures the root key is wiped even if we return early.
        let mut root_key = Zeroizing::new([0u8; 32]);
        argon2
            .hash_password_into(password.as_bytes(), salt, root_key.as_mut())
            .map_err(|e| VaultError::Kdf(e.to_string()))?;

        // Step 2: HKDF-SHA256 — fan out the root key into four sub-keys.
        // The salt argument to HKDF is the vault salt (public), giving each
        // derivation a unique PRK even if two vaults share a password.
        let hk = Hkdf::<Sha256>::new(Some(salt), root_key.as_ref());

        Ok(Self {
            k_tag:   derive_subkey(&hk, INFO_TAG)?,
            k_val:   derive_subkey(&hk, INFO_VAL)?,
            k_state: derive_subkey(&hk, INFO_STATE)?,
            k_srch:  derive_subkey(&hk, INFO_SRCH)?,
        })
    }
}

/// Expand one HKDF output block into a `SecretKey`.
fn derive_subkey(hk: &Hkdf<Sha256>, info: &[u8]) -> Result<SecretKey, VaultError> {
    let mut out = [0u8; LAMBDA];
    hk.expand(info, &mut out)
        .map_err(|_| VaultError::Kdf("HKDF expand failed".into()))?;
    Ok(SecretKey::from_bytes(out))
}