#![cfg(feature = "fido2")]
use zeroize::Zeroizing;
use crate::audit;
use crate::error::Error;
use crate::vault::fido2::Fido2Authenticator;
use crate::vault::keychain::fido2_unlock;
use crate::vault::keychain::fido2_unlock::{
credential_id_hash, disable_fido2_keep_master, enroll_fido2_on_existing_master,
fido2_status_at, unlock_master_key_with_fido2, Fido2Status,
};
use crate::vault::keychain::master_key_path;
use crate::vault::Vault;
use crate::{gui, security_config};
#[derive(Debug, Clone)]
pub struct Fido2StatusReport {
pub vault_initialized: bool,
pub enrolled: bool,
pub credential_id_hash: Option<String>,
pub hardware_backend_available: bool,
}
pub fn status() -> Result<Fido2StatusReport, Error> {
let root = crate::ops::vault_root()?;
let mk_path = master_key_path(&root);
let st = fido2_status_at(&mk_path)?;
let (vault_initialized, enrolled, credential_id_hash) = match st {
Fido2Status::NoVault => (false, false, None),
Fido2Status::NotEnrolled => (true, false, None),
Fido2Status::Enrolled { credential_id } => {
(true, true, Some(self::credential_id_hash_local(&credential_id)))
}
};
Ok(Fido2StatusReport {
vault_initialized,
enrolled,
credential_id_hash,
hardware_backend_available: cfg!(feature = "fido2-hardware"),
})
}
pub fn enroll<A: Fido2Authenticator>(authenticator: &mut A) -> Result<(), Error> {
let root = crate::ops::vault_root()?;
let mk_path = master_key_path(&root);
match fido2_status_at(&mk_path)? {
Fido2Status::NoVault => {
return Err(Error::CryptoFailure(
"vault does not exist — store a secret first to initialize it".to_string(),
));
}
Fido2Status::Enrolled { .. } => {
return Err(Error::CryptoFailure(
"vault is already FIDO2-enrolled. Run `envseal security \
fido2-disable` first if you want to switch authenticators."
.to_string(),
));
}
Fido2Status::NotEnrolled => {}
}
let cfg = security_config::load_system_defaults();
let passphrase: Zeroizing<String> = gui::request_passphrase(false, &cfg)?;
let vault = Vault::open_default_with_passphrase(&passphrase)?;
enroll_fido2_on_existing_master(&mk_path, vault.master_key(), &passphrase, authenticator)?;
if let Fido2Status::Enrolled { credential_id } = fido2_status_at(&mk_path)? {
let _ = audit::log_required(&audit::AuditEvent::Fido2Enrolled {
credential_id_hash: credential_id_hash(&credential_id),
});
}
Ok(())
}
pub fn disable<A: Fido2Authenticator>(authenticator: &mut A) -> Result<(), Error> {
let root = crate::ops::vault_root()?;
let mk_path = master_key_path(&root);
let pre_status = fido2_status_at(&mk_path)?;
let credential_id = match pre_status {
Fido2Status::Enrolled { ref credential_id } => credential_id.clone(),
_ => return Err(Error::Fido2NotEnrolled),
};
let cfg = security_config::load_system_defaults();
let passphrase = gui::request_passphrase(false, &cfg)?;
let master = unlock_master_key_with_fido2(&mk_path, &passphrase, authenticator)?;
disable_fido2_keep_master(&mk_path, &master, &passphrase)?;
let _ = audit::log_required(&audit::AuditEvent::Fido2Disabled {
credential_id_hash: credential_id_hash(&credential_id),
});
Ok(())
}
fn credential_id_hash_local(credential_id: &[u8]) -> String {
fido2_unlock::credential_id_hash(credential_id)
}
#[cfg(feature = "fido2-hardware")]
pub fn enroll_with_hardware() -> Result<(), Error> {
let mut auth = crate::vault::fido2_hardware::HwAuthenticator::discover()?;
enroll(&mut auth)
}
#[cfg(feature = "fido2-hardware")]
pub fn disable_with_hardware() -> Result<(), Error> {
let mut auth = crate::vault::fido2_hardware::HwAuthenticator::discover()?;
disable(&mut auth)
}
#[cfg(not(feature = "fido2-hardware"))]
pub fn enroll_with_hardware() -> Result<(), Error> {
Err(Error::CryptoFailure(
"this build of envseal does not include the FIDO2 hardware backend. \
rebuild with `cargo install envseal-cli --features fido2-hardware` \
to enable security-key support."
.to_string(),
))
}
#[cfg(not(feature = "fido2-hardware"))]
pub fn disable_with_hardware() -> Result<(), Error> {
Err(Error::CryptoFailure(
"this build of envseal does not include the FIDO2 hardware backend. \
rebuild with `cargo install envseal-cli --features fido2-hardware` \
to enable security-key support."
.to_string(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_report_shape_serializes_cleanly() {
let r = Fido2StatusReport {
vault_initialized: true,
enrolled: false,
credential_id_hash: None,
hardware_backend_available: false,
};
assert!(r.vault_initialized);
assert!(!r.enrolled);
assert!(r.credential_id_hash.is_none());
}
#[test]
fn credential_id_hash_local_matches_keychain_helper() {
let id = b"some-opaque-credential-id-xyz";
let a = credential_id_hash_local(id);
let b = fido2_unlock::credential_id_hash(id);
assert_eq!(a, b);
assert_eq!(a.len(), 64);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
}