use std::env;
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use crate::secret_file;
use crate::target::filelift_home_dir;
pub const KEY_LEN: usize = 32;
const MASTER_KEY_ENV: &str = "FILELIFT_MASTER_KEY_HEX";
const KEY_DOMAIN: &str = "filelift:secret-store:v1";
const MASTER_KEY_FILE: &str = "master.key";
pub fn store_key() -> Result<[u8; KEY_LEN]> {
if let Ok(value) = env::var(MASTER_KEY_ENV) {
return decode_hex_key(&value)
.with_context(|| format!("invalid {MASTER_KEY_ENV} (expected 64 hex characters)"));
}
match machine_uid::get() {
Ok(machine_id) if !machine_id.trim().is_empty() => Ok(derive_key(&machine_id, &username())),
_ => persisted_random_key(),
}
}
fn derive_key(machine_id: &str, username: &str) -> [u8; KEY_LEN] {
let mut hasher = Sha256::new();
hasher.update(KEY_DOMAIN.as_bytes());
hasher.update([0]);
hasher.update(machine_id.trim().as_bytes());
hasher.update([0]);
hasher.update(username.as_bytes());
hasher.finalize().into()
}
fn username() -> String {
env::var("USERNAME")
.or_else(|_| env::var("USER"))
.unwrap_or_else(|_| "filelift".to_string())
}
fn persisted_random_key() -> Result<[u8; KEY_LEN]> {
let path = filelift_home_dir()?.join(MASTER_KEY_FILE);
if let Some(existing) = secret_file::read_to_string(&path)?
&& let Ok(key) = decode_hex_key(existing.trim())
{
return Ok(key);
}
let key = secret_file::random_bytes::<KEY_LEN>();
secret_file::write_secure(&path, encode_hex_key(&key).as_bytes()).with_context(|| {
format!(
"failed to persist fallback master key at {}",
path.display()
)
})?;
Ok(key)
}
fn decode_hex_key(value: &str) -> Result<[u8; KEY_LEN]> {
let value = value.trim();
anyhow::ensure!(
value.len() == KEY_LEN * 2,
"key must be {} hex characters",
KEY_LEN * 2
);
let mut key = [0_u8; KEY_LEN];
for (index, byte) in key.iter_mut().enumerate() {
let start = index * 2;
*byte = u8::from_str_radix(&value[start..start + 2], 16)
.context("key contains non-hex characters")?;
}
Ok(key)
}
fn encode_hex_key(bytes: &[u8; KEY_LEN]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_key_is_deterministic_for_same_inputs() {
let first = derive_key("machine-abc", "alice");
let second = derive_key("machine-abc", "alice");
assert_eq!(first, second);
}
#[test]
fn derive_key_differs_across_machines_and_users() {
let base = derive_key("machine-abc", "alice");
assert_ne!(base, derive_key("machine-xyz", "alice"));
assert_ne!(base, derive_key("machine-abc", "bob"));
}
#[test]
fn decode_hex_key_roundtrips_encoding() {
let key = [7_u8; KEY_LEN];
let decoded = decode_hex_key(&encode_hex_key(&key)).unwrap();
assert_eq!(decoded, key);
}
#[test]
fn decode_hex_key_rejects_wrong_length() {
assert!(decode_hex_key("00ff").is_err());
}
#[test]
fn decode_hex_key_rejects_non_hex() {
let invalid = "zz".repeat(KEY_LEN);
assert!(decode_hex_key(&invalid).is_err());
}
}