use std::sync::Mutex;
use std::time::Instant;
use serde::Serialize;
use tracing::{info, warn};
use zeroize::Zeroize;
use crate::error::{AppError, tee_attestation_error};
pub struct MnemonicExportGuard {
inner: Mutex<GuardState>,
}
struct GuardState {
entropy: Option<[u8; 32]>,
created_at: Instant,
window_secs: u64,
exported: bool,
}
impl Drop for GuardState {
fn drop(&mut self) {
self.wipe_entropy();
}
}
impl GuardState {
fn wipe_entropy(&mut self) {
if let Some(ref mut e) = self.entropy {
e.zeroize();
}
self.entropy = None;
}
}
#[derive(Debug, Serialize)]
pub struct MnemonicExportResponse {
pub mnemonic: String,
pub window_remaining_secs: u64,
}
#[derive(Debug, Serialize)]
pub struct MnemonicExportStatus {
pub window_active: bool,
pub already_exported: bool,
pub entropy_available: bool,
pub window_remaining_secs: u64,
}
impl MnemonicExportGuard {
pub fn new(entropy: [u8; 32], window_secs: u64) -> Self {
info!(
window_secs,
"mnemonic export guard created — window open for {window_secs}s"
);
Self {
inner: Mutex::new(GuardState {
entropy: Some(entropy),
created_at: Instant::now(),
window_secs,
exported: false,
}),
}
}
pub fn empty() -> Self {
Self {
inner: Mutex::new(GuardState {
entropy: None,
created_at: Instant::now(),
window_secs: 0,
exported: false,
}),
}
}
pub fn status(&self) -> MnemonicExportStatus {
let guard = self.inner.lock().unwrap();
let elapsed = guard.created_at.elapsed().as_secs();
let window_active =
guard.entropy.is_some() && !guard.exported && elapsed < guard.window_secs;
let remaining = if window_active {
guard.window_secs.saturating_sub(elapsed)
} else {
0
};
MnemonicExportStatus {
window_active,
already_exported: guard.exported,
entropy_available: guard.entropy.is_some(),
window_remaining_secs: remaining,
}
}
pub fn export(&self) -> Result<MnemonicExportResponse, AppError> {
let mut guard = self.inner.lock().unwrap();
let entropy = match guard.entropy {
Some(e) => e,
None => {
return Err(tee_attestation_error(
"no mnemonic available — entropy only exists on first boot",
));
}
};
if guard.exported {
return Err(tee_attestation_error(
"mnemonic already exported — one-time operation",
));
}
let elapsed = guard.created_at.elapsed().as_secs();
if elapsed >= guard.window_secs {
guard.wipe_entropy();
warn!("mnemonic export attempted after window expired — entropy zeroed");
return Err(tee_attestation_error(format!(
"mnemonic export window expired ({elapsed}s elapsed, window was {}s)",
guard.window_secs
)));
}
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
.map_err(|e| tee_attestation_error(format!("failed to derive mnemonic: {e}")))?;
let remaining = guard.window_secs.saturating_sub(elapsed);
guard.exported = true;
guard.wipe_entropy();
info!(
remaining_secs = remaining,
"mnemonic exported to authenticated super admin — entropy zeroed"
);
let mut mnemonic_str = mnemonic.to_string();
let response = MnemonicExportResponse {
mnemonic: mnemonic_str.clone(),
window_remaining_secs: remaining,
};
mnemonic_str.zeroize();
Ok(response)
}
}