1use aes_gcm::{
2 aead::{Aead, KeyInit, OsRng},
3 Aes256Gcm, Nonce,
4};
5use anyhow::{anyhow, Context, Result};
6use argon2::{
7 password_hash::{rand_core::RngCore, SaltString},
8 Argon2, PasswordHasher,
9};
10use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14const NONCE_SIZE: usize = 12;
16
17#[derive(Debug, Serialize, Deserialize)]
19pub struct EncryptedCredentials {
20 pub version: u32,
22 pub salt: String,
24 pub credentials: HashMap<String, EncryptedToken>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct EncryptedToken {
31 pub nonce: String,
33 pub ciphertext: String,
35}
36
37impl Default for EncryptedCredentials {
38 fn default() -> Self {
39 Self {
40 version: 1,
41 salt: String::new(),
42 credentials: HashMap::new(),
43 }
44 }
45}
46
47pub fn derive_key() -> Result<[u8; 32]> {
50 let machine_id = machine_uid::get().map_err(|e| anyhow!("Failed to get machine ID: {}", e))?;
51 let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
52
53 let password = format!("{}:{}", machine_id, username);
55
56 let salt_string = SaltString::encode_b64(machine_id.as_bytes())
59 .map_err(|e| anyhow!("Failed to encode salt: {}", e))?;
60
61 let argon2 = Argon2::default();
62
63 let hash = argon2
65 .hash_password(password.as_bytes(), &salt_string)
66 .map_err(|e| anyhow!("Failed to hash password: {}", e))?;
67
68 let hash_bytes = hash.hash.ok_or_else(|| anyhow!("Hash output is missing"))?;
70
71 let mut key = [0u8; 32];
72 key.copy_from_slice(&hash_bytes.as_bytes()[..32]);
73
74 Ok(key)
75}
76
77pub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<(String, String)> {
79 let cipher = Aes256Gcm::new(key.into());
80
81 let mut nonce_bytes = [0u8; NONCE_SIZE];
83 OsRng.fill_bytes(&mut nonce_bytes);
84 let nonce = Nonce::from_slice(&nonce_bytes);
85
86 let ciphertext = cipher
88 .encrypt(nonce, plaintext.as_bytes())
89 .map_err(|e| anyhow!("Encryption failed: {}", e))?;
90
91 let nonce_b64 = BASE64.encode(nonce_bytes);
93 let ciphertext_b64 = BASE64.encode(ciphertext);
94
95 Ok((nonce_b64, ciphertext_b64))
96}
97
98pub fn decrypt(ciphertext_b64: &str, nonce_b64: &str, key: &[u8; 32]) -> Result<String> {
100 let cipher = Aes256Gcm::new(key.into());
101
102 let nonce_bytes = BASE64
104 .decode(nonce_b64)
105 .context("Failed to decode nonce from base64")?;
106 let ciphertext = BASE64
107 .decode(ciphertext_b64)
108 .context("Failed to decode ciphertext from base64")?;
109
110 if nonce_bytes.len() != NONCE_SIZE {
111 return Err(anyhow!(
112 "Invalid nonce size: expected {}, got {}",
113 NONCE_SIZE,
114 nonce_bytes.len()
115 ));
116 }
117
118 let nonce = Nonce::from_slice(&nonce_bytes);
119
120 let plaintext = cipher
122 .decrypt(nonce, ciphertext.as_ref())
123 .map_err(|e| anyhow!("Decryption failed: {}", e))?;
124
125 String::from_utf8(plaintext).context("Decrypted data is not valid UTF-8")
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_derive_key_deterministic() {
134 let key1 = derive_key().expect("Failed to derive key");
136 let key2 = derive_key().expect("Failed to derive key");
137 assert_eq!(key1, key2, "Key derivation should be deterministic");
138 }
139
140 #[test]
141 fn test_encrypt_decrypt_roundtrip() {
142 let key = derive_key().expect("Failed to derive key");
143 let plaintext = "my-secret-token-12345";
144
145 let (nonce, ciphertext) = encrypt(plaintext, &key).expect("Encryption failed");
146
147 assert_ne!(ciphertext, plaintext);
149 assert!(!ciphertext.contains("secret"));
150
151 let decrypted = decrypt(&ciphertext, &nonce, &key).expect("Decryption failed");
152 assert_eq!(decrypted, plaintext, "Decrypted text should match original");
153 }
154
155 #[test]
156 fn test_encrypt_produces_different_ciphertext() {
157 let key = derive_key().expect("Failed to derive key");
158 let plaintext = "same-plaintext";
159
160 let (nonce1, ciphertext1) = encrypt(plaintext, &key).expect("Encryption failed");
162 let (nonce2, ciphertext2) = encrypt(plaintext, &key).expect("Encryption failed");
163
164 assert_ne!(nonce1, nonce2, "Nonces should be randomly generated");
166
167 assert_ne!(
169 ciphertext1, ciphertext2,
170 "Ciphertexts should differ with different nonces"
171 );
172
173 assert_eq!(decrypt(&ciphertext1, &nonce1, &key).unwrap(), plaintext);
175 assert_eq!(decrypt(&ciphertext2, &nonce2, &key).unwrap(), plaintext);
176 }
177
178 #[test]
179 fn test_decrypt_with_wrong_key_fails() {
180 let key1 = derive_key().expect("Failed to derive key");
181 let mut key2 = key1;
182 key2[0] ^= 0xFF; let plaintext = "secret-data";
185 let (nonce, ciphertext) = encrypt(plaintext, &key1).expect("Encryption failed");
186
187 let result = decrypt(&ciphertext, &nonce, &key2);
189 assert!(result.is_err(), "Decryption with wrong key should fail");
190 }
191
192 #[test]
193 fn test_decrypt_with_wrong_nonce_fails() {
194 let key = derive_key().expect("Failed to derive key");
195 let plaintext = "secret-data";
196
197 let (_, ciphertext) = encrypt(plaintext, &key).expect("Encryption failed");
198 let (wrong_nonce, _) = encrypt("other", &key).expect("Encryption failed");
199
200 let result = decrypt(&ciphertext, &wrong_nonce, &key);
202 assert!(result.is_err(), "Decryption with wrong nonce should fail");
203 }
204
205 #[test]
206 fn test_encrypted_credentials_serialization() {
207 let mut creds = EncryptedCredentials {
208 salt: "test-salt".to_string(),
209 ..Default::default()
210 };
211 creds.credentials.insert(
212 "account1".to_string(),
213 EncryptedToken {
214 nonce: "nonce-b64".to_string(),
215 ciphertext: "cipher-b64".to_string(),
216 },
217 );
218
219 let json = serde_json::to_string(&creds).expect("Serialization failed");
220 let deserialized: EncryptedCredentials =
221 serde_json::from_str(&json).expect("Deserialization failed");
222
223 assert_eq!(deserialized.version, 1);
224 assert_eq!(deserialized.salt, "test-salt");
225 assert_eq!(deserialized.credentials.len(), 1);
226 }
227}