whisky_wallet/encryption/
cipher.rs

1use aes_gcm::{aead::AeadMut, Aes256Gcm, KeyInit, Nonce}; // AES-GCM encryption
2use base64::{engine::general_purpose, Engine as _};
3use pbkdf2::pbkdf2_hmac; // PBKDF2 key derivation
4use rand::RngCore; // Random number generation
5use serde_json::json;
6use sha2::Sha256;
7use whisky_common::WError; // New base64 encoding
8
9const IV_LENGTH: usize = 16;
10
11pub fn encrypt_with_cipher(
12    data: &str,
13    key: &str,
14    initialization_vector_size: Option<usize>,
15) -> Result<String, WError> {
16    // Validate the initialization vector size
17    let initialization_vector_size = initialization_vector_size.unwrap_or(IV_LENGTH);
18
19    // Generate a random salt for PBKDF2 key derivation
20    let mut salt = vec![0u8; initialization_vector_size];
21    rand::thread_rng().fill_bytes(&mut salt);
22
23    let mut derived_key = vec![0u8; 32]; // AES-256 requires a 256-bit key (32 bytes)
24
25    // PBKDF2 key derivation (HMAC-SHA-256)
26    pbkdf2_hmac::<Sha256>(key.as_bytes(), &salt, 100_000, &mut derived_key);
27
28    // Initialize AES-GCM cipher
29    let mut cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(WError::from_err(
30        "encrypt_with_cipher - Aes256Gcm::new_from_slice",
31    ))?;
32
33    // Generate a random IV
34    let mut iv = vec![0u8; initialization_vector_size];
35    rand::thread_rng().fill_bytes(&mut iv);
36    let nonce = Nonce::from_slice(&iv); // AES-GCM requires a 12-byte nonce
37
38    // Encrypt the data
39    let ciphertext = cipher
40        .encrypt(nonce, data.as_bytes())
41        .map_err(WError::from_err("encrypt_with_cipher - cipher.encrypt"))?;
42
43    // Return the encrypted data as a JSON-like string (base64 encoding)
44    let iv_base64 = general_purpose::STANDARD.encode(&iv);
45    let salt_base64 = general_purpose::STANDARD.encode(&salt);
46    let ciphertext_base64 = general_purpose::STANDARD.encode(&ciphertext);
47
48    let result = json!({
49        "iv": iv_base64,
50        "salt": salt_base64,
51        "ciphertext": ciphertext_base64,
52    });
53
54    Ok(result.to_string())
55}
56
57pub fn decrypt_with_cipher(encrypted_data_json: &str, key: &str) -> Result<String, WError> {
58    // Parse the encrypted data from JSON
59    let encrypted_data: serde_json::Value = serde_json::from_str(encrypted_data_json).map_err(
60        WError::from_err("decrypt_with_cipher - JSON parsing failed"),
61    )?;
62
63    let iv_base64 = encrypted_data["iv"]
64        .as_str()
65        .ok_or_else(WError::from_opt("decrypt_with_cipher", "Missing IV"))?;
66    let ciphertext_base64 = encrypted_data["ciphertext"]
67        .as_str()
68        .ok_or_else(WError::from_opt(
69            "decrypt_with_cipher",
70            "Missing ciphertext",
71        ))?;
72
73    // Decode the IV and ciphertext from base64
74    let iv = general_purpose::STANDARD
75        .decode(iv_base64)
76        .map_err(WError::from_err(
77            "decrypt_with_cipher - Base64 decode of IV failed",
78        ))?;
79    let ciphertext = general_purpose::STANDARD
80        .decode(ciphertext_base64)
81        .map_err(WError::from_err(
82            "decrypt_with_cipher - Base64 decode of ciphertext failed",
83        ))?;
84
85    // Handle salt - support both new format (with salt) and legacy format (without salt)
86    let salt = if let Some(salt_base64) = encrypted_data["salt"].as_str() {
87        // New format: use the provided salt
88        if !salt_base64.is_empty() {
89            general_purpose::STANDARD
90                .decode(salt_base64)
91                .map_err(WError::from_err(
92                    "decrypt_with_cipher - Base64 decode of salt failed",
93                ))?
94        } else {
95            // Empty salt string: use zero-filled salt of IV length for backward compatibility
96            vec![0u8; iv.len()]
97        }
98    } else {
99        // Legacy format: use zero-filled salt of IV length for backward compatibility
100        vec![0u8; iv.len()]
101    };
102
103    // Derive a cryptographic key from the input key using PBKDF2 and SHA-256
104    // Matches frontend: 100,000 iterations, SHA-256, 256-bit key
105    let mut derived_key = vec![0u8; 32]; // AES-256 requires a 256-bit key (32 bytes)
106
107    // PBKDF2 key derivation (HMAC-SHA-256)
108    pbkdf2_hmac::<Sha256>(key.as_bytes(), &salt, 100_000, &mut derived_key);
109
110    // Initialize AES-GCM cipher for decryption
111    let mut cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(WError::from_err(
112        "decrypt_with_cipher - Aes256Gcm::new_from_slice",
113    ))?;
114
115    // Create a nonce from the IV
116    let nonce = Nonce::from_slice(&iv);
117
118    // Decrypt the data
119    let decrypted_data = cipher
120        .decrypt(nonce, ciphertext.as_ref())
121        .map_err(WError::from_err(
122            "decrypt_with_cipher - Decryption failed (incorrect password or corrupted data)",
123        ))?;
124
125    // Convert the decrypted data back to a string
126    let decrypted_str = String::from_utf8(decrypted_data).map_err(WError::from_err(
127        "decrypt_with_cipher - Failed to convert decrypted data to UTF-8",
128    ))?;
129
130    Ok(decrypted_str)
131}