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/aead.rs
//
// Authenticated Encryption: AES-256-GCM wrappers.
//
// Design decisions (locked):
//   • AES-256-GCM (NIST SP 800-38D) — provides confidentiality + integrity.
//     The GCM authentication tag catches any server-side tampering.
//   • Random 12-byte nonce prepended to every ciphertext.
//     Format: [nonce: 12 B][ciphertext][GCM tag: 16 B]
//   • Per-entry keys (derived from K_val) so nonce-reuse across entries is
//     impossible in practice even if the RNG is weak.

use aes_gcm::{
    aead::{Aead, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce,
};
use rand::RngCore;

use crate::crypto::primitives::EncValue;
use crate::error::VaultError;

const NONCE_LEN: usize = 12;

// ── Encrypt ────────────────────────────────────────────────────────────────────

/// Encrypt `plaintext` under a 256-bit `key`.
/// Returns an `EncValue` whose bytes are:  nonce (12) || ciphertext || tag (16)
///
/// A fresh random nonce is generated for every call.
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<EncValue, VaultError> {
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));

    // Generate a random 96-bit nonce (NIST recommendation for GCM).
    let mut nonce_bytes = [0u8; NONCE_LEN];
    OsRng.fill_bytes(&mut nonce_bytes);
    let nonce = Nonce::from_slice(&nonce_bytes);

    let ciphertext = cipher
        .encrypt(nonce, plaintext)
        .map_err(|_| VaultError::Crypto("AES-GCM encrypt failed".into()))?;

    // Prepend the nonce so decryption is self-contained.
    let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
    out.extend_from_slice(&nonce_bytes);
    out.extend_from_slice(&ciphertext);

    Ok(EncValue(out))
}

// ── Decrypt ────────────────────────────────────────────────────────────────────

/// Decrypt and verify an `EncValue` produced by `encrypt`.
///
/// Returns `VaultError::Tampered` if the GCM tag fails — this means the
/// server returned a corrupted or forged entry.
pub fn decrypt(key: &[u8; 32], value: &EncValue) -> Result<Vec<u8>, VaultError> {
    let bytes = &value.0;
    if bytes.len() < NONCE_LEN + 16 {
        // Minimum: nonce (12) + empty plaintext + GCM tag (16) = 28 bytes.
        return Err(VaultError::Crypto("EncValue too short".into()));
    }

    let (nonce_bytes, ciphertext) = bytes.split_at(NONCE_LEN);
    let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
    let nonce  = Nonce::from_slice(nonce_bytes);

    cipher
        .decrypt(nonce, ciphertext)
        .map_err(|_| VaultError::Tampered)
}