p47h_engine/
vault.rs

1use argon2::{Algorithm, Argon2, Params, Version};
2use chacha20poly1305::{
3    aead::{Aead, KeyInit},
4    XChaCha20Poly1305, XNonce,
5};
6use wasm_bindgen::prelude::*;
7
8const MAGIC_BYTES: &[u8] = b"P47H_VAULT_V2"; // V2 for WASM vault
9const SALT_LEN: usize = 16;
10const NONCE_LEN: usize = 24;
11
12/// Crypto utilities for the Identity Vault
13#[wasm_bindgen]
14pub struct VaultCrypto;
15
16#[wasm_bindgen]
17impl VaultCrypto {
18    /// Encrypts data using XChaCha20Poly1305 with a key derived from Argon2id
19    /// Output format: [MAGIC_BYTES (13)] [SALT (16)] [NONCE (24)] [CIPHERTEXT]
20    #[wasm_bindgen]
21    pub fn encrypt_vault(data: &[u8], password: &str) -> Result<Vec<u8>, JsValue> {
22        let mut salt = [0u8; SALT_LEN];
23        getrandom::getrandom(&mut salt)
24            .map_err(|e| JsValue::from_str(&format!("RNG error: {}", e)))?;
25
26        let key = derive_key(password, &salt)?;
27
28        let mut nonce_bytes = [0u8; NONCE_LEN];
29        getrandom::getrandom(&mut nonce_bytes)
30            .map_err(|e| JsValue::from_str(&format!("RNG error: {}", e)))?;
31        let nonce = XNonce::from_slice(&nonce_bytes);
32
33        let cipher = XChaCha20Poly1305::new(&key);
34        let ciphertext = cipher
35            .encrypt(nonce, data)
36            .map_err(|e| JsValue::from_str(&format!("Encryption failed: {}", e)))?;
37
38        let mut result =
39            Vec::with_capacity(MAGIC_BYTES.len() + SALT_LEN + NONCE_LEN + ciphertext.len());
40        result.extend_from_slice(MAGIC_BYTES);
41        result.extend_from_slice(&salt);
42        result.extend_from_slice(&nonce_bytes);
43        result.extend_from_slice(&ciphertext);
44
45        Ok(result)
46    }
47
48    /// Decrypts a vault blob
49    #[wasm_bindgen]
50    pub fn decrypt_vault(blob: &[u8], password: &str) -> Result<Vec<u8>, JsValue> {
51        if blob.len() < MAGIC_BYTES.len() + SALT_LEN + NONCE_LEN {
52            return Err(JsValue::from_str("Invalid vault: too short"));
53        }
54
55        if &blob[0..MAGIC_BYTES.len()] != MAGIC_BYTES {
56            return Err(JsValue::from_str("Invalid vault: wrong magic bytes"));
57        }
58
59        let offset_salt = MAGIC_BYTES.len();
60        let offset_nonce = offset_salt + SALT_LEN;
61        let offset_cipher = offset_nonce + NONCE_LEN;
62
63        let salt = &blob[offset_salt..offset_nonce];
64        let nonce_bytes = &blob[offset_nonce..offset_cipher];
65        let ciphertext = &blob[offset_cipher..];
66
67        let key = derive_key(password, salt)?;
68        let cipher = XChaCha20Poly1305::new(&key);
69        let nonce = XNonce::from_slice(nonce_bytes);
70
71        let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|_| {
72            JsValue::from_str("Decryption failed: wrong password or corrupted data")
73        })?;
74
75        Ok(plaintext)
76    }
77
78    /// Derives a session key from password and salt
79    #[wasm_bindgen]
80    pub fn derive_session_key(password: &str, salt: &[u8]) -> Result<Vec<u8>, JsValue> {
81        let key = derive_key(password, salt)?;
82        Ok(key.to_vec())
83    }
84}
85
86/// Argon2id key derivation with WASM-optimized parameters.
87/// Memory: 19 MiB, Iterations: 2, Parallelism: 1
88fn derive_key(password: &str, salt: &[u8]) -> Result<chacha20poly1305::Key, JsValue> {
89    let mut output_key = [0u8; 32];
90
91    let params = Params::new(
92        Params::DEFAULT_M_COST,
93        Params::DEFAULT_T_COST,
94        Params::DEFAULT_P_COST,
95        Some(32),
96    )
97    .map_err(|e| JsValue::from_str(&format!("Argon2 params error: {}", e)))?;
98
99    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
100
101    argon2
102        .hash_password_into(password.as_bytes(), salt, &mut output_key)
103        .map_err(|e| JsValue::from_str(&format!("Key derivation failed: {}", e)))?;
104
105    Ok(*chacha20poly1305::Key::from_slice(&output_key))
106}