1use aes_gcm::{
6 aead::{Aead, KeyInit},
7 Aes256Gcm, Nonce,
8};
9use argon2::{password_hash::SaltString, Argon2, Params};
10use rand::{rngs::OsRng, RngCore};
11use serde::{Deserialize, Serialize};
12use zeroize::Zeroizing;
13
14use crate::error::ZincError;
15
16const V1_M_COST: u32 = 65536; const V1_T_COST: u32 = 3;
20const V1_P_COST: u32 = 1;
21
22const V2_M_COST: u32 = 32768; const V2_T_COST: u32 = 1;
25const V2_P_COST: u32 = 1;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct EncryptedWallet {
30 pub salt: String,
32 pub nonce: String,
34 pub ciphertext: String,
36 pub version: u8,
39}
40
41pub fn encrypt_seed(seed: &[u8], password: &str) -> Result<EncryptedWallet, ZincError> {
43 let salt = SaltString::generate(&mut OsRng);
45
46 let version = 2;
48
49 let key = derive_key(password, salt.as_str(), version)?;
51
52 let mut nonce_bytes = [0u8; 12];
54 OsRng.fill_bytes(&mut nonce_bytes);
55 let nonce = Nonce::from_slice(&nonce_bytes);
56
57 let cipher =
59 Aes256Gcm::new_from_slice(&*key).map_err(|e| ZincError::EncryptionError(e.to_string()))?;
60
61 let ciphertext = cipher
62 .encrypt(nonce, seed)
63 .map_err(|e| ZincError::EncryptionError(e.to_string()))?;
64
65 Ok(EncryptedWallet {
66 salt: salt.to_string(),
67 nonce: base64_encode(&nonce_bytes),
68 ciphertext: base64_encode(&ciphertext),
69 version,
70 })
71}
72
73pub fn decrypt_seed(
75 encrypted: &EncryptedWallet,
76 password: &str,
77) -> Result<Zeroizing<Vec<u8>>, ZincError> {
78 let key = derive_key(password, &encrypted.salt, encrypted.version)?;
80
81 let nonce_bytes = base64_decode(&encrypted.nonce)?;
83 let ciphertext = base64_decode(&encrypted.ciphertext)?;
84
85 if nonce_bytes.len() != 12 {
87 return Err(ZincError::DecryptionError);
88 }
89
90 let nonce = Nonce::from_slice(&nonce_bytes);
91
92 let cipher = Aes256Gcm::new_from_slice(&*key).map_err(|_| ZincError::DecryptionError)?;
94
95 let plaintext = cipher
96 .decrypt(nonce, ciphertext.as_slice())
97 .map_err(|_| ZincError::DecryptionError)?;
98
99 Ok(Zeroizing::new(plaintext))
100}
101
102fn derive_key(password: &str, salt: &str, version: u8) -> Result<Zeroizing<[u8; 32]>, ZincError> {
104 let (m, t, p) = match version {
105 1 => (V1_M_COST, V1_T_COST, V1_P_COST),
106 2 => (V2_M_COST, V2_T_COST, V2_P_COST),
107 _ => {
108 return Err(ZincError::EncryptionError(format!(
109 "Unsupported wallet version: {}",
110 version
111 )))
112 }
113 };
114
115 let params =
116 Params::new(m, t, p, Some(32)).map_err(|e| ZincError::EncryptionError(e.to_string()))?;
117
118 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
119
120 let mut key = Zeroizing::new([0u8; 32]);
121 argon2
122 .hash_password_into(password.as_bytes(), salt.as_bytes(), &mut *key)
123 .map_err(|e| ZincError::EncryptionError(e.to_string()))?;
124
125 Ok(key)
126}
127
128fn base64_encode(data: &[u8]) -> String {
129 use base64::{engine::general_purpose::STANDARD, Engine};
130 STANDARD.encode(data)
131}
132
133fn base64_decode(data: &str) -> Result<Vec<u8>, ZincError> {
134 use base64::{engine::general_purpose::STANDARD, Engine};
135 STANDARD
136 .decode(data)
137 .map_err(|e| ZincError::SerializationError(e.to_string()))
138}
139
140#[cfg(test)]
141#[allow(clippy::unwrap_used)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_encrypt_decrypt_roundtrip() {
147 let seed = b"this is a test seed for encryption";
148 let password = "secure_password_123!";
149
150 let encrypted = encrypt_seed(seed, password).unwrap();
151 let decrypted = decrypt_seed(&encrypted, password).unwrap();
152
153 assert_eq!(seed.as_slice(), decrypted.as_slice());
154 }
155
156 #[test]
157 fn test_wrong_password_fails() {
158 let seed = b"this is a test seed for encryption";
159 let password = "correct_password";
160 let wrong_password = "wrong_password";
161
162 let encrypted = encrypt_seed(seed, password).unwrap();
163 let result = decrypt_seed(&encrypted, wrong_password);
164
165 assert!(result.is_err());
166 }
167
168 #[test]
169 fn test_encrypted_wallet_serialization() {
170 let seed = b"test seed";
171 let password = "password";
172
173 let encrypted = encrypt_seed(seed, password).unwrap();
174 let json = serde_json::to_string(&encrypted).unwrap();
175 let parsed: EncryptedWallet = serde_json::from_str(&json).unwrap();
176
177 let decrypted = decrypt_seed(&parsed, password).unwrap();
178 assert_eq!(seed.as_slice(), decrypted.as_slice());
179 }
180
181 #[test]
182 fn test_malformed_nonce_length_fails_without_panic() {
183 let seed = b"test seed";
184 let password = "password";
185
186 let mut encrypted = encrypt_seed(seed, password).unwrap();
187 encrypted.nonce = base64_encode(&[0u8; 8]);
188
189 let result = decrypt_seed(&encrypted, password);
190 assert!(matches!(result, Err(ZincError::DecryptionError)));
191 }
192}