diff --git a/crates/enclaveapp-apple/src/encrypt.rs b/crates/enclaveapp-apple/src/encrypt.rs index 5cd9c45..62b328f 100644 --- a/crates/enclaveapp-apple/src/encrypt.rs +++ b/crates/enclaveapp-apple/src/encrypt.rs @@ -118,9 +118,11 @@ impl EnclaveEncryptor for SecureEnclaveEncryptor { }; if rc != 0 { - return Err(Error::EncryptFailed { - detail: format!("FFI returned error code {rc}"), - }); + let detail = match keychain::last_bridge_error() { + Some(msg) => format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; + return Err(Error::EncryptFailed { detail }); } ciphertext.truncate(ciphertext_len as usize); @@ -155,9 +157,11 @@ impl EnclaveEncryptor for SecureEnclaveEncryptor { }; if rc != 0 { - return Err(Error::DecryptFailed { - detail: format!("FFI returned error code {rc}"), - }); + let detail = match keychain::last_bridge_error() { + Some(msg) => format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; + return Err(Error::DecryptFailed { detail }); } plaintext.truncate(plaintext_len as usize); diff --git a/crates/enclaveapp-apple/src/ffi.rs b/crates/enclaveapp-apple/src/ffi.rs index a711f27..5e83807 100644 --- a/crates/enclaveapp-apple/src/ffi.rs +++ b/crates/enclaveapp-apple/src/ffi.rs @@ -71,6 +71,8 @@ extern "C" { pub_key_len: *mut i32, ) -> i32; + pub fn enclaveapp_se_last_error(buf: *mut u8, buf_len: *mut i32) -> i32; + pub fn enclaveapp_se_delete_key(data_rep: *const u8, data_rep_len: i32) -> i32; pub fn enclaveapp_se_encrypt( diff --git a/crates/enclaveapp-apple/src/keychain.rs b/crates/enclaveapp-apple/src/keychain.rs index df48bf5..8fb1fd9 100644 --- a/crates/enclaveapp-apple/src/keychain.rs +++ b/crates/enclaveapp-apple/src/keychain.rs @@ -14,6 +14,19 @@ use zeroize::Zeroizing; const SE_ERR_BUFFER_TOO_SMALL: i32 = 4; +#[allow(unsafe_code)] +pub(crate) fn last_bridge_error() -> Option { + let mut buf = vec![0_u8; 1024]; + let mut buf_len: i32 = buf.len() as i32; + let rc = unsafe { ffi::enclaveapp_se_last_error(buf.as_mut_ptr(), &mut buf_len) }; + if rc != 0 || buf_len <= 0 { + return None; + } + let len = buf_len as usize; + buf.truncate(len); + String::from_utf8(buf).ok().filter(|s| !s.is_empty()) +} + /// Configuration for keychain operations, scoped to an application. #[derive(Debug)] pub struct KeychainConfig { @@ -195,9 +208,11 @@ where } if rc != 0 { - return Err(Error::GenerateFailed { - detail: format!("FFI returned error code {rc}"), - }); + let detail = match last_bridge_error() { + Some(msg) => format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; + return Err(Error::GenerateFailed { detail }); } // Contract sanity: pub_key buffer is fixed at 65 bytes. @@ -552,9 +567,13 @@ pub fn public_key_from_data_rep(key_type: KeyType, data_rep: &[u8]) -> Result format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; return Err(Error::KeyOperation { operation: "public_key".into(), - detail: format!("FFI returned error code {rc}"), + detail, }); } @@ -891,9 +910,13 @@ fn delete_key_from_data_rep(data_rep: &[u8]) -> Result<()> { if rc == 0 { Ok(()) } else { + let detail = match last_bridge_error() { + Some(msg) => format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; Err(Error::KeyOperation { operation: "delete_key".into(), - detail: format!("FFI returned error code {rc}"), + detail, }) } } diff --git a/crates/enclaveapp-apple/src/sign.rs b/crates/enclaveapp-apple/src/sign.rs index e915be3..1b8a47a 100644 --- a/crates/enclaveapp-apple/src/sign.rs +++ b/crates/enclaveapp-apple/src/sign.rs @@ -121,9 +121,13 @@ impl SecureEnclaveSigner { // The LAContext is still valid; don't evict it. label: label.to_string(), }, - _ => Error::SignFailed { - detail: format!("FFI returned error code {rc}"), - }, + _ => { + let detail = match keychain::last_bridge_error() { + Some(msg) => format!("FFI returned error code {rc}: {msg}"), + None => format!("FFI returned error code {rc}"), + }; + Error::SignFailed { detail } + } }); } diff --git a/crates/enclaveapp-apple/swift/bridge.swift b/crates/enclaveapp-apple/swift/bridge.swift index 577dc40..2001024 100644 --- a/crates/enclaveapp-apple/swift/bridge.swift +++ b/crates/enclaveapp-apple/swift/bridge.swift @@ -70,6 +70,41 @@ let SE_ERR_KEYCHAIN_NO_WINDOW_SERVER: Int32 = 15 /// NOT evict the LAContext cache for this label. let SE_ERR_USER_CANCEL: Int32 = 16 +// MARK: - Thread-local last error detail + +private var _lastError: String = "" +private let _lastErrorLock = NSLock() + +func setLastError(_ msg: String) { + _lastErrorLock.lock() + _lastError = msg + _lastErrorLock.unlock() +} + +func clearLastError() { + _lastErrorLock.lock() + _lastError = "" + _lastErrorLock.unlock() +} + +@_cdecl("enclaveapp_se_last_error") +public func enclaveapp_se_last_error( + _ buf: UnsafeMutablePointer, + _ buf_len: UnsafeMutablePointer +) -> Int32 { + _lastErrorLock.lock() + let msg = _lastError + _lastErrorLock.unlock() + let data = Data(msg.utf8) + if buf_len.pointee < Int32(data.count) { + buf_len.pointee = Int32(data.count) + return SE_ERR_BUFFER_TOO_SMALL + } + data.copyBytes(to: buf, count: data.count) + buf_len.pointee = Int32(data.count) + return SE_OK +} + // MARK: - ECIES format constants let ECIES_VERSION: UInt8 = 0x01 @@ -232,11 +267,19 @@ func makeAccessControl(_ authPolicy: Int32) -> SecAccessControl? { case 1: flags.insert(.userPresence) case 2: flags.insert(.biometryAny) case 3: flags.insert(.devicePasscode) - default: return nil + default: + setLastError("unsupported auth_policy value \(authPolicy)") + return nil } - return SecAccessControlCreateWithFlags( - nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, nil + var error: Unmanaged? + let ac = SecAccessControlCreateWithFlags( + nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, &error ) + if ac == nil { + let desc = error.map { $0.takeRetainedValue().localizedDescription } ?? "unknown" + setLastError("SecAccessControlCreateWithFlags failed: \(desc)") + } + return ac } // MARK: - Helper: copy uncompressed public key @@ -340,6 +383,7 @@ public func enclaveapp_se_generate_signing_key( return copyDataRep(key.dataRepresentation, data_rep_out, data_rep_len) } catch { + setLastError("key generation failed: \(error.localizedDescription)") return SE_ERR_GENERATE } } @@ -359,6 +403,7 @@ public func enclaveapp_se_signing_public_key( let key = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: data) return copyUncompressedPubKey(key.publicKey.rawRepresentation, pub_key_out, pub_key_len) } catch { + setLastError("key load failed: \(error.localizedDescription)") return SE_ERR_LOAD } } @@ -437,6 +482,7 @@ public func enclaveapp_se_sign( "enclaveapp: se_sign: \(nsErr.domain) \(nsErr.code): \(nsErr.localizedDescription)\n" ).utf8)) } + setLastError("sign failed: \(nsErr.domain) \(nsErr.code): \(nsErr.localizedDescription)") return SE_ERR_SIGN } } @@ -474,6 +520,7 @@ public func enclaveapp_se_generate_encryption_key( return copyDataRep(key.dataRepresentation, data_rep_out, data_rep_len) } catch { + setLastError("key generation failed: \(error.localizedDescription)") return SE_ERR_GENERATE } } @@ -491,6 +538,7 @@ public func enclaveapp_se_encryption_public_key( let key = try SecureEnclave.P256.KeyAgreement.PrivateKey(dataRepresentation: data) return copyUncompressedPubKey(key.publicKey.rawRepresentation, pub_key_out, pub_key_len) } catch { + setLastError("key load failed: \(error.localizedDescription)") return SE_ERR_LOAD } } @@ -575,6 +623,7 @@ public func enclaveapp_se_encrypt( return SE_OK } catch { + setLastError("encrypt failed: \(error.localizedDescription)") return SE_ERR_ENCRYPT } } @@ -656,6 +705,7 @@ public func enclaveapp_se_decrypt( return SE_OK } catch { + setLastError("decrypt failed: \(error.localizedDescription)") return SE_ERR_DECRYPT } }