use aes_gcm::{ Aes256Gcm, Key, Nonce, aead::{Aead, AeadCore, KeyInit, OsRng}, }; use anyhow::{Context, Result, bail}; use argon2::{Argon2, Params, Version}; use serde_json::Value; const KEYRING_SERVICE: &str = "secrets-cli"; const KEYRING_USER: &str = "master-key"; const NONCE_LEN: usize = 12; // Argon2id parameters — OWASP recommended (m=64 MiB, t=3 iterations, p=4 threads, key=32 B) const ARGON2_M_COST: u32 = 65_536; const ARGON2_T_COST: u32 = 3; const ARGON2_P_COST: u32 = 4; const ARGON2_KEY_LEN: usize = 32; // ─── Argon2id key derivation ───────────────────────────────────────────────── /// Derive a 32-byte Master Key from a password and salt using Argon2id. /// Parameters: m=65536 KiB (64 MB), t=3, p=4 — OWASP recommended. pub fn derive_master_key(password: &str, salt: &[u8]) -> Result<[u8; 32]> { let params = Params::new( ARGON2_M_COST, ARGON2_T_COST, ARGON2_P_COST, Some(ARGON2_KEY_LEN), ) .context("invalid Argon2id params")?; let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, params); let mut key = [0u8; 32]; argon2 .hash_password_into(password.as_bytes(), salt, &mut key) .map_err(|e| anyhow::anyhow!("Argon2id derivation failed: {}", e))?; Ok(key) } // ─── AES-256-GCM encrypt / decrypt ─────────────────────────────────────────── /// Encrypt plaintext bytes with AES-256-GCM. /// Returns `nonce (12 B) || ciphertext+tag`. pub fn encrypt(master_key: &[u8; 32], plaintext: &[u8]) -> Result> { let key = Key::::from_slice(master_key); let cipher = Aes256Gcm::new(key); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext) .map_err(|e| anyhow::anyhow!("AES-256-GCM encryption failed: {}", e))?; let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len()); out.extend_from_slice(&nonce); out.extend_from_slice(&ciphertext); Ok(out) } /// Decrypt `nonce (12 B) || ciphertext+tag` with AES-256-GCM. pub fn decrypt(master_key: &[u8; 32], data: &[u8]) -> Result> { if data.len() < NONCE_LEN { bail!( "encrypted data too short ({}B); possibly corrupted", data.len() ); } let (nonce_bytes, ciphertext) = data.split_at(NONCE_LEN); let key = Key::::from_slice(master_key); let cipher = Aes256Gcm::new(key); let nonce = Nonce::from_slice(nonce_bytes); cipher .decrypt(nonce, ciphertext) .map_err(|_| anyhow::anyhow!("decryption failed — wrong master key or corrupted data")) } // ─── JSON helpers ───────────────────────────────────────────────────────────── /// Serialize a JSON Value and encrypt it. Returns the encrypted blob. pub fn encrypt_json(master_key: &[u8; 32], value: &Value) -> Result> { let bytes = serde_json::to_vec(value).context("serialize JSON for encryption")?; encrypt(master_key, &bytes) } /// Decrypt an encrypted blob and deserialize it as a JSON Value. pub fn decrypt_json(master_key: &[u8; 32], data: &[u8]) -> Result { let bytes = decrypt(master_key, data)?; serde_json::from_slice(&bytes).context("deserialize decrypted JSON") } // ─── OS Keychain ────────────────────────────────────────────────────────────── /// Load the Master Key from the OS Keychain. /// Returns an error with a helpful message if it hasn't been initialized. pub fn load_master_key() -> Result<[u8; 32]> { let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER).context("create keychain entry")?; let hex = entry.get_password().map_err(|_| { anyhow::anyhow!("Master key not found in keychain. Run `secrets init` first.") })?; let bytes = hex::decode_hex(&hex)?; if bytes.len() != 32 { bail!( "stored master key has unexpected length {}; re-run `secrets init`", bytes.len() ); } let mut key = [0u8; 32]; key.copy_from_slice(&bytes); Ok(key) } /// Store the Master Key in the OS Keychain (overwrites any existing value). pub fn store_master_key(key: &[u8; 32]) -> Result<()> { let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER).context("create keychain entry")?; let hex = hex::encode_hex(key); entry .set_password(&hex) .map_err(|e| anyhow::anyhow!("keychain write failed: {}", e))?; Ok(()) } // ─── Minimal hex helpers (avoid extra dep) ──────────────────────────────────── mod hex { use anyhow::{Result, bail}; pub fn encode_hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{:02x}", b)).collect() } pub fn decode_hex(s: &str) -> Result> { if !s.len().is_multiple_of(2) { bail!("hex string has odd length"); } (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| anyhow::anyhow!("{}", e))) .collect() } } #[cfg(test)] mod tests { use super::*; #[test] fn roundtrip_encrypt_decrypt() { let key = [0x42u8; 32]; let plaintext = b"hello world"; let enc = encrypt(&key, plaintext).unwrap(); let dec = decrypt(&key, &enc).unwrap(); assert_eq!(dec, plaintext); } #[test] fn encrypt_produces_different_ciphertexts() { let key = [0x42u8; 32]; let plaintext = b"hello world"; let enc1 = encrypt(&key, plaintext).unwrap(); let enc2 = encrypt(&key, plaintext).unwrap(); // Different nonces → different ciphertexts assert_ne!(enc1, enc2); } #[test] fn wrong_key_fails_decryption() { let key1 = [0x42u8; 32]; let key2 = [0x43u8; 32]; let enc = encrypt(&key1, b"secret").unwrap(); assert!(decrypt(&key2, &enc).is_err()); } #[test] fn json_roundtrip() { let key = [0x42u8; 32]; let value = serde_json::json!({"token": "abc123", "password": "hunter2"}); let enc = encrypt_json(&key, &value).unwrap(); let dec = decrypt_json(&key, &enc).unwrap(); assert_eq!(dec, value); } #[test] fn derive_master_key_deterministic() { let salt = b"fixed_test_salt_"; let k1 = derive_master_key("password", salt).unwrap(); let k2 = derive_master_key("password", salt).unwrap(); assert_eq!(k1, k2); } #[test] fn derive_master_key_different_passwords() { let salt = b"fixed_test_salt_"; let k1 = derive_master_key("password1", salt).unwrap(); let k2 = derive_master_key("password2", salt).unwrap(); assert_ne!(k1, k2); } }