#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use super::ffi;
use crate::internal::core::{Error, Result};
use rand::RngCore;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
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 META_HMAC_ACCOUNT: &str = "__meta_hmac_key__";
fn service_name_for(app_name: &str) -> String {
let safe = crate::internal::apple::signing::ensure_safe_app_name(app_name);
format!("com.godaddy.{safe}.meta-hmac")
}
pub fn load_existing(app_name: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
if let Some(cached) = cache_lookup(app_name) {
return Ok(Some(cached));
}
if let Some(existing) = load(app_name)? {
if existing.len() == META_HMAC_KEY_LEN {
let mut buf = [0_u8; META_HMAC_KEY_LEN];
buf.copy_from_slice(&existing);
cache_insert(app_name, buf);
buf.zeroize();
}
return Ok(Some(existing));
}
Ok(None)
}
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));
}
if let Some(existing) = load(app_name)? {
if existing.len() == META_HMAC_KEY_LEN {
let mut buf = [0_u8; META_HMAC_KEY_LEN];
buf.copy_from_slice(&existing);
cache_insert(app_name, buf);
buf.zeroize();
}
return Ok(Some(existing));
}
let created = create_and_store(app_name)?;
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))
}
#[allow(unsafe_code)] fn load(app_name: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
let service = service_name_for(app_name);
let service_bytes = service.as_bytes();
let account_bytes = META_HMAC_ACCOUNT.as_bytes();
let service_len = i32::try_from(service_bytes.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_load".into(),
detail: "service name too long".into(),
})?;
let account_len = i32::try_from(account_bytes.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_load".into(),
detail: "account name too long".into(),
})?;
let mut out = [0_u8; META_HMAC_KEY_LEN];
let mut out_len: i32 = out.len() as i32;
let rc = unsafe {
ffi::enclaveapp_keychain_load(
service_bytes.as_ptr(),
service_len,
account_bytes.as_ptr(),
account_len,
out.as_mut_ptr(),
&mut out_len,
std::ptr::null(), 0,
0, )
};
match rc {
0 => {
if out_len as usize != META_HMAC_KEY_LEN {
out.zeroize();
return Err(Error::KeyOperation {
operation: "meta_hmac_load".into(),
detail: format!(
"loaded meta-HMAC key has unexpected length {out_len}, \
expected {META_HMAC_KEY_LEN}"
),
});
}
let value = Zeroizing::new(out.to_vec());
out.zeroize();
Ok(Some(value))
}
12 => {
out.zeroize();
Ok(None)
}
_ => {
out.zeroize();
tracing::debug!(rc, "meta_hmac_load: keychain unreachable; returning None");
Ok(None)
}
}
}
#[allow(unsafe_code)] fn create_and_store(app_name: &str) -> Result<Zeroizing<Vec<u8>>> {
let mut key = [0_u8; META_HMAC_KEY_LEN];
rand::rng().fill_bytes(&mut key);
let service = service_name_for(app_name);
let service_bytes = service.as_bytes();
let account_bytes = META_HMAC_ACCOUNT.as_bytes();
let service_len = i32::try_from(service_bytes.len()).map_err(|_| {
key.zeroize();
Error::KeyOperation {
operation: "meta_hmac_store".into(),
detail: "service name too long".into(),
}
})?;
let account_len = i32::try_from(account_bytes.len()).map_err(|_| {
key.zeroize();
Error::KeyOperation {
operation: "meta_hmac_store".into(),
detail: "account name too long".into(),
}
})?;
let rc = unsafe {
ffi::enclaveapp_keychain_store(
service_bytes.as_ptr(),
service_len,
account_bytes.as_ptr(),
account_len,
key.as_ptr(),
META_HMAC_KEY_LEN as i32,
0, std::ptr::null(), 0,
)
};
if rc != 0 {
key.zeroize();
return Err(Error::KeyOperation {
operation: "meta_hmac_store".into(),
detail: format!("Swift bridge returned error code {rc}"),
});
}
let value = Zeroizing::new(key.to_vec());
key.zeroize();
Ok(value)
}
#[allow(unsafe_code)] pub fn delete(app_name: &str) -> Result<()> {
let service = service_name_for(app_name);
let service_bytes = service.as_bytes();
let account_bytes = META_HMAC_ACCOUNT.as_bytes();
let service_len = i32::try_from(service_bytes.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_delete".into(),
detail: "service name too long".into(),
})?;
let account_len = i32::try_from(account_bytes.len()).map_err(|_| Error::KeyOperation {
operation: "meta_hmac_delete".into(),
detail: "account name too long".into(),
})?;
let rc = unsafe {
ffi::enclaveapp_keychain_delete(
service_bytes.as_ptr(),
service_len,
account_bytes.as_ptr(),
account_len,
std::ptr::null(),
0,
)
};
cache_evict(app_name);
match rc {
0 | 12 => Ok(()), _ => Err(Error::KeyOperation {
operation: "meta_hmac_delete".into(),
detail: format!("Swift bridge returned error code {rc}"),
}),
}
}
#[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-apple-meta-hmac-test-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::SeqCst),
)
}
#[test]
fn service_name_includes_app_and_suffix() {
assert_eq!(
service_name_for("sshenc"),
"com.godaddy.sshenc-unsigned.meta-hmac"
);
assert_eq!(
service_name_for("awsenc"),
"com.godaddy.awsenc-unsigned.meta-hmac"
);
}
#[test]
fn service_name_for_npmenc() {
assert_eq!(
service_name_for("npmenc"),
"com.godaddy.npmenc-unsigned.meta-hmac"
);
}
#[test]
fn cache_miss_returns_none_for_unknown_app() {
let app = unique_app();
assert!(cache_lookup(&app).is_none());
}
#[test]
fn cache_insert_then_lookup_returns_correct_bytes() {
let app = unique_app();
let key = [0x42_u8; META_HMAC_KEY_LEN];
cache_insert(&app, key);
let result = cache_lookup(&app).expect("cache hit after insert");
assert_eq!(&result[..], &key[..]);
cache_evict(&app);
}
#[test]
fn cache_evict_removes_entry() {
let app = unique_app();
let key = [0x77_u8; META_HMAC_KEY_LEN];
cache_insert(&app, key);
assert!(cache_lookup(&app).is_some());
cache_evict(&app);
assert!(cache_lookup(&app).is_none());
}
#[test]
fn cache_insert_overwrites_existing_entry() {
let app = unique_app();
let key_a = [0x11_u8; META_HMAC_KEY_LEN];
let key_b = [0x22_u8; META_HMAC_KEY_LEN];
cache_insert(&app, key_a);
cache_insert(&app, key_b);
let result = cache_lookup(&app).expect("cache hit");
assert_eq!(&result[..], &key_b[..]);
cache_evict(&app);
}
#[test]
fn cache_evict_on_missing_app_is_noop() {
let app = unique_app();
cache_evict(&app);
assert!(cache_lookup(&app).is_none());
}
#[test]
#[ignore = "hits the real macOS Keychain; run locally"]
fn store_load_delete_roundtrip() {
let app = unique_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!(loaded.len(), META_HMAC_KEY_LEN);
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_eq!(regenerated.len(), META_HMAC_KEY_LEN);
assert_ne!(
&created[..],
®enerated[..],
"regen after delete produces a different key"
);
let _ = delete(&app);
}
#[test]
#[ignore = "hits the real macOS Keychain; run locally"]
fn delete_is_idempotent_on_missing() {
let app = unique_app();
delete(&app).expect("delete on missing key is Ok");
delete(&app).expect("second delete is also Ok");
}
}