1use aes_gcm::{
2 aead::{Aead, KeyInit},
3 Aes256Gcm, Nonce,
4};
5use sha2::{Digest, Sha256};
6
7use crate::error::{CliError, Result};
8
9const NONCE_SIZE: usize = 12;
10
11fn derive_key() -> Result<[u8; 32]> {
13 let mut hasher = Sha256::new();
14
15 if let Ok(hostname) = hostname::get() {
17 hasher.update(hostname.as_encoded_bytes());
18 }
19
20 if let Some(home) = directories::BaseDirs::new() {
22 hasher.update(home.home_dir().to_string_lossy().as_bytes());
23 }
24
25 hasher.update(b"oauth-db-cli-v1");
27
28 let result = hasher.finalize();
29 let mut key = [0u8; 32];
30 key.copy_from_slice(&result);
31 Ok(key)
32}
33
34pub fn encrypt_token(token: &str) -> Result<String> {
36 let key = derive_key()?;
37 let cipher = Aes256Gcm::new(&key.into());
38
39 let mut nonce_bytes = [0u8; NONCE_SIZE];
41 getrandom::getrandom(&mut nonce_bytes)
42 .map_err(|e| CliError::EncryptionError(format!("Failed to generate nonce: {}", e)))?;
43 let nonce = Nonce::from_slice(&nonce_bytes);
44
45 let ciphertext = cipher
47 .encrypt(nonce, token.as_bytes())
48 .map_err(|e| CliError::EncryptionError(format!("Encryption failed: {}", e)))?;
49
50 let mut result = nonce_bytes.to_vec();
52 result.extend_from_slice(&ciphertext);
53 Ok(format!("encrypted:{}", base64_encode(&result)))
54}
55
56pub fn decrypt_token(encrypted: &str) -> Result<String> {
58 let encrypted = encrypted
60 .strip_prefix("encrypted:")
61 .ok_or_else(|| CliError::EncryptionError("Invalid encrypted token format".to_string()))?;
62
63 let data = base64_decode(encrypted)?;
65
66 if data.len() < NONCE_SIZE {
67 return Err(CliError::EncryptionError(
68 "Encrypted data too short".to_string(),
69 ));
70 }
71
72 let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE);
74 let nonce = Nonce::from_slice(nonce_bytes);
75
76 let key = derive_key()?;
78 let cipher = Aes256Gcm::new(&key.into());
79
80 let plaintext = cipher
81 .decrypt(nonce, ciphertext)
82 .map_err(|e| CliError::EncryptionError(format!("Decryption failed: {}", e)))?;
83
84 String::from_utf8(plaintext)
85 .map_err(|e| CliError::EncryptionError(format!("Invalid UTF-8: {}", e)))
86}
87
88fn base64_encode(data: &[u8]) -> String {
90 use base64::Engine;
91 base64::engine::general_purpose::STANDARD.encode(data)
92}
93
94fn base64_decode(data: &str) -> Result<Vec<u8>> {
96 use base64::Engine;
97 base64::engine::general_purpose::STANDARD
98 .decode(data)
99 .map_err(|e| CliError::EncryptionError(format!("Base64 decode failed: {}", e)))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_encrypt_decrypt() {
108 let token = "test_token_12345";
109 let encrypted = encrypt_token(token).unwrap();
110 assert!(encrypted.starts_with("encrypted:"));
111
112 let decrypted = decrypt_token(&encrypted).unwrap();
113 assert_eq!(decrypted, token);
114 }
115
116 #[test]
117 fn test_invalid_format() {
118 let result = decrypt_token("invalid_format");
119 assert!(result.is_err());
120 }
121
122 #[test]
123 fn test_different_tokens_produce_different_ciphertext() {
124 let token1 = "token1";
125 let token2 = "token2";
126 let encrypted1 = encrypt_token(token1).unwrap();
127 let encrypted2 = encrypt_token(token2).unwrap();
128 assert_ne!(encrypted1, encrypted2);
129 }
130}