#![cfg(target_os = "macos")]
#![allow(non_upper_case_globals)]
use std::os::raw::{c_int, c_long, c_void};
use std::ptr;
use crate::error::Error;
const APP_TAG: &[u8] = b"dev.santh.envseal.master.v1";
#[repr(C)]
struct __CFType(c_void);
type CFTypeRef = *const __CFType;
type CFAllocatorRef = CFTypeRef;
type CFDictionaryRef = CFTypeRef;
type CFMutableDictionaryRef = CFTypeRef;
type CFDataRef = CFTypeRef;
type CFStringRef = CFTypeRef;
type CFNumberRef = CFTypeRef;
type CFErrorRef = CFTypeRef;
type CFBooleanRef = CFTypeRef;
type CFIndex = c_long;
type Boolean = u8;
type OSStatus = i32;
type SecKeyRef = CFTypeRef;
type SecKeyAlgorithm = CFStringRef;
#[repr(C)]
struct CFDictionaryKeyCallBacks {
_opaque: [u8; 0],
}
#[repr(C)]
struct CFDictionaryValueCallBacks {
_opaque: [u8; 0],
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
static kCFAllocatorDefault: CFAllocatorRef;
static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks;
static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks;
static kCFBooleanTrue: CFBooleanRef;
fn CFRelease(cf: CFTypeRef);
fn CFDictionaryCreateMutable(
allocator: CFAllocatorRef,
capacity: CFIndex,
keyCallBacks: *const CFDictionaryKeyCallBacks,
valueCallBacks: *const CFDictionaryValueCallBacks,
) -> CFMutableDictionaryRef;
fn CFDictionaryAddValue(theDict: CFMutableDictionaryRef, key: CFTypeRef, value: CFTypeRef);
fn CFDataCreate(allocator: CFAllocatorRef, bytes: *const u8, length: CFIndex) -> CFDataRef;
fn CFDataGetLength(data: CFDataRef) -> CFIndex;
fn CFDataGetBytePtr(data: CFDataRef) -> *const u8;
fn CFNumberCreate(
allocator: CFAllocatorRef,
the_type: c_int,
value_ptr: *const c_void,
) -> CFNumberRef;
fn CFErrorCopyDescription(err: CFErrorRef) -> CFStringRef;
fn CFStringGetCString(s: CFStringRef, buf: *mut u8, size: CFIndex, encoding: u32) -> Boolean;
}
const kCFNumberSInt32Type: c_int = 3;
const kCFStringEncodingUTF8: u32 = 0x0800_0100;
#[link(name = "Security", kind = "framework")]
extern "C" {
static kSecAttrKeyType: CFStringRef;
static kSecAttrKeyTypeECSECPrimeRandom: CFStringRef;
static kSecAttrKeySizeInBits: CFStringRef;
static kSecAttrTokenID: CFStringRef;
static kSecAttrTokenIDSecureEnclave: CFStringRef;
static kSecAttrIsPermanent: CFStringRef;
static kSecAttrApplicationTag: CFStringRef;
static kSecPrivateKeyAttrs: CFStringRef;
static kSecClass: CFStringRef;
static kSecClassKey: CFStringRef;
static kSecAttrKeyClass: CFStringRef;
static kSecAttrKeyClassPrivate: CFStringRef;
static kSecReturnRef: CFStringRef;
static kSecMatchLimit: CFStringRef;
static kSecMatchLimitOne: CFStringRef;
static kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM: SecKeyAlgorithm;
fn SecKeyCreateRandomKey(parameters: CFDictionaryRef, error: *mut CFErrorRef) -> SecKeyRef;
fn SecKeyCopyPublicKey(key: SecKeyRef) -> SecKeyRef;
fn SecKeyCreateEncryptedData(
key: SecKeyRef,
algorithm: SecKeyAlgorithm,
plaintext: CFDataRef,
error: *mut CFErrorRef,
) -> CFDataRef;
fn SecKeyCreateDecryptedData(
key: SecKeyRef,
algorithm: SecKeyAlgorithm,
ciphertext: CFDataRef,
error: *mut CFErrorRef,
) -> CFDataRef;
fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
}
const errSecItemNotFound: OSStatus = -25300;
struct CFOwned(CFTypeRef);
impl CFOwned {
fn new(p: CFTypeRef) -> Option<Self> {
if p.is_null() {
None
} else {
Some(Self(p))
}
}
fn as_ref(&self) -> CFTypeRef {
self.0
}
}
impl Drop for CFOwned {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
CFRelease(self.0);
}
}
}
}
fn cf_data(bytes: &[u8]) -> Option<CFOwned> {
let len = CFIndex::try_from(bytes.len()).ok()?;
let p = unsafe { CFDataCreate(kCFAllocatorDefault, bytes.as_ptr(), len) };
CFOwned::new(p)
}
fn cf_number_i32(v: i32) -> Option<CFOwned> {
let p = unsafe {
CFNumberCreate(
kCFAllocatorDefault,
kCFNumberSInt32Type,
std::ptr::from_ref(&v).cast::<c_void>(),
)
};
CFOwned::new(p)
}
fn cf_dict_mutable() -> Option<CFOwned> {
let p = unsafe {
CFDictionaryCreateMutable(
kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
)
};
CFOwned::new(p)
}
fn cf_error_string(err: CFErrorRef) -> String {
if err.is_null() {
return "<no error info>".to_string();
}
let desc = unsafe { CFErrorCopyDescription(err) };
if desc.is_null() {
return "<no description>".to_string();
}
let mut buf = [0u8; 1024];
let buflen = CFIndex::try_from(buf.len()).unwrap_or(CFIndex::MAX);
let ok = unsafe { CFStringGetCString(desc, buf.as_mut_ptr(), buflen, kCFStringEncodingUTF8) };
unsafe {
CFRelease(desc);
}
if ok == 0 {
return "<encoding failed>".to_string();
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..nul]).into_owned()
}
pub struct SecureEnclaveKeystore;
impl SecureEnclaveKeystore {
pub fn try_new() -> Option<Self> {
match find_private_key() {
Ok(Some(_)) => Some(Self),
Ok(None) => match create_keypair() {
Ok(()) => Some(Self),
Err(_) => None,
},
Err(_) => None,
}
}
pub fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
let priv_key = require_private_key()?;
let pub_key = unsafe { SecKeyCopyPublicKey(priv_key.as_ref()) };
let pub_key = CFOwned::new(pub_key)
.ok_or_else(|| Error::CryptoFailure("SecKeyCopyPublicKey returned null".to_string()))?;
let plaintext_cf = cf_data(plaintext)
.ok_or_else(|| Error::CryptoFailure("CFDataCreate failed for plaintext".to_string()))?;
let mut err: CFErrorRef = ptr::null();
let cipher = unsafe {
SecKeyCreateEncryptedData(
pub_key.as_ref(),
kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM,
plaintext_cf.as_ref(),
&mut err,
)
};
if cipher.is_null() {
let msg = cf_error_string(err);
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
return Err(Error::CryptoFailure(format!(
"SecKeyCreateEncryptedData failed: {msg}"
)));
}
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
let cipher = CFOwned::new(cipher).ok_or_else(|| {
Error::CryptoFailure("CFOwned::new returned null for non-null cipher".to_string())
})?;
Ok(copy_cfdata(cipher.as_ref()))
}
pub fn unseal(&self, sealed: &[u8]) -> Result<Vec<u8>, Error> {
let priv_key = require_private_key()?;
let cipher_cf = cf_data(sealed).ok_or_else(|| {
Error::CryptoFailure("CFDataCreate failed for ciphertext".to_string())
})?;
let mut err: CFErrorRef = ptr::null();
let plain = unsafe {
SecKeyCreateDecryptedData(
priv_key.as_ref(),
kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM,
cipher_cf.as_ref(),
&mut err,
)
};
if plain.is_null() {
let msg = cf_error_string(err);
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
return Err(Error::CryptoFailure(format!(
"SecKeyCreateDecryptedData failed (different SEP key, revoked, or corrupted blob): {msg}"
)));
}
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
let plain = CFOwned::new(plain).ok_or_else(|| {
Error::CryptoFailure("CFOwned::new returned null for non-null plain".to_string())
})?;
Ok(copy_cfdata(plain.as_ref()))
}
}
fn copy_cfdata(data: CFDataRef) -> Vec<u8> {
let len_signed = unsafe { CFDataGetLength(data) };
let Ok(len) = usize::try_from(len_signed) else {
return Vec::new();
};
let ptr = unsafe { CFDataGetBytePtr(data) };
if ptr.is_null() || len == 0 {
return Vec::new();
}
let mut out = Vec::with_capacity(len);
unsafe {
std::ptr::copy_nonoverlapping(ptr, out.as_mut_ptr(), len);
out.set_len(len);
}
out
}
fn require_private_key() -> Result<CFOwned, Error> {
if let Some(k) = find_private_key()? {
return Ok(k);
}
create_keypair()?;
find_private_key()?.ok_or_else(|| {
Error::CryptoFailure(
"SEP keypair created but lookup failed immediately after — keychain inconsistency"
.to_string(),
)
})
}
fn find_private_key() -> Result<Option<CFOwned>, Error> {
let tag = cf_data(APP_TAG).ok_or_else(|| {
Error::CryptoFailure("CFDataCreate failed for application tag".to_string())
})?;
let query = cf_dict_mutable()
.ok_or_else(|| Error::CryptoFailure("CFDictionaryCreateMutable failed".to_string()))?;
unsafe {
CFDictionaryAddValue(query.as_ref(), kSecClass, kSecClassKey);
CFDictionaryAddValue(query.as_ref(), kSecAttrKeyClass, kSecAttrKeyClassPrivate);
CFDictionaryAddValue(query.as_ref(), kSecAttrApplicationTag, tag.as_ref());
CFDictionaryAddValue(query.as_ref(), kSecMatchLimit, kSecMatchLimitOne);
CFDictionaryAddValue(query.as_ref(), kSecReturnRef, kCFBooleanTrue);
}
let mut result: CFTypeRef = ptr::null();
let status = unsafe { SecItemCopyMatching(query.as_ref(), &mut result) };
match status {
0 => Ok(CFOwned::new(result)),
s if s == errSecItemNotFound => Ok(None),
s => Err(Error::CryptoFailure(format!(
"SecItemCopyMatching failed: OSStatus={s}"
))),
}
}
fn create_keypair() -> Result<(), Error> {
let priv_attrs = cf_dict_mutable().ok_or_else(|| {
Error::CryptoFailure("CFDictionaryCreateMutable failed (priv_attrs)".to_string())
})?;
let tag = cf_data(APP_TAG).ok_or_else(|| {
Error::CryptoFailure("CFDataCreate failed for application tag".to_string())
})?;
unsafe {
CFDictionaryAddValue(priv_attrs.as_ref(), kSecAttrIsPermanent, kCFBooleanTrue);
CFDictionaryAddValue(priv_attrs.as_ref(), kSecAttrApplicationTag, tag.as_ref());
}
let params = cf_dict_mutable().ok_or_else(|| {
Error::CryptoFailure("CFDictionaryCreateMutable failed (params)".to_string())
})?;
let size = cf_number_i32(256)
.ok_or_else(|| Error::CryptoFailure("CFNumberCreate failed".to_string()))?;
unsafe {
CFDictionaryAddValue(
params.as_ref(),
kSecAttrKeyType,
kSecAttrKeyTypeECSECPrimeRandom,
);
CFDictionaryAddValue(params.as_ref(), kSecAttrKeySizeInBits, size.as_ref());
CFDictionaryAddValue(
params.as_ref(),
kSecAttrTokenID,
kSecAttrTokenIDSecureEnclave,
);
CFDictionaryAddValue(params.as_ref(), kSecPrivateKeyAttrs, priv_attrs.as_ref());
}
let mut err: CFErrorRef = ptr::null();
let key = unsafe { SecKeyCreateRandomKey(params.as_ref(), &mut err) };
if key.is_null() {
let msg = cf_error_string(err);
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
return Err(Error::CryptoFailure(format!(
"SecKeyCreateRandomKey (Secure Enclave) failed: {msg}"
)));
}
unsafe {
CFRelease(key);
}
if !err.is_null() {
unsafe {
CFRelease(err);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_then_unseal_roundtrips_when_sep_present() {
let Some(ks) = SecureEnclaveKeystore::try_new() else {
eprintln!("SEP not available on this host — skipping");
return;
};
let plaintext = b"the master key wrapped envelope";
let sealed = ks.seal(plaintext).expect("SEP seal must succeed");
assert_ne!(sealed.as_slice(), plaintext);
let recovered = ks.unseal(&sealed).expect("SEP unseal must succeed");
assert_eq!(recovered, plaintext);
}
#[test]
fn unseal_rejects_corrupted_blob_when_sep_present() {
let Some(ks) = SecureEnclaveKeystore::try_new() else {
return;
};
let mut sealed = ks.seal(b"some payload").unwrap();
let last = sealed.len() - 1;
sealed[last] ^= 0x01;
assert!(ks.unseal(&sealed).is_err());
}
}