use aes_gcm::{
Aes256Gcm,
aead::{KeyInit, OsRng},
};
use zeroize::Zeroizing;
use crate::crypto::CryptoError;
const SERVICE_NAME: &str = "envy";
const ACCOUNT_NAME: &str = "master-key";
fn encode_key(key: &[u8; 32]) -> String {
key.iter().map(|b| format!("{:02x}", b)).collect()
}
fn decode_key(s: &str) -> Result<[u8; 32], CryptoError> {
if s.len() != 64 {
return Err(CryptoError::KeyCorrupted);
}
let mut key = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
key[i] = (hi << 4) | lo;
}
Ok(key)
}
fn hex_nibble(c: u8) -> Result<u8, CryptoError> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'A'..=b'F' => Ok(c - b'A' + 10),
_ => Err(CryptoError::KeyCorrupted),
}
}
fn retrieve_key() -> Result<[u8; 32], CryptoError> {
let entry = keyring::Entry::new(SERVICE_NAME, ACCOUNT_NAME)
.map_err(|e| CryptoError::KeyringUnavailable(e.to_string()))?;
match entry.get_password() {
Ok(pw) => decode_key(&pw),
Err(keyring::Error::NoEntry) => Err(CryptoError::KeyNotFound),
Err(e) => Err(CryptoError::KeyringUnavailable(e.to_string())),
}
}
pub fn get_or_create_master_key() -> Result<Zeroizing<[u8; 32]>, CryptoError> {
match retrieve_key() {
Ok(key) => Ok(Zeroizing::new(key)),
Err(CryptoError::KeyNotFound) => {
let key_ga = Aes256Gcm::generate_key(OsRng);
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(key_ga.as_slice());
let entry = match keyring::Entry::new(SERVICE_NAME, ACCOUNT_NAME) {
Ok(e) => e,
Err(e) => {
return ci_fallback(CryptoError::KeyringUnavailable(e.to_string()));
}
};
entry
.set_password(&encode_key(&key_bytes))
.map_err(|e| CryptoError::KeyringUnavailable(e.to_string()))?;
Ok(Zeroizing::new(key_bytes))
}
Err(CryptoError::KeyringUnavailable(msg)) => {
ci_fallback(CryptoError::KeyringUnavailable(msg))
}
Err(e) => Err(e),
}
}
fn ci_fallback(err: CryptoError) -> Result<Zeroizing<[u8; 32]>, CryptoError> {
if std::env::var("ENVY_PASSPHRASE").is_ok() || std::env::var("CI").is_ok() {
Ok(Zeroizing::new([0u8; 32]))
} else {
Err(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn get_or_create_master_key_is_idempotent() {
let key1 = get_or_create_master_key().expect("must succeed with a live keyring daemon");
assert_eq!(key1.len(), 32, "returned key must be exactly 32 bytes");
let key2 = get_or_create_master_key().expect("second call must also succeed");
assert_eq!(
*key1, *key2,
"key must be identical on repeated calls (idempotent)"
);
}
#[test]
fn key_length_validated() {
let result = decode_key("deadbeefdeadbeefdeadbeef"); assert!(
matches!(result, Err(CryptoError::KeyCorrupted)),
"short hex string must return KeyCorrupted, got: {:?}",
result
);
let long_hex = "a".repeat(66);
let result = decode_key(&long_hex);
assert!(
matches!(result, Err(CryptoError::KeyCorrupted)),
"66-char hex string must return KeyCorrupted, got: {:?}",
result
);
let valid_hex = "42".repeat(32); let result = decode_key(&valid_hex);
assert!(
result.is_ok(),
"valid 64-char hex string must succeed, got: {:?}",
result
);
let key = result.unwrap();
assert!(
key.iter().all(|&b| b == 0x42),
"decoded key bytes must match the hex input"
);
}
}