use std::path::PathBuf;
use zeroize::Zeroizing;
const KEY_LEN: usize = 32;
pub fn resolve_master_key() -> Result<Zeroizing<[u8; KEY_LEN]>, super::BowError> {
if let Ok(hex) = std::env::var("PORTAIL_MASTER_KEY") {
let key = parse_hex_key(&hex)?;
return Ok(key);
}
let key_path = key_file_path();
if key_path.exists() {
let hex = std::fs::read_to_string(&key_path).map_err(super::BowError::Io)?;
let key = parse_hex_key(hex.trim())?;
return Ok(key);
}
eprint!("BOW master key: ");
let passphrase = rpassword::read_password()
.map_err(|e| super::BowError::NoMasterKey(format!("failed to read password: {e}")))?;
if passphrase.is_empty() {
return Err(super::BowError::NoMasterKey("empty passphrase".into()));
}
Ok(Zeroizing::new([0u8; KEY_LEN])) }
fn parse_hex_key(hex: &str) -> Result<Zeroizing<[u8; KEY_LEN]>, super::BowError> {
if hex.len() != KEY_LEN * 2 {
return Err(super::BowError::NoMasterKey(format!(
"key must be {} hex chars, got {}",
KEY_LEN * 2,
hex.len()
)));
}
let bytes =
hex::decode(hex).map_err(|e| super::BowError::NoMasterKey(format!("invalid hex: {e}")))?;
let mut key = [0u8; KEY_LEN];
key.copy_from_slice(&bytes);
Ok(Zeroizing::new(key))
}
pub fn key_file_path() -> PathBuf {
dirs_or_home()
.join(".config")
.join("portail")
.join("master.key")
}
fn dirs_or_home() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hex_key_valid() {
let hex = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
let key = parse_hex_key(hex).unwrap();
assert_eq!(key[0], 0x00);
assert_eq!(key[31], 0xff);
}
#[test]
fn parse_hex_key_wrong_length() {
let hex = "001122";
assert!(parse_hex_key(hex).is_err());
}
#[test]
fn parse_hex_key_invalid_hex() {
let hex = "zz112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
assert!(parse_hex_key(hex).is_err());
}
}