#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use crate::internal::core::{Error, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use windows::Win32::Foundation::{LocalFree, HLOCAL};
use windows::Win32::Security::Cryptography::{
BCryptGenRandom, CryptProtectData, CryptUnprotectData, BCRYPT_USE_SYSTEM_PREFERRED_RNG,
CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB,
};
use zeroize::{Zeroize, Zeroizing};
const META_HMAC_KEY_LEN: usize = 32;
type CachedKey = Box<Zeroizing<[u8; META_HMAC_KEY_LEN]>>;
type CacheMap = HashMap<String, CachedKey>;
fn cache() -> &'static Mutex<CacheMap> {
static CACHE: OnceLock<Mutex<CacheMap>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn cache_lookup(app_name: &str) -> Option<Zeroizing<Vec<u8>>> {
let guard = cache().lock().ok()?;
let entry = guard.get(app_name)?;
Some(Zeroizing::new(entry.to_vec()))
}
fn cache_insert(app_name: &str, key: [u8; META_HMAC_KEY_LEN]) {
if let Ok(mut guard) = cache().lock() {
guard.insert(app_name.to_string(), Box::new(Zeroizing::new(key)));
}
}
fn cache_evict(app_name: &str) {
if let Ok(mut guard) = cache().lock() {
guard.remove(app_name);
}
}
const BLOB_FILENAME: &str = ".meta-hmac.dpapi";
fn blob_path(app_name: &str) -> PathBuf {
let base = dirs::data_dir().unwrap_or_else(std::env::temp_dir);
base.join(app_name).join(BLOB_FILENAME)
}
pub fn load_or_create(app_name: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
if let Some(cached) = cache_lookup(app_name) {
return Ok(Some(cached));
}
let path = blob_path(app_name);
if let Some(key) = load_blob_at(&path)? {
if key.len() == META_HMAC_KEY_LEN {
let mut buf = [0_u8; META_HMAC_KEY_LEN];
buf.copy_from_slice(&key);
cache_insert(app_name, buf);
buf.zeroize();
}
return Ok(Some(key));
}
let created = create_and_persist(&path)?;
if created.len() == META_HMAC_KEY_LEN {
let mut buf = [0_u8; META_HMAC_KEY_LEN];
buf.copy_from_slice(&created);
cache_insert(app_name, buf);
buf.zeroize();
}
Ok(Some(created))
}
pub fn load_existing(app_name: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
if let Some(cached) = cache_lookup(app_name) {
return Ok(Some(cached));
}
let path = blob_path(app_name);
let key = match load_blob_at(&path)? {
Some(k) => k,
None => return Ok(None),
};
if key.len() == META_HMAC_KEY_LEN {
let mut buf = [0_u8; META_HMAC_KEY_LEN];
buf.copy_from_slice(&key);
cache_insert(app_name, buf);
buf.zeroize();
}
Ok(Some(key))
}
fn load_blob_at(path: &Path) -> Result<Option<Zeroizing<Vec<u8>>>> {
let blob = match std::fs::read(path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(Error::Io(e)),
};
decrypt(&blob).map(Some)
}
fn create_and_persist(path: &Path) -> Result<Zeroizing<Vec<u8>>> {
let mut key = [0_u8; META_HMAC_KEY_LEN];
gen_random(&mut key)?;
let blob = encrypt(&key)?;
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
key.zeroize();
return Err(Error::Io(e));
}
}
if let Err(e) = crate::internal::core::metadata::atomic_write(path, &blob) {
key.zeroize();
return Err(e);
}
let value = Zeroizing::new(key.to_vec());
key.zeroize();
Ok(value)
}
pub fn delete(app_name: &str) -> Result<()> {
cache_evict(app_name);
let path = blob_path(app_name);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::Io(e)),
}
}
#[allow(unsafe_code)] fn gen_random(out: &mut [u8]) -> Result<()> {
let status = unsafe {
BCryptGenRandom(
windows::Win32::Security::Cryptography::BCRYPT_ALG_HANDLE::default(),
out,
BCRYPT_USE_SYSTEM_PREFERRED_RNG,
)
};
status.ok().map_err(|e| Error::KeyOperation {
operation: "meta_hmac_rng".into(),
detail: format!("BCryptGenRandom: {e}"),
})
}
#[allow(unsafe_code)] fn encrypt(plaintext: &[u8]) -> Result<Vec<u8>> {
let mut input = CRYPT_INTEGER_BLOB {
cbData: u32::try_from(plaintext.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_encrypt".into(),
detail: "plaintext too large".into(),
})?,
pbData: plaintext.as_ptr() as *mut u8,
};
let mut output = CRYPT_INTEGER_BLOB::default();
let result = unsafe {
CryptProtectData(
&input,
windows::core::PCWSTR::null(),
None,
None,
None,
CRYPTPROTECT_UI_FORBIDDEN,
&mut output,
)
};
let _ = &mut input;
result.map_err(|e| Error::KeyOperation {
operation: "meta_hmac_encrypt".into(),
detail: format!("CryptProtectData: {e}"),
})?;
copy_and_free_blob(&output)
}
#[allow(unsafe_code)] fn decrypt(blob: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
let mut input = CRYPT_INTEGER_BLOB {
cbData: u32::try_from(blob.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_decrypt".into(),
detail: "blob too large".into(),
})?,
pbData: blob.as_ptr() as *mut u8,
};
let mut output = CRYPT_INTEGER_BLOB::default();
let result = unsafe {
CryptUnprotectData(
&input,
None,
None,
None,
None,
CRYPTPROTECT_UI_FORBIDDEN,
&mut output,
)
};
let _ = &mut input;
result.map_err(|e| Error::KeyOperation {
operation: "meta_hmac_decrypt".into(),
detail: format!("CryptUnprotectData: {e}"),
})?;
let plaintext = copy_and_free_blob(&output)?;
if plaintext.len() != META_HMAC_KEY_LEN {
let mut p = plaintext;
p.zeroize();
return Err(Error::KeyOperation {
operation: "meta_hmac_decrypt".into(),
detail: format!(
"decrypted meta-HMAC key has unexpected length {}, expected {META_HMAC_KEY_LEN}",
p.len()
),
});
}
Ok(Zeroizing::new(plaintext))
}
#[allow(unsafe_code)] fn copy_and_free_blob(blob: &CRYPT_INTEGER_BLOB) -> Result<Vec<u8>> {
let len = blob.cbData as usize;
if blob.pbData.is_null() || len == 0 {
return Err(Error::KeyOperation {
operation: "meta_hmac_blob_copy".into(),
detail: "DPAPI returned an empty or null blob".into(),
});
}
let copied = unsafe { std::slice::from_raw_parts(blob.pbData, len).to_vec() };
let _ = unsafe { LocalFree(HLOCAL(blob.pbData.cast())) };
Ok(copied)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn unique_app() -> String {
format!(
"enclaveapp-windows-meta-hmac-test-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::SeqCst),
)
}
fn cleanup(app: &str) {
let path = blob_path(app);
let _ = std::fs::remove_file(&path);
if let Some(parent) = path.parent() {
let _ = std::fs::remove_dir(parent);
}
}
#[test]
fn blob_path_lives_under_data_dir() {
let p = blob_path("sshenc");
assert!(
p.ends_with(format!("sshenc/{BLOB_FILENAME}"))
|| p.ends_with(format!("sshenc\\{BLOB_FILENAME}"))
);
}
#[test]
#[ignore = "hits real DPAPI; run on the Windows matrix or locally"]
fn store_load_delete_roundtrip() {
let app = unique_app();
cleanup(&app);
let created = load_or_create(&app)
.expect("create succeeds")
.expect("created key is Some");
assert_eq!(created.len(), META_HMAC_KEY_LEN);
let loaded = load_or_create(&app)
.expect("re-load succeeds")
.expect("re-loaded key is Some");
assert_eq!(&created[..], &loaded[..], "second load returns same bytes");
delete(&app).expect("delete succeeds");
let regenerated = load_or_create(&app)
.expect("regen succeeds")
.expect("regen key is Some");
assert_ne!(
&created[..],
®enerated[..],
"regen after delete produces a different key"
);
cleanup(&app);
}
#[test]
#[ignore = "hits real DPAPI; run on the Windows matrix or locally"]
fn delete_is_idempotent_on_missing() {
let app = unique_app();
cleanup(&app);
delete(&app).expect("delete on missing blob is Ok");
delete(&app).expect("second delete is also Ok");
}
}