use std::fmt;
use std::path::{Path, PathBuf};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::credential_file::{self, CredentialFileError};
pub const SIGNING_KEY_ENV_REJECTED: &str = "CAIRN_SIGNING_KEY";
#[derive(Debug, Error)]
pub enum KeyLoadError {
#[error("signing key file: {0}")]
CredentialFile(#[from] CredentialFileError),
#[error(
"signing key file {path} is not valid hex (expected 64 hex chars, optional trailing newline)"
)]
NotHex {
path: PathBuf,
},
#[error("signing key file {path} decodes to {got} bytes; expected 32")]
WrongLength {
path: PathBuf,
got: usize,
},
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SigningKey([u8; 32]);
impl SigningKey {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub(crate) fn expose_secret(&self) -> &[u8; 32] {
&self.0
}
pub fn load_from_file(path: &Path) -> Result<Self, KeyLoadError> {
credential_file::reject_env_override(SIGNING_KEY_ENV_REJECTED)?;
credential_file::check_mode_and_owner(path)?;
let mut raw = std::fs::read(path)
.map_err(|e| KeyLoadError::CredentialFile(CredentialFileError::Io(e)))?;
let start = raw
.iter()
.position(|b| !b.is_ascii_whitespace())
.unwrap_or(raw.len());
let end = raw
.iter()
.rposition(|b| !b.is_ascii_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let trimmed = &raw[start..end];
let mut decoded = match hex::decode(trimmed) {
Ok(v) => v,
Err(_) => {
raw.zeroize();
return Err(KeyLoadError::NotHex {
path: path.to_path_buf(),
});
}
};
raw.zeroize();
if decoded.len() != 32 {
let got = decoded.len();
decoded.zeroize();
return Err(KeyLoadError::WrongLength {
path: path.to_path_buf(),
got,
});
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&decoded);
decoded.zeroize();
let key = SigningKey::from_bytes(bytes);
bytes.zeroize();
Ok(key)
}
}
impl fmt::Debug for SigningKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SigningKey(<redacted>)")
}
}
#[cfg(test)]
mod tests {
use super::SigningKey;
#[test]
fn debug_redacts_bytes() {
let key = SigningKey::from_bytes([0xAB; 32]);
let dbg = format!("{key:?}");
assert_eq!(dbg, "SigningKey(<redacted>)");
assert!(!dbg.contains("AB"));
assert!(!dbg.contains("ab"));
assert!(!dbg.contains("171")); }
}