oauth-db-cli 0.1.0

Command-line tool for managing OAuth-DB platform
Documentation
use aes_gcm::{
    aead::{Aead, KeyInit},
    Aes256Gcm, Nonce,
};
use sha2::{Digest, Sha256};

use crate::error::{CliError, Result};

const NONCE_SIZE: usize = 12;

/// Derive encryption key from machine-specific identifiers
fn derive_key() -> Result<[u8; 32]> {
    let mut hasher = Sha256::new();

    // Use hostname as part of key derivation
    if let Ok(hostname) = hostname::get() {
        hasher.update(hostname.as_encoded_bytes());
    }

    // Use home directory path
    if let Some(home) = directories::BaseDirs::new() {
        hasher.update(home.home_dir().to_string_lossy().as_bytes());
    }

    // Add a static salt
    hasher.update(b"oauth-db-cli-v1");

    let result = hasher.finalize();
    let mut key = [0u8; 32];
    key.copy_from_slice(&result);
    Ok(key)
}

/// Encrypt a token using AES-256-GCM
pub fn encrypt_token(token: &str) -> Result<String> {
    let key = derive_key()?;
    let cipher = Aes256Gcm::new(&key.into());

    // Generate random nonce
    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);

    // Encrypt the token
    let ciphertext = cipher
        .encrypt(nonce, token.as_bytes())
        .map_err(|e| CliError::EncryptionError(format!("Encryption failed: {}", e)))?;

    // Combine nonce + ciphertext and encode as base64
    let mut result = nonce_bytes.to_vec();
    result.extend_from_slice(&ciphertext);
    Ok(format!("encrypted:{}", base64_encode(&result)))
}

/// Decrypt a token using AES-256-GCM
pub fn decrypt_token(encrypted: &str) -> Result<String> {
    // Remove "encrypted:" prefix
    let encrypted = encrypted
        .strip_prefix("encrypted:")
        .ok_or_else(|| CliError::EncryptionError("Invalid encrypted token format".to_string()))?;

    // Decode from base64
    let data = base64_decode(encrypted)?;

    if data.len() < NONCE_SIZE {
        return Err(CliError::EncryptionError(
            "Encrypted data too short".to_string(),
        ));
    }

    // Split nonce and ciphertext
    let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE);
    let nonce = Nonce::from_slice(nonce_bytes);

    // Decrypt
    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)))
}

/// Base64 encode
fn base64_encode(data: &[u8]) -> String {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD.encode(data)
}

/// Base64 decode
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);
    }
}