Skip to main content

oauth_db_cli/
crypto.rs

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
11/// Derive encryption key from machine-specific identifiers
12fn derive_key() -> Result<[u8; 32]> {
13    let mut hasher = Sha256::new();
14
15    // Use hostname as part of key derivation
16    if let Ok(hostname) = hostname::get() {
17        hasher.update(hostname.as_encoded_bytes());
18    }
19
20    // Use home directory path
21    if let Some(home) = directories::BaseDirs::new() {
22        hasher.update(home.home_dir().to_string_lossy().as_bytes());
23    }
24
25    // Add a static salt
26    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
34/// Encrypt a token using AES-256-GCM
35pub fn encrypt_token(token: &str) -> Result<String> {
36    let key = derive_key()?;
37    let cipher = Aes256Gcm::new(&key.into());
38
39    // Generate random nonce
40    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    // Encrypt the token
46    let ciphertext = cipher
47        .encrypt(nonce, token.as_bytes())
48        .map_err(|e| CliError::EncryptionError(format!("Encryption failed: {}", e)))?;
49
50    // Combine nonce + ciphertext and encode as base64
51    let mut result = nonce_bytes.to_vec();
52    result.extend_from_slice(&ciphertext);
53    Ok(format!("encrypted:{}", base64_encode(&result)))
54}
55
56/// Decrypt a token using AES-256-GCM
57pub fn decrypt_token(encrypted: &str) -> Result<String> {
58    // Remove "encrypted:" prefix
59    let encrypted = encrypted
60        .strip_prefix("encrypted:")
61        .ok_or_else(|| CliError::EncryptionError("Invalid encrypted token format".to_string()))?;
62
63    // Decode from base64
64    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    // Split nonce and ciphertext
73    let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE);
74    let nonce = Nonce::from_slice(nonce_bytes);
75
76    // Decrypt
77    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
88/// Base64 encode
89fn base64_encode(data: &[u8]) -> String {
90    use base64::Engine;
91    base64::engine::general_purpose::STANDARD.encode(data)
92}
93
94/// Base64 decode
95fn 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}