use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use sha2::{Digest, Sha256};
use crate::error::{CliError, Result};
const NONCE_SIZE: usize = 12;
fn derive_key() -> Result<[u8; 32]> {
let mut hasher = Sha256::new();
if let Ok(hostname) = hostname::get() {
hasher.update(hostname.as_encoded_bytes());
}
if let Some(home) = directories::BaseDirs::new() {
hasher.update(home.home_dir().to_string_lossy().as_bytes());
}
hasher.update(b"oauth-db-cli-v1");
let result = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&result);
Ok(key)
}
pub fn encrypt_token(token: &str) -> Result<String> {
let key = derive_key()?;
let cipher = Aes256Gcm::new(&key.into());
let mut nonce_bytes = [0u8; NONCE_SIZE];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| CliError::EncryptionError(format!("Failed to generate nonce: {}", e)))?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, token.as_bytes())
.map_err(|e| CliError::EncryptionError(format!("Encryption failed: {}", e)))?;
let mut result = nonce_bytes.to_vec();
result.extend_from_slice(&ciphertext);
Ok(format!("encrypted:{}", base64_encode(&result)))
}
pub fn decrypt_token(encrypted: &str) -> Result<String> {
let encrypted = encrypted
.strip_prefix("encrypted:")
.ok_or_else(|| CliError::EncryptionError("Invalid encrypted token format".to_string()))?;
let data = base64_decode(encrypted)?;
if data.len() < NONCE_SIZE {
return Err(CliError::EncryptionError(
"Encrypted data too short".to_string(),
));
}
let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE);
let nonce = Nonce::from_slice(nonce_bytes);
let key = derive_key()?;
let cipher = Aes256Gcm::new(&key.into());
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| CliError::EncryptionError(format!("Decryption failed: {}", e)))?;
String::from_utf8(plaintext)
.map_err(|e| CliError::EncryptionError(format!("Invalid UTF-8: {}", e)))
}
fn base64_encode(data: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(data)
}
fn base64_decode(data: &str) -> Result<Vec<u8>> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(|e| CliError::EncryptionError(format!("Base64 decode failed: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt() {
let token = "test_token_12345";
let encrypted = encrypt_token(token).unwrap();
assert!(encrypted.starts_with("encrypted:"));
let decrypted = decrypt_token(&encrypted).unwrap();
assert_eq!(decrypted, token);
}
#[test]
fn test_invalid_format() {
let result = decrypt_token("invalid_format");
assert!(result.is_err());
}
#[test]
fn test_different_tokens_produce_different_ciphertext() {
let token1 = "token1";
let token2 = "token2";
let encrypted1 = encrypt_token(token1).unwrap();
let encrypted2 = encrypt_token(token2).unwrap();
assert_ne!(encrypted1, encrypted2);
}
}