#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
#[allow(unused_imports)]
use super::error::{Result, StorageError};
use super::platform::BackendKind;
use super::StorageConfig;
#[allow(unused_imports)]
use crate::internal::core::metadata;
#[allow(unused_imports)]
use crate::internal::core::traits::{EnclaveEncryptor, EnclaveKeyManager};
#[allow(unused_imports)]
use crate::internal::core::types::{AccessPolicy, KeyType};
#[allow(unused_imports)]
use tracing::{debug, warn};
pub struct AppEncryptionStorage {
kind: BackendKind,
app_name: String,
key_label: String,
access_policy: AccessPolicy,
keys_dir: std::path::PathBuf,
inner: StorageInner,
}
enum StorageInner {
#[cfg(target_os = "macos")]
SecureEnclave(crate::internal::apple::SecureEnclaveEncryptor),
#[cfg(target_os = "windows")]
Tpm(crate::internal::windows::TpmEncryptor),
#[cfg(target_os = "windows")]
WindowsDpapi(crate::internal::windows::DpapiEncryptor),
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
LinuxTpm(crate::internal::linux_tpm::LinuxTpmEncryptor),
#[cfg(target_os = "linux")]
Software(crate::internal::keyring::SoftwareEncryptor),
#[cfg(target_os = "linux")]
WslBridge(BridgeEncryptorWrapper),
}
#[cfg(target_os = "linux")]
struct BridgeEncryptorWrapper {
bridge_path: std::path::PathBuf,
app_name: String,
key_label: String,
access_policy: AccessPolicy,
}
#[cfg(target_os = "linux")]
impl EnclaveKeyManager for BridgeEncryptorWrapper {
fn generate(
&self,
label: &str,
_key_type: KeyType,
policy: AccessPolicy,
) -> crate::internal::core::Result<Vec<u8>> {
crate::internal::bridge::bridge_init(&self.bridge_path, &self.app_name, label, policy)?;
self.public_key(label)
}
fn public_key(&self, label: &str) -> crate::internal::core::Result<Vec<u8>> {
crate::internal::bridge::bridge_public_key(
&self.bridge_path,
&self.app_name,
label,
self.access_policy,
)
}
fn list_keys(&self) -> crate::internal::core::Result<Vec<String>> {
crate::internal::bridge::bridge_list_keys(
&self.bridge_path,
&self.app_name,
&self.key_label,
self.access_policy,
)
}
fn delete_key(&self, label: &str) -> crate::internal::core::Result<()> {
crate::internal::bridge::bridge_destroy(&self.bridge_path, &self.app_name, label)
}
fn is_available(&self) -> bool {
true
}
fn key_exists(&self, label: &str) -> crate::internal::core::Result<bool> {
match self.public_key(label) {
Ok(_) => Ok(true),
Err(crate::internal::core::Error::KeyNotFound { .. }) => Ok(false),
Err(e) => Err(e),
}
}
}
#[cfg(target_os = "linux")]
impl EnclaveEncryptor for BridgeEncryptorWrapper {
fn encrypt(&self, label: &str, plaintext: &[u8]) -> crate::internal::core::Result<Vec<u8>> {
crate::internal::bridge::bridge_encrypt(
&self.bridge_path,
&self.app_name,
label,
plaintext,
self.access_policy,
)
}
fn decrypt(&self, label: &str, ciphertext: &[u8]) -> crate::internal::core::Result<Vec<u8>> {
crate::internal::bridge::bridge_decrypt(
&self.bridge_path,
&self.app_name,
label,
ciphertext,
self.access_policy,
)
}
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
unsafe impl Send for BridgeEncryptorWrapper {}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
unsafe impl Sync for BridgeEncryptorWrapper {}
impl std::fmt::Debug for AppEncryptionStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppEncryptionStorage")
.field("kind", &self.kind)
.field("app_name", &self.app_name)
.field("key_label", &self.key_label)
.field("access_policy", &self.access_policy)
.finish()
}
}
impl AppEncryptionStorage {
#[allow(clippy::needless_return, unreachable_code)]
pub fn init(mut config: StorageConfig) -> Result<Self> {
config.app_name = crate::internal::core::signing::ensure_safe_app_name(&config.app_name);
#[cfg(target_os = "macos")]
{
return Self::init_macos(&config);
}
#[cfg(target_os = "windows")]
{
return Self::init_windows(&config);
}
#[cfg(target_os = "linux")]
{
if config.force_keyring {
debug!("--keyring flag: forcing software keyring backend for encryption");
return Self::init_linux_keyring(&config);
}
if crate::internal::wsl::is_wsl() {
return Self::init_wsl(&config);
}
return Self::init_linux(&config);
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = config;
Err(StorageError::NotAvailable)
}
}
fn resolved_keys_dir(config: &StorageConfig) -> std::path::PathBuf {
config
.keys_dir
.clone()
.unwrap_or_else(|| metadata::keys_dir(&config.app_name))
}
#[cfg(target_os = "macos")]
fn init_macos(config: &StorageConfig) -> Result<Self> {
let keys_dir = Self::resolved_keys_dir(config);
let mut keychain_config = crate::internal::apple::KeychainConfig::with_keys_dir(
&config.app_name,
keys_dir.clone(),
)
.with_user_presence(config.wrapping_key_user_presence)
.with_cache_ttl(config.wrapping_key_cache_ttl);
if let Some(ref group) = config.keychain_access_group {
keychain_config = keychain_config.with_access_group(group.clone());
}
let encryptor =
crate::internal::apple::SecureEnclaveEncryptor::with_config(keychain_config);
if !encryptor.is_available() {
return Err(StorageError::NotAvailable);
}
Self::ensure_key(&encryptor, config, &keys_dir, config.access_policy)?;
debug!(
"Secure Enclave encryption ready (app={}, label={}, policy={:?})",
config.app_name, config.key_label, config.access_policy
);
Ok(Self {
kind: BackendKind::SecureEnclave,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: config.access_policy,
keys_dir,
inner: StorageInner::SecureEnclave(encryptor),
})
}
#[cfg(target_os = "windows")]
fn init_windows(config: &StorageConfig) -> Result<Self> {
match Self::init_windows_tpm(config) {
Ok(storage) => Ok(storage),
Err(err) => {
if config.windows_software_fallback
!= crate::internal::app_storage::WindowsSoftwareFallback::VmOnly
{
return Err(err);
}
let decision =
crate::internal::windows::dpapi_fallback::should_use_dpapi_after_tpm_failure(
&format!("{err:#}"),
);
if !decision.allowed {
tracing::warn!(
app = %config.app_name,
reason = %decision.reason,
"Windows DPAPI fallback denied after TPM storage failure"
);
return Err(err);
}
tracing::warn!(
app = %config.app_name,
reason = %decision.reason,
"Windows TPM storage unavailable on VM host; using per-user DPAPI fallback"
);
Self::init_windows_dpapi(config)
}
}
}
#[cfg(target_os = "windows")]
fn init_windows_tpm(config: &StorageConfig) -> Result<Self> {
let keys_dir = Self::resolved_keys_dir(config);
let (effective_policy, hello_gate) = if config.prefer_windows_hello_ux {
let gate = std::sync::Arc::new(crate::internal::windows::hello_gate::HelloGate::new());
if config.access_policy != AccessPolicy::None {
tracing::info!(
app = %config.app_name,
requested_policy = ?config.access_policy,
"prefer_windows_hello_ux: TPM key created without NCRYPT_UI_PROTECT_KEY_FLAG \
and on-disk AccessPolicy recorded as None; Hello consent is enforced at the \
application level via UserConsentVerifier (soft gate). The caller-requested \
AccessPolicy is honored only as a UX intent signal, not as an OS-mediated \
hardware policy."
);
}
(AccessPolicy::None, Some(gate))
} else {
(config.access_policy, None)
};
let mut encryptor = crate::internal::windows::TpmEncryptor::with_keys_dir(
&config.app_name,
keys_dir.clone(),
);
if let Some(gate) = hello_gate.clone() {
encryptor = encryptor.with_hello_gate(gate, config.wrapping_key_cache_ttl);
}
if !encryptor.is_available() {
return Err(StorageError::NotAvailable);
}
Self::ensure_key(&encryptor, config, &keys_dir, effective_policy)?;
debug!(
"TPM encryption ready (app={}, label={}, requested_policy={:?}, effective_policy={:?}, hello_gate={})",
config.app_name,
config.key_label,
config.access_policy,
effective_policy,
hello_gate.is_some(),
);
Ok(Self {
kind: BackendKind::Tpm,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: effective_policy,
keys_dir,
inner: StorageInner::Tpm(encryptor),
})
}
#[cfg(target_os = "windows")]
fn init_windows_dpapi(config: &StorageConfig) -> Result<Self> {
let keys_dir = Self::resolved_keys_dir(config);
if config.access_policy != AccessPolicy::None {
tracing::warn!(
app = %config.app_name,
requested_policy = ?config.access_policy,
"Windows DPAPI fallback does not enforce AccessPolicy; recording AccessPolicy::None"
);
}
let effective_policy = AccessPolicy::None;
let mut encryptor = crate::internal::windows::DpapiEncryptor::with_keys_dir(
&config.app_name,
keys_dir.clone(),
);
if let Some(ak) = config.dpapi_app_key {
encryptor = encryptor.with_app_key(ak);
}
Self::ensure_key(&encryptor, config, &keys_dir, effective_policy)?;
Ok(Self {
kind: BackendKind::WindowsDpapi,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: effective_policy,
keys_dir,
inner: StorageInner::WindowsDpapi(encryptor),
})
}
#[cfg(target_os = "linux")]
fn init_linux(config: &StorageConfig) -> Result<Self> {
#[cfg(all(target_env = "gnu", feature = "linux-tpm"))]
if crate::internal::linux_tpm::is_available() {
let keys_dir = Self::resolved_keys_dir(config);
let encryptor = crate::internal::linux_tpm::LinuxTpmEncryptor::with_keys_dir(
&config.app_name,
keys_dir.clone(),
);
Self::ensure_key(&encryptor, config, &keys_dir, config.access_policy)?;
debug!("Linux TPM encryption ready (app={})", config.app_name);
return Ok(Self {
kind: BackendKind::Tpm,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: config.access_policy,
keys_dir,
inner: StorageInner::LinuxTpm(encryptor),
});
}
Self::init_linux_keyring(config)
}
#[cfg(target_os = "linux")]
fn init_linux_keyring(config: &StorageConfig) -> Result<Self> {
if !crate::internal::keyring::has_keyring_feature() {
return Err(StorageError::NotAvailable);
}
let keys_dir = Self::resolved_keys_dir(config);
if config.access_policy != AccessPolicy::None {
#[allow(clippy::print_stderr)]
{
eprintln!(
"warning: biometric/user-presence has no effect on Linux \
software fallback"
);
}
}
let encryptor = crate::internal::keyring::SoftwareEncryptor::with_keys_dir(
&config.app_name,
keys_dir.clone(),
);
Self::ensure_key(&encryptor, config, &keys_dir, AccessPolicy::None)?;
debug!(
"Linux keyring encryption backend ready (app={})",
config.app_name
);
Ok(Self {
kind: BackendKind::Keyring,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: config.access_policy,
keys_dir,
inner: StorageInner::Software(encryptor),
})
}
#[cfg(target_os = "linux")]
fn init_wsl(config: &StorageConfig) -> Result<Self> {
let bridge_path = crate::internal::app_storage::platform::find_bridge_executable(
&config.app_name,
&config.extra_bridge_paths,
)
.ok_or(StorageError::NotAvailable)?;
debug!(
"WSL TPM bridge found at {} (app={})",
bridge_path.display(),
config.app_name
);
crate::internal::bridge::bridge_init(
&bridge_path,
&config.app_name,
&config.key_label,
config.access_policy,
)
.map_err(|e| StorageError::KeyInitFailed(e.to_string()))?;
let wrapper = BridgeEncryptorWrapper {
bridge_path,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: config.access_policy,
};
Ok(Self {
kind: BackendKind::TpmBridge,
app_name: config.app_name.clone(),
key_label: config.key_label.clone(),
access_policy: config.access_policy,
keys_dir: Self::resolved_keys_dir(config),
inner: StorageInner::WslBridge(wrapper),
})
}
fn ensure_key(
encryptor: &impl EnclaveEncryptor,
config: &StorageConfig,
keys_dir: &std::path::Path,
expected_policy: AccessPolicy,
) -> Result<()> {
if encryptor.public_key(&config.key_label).is_ok() {
let meta_path = keys_dir.join(format!("{}.meta", config.key_label));
let meta_exists = meta_path.exists();
#[cfg(test)]
let _ = meta_exists;
#[cfg(test)]
let hmac_key: Option<zeroize::Zeroizing<Vec<u8>>> = None;
#[cfg(not(test))]
let hmac_key = meta_exists
.then(|| crate::internal::app_storage::platform::meta_hmac_key(&config.app_name))
.flatten();
if let Some(hmac_key) = hmac_key {
let strict = metadata::load_meta_with_hmac(
keys_dir,
&config.key_label,
hmac_key.as_slice(),
metadata::MetaIntegrityMode::RequireSidecar,
);
if let Err(e) = strict {
let msg = e.to_string();
if msg.contains(metadata::META_HMAC_VERIFY_OP) {
return Err(StorageError::KeyInitFailed(msg));
}
if msg.contains(metadata::META_HMAC_MISSING_OP) {
warn!(
label = %config.key_label,
"`.meta.hmac` sidecar missing — migrating from existing meta. \
If you did not just upgrade, treat this as suspicious and \
regenerate the key."
);
if let Err(migrate_err) = metadata::migrate_meta_to_hmac(
keys_dir,
&config.key_label,
hmac_key.as_slice(),
) {
return Err(StorageError::KeyInitFailed(migrate_err.to_string()));
}
}
}
}
if let Ok(meta) = metadata::load_meta(keys_dir, &config.key_label) {
if meta.access_policy != expected_policy {
warn!(
"key policy mismatch: existing={:?}, requested={:?}; re-generating key",
meta.access_policy, expected_policy
);
encryptor
.delete_key(&config.key_label)
.map_err(|e| StorageError::KeyInitFailed(e.to_string()))?;
} else {
return Ok(());
}
} else {
warn!(
"key exists but metadata missing (label={}); using key with unknown policy",
config.key_label
);
return Ok(());
}
}
debug!("generating new encryption key (label={})", config.key_label);
encryptor
.generate(&config.key_label, KeyType::Encryption, expected_policy)
.map_err(|e| StorageError::KeyInitFailed(e.to_string()))?;
Self::stamp_trust_anchor(&config.app_name, &config.key_label, keys_dir);
Ok(())
}
#[cfg_attr(
not(any(target_os = "macos", target_os = "windows", target_os = "linux")),
allow(unused_variables)
)]
fn stamp_trust_anchor(app_name: &str, label: &str, keys_dir: &std::path::Path) {
#[cfg(all(not(test), target_os = "macos"))]
{
if let Ok(Some(hk)) = crate::internal::apple::meta_hmac::load_existing(app_name) {
let meta_path = keys_dir.join(format!("{label}.meta"));
if let Ok(meta_bytes) = std::fs::read(&meta_path) {
let tag = metadata::compute_meta_hmac_bytes(hk.as_slice(), &meta_bytes);
if let Err(e) = crate::internal::apple::meta_tag::store(app_name, label, &tag) {
warn!(
label = %label,
error = %e,
"encryption keygen meta-tag stamp failed"
);
}
}
}
}
#[cfg(all(not(test), target_os = "windows"))]
{
if let Ok(Some(hk)) = crate::internal::windows::meta_hmac::load_or_create(app_name) {
if let Err(e) = crate::internal::windows::meta_tag::stamp_from_disk(
app_name,
label,
keys_dir,
hk.as_slice(),
) {
warn!(
label = %label,
error = %e,
"encryption keygen meta-tag stamp failed"
);
}
}
}
#[cfg(all(not(test), target_os = "linux"))]
{
if let Ok(Some(hk)) = crate::internal::keyring::meta_hmac_key_existing(app_name) {
if let Err(e) = crate::internal::keyring::meta_tag::stamp_from_disk(
app_name,
label,
keys_dir,
hk.as_slice(),
) {
warn!(
label = %label,
error = %e,
"encryption keygen meta-tag stamp failed"
);
}
}
}
#[cfg(test)]
let _ = (app_name, label, keys_dir);
}
pub fn encryptor(&self) -> &dyn EnclaveEncryptor {
match &self.inner {
#[cfg(target_os = "macos")]
StorageInner::SecureEnclave(e) => e,
#[cfg(target_os = "windows")]
StorageInner::Tpm(e) => e,
#[cfg(target_os = "windows")]
StorageInner::WindowsDpapi(e) => e,
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
StorageInner::LinuxTpm(e) => e,
#[cfg(target_os = "linux")]
StorageInner::Software(e) => e,
#[cfg(target_os = "linux")]
StorageInner::WslBridge(w) => w,
}
}
pub fn key_manager(&self) -> &dyn EnclaveKeyManager {
match &self.inner {
#[cfg(target_os = "macos")]
StorageInner::SecureEnclave(e) => e,
#[cfg(target_os = "windows")]
StorageInner::Tpm(e) => e,
#[cfg(target_os = "windows")]
StorageInner::WindowsDpapi(e) => e,
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
StorageInner::LinuxTpm(e) => e,
#[cfg(target_os = "linux")]
StorageInner::Software(e) => e,
#[cfg(target_os = "linux")]
StorageInner::WslBridge(w) => w,
}
}
}
pub trait EncryptionStorage: Send + Sync {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>>;
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>>;
fn destroy(&self) -> Result<()>;
fn is_available(&self) -> bool;
fn backend_name(&self) -> &'static str;
fn backend_kind(&self) -> BackendKind;
}
impl EncryptionStorage for AppEncryptionStorage {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
match &self.inner {
#[cfg(target_os = "macos")]
StorageInner::SecureEnclave(enc) => enc
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::Tpm(enc) => enc
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::WindowsDpapi(enc) => enc
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
StorageInner::LinuxTpm(enc) => enc
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::Software(enc) => enc
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::WslBridge(w) => w
.encrypt(&self.key_label, plaintext)
.map_err(|e| StorageError::EncryptionFailed(e.to_string())),
}
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
crate::internal::app_storage::platform::check_meta_integrity(
&self.app_name,
&self.key_label,
&self.keys_dir,
)?;
match &self.inner {
#[cfg(target_os = "macos")]
StorageInner::SecureEnclave(enc) => enc
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::Tpm(enc) => enc
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::WindowsDpapi(enc) => enc
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
StorageInner::LinuxTpm(enc) => enc
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::Software(enc) => enc
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::WslBridge(w) => w
.decrypt(&self.key_label, ciphertext)
.map_err(|e| StorageError::DecryptionFailed(e.to_string())),
}
}
fn destroy(&self) -> Result<()> {
match &self.inner {
#[cfg(target_os = "macos")]
StorageInner::SecureEnclave(enc) => enc
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::Tpm(enc) => enc
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
#[cfg(target_os = "windows")]
StorageInner::WindowsDpapi(enc) => enc
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
#[cfg(all(target_os = "linux", target_env = "gnu", feature = "linux-tpm"))]
StorageInner::LinuxTpm(enc) => enc
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::Software(enc) => enc
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
#[cfg(target_os = "linux")]
StorageInner::WslBridge(w) => w
.delete_key(&self.key_label)
.map_err(|e| StorageError::KeyNotFound(e.to_string())),
}
}
fn is_available(&self) -> bool {
true
}
fn backend_name(&self) -> &'static str {
match self.kind {
BackendKind::SecureEnclave => "Secure Enclave",
BackendKind::Tpm => "TPM 2.0",
BackendKind::WindowsDpapi => "Windows DPAPI",
BackendKind::TpmBridge => "TPM 2.0 (WSL Bridge)",
BackendKind::Keyring => "Linux (keyring)",
}
}
fn backend_kind(&self) -> BackendKind {
self.kind
}
}
#[allow(unsafe_code)]
unsafe impl Send for AppEncryptionStorage {}
#[allow(unsafe_code)]
unsafe impl Sync for AppEncryptionStorage {}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::unwrap_in_result, clippy::panic)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
struct TestEncryptorBackend {
keys: Mutex<HashMap<String, Vec<u8>>>,
}
impl TestEncryptorBackend {
fn new() -> Self {
Self {
keys: Mutex::new(HashMap::new()),
}
}
fn derive_pubkey(label: &str) -> Vec<u8> {
let hash = Sha256::digest(label.as_bytes());
let mut body = Vec::with_capacity(64);
body.extend_from_slice(&hash);
body.extend_from_slice(&hash);
let mut pubkey = Vec::with_capacity(65);
pubkey.push(0x04);
pubkey.extend_from_slice(&body);
pubkey
}
}
impl crate::internal::core::traits::EnclaveKeyManager for TestEncryptorBackend {
fn generate(
&self,
label: &str,
_key_type: KeyType,
_policy: AccessPolicy,
) -> crate::internal::core::Result<Vec<u8>> {
let pubkey = Self::derive_pubkey(label);
self.keys
.lock()
.unwrap()
.insert(label.to_string(), pubkey.clone());
Ok(pubkey)
}
fn public_key(&self, label: &str) -> crate::internal::core::Result<Vec<u8>> {
self.keys
.lock()
.unwrap()
.get(label)
.cloned()
.ok_or_else(|| crate::internal::core::Error::KeyNotFound {
label: label.to_string(),
})
}
fn list_keys(&self) -> crate::internal::core::Result<Vec<String>> {
Ok(self.keys.lock().unwrap().keys().cloned().collect())
}
fn delete_key(&self, label: &str) -> crate::internal::core::Result<()> {
self.keys
.lock()
.unwrap()
.remove(label)
.map(|_| ())
.ok_or_else(|| crate::internal::core::Error::KeyNotFound {
label: label.to_string(),
})
}
fn is_available(&self) -> bool {
true
}
fn key_exists(&self, label: &str) -> crate::internal::core::Result<bool> {
Ok(self.keys.lock().unwrap().contains_key(label))
}
}
impl crate::internal::core::traits::EnclaveEncryptor for TestEncryptorBackend {
fn encrypt(
&self,
_label: &str,
plaintext: &[u8],
) -> crate::internal::core::Result<Vec<u8>> {
let mut out = Vec::with_capacity(1 + plaintext.len());
out.push(0x01);
out.extend_from_slice(plaintext);
Ok(out)
}
fn decrypt(
&self,
_label: &str,
ciphertext: &[u8],
) -> crate::internal::core::Result<Vec<u8>> {
if ciphertext.is_empty() || ciphertext[0] != 0x01 {
return Err(crate::internal::core::Error::DecryptFailed {
detail: "missing version byte 0x01".to_string(),
});
}
Ok(ciphertext[1..].to_vec())
}
}
#[allow(unsafe_code)]
unsafe impl Send for TestEncryptorBackend {}
#[allow(unsafe_code)]
unsafe impl Sync for TestEncryptorBackend {}
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_dir() -> std::path::PathBuf {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("enclaveapp-enc-test-{pid}-{id}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn make_config(keys_dir: &std::path::Path) -> StorageConfig {
StorageConfig {
app_name: "test-app".into(),
key_label: "test-key".into(),
access_policy: AccessPolicy::None,
extra_bridge_paths: vec![],
keys_dir: Some(keys_dir.to_path_buf()),
force_keyring: false,
wrapping_key_user_presence: false,
wrapping_key_cache_ttl: std::time::Duration::ZERO,
keychain_access_group: None,
prefer_windows_hello_ux: false,
windows_software_fallback:
crate::internal::app_storage::WindowsSoftwareFallback::Disabled,
dpapi_app_key: None,
}
}
#[test]
fn ensure_key_generates_new_key_when_none_exists() {
let dir = test_dir();
let backend = TestEncryptorBackend::new();
let config = make_config(&dir);
AppEncryptionStorage::ensure_key(&backend, &config, &dir, AccessPolicy::None).unwrap();
assert!(backend.public_key("test-key").is_ok());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn ensure_key_with_matching_policy_is_noop() {
let dir = test_dir();
let backend = TestEncryptorBackend::new();
let config = make_config(&dir);
backend
.generate("test-key", KeyType::Encryption, AccessPolicy::None)
.unwrap();
let meta = metadata::KeyMeta::new("test-key", KeyType::Encryption, AccessPolicy::None);
metadata::save_meta(&dir, "test-key", &meta).unwrap();
AppEncryptionStorage::ensure_key(&backend, &config, &dir, AccessPolicy::None).unwrap();
assert!(backend.public_key("test-key").is_ok());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn ensure_key_with_mismatched_policy_deletes_and_regenerates() {
let dir = test_dir();
let backend = TestEncryptorBackend::new();
let mut config = make_config(&dir);
config.access_policy = AccessPolicy::BiometricOnly;
backend
.generate("test-key", KeyType::Encryption, AccessPolicy::BiometricOnly)
.unwrap();
let original_pub = backend.public_key("test-key").unwrap();
let meta =
metadata::KeyMeta::new("test-key", KeyType::Encryption, AccessPolicy::BiometricOnly);
metadata::save_meta(&dir, "test-key", &meta).unwrap();
AppEncryptionStorage::ensure_key(&backend, &config, &dir, AccessPolicy::None).unwrap();
let new_pub = backend.public_key("test-key").unwrap();
assert_eq!(original_pub, new_pub);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn ensure_key_with_missing_metadata_but_existing_key_uses_as_is() {
let dir = test_dir();
let backend = TestEncryptorBackend::new();
let config = make_config(&dir);
backend
.generate("test-key", KeyType::Encryption, AccessPolicy::None)
.unwrap();
AppEncryptionStorage::ensure_key(&backend, &config, &dir, AccessPolicy::None).unwrap();
assert!(backend.public_key("test-key").is_ok());
assert_eq!(backend.list_keys().unwrap().len(), 1);
std::fs::remove_dir_all(&dir).unwrap();
}
}