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"; const SALT_LEN: usize = 16;
10const NONCE_LEN: usize = 24;
11
12#[wasm_bindgen]
14pub struct VaultCrypto;
15
16#[wasm_bindgen]
17impl VaultCrypto {
18 #[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 #[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 #[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
86fn 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}