use std::fs;
use std::path::Path;
use crate::crypto::base64::base64_decode;
use crate::entropy::EntropyError;
pub const ENTROPY_KEY_LEN: usize = 16;
pub const ENTROPY_IV_LEN: usize = 16;
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct EntropyKey([u8; ENTROPY_KEY_LEN]);
impl EntropyKey {
#[must_use]
pub fn from_bytes(bytes: [u8; ENTROPY_KEY_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; ENTROPY_KEY_LEN] {
&self.0
}
}
impl std::fmt::Debug for EntropyKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntropyKey")
.field("len", &ENTROPY_KEY_LEN)
.finish()
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct EntropyIv([u8; ENTROPY_IV_LEN]);
impl EntropyIv {
#[must_use]
pub fn from_bytes(bytes: [u8; ENTROPY_IV_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; ENTROPY_IV_LEN] {
&self.0
}
}
impl std::fmt::Debug for EntropyIv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EntropyIv")
.field("len", &ENTROPY_IV_LEN)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntropyMaterial {
key: EntropyKey,
iv: EntropyIv,
}
impl EntropyMaterial {
#[must_use]
pub fn new(key: EntropyKey, iv: EntropyIv) -> Self {
Self { key, iv }
}
#[must_use]
pub fn key(&self) -> &EntropyKey {
&self.key
}
#[must_use]
pub fn iv(&self) -> &EntropyIv {
&self.iv
}
}
fn parse_secret_bytes(text: &str) -> Result<Vec<u8>, EntropyError> {
if text.contains("-----BEGIN") {
return decode_pem_block(text);
}
let trimmed = text.trim_end_matches(['\r', '\n', ' ', '\t']);
Ok(trimmed.as_bytes().to_vec())
}
fn decode_pem_block(text: &str) -> Result<Vec<u8>, EntropyError> {
let mut lines = text.lines();
while let Some(line) = lines.next() {
if line.trim_start().starts_with("-----BEGIN") {
let mut body = String::new();
let mut saw_end = false;
for inner in lines.by_ref() {
let trimmed = inner.trim();
if trimmed.starts_with("-----END") {
saw_end = true;
break;
}
body.push_str(trimmed);
}
if !saw_end {
return Err(EntropyError::KeyMaterial(
"PEM block missing END marker".to_string(),
));
}
return base64_decode(&body)
.map_err(|e| EntropyError::KeyMaterial(format!("PEM base64 decode: {e}")));
}
}
Err(EntropyError::KeyMaterial(
"PEM block missing BEGIN marker".to_string(),
))
}
pub fn load_key_file(path: &Path) -> Result<EntropyKey, EntropyError> {
let raw = fs::read_to_string(path).map_err(|e| io_err(path, "read key file", &e))?;
let bytes = parse_secret_bytes(&raw)?;
if bytes.len() < ENTROPY_KEY_LEN {
return Err(EntropyError::KeyMaterial(format!(
"expected at least {ENTROPY_KEY_LEN} bytes of key material in {}, got {}",
path.display(),
bytes.len()
)));
}
let mut out = [0u8; ENTROPY_KEY_LEN];
out.copy_from_slice(&bytes[..ENTROPY_KEY_LEN]);
Ok(EntropyKey(out))
}
pub fn load_iv_file(path: &Path) -> Result<EntropyIv, EntropyError> {
let raw = fs::read_to_string(path).map_err(|e| io_err(path, "read iv file", &e))?;
let bytes = parse_secret_bytes(&raw)?;
if bytes.len() < ENTROPY_IV_LEN {
return Err(EntropyError::KeyMaterial(format!(
"expected at least {ENTROPY_IV_LEN} bytes of IV material in {}, got {}",
path.display(),
bytes.len()
)));
}
let mut out = [0u8; ENTROPY_IV_LEN];
out.copy_from_slice(&bytes[..ENTROPY_IV_LEN]);
Ok(EntropyIv(out))
}
pub fn load_material(key_file: &Path, iv_file: &Path) -> Result<EntropyMaterial, EntropyError> {
let key = load_key_file(key_file)?;
let iv = load_iv_file(iv_file)?;
Ok(EntropyMaterial::new(key, iv))
}
fn io_err(path: &Path, what: &str, e: &std::io::Error) -> EntropyError {
EntropyError::Io(std::io::Error::new(
e.kind(),
format!("{what} {}: {e}", path.display()),
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn write_temp(contents: &[u8]) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(contents).unwrap();
f.flush().unwrap();
f
}
#[test]
fn loads_raw_16_byte_key() {
let f = write_temp(b"0123456789012345\n");
let key = load_key_file(f.path()).unwrap();
assert_eq!(key.as_bytes(), b"0123456789012345");
}
#[test]
fn loads_raw_16_byte_iv() {
let f = write_temp(b"0123456789012345\n");
let iv = load_iv_file(f.path()).unwrap();
assert_eq!(iv.as_bytes(), b"0123456789012345");
}
#[test]
fn rejects_short_key() {
let f = write_temp(b"short\n");
let err = load_key_file(f.path()).unwrap_err();
assert!(matches!(err, EntropyError::KeyMaterial(_)));
}
#[test]
fn rejects_short_iv() {
let f = write_temp(b"short\n");
let err = load_iv_file(f.path()).unwrap_err();
assert!(matches!(err, EntropyError::KeyMaterial(_)));
}
#[test]
fn truncates_oversized_key_to_16_bytes() {
let f = write_temp(b"01234567890123456\n");
let key = load_key_file(f.path()).unwrap();
assert_eq!(key.as_bytes(), b"0123456789012345");
}
#[test]
fn truncates_oversized_iv_to_16_bytes() {
let f = write_temp(b"01234567890123456\n");
let iv = load_iv_file(f.path()).unwrap();
assert_eq!(iv.as_bytes(), b"0123456789012345");
}
#[test]
fn loads_pem_armored_16_bytes() {
let body: [u8; 16] = [0x42; 16];
let armored = format!(
"-----BEGIN ENTROPY KEY-----\n{}\n-----END ENTROPY KEY-----\n",
crate::crypto::base64::base64_encode(&body)
);
let f = write_temp(armored.as_bytes());
let key = load_key_file(f.path()).unwrap();
assert_eq!(key.as_bytes(), &body);
}
#[test]
fn missing_file_is_io_error() {
let path = Path::new("/nonexistent/dynomite/no-such-key");
let err = load_key_file(path).unwrap_err();
assert!(matches!(err, EntropyError::Io(_)));
}
#[test]
fn loads_bundled_recon_fixtures() {
let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
let key_path = crate_root.join("tests/fixtures/recon/recon_key.pem");
let iv_path = crate_root.join("tests/fixtures/recon/recon_iv.pem");
let key = load_key_file(&key_path).unwrap();
let iv = load_iv_file(&iv_path).unwrap();
assert_eq!(key.as_bytes(), b"0123456789012345");
assert_eq!(iv.as_bytes(), b"0123456789012345");
}
}