use crate::config::KeySource;
use crate::crypto::{derive_key_from_password, EncryptionKey};
use crate::{Error, Result};
use std::path::Path;
use zeroize::Zeroizing;
#[derive(Debug, Clone)]
pub struct KeyRotationMetadata {
pub rotated_at: String,
pub previous_key_id: String,
pub current_key_id: String,
pub status: String,
}
pub struct KeyManager {
key: Zeroizing<EncryptionKey>,
previous_key: Option<Zeroizing<EncryptionKey>>,
source: KeySource,
rotation_metadata: Option<KeyRotationMetadata>,
}
impl KeyManager {
pub fn from_source(source: &KeySource) -> Result<Self> {
let key = Self::load_key_from_source(source)?;
Ok(Self {
key: Zeroizing::new(key),
previous_key: None,
source: source.clone(),
rotation_metadata: None,
})
}
fn load_key_from_source(source: &KeySource) -> Result<EncryptionKey> {
match source {
KeySource::Environment(var_name) => Self::load_from_env(var_name),
KeySource::File(path) => Self::load_from_file(path),
KeySource::Kms { provider, key_id } => {
Err(Error::encryption(format!(
"KMS provider '{}' not yet supported (key_id: {})",
provider, key_id
)))
}
}
}
fn load_from_env(var_name: &str) -> Result<EncryptionKey> {
let key_str = std::env::var(var_name).map_err(|_| {
Error::encryption(format!(
"Environment variable '{}' not found. \
Set it with a 64-character hex string (32 bytes).",
var_name
))
})?;
Self::parse_hex_key(&key_str)
}
fn load_from_file(path: &Path) -> Result<EncryptionKey> {
let key_bytes = std::fs::read(path).map_err(|e| {
Error::encryption(format!(
"Failed to read key file '{}': {}",
path.display(),
e
))
})?;
if key_bytes.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes);
Ok(key)
} else {
let key_str = String::from_utf8(key_bytes).map_err(|_| {
Error::encryption("Key file must contain either 32 raw bytes or 64 hex characters")
})?;
Self::parse_hex_key(key_str.trim())
}
}
fn parse_hex_key(hex_str: &str) -> Result<EncryptionKey> {
let hex_str = hex_str.trim();
if hex_str.len() != 64 {
return Err(Error::encryption(format!(
"Hex key must be 64 characters (32 bytes), got {}",
hex_str.len()
)));
}
let mut key = [0u8; 32];
for (i, chunk) in hex_str.as_bytes().chunks(2).enumerate() {
let hex_byte = std::str::from_utf8(chunk)
.map_err(|_| Error::encryption("Invalid hex string"))?;
let dest = key.get_mut(i).ok_or_else(|| Error::encryption("Key index out of bounds"))?;
*dest = u8::from_str_radix(hex_byte, 16)
.map_err(|_| Error::encryption(format!("Invalid hex byte: {}", hex_byte)))?;
}
Ok(key)
}
pub fn from_password(password: &str, salt: &[u8]) -> Result<Self> {
if salt.len() < 16 {
return Err(Error::encryption("Salt must be at least 16 bytes"));
}
let key = derive_key_from_password(password, salt)?;
Ok(Self {
key: Zeroizing::new(key),
previous_key: None,
source: KeySource::Environment("password-derived".to_string()),
rotation_metadata: None,
})
}
pub fn key(&self) -> &EncryptionKey {
&self.key
}
pub fn source(&self) -> &KeySource {
&self.source
}
pub fn generate_random() -> Self {
let key: EncryptionKey = rand::random();
Self {
key: Zeroizing::new(key),
previous_key: None,
source: KeySource::Environment("generated".to_string()),
rotation_metadata: None,
}
}
pub fn export_as_hex(&self) -> String {
hex::encode(&self.key)
}
pub fn rotate(&mut self, new_key: EncryptionKey) -> Result<()> {
use chrono::Utc;
let old_key_id = Self::compute_key_id(&self.key);
let new_key_id = Self::compute_key_id(&new_key);
self.previous_key = Some(self.key.clone());
self.key = Zeroizing::new(new_key);
self.rotation_metadata = Some(KeyRotationMetadata {
rotated_at: Utc::now().to_rfc3339(),
previous_key_id: old_key_id,
current_key_id: new_key_id,
status: "completed".to_string(),
});
Ok(())
}
fn compute_key_id(key: &EncryptionKey) -> String {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(key);
let result = hasher.finalize();
hex::encode(result.get(0..8).unwrap_or(&result)) }
pub fn previous_key(&self) -> Option<&EncryptionKey> {
self.previous_key.as_ref().map(|k| &**k)
}
pub fn rotation_metadata(&self) -> Option<&KeyRotationMetadata> {
self.rotation_metadata.as_ref()
}
}
impl Drop for KeyManager {
fn drop(&mut self) {
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_generate_random_key() {
let km = KeyManager::generate_random();
assert_eq!(km.key().len(), 32);
}
#[test]
fn test_from_password() {
let password = "test_password_123";
let salt = b"random_salt_1234";
let km = KeyManager::from_password(password, salt)
.expect("Failed to create key manager from password");
assert_eq!(km.key().len(), 32);
}
#[test]
fn test_parse_hex_key() {
let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let key = KeyManager::parse_hex_key(hex_key)
.expect("Failed to parse hex key");
assert_eq!(key.len(), 32);
assert_eq!(key[0], 0x01);
assert_eq!(key[1], 0x23);
}
#[test]
fn test_export_as_hex() {
let km = KeyManager::generate_random();
let hex = km.export_as_hex();
assert_eq!(hex.len(), 64);
let _parsed = KeyManager::parse_hex_key(&hex)
.expect("Failed to parse exported hex");
}
#[test]
fn test_load_from_env() {
let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
std::env::set_var("TEST_HELIOSDB_KEY", hex_key);
let key = KeyManager::load_from_env("TEST_HELIOSDB_KEY")
.expect("Failed to load key from env");
assert_eq!(key.len(), 32);
std::env::remove_var("TEST_HELIOSDB_KEY");
}
#[test]
fn test_key_manager_from_env_source() {
let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
std::env::set_var("TEST_HELIOSDB_KEY_2", hex_key);
let source = KeySource::Environment("TEST_HELIOSDB_KEY_2".to_string());
let km = KeyManager::from_source(&source)
.expect("Failed to create key manager from source");
assert_eq!(km.key().len(), 32);
std::env::remove_var("TEST_HELIOSDB_KEY_2");
}
}