1use aes_gcm::{
2 aead::{Aead, Generate, KeyInit, Nonce},
3 Aes256Gcm,
4};
5use anyhow::{anyhow, Context, Result};
6use argon2::{Algorithm, Argon2, Params, Version};
7use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11const NONCE_SIZE: usize = 12;
13
14#[derive(Debug, Serialize, Deserialize)]
16pub struct EncryptedCredentials {
17 pub version: u32,
19 pub salt: String,
21 pub credentials: HashMap<String, EncryptedToken>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct EncryptedToken {
28 pub nonce: String,
30 pub ciphertext: String,
32}
33
34impl Default for EncryptedCredentials {
35 fn default() -> Self {
36 Self {
37 version: 1,
38 salt: String::new(),
39 credentials: HashMap::new(),
40 }
41 }
42}
43
44pub fn derive_key() -> Result<[u8; 32]> {
47 let machine_id = machine_uid::get().map_err(|e| anyhow!("Failed to get machine ID: {}", e))?;
48 let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
49 derive_key_from(&machine_id, &username)
50}
51
52const KDF_M_COST_KIB: u32 = 19 * 1024;
58const KDF_T_COST: u32 = 2;
59const KDF_P_COST: u32 = 1;
60const KEY_LEN: usize = 32;
61
62fn derive_key_from(machine_id: &str, username: &str) -> Result<[u8; 32]> {
74 let password = format!("{}:{}", machine_id, username);
76
77 let params = Params::new(KDF_M_COST_KIB, KDF_T_COST, KDF_P_COST, Some(KEY_LEN))
78 .map_err(|e| anyhow!("Invalid Argon2 parameters: {}", e))?;
79 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
80
81 let mut key = [0u8; KEY_LEN];
82 argon2
83 .hash_password_into(password.as_bytes(), machine_id.as_bytes(), &mut key)
84 .map_err(|e| anyhow!("Failed to derive key: {}", e))?;
85
86 Ok(key)
87}
88
89pub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<(String, String)> {
91 let cipher = Aes256Gcm::new(key.into());
92
93 let nonce = Nonce::<Aes256Gcm>::try_generate()
98 .map_err(|e| anyhow!("Failed to generate nonce from system RNG: {}", e))?;
99
100 let ciphertext = cipher
102 .encrypt(&nonce, plaintext.as_bytes())
103 .map_err(|e| anyhow!("Encryption failed: {}", e))?;
104
105 let nonce_b64 = BASE64.encode(nonce);
107 let ciphertext_b64 = BASE64.encode(ciphertext);
108
109 Ok((nonce_b64, ciphertext_b64))
110}
111
112pub fn decrypt(ciphertext_b64: &str, nonce_b64: &str, key: &[u8; 32]) -> Result<String> {
114 let cipher = Aes256Gcm::new(key.into());
115
116 let nonce_bytes = BASE64
118 .decode(nonce_b64)
119 .context("Failed to decode nonce from base64")?;
120 let ciphertext = BASE64
121 .decode(ciphertext_b64)
122 .context("Failed to decode ciphertext from base64")?;
123
124 if nonce_bytes.len() != NONCE_SIZE {
125 return Err(anyhow!(
126 "Invalid nonce size: expected {}, got {}",
127 NONCE_SIZE,
128 nonce_bytes.len()
129 ));
130 }
131
132 let nonce = Nonce::<Aes256Gcm>::try_from(nonce_bytes.as_slice())
135 .map_err(|_| anyhow!("Invalid nonce size: expected {}", NONCE_SIZE))?;
136
137 let plaintext = cipher
139 .decrypt(&nonce, ciphertext.as_ref())
140 .map_err(|e| anyhow!("Decryption failed: {}", e))?;
141
142 String::from_utf8(plaintext).context("Decrypted data is not valid UTF-8")
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
156 fn decrypts_ciphertext_written_by_aes_gcm_0_10() {
157 let key = [7u8; 32];
158 let nonce_b64 = "Rm9oYDGUcR47yGPD";
159 let ciphertext_b64 = "iwXkjpxTMOS8N/vvR/y0Yvt3G7fE0GW4sl7KvlyPIJ3rW50U/81e";
160
161 let plaintext = decrypt(ciphertext_b64, nonce_b64, &key)
162 .expect("aes-gcm 0.11 must decrypt ciphertext written by 0.10");
163 assert_eq!(plaintext, "hunter2-atlassian-token");
164 }
165
166 #[test]
178 fn derive_key_known_answer() {
179 let key = derive_key_from("00000000-1111-2222-3333-444444444444", "synthetic-user")
180 .expect("key derivation must succeed for a UUID-shaped machine id");
181 let hex: String = key.iter().map(|b| format!("{:02x}", b)).collect();
182 assert_eq!(
183 hex, "add8654d98e33b867373ecac2765c95fda969e03b12af6620399f41395994255",
184 "derive_key output changed: existing credentials.enc files would no longer decrypt"
185 );
186 }
187
188 #[test]
189 fn nonce_is_12_bytes() {
190 let (nonce_b64, _) = encrypt("x", &[0u8; 32]).unwrap();
192 assert_eq!(BASE64.decode(nonce_b64).unwrap().len(), NONCE_SIZE);
193 assert_eq!(NONCE_SIZE, 12);
194 }
195
196 #[test]
197 fn test_derive_key_deterministic() {
198 let key1 = derive_key().expect("Failed to derive key");
200 let key2 = derive_key().expect("Failed to derive key");
201 assert_eq!(key1, key2, "Key derivation should be deterministic");
202 }
203
204 #[test]
205 fn test_encrypt_decrypt_roundtrip() {
206 let key = derive_key().expect("Failed to derive key");
207 let plaintext = "my-secret-token-12345";
208
209 let (nonce, ciphertext) = encrypt(plaintext, &key).expect("Encryption failed");
210
211 assert_ne!(ciphertext, plaintext);
213 assert!(!ciphertext.contains("secret"));
214
215 let decrypted = decrypt(&ciphertext, &nonce, &key).expect("Decryption failed");
216 assert_eq!(decrypted, plaintext, "Decrypted text should match original");
217 }
218
219 #[test]
220 fn test_encrypt_produces_different_ciphertext() {
221 let key = derive_key().expect("Failed to derive key");
222 let plaintext = "same-plaintext";
223
224 let (nonce1, ciphertext1) = encrypt(plaintext, &key).expect("Encryption failed");
226 let (nonce2, ciphertext2) = encrypt(plaintext, &key).expect("Encryption failed");
227
228 assert_ne!(nonce1, nonce2, "Nonces should be randomly generated");
230
231 assert_ne!(
233 ciphertext1, ciphertext2,
234 "Ciphertexts should differ with different nonces"
235 );
236
237 assert_eq!(decrypt(&ciphertext1, &nonce1, &key).unwrap(), plaintext);
239 assert_eq!(decrypt(&ciphertext2, &nonce2, &key).unwrap(), plaintext);
240 }
241
242 #[test]
243 fn test_decrypt_with_wrong_key_fails() {
244 let key1 = derive_key().expect("Failed to derive key");
245 let mut key2 = key1;
246 key2[0] ^= 0xFF; let plaintext = "secret-data";
249 let (nonce, ciphertext) = encrypt(plaintext, &key1).expect("Encryption failed");
250
251 let result = decrypt(&ciphertext, &nonce, &key2);
253 assert!(result.is_err(), "Decryption with wrong key should fail");
254 }
255
256 #[test]
257 fn test_decrypt_with_wrong_nonce_fails() {
258 let key = derive_key().expect("Failed to derive key");
259 let plaintext = "secret-data";
260
261 let (_, ciphertext) = encrypt(plaintext, &key).expect("Encryption failed");
262 let (wrong_nonce, _) = encrypt("other", &key).expect("Encryption failed");
263
264 let result = decrypt(&ciphertext, &wrong_nonce, &key);
266 assert!(result.is_err(), "Decryption with wrong nonce should fail");
267 }
268
269 #[test]
270 fn test_encrypted_credentials_serialization() {
271 let mut creds = EncryptedCredentials {
272 salt: "test-salt".to_string(),
273 ..Default::default()
274 };
275 creds.credentials.insert(
276 "account1".to_string(),
277 EncryptedToken {
278 nonce: "nonce-b64".to_string(),
279 ciphertext: "cipher-b64".to_string(),
280 },
281 );
282
283 let json = serde_json::to_string(&creds).expect("Serialization failed");
284 let deserialized: EncryptedCredentials =
285 serde_json::from_str(&json).expect("Deserialization failed");
286
287 assert_eq!(deserialized.version, 1);
288 assert_eq!(deserialized.salt, "test-salt");
289 assert_eq!(deserialized.credentials.len(), 1);
290 }
291}