#[cfg(windows)]
pub mod dpapi;
#[cfg(target_os = "macos")]
pub mod secure_enclave;
#[cfg(target_os = "linux")]
pub mod tpm2;
use crate::error::Error;
pub const V2_MAGIC: [u8; 4] = *b"ES2\0";
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
None = 0,
Dpapi = 1,
SecureEnclave = 2,
Tpm2 = 3,
}
impl Backend {
pub fn from_id(id: u8) -> Option<Self> {
match id {
0 => Some(Self::None),
1 => Some(Self::Dpapi),
2 => Some(Self::SecureEnclave),
3 => Some(Self::Tpm2),
_ => None,
}
}
pub fn id(&self) -> u8 {
*self as u8
}
pub fn name(&self) -> &'static str {
match self {
Self::None => "passphrase-only (no hardware seal)",
Self::Dpapi => "Windows DPAPI",
Self::SecureEnclave => "macOS Secure Enclave",
Self::Tpm2 => "Linux TPM 2.0",
}
}
pub fn tier(&self) -> u8 {
match self {
Self::SecureEnclave | Self::Tpm2 => 1,
Self::Dpapi => 2,
Self::None => 3,
}
}
}
pub enum DeviceKeystore {
None,
#[cfg(windows)]
Dpapi(dpapi::DpapiKeystore),
#[cfg(target_os = "macos")]
SecureEnclave(secure_enclave::SecureEnclaveKeystore),
#[cfg(target_os = "linux")]
Tpm2(tpm2::Tpm2Keystore),
}
impl DeviceKeystore {
pub fn select() -> Self {
#[cfg(windows)]
{
Self::Dpapi(dpapi::DpapiKeystore::new())
}
#[cfg(target_os = "macos")]
{
match secure_enclave::SecureEnclaveKeystore::try_new() {
Some(ks) => Self::SecureEnclave(ks),
None => Self::None,
}
}
#[cfg(target_os = "linux")]
{
match tpm2::Tpm2Keystore::try_new() {
Some(ks) => Self::Tpm2(ks),
None => Self::None,
}
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
{
Self::None
}
}
pub fn backend(&self) -> Backend {
match self {
Self::None => Backend::None,
#[cfg(windows)]
Self::Dpapi(_) => Backend::Dpapi,
#[cfg(target_os = "macos")]
Self::SecureEnclave(_) => Backend::SecureEnclave,
#[cfg(target_os = "linux")]
Self::Tpm2(_) => Backend::Tpm2,
}
}
pub fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
match self {
Self::None => Ok(plaintext.to_vec()),
#[cfg(windows)]
Self::Dpapi(ks) => ks.seal(plaintext),
#[cfg(target_os = "macos")]
Self::SecureEnclave(ks) => ks.seal(plaintext),
#[cfg(target_os = "linux")]
Self::Tpm2(ks) => ks.seal(plaintext),
}
}
pub fn unseal(&self, sealed: &[u8]) -> Result<Vec<u8>, Error> {
match self {
Self::None => Ok(sealed.to_vec()),
#[cfg(windows)]
Self::Dpapi(ks) => ks.unseal(sealed),
#[cfg(target_os = "macos")]
Self::SecureEnclave(ks) => ks.unseal(sealed),
#[cfg(target_os = "linux")]
Self::Tpm2(ks) => ks.unseal(sealed),
}
}
}
pub fn pack_v2(backend: Backend, sealed: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + 1 + 4 + sealed.len());
out.extend_from_slice(&V2_MAGIC);
out.push(backend.id());
let len_le = u32::try_from(sealed.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len_le.to_le_bytes());
out.extend_from_slice(sealed);
out
}
#[derive(Debug)]
pub struct V2Envelope<'a> {
pub backend: Backend,
pub sealed: &'a [u8],
}
pub fn parse_v2(buf: &[u8]) -> Result<V2Envelope<'_>, Error> {
if buf.len() < 4 || buf[..4] != V2_MAGIC {
return Err(Error::CryptoFailure(
"master.key missing v2 magic — file is corrupted or from an \
incompatible envseal build"
.to_string(),
));
}
if buf.len() < 9 {
return Err(Error::CryptoFailure(
"master.key v2 header truncated".to_string(),
));
}
let backend = Backend::from_id(buf[4]).ok_or_else(|| {
Error::CryptoFailure(format!("master.key v2 unknown backend id {}", buf[4]))
})?;
let mut len_bytes = [0u8; 4];
len_bytes.copy_from_slice(&buf[5..9]);
let sealed_len = u32::from_le_bytes(len_bytes) as usize;
let body = &buf[9..];
if body.len() != sealed_len {
return Err(Error::CryptoFailure(format!(
"master.key v2 length mismatch: header says {sealed_len}, body has {}",
body.len()
)));
}
Ok(V2Envelope {
backend,
sealed: body,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_then_parse_roundtrips() {
let inner = b"hello world";
let packed = pack_v2(Backend::Dpapi, inner);
let env = parse_v2(&packed).expect("v2 detected");
assert_eq!(env.backend, Backend::Dpapi);
assert_eq!(env.sealed, inner);
}
#[test]
fn missing_magic_rejected_as_corruption() {
let bad = vec![0u8; 76];
assert!(parse_v2(&bad).is_err());
}
#[test]
fn truncated_v2_header_rejected() {
let mut bad = V2_MAGIC.to_vec();
bad.push(Backend::Dpapi.id());
assert!(parse_v2(&bad).is_err());
}
#[test]
fn unknown_backend_id_rejected() {
let mut bad = V2_MAGIC.to_vec();
bad.push(99); bad.extend_from_slice(&0u32.to_le_bytes());
assert!(parse_v2(&bad).is_err());
}
#[test]
fn length_mismatch_rejected() {
let mut bad = V2_MAGIC.to_vec();
bad.push(Backend::None.id());
bad.extend_from_slice(&100u32.to_le_bytes()); bad.extend_from_slice(&[0u8; 5]); assert!(parse_v2(&bad).is_err());
}
#[test]
fn none_backend_seal_is_passthrough() {
let ks = DeviceKeystore::None;
let p = b"plaintext";
let s = ks.seal(p).unwrap();
assert_eq!(&s, p);
assert_eq!(ks.unseal(&s).unwrap(), p);
}
#[test]
fn backend_tiers_are_well_defined() {
assert_eq!(Backend::SecureEnclave.tier(), 1);
assert_eq!(Backend::Tpm2.tier(), 1);
assert_eq!(Backend::Dpapi.tier(), 2);
assert_eq!(Backend::None.tier(), 3);
}
}