From d4f557631b526c5653b63382c33f170f94f86e0a Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 15 Jul 2025 09:37:02 +0200 Subject: [PATCH] prototype secure enclave interface --- internal/macse/enclave.go | 313 +++++++++++++++++++++++++++++++++ internal/macse/enclave_test.go | 77 ++++++++ 2 files changed, 390 insertions(+) create mode 100644 internal/macse/enclave.go create mode 100644 internal/macse/enclave_test.go diff --git a/internal/macse/enclave.go b/internal/macse/enclave.go new file mode 100644 index 0000000..117db03 --- /dev/null +++ b/internal/macse/enclave.go @@ -0,0 +1,313 @@ +//go:build darwin +// +build darwin + +package macse + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Security -framework LocalAuthentication +#import +#import +#import + +typedef struct { + const void* data; + int len; + int error; +} DataResult; + +typedef struct { + SecKeyRef privateKey; + const void* salt; + int saltLen; + int error; +} KeyResult; + +KeyResult createEnclaveKey(bool requireBiometric) { + KeyResult result = {NULL, NULL, 0, 0}; + + // Create authentication context + LAContext* authContext = [[LAContext alloc] init]; + authContext.localizedReason = @"Create Secure Enclave key"; + + CFMutableDictionaryRef attributes = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(attributes, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom); + CFDictionarySetValue(attributes, kSecAttrKeySizeInBits, (__bridge CFNumberRef)@256); + CFDictionarySetValue(attributes, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave); + + CFMutableDictionaryRef privateKeyAttrs = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(privateKeyAttrs, kSecAttrIsPermanent, kCFBooleanFalse); + + SecAccessControlCreateFlags flags = kSecAccessControlPrivateKeyUsage; + if (requireBiometric) { + flags |= kSecAccessControlBiometryCurrentSet; + } + + SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + flags, + NULL); + if (!access) { + result.error = -1; + return result; + } + + CFDictionarySetValue(privateKeyAttrs, kSecAttrAccessControl, access); + CFDictionarySetValue(privateKeyAttrs, kSecUseAuthenticationContext, (__bridge CFTypeRef)authContext); + CFDictionarySetValue(attributes, kSecPrivateKeyAttrs, privateKeyAttrs); + + CFErrorRef error = NULL; + SecKeyRef privateKey = SecKeyCreateRandomKey(attributes, &error); + + CFRelease(attributes); + CFRelease(privateKeyAttrs); + CFRelease(access); + + if (error || !privateKey) { + if (error) { + result.error = (int)CFErrorGetCode(error); + CFRelease(error); + } else { + result.error = -3; + } + return result; + } + + // Generate random salt + uint8_t* saltBytes = malloc(64); + if (SecRandomCopyBytes(kSecRandomDefault, 64, saltBytes) != 0) { + result.error = -2; + free(saltBytes); + if (privateKey) CFRelease(privateKey); + return result; + } + + result.privateKey = privateKey; + result.salt = saltBytes; + result.saltLen = 64; + + // Retain the key so it's not released + CFRetain(privateKey); + + return result; +} + +DataResult encryptData(SecKeyRef privateKey, const void* saltData, int saltLen, const void* plainData, int plainLen) { + DataResult result = {NULL, 0, 0}; + + // Get public key from private key + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + if (!publicKey) { + result.error = -1; + return result; + } + + // Perform ECDH key agreement with self + CFErrorRef error = NULL; + CFMutableDictionaryRef params = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDataRef sharedSecret = SecKeyCopyKeyExchangeResult(privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard, publicKey, params, &error); + CFRelease(params); + + if (error) { + result.error = (int)CFErrorGetCode(error); + CFRelease(error); + CFRelease(publicKey); + return result; + } + + // For simplicity, we'll use the shared secret directly as a symmetric key + // In production, you'd want to use HKDF as shown in the Swift code + + // Create encryption key from shared secret + const uint8_t* secretBytes = CFDataGetBytePtr(sharedSecret); + size_t secretLen = CFDataGetLength(sharedSecret); + + // Simple XOR encryption for demonstration (NOT SECURE - use proper encryption in production) + uint8_t* encrypted = malloc(plainLen); + for (int i = 0; i < plainLen; i++) { + encrypted[i] = ((uint8_t*)plainData)[i] ^ secretBytes[i % secretLen]; + } + + result.data = encrypted; + result.len = plainLen; + + CFRelease(publicKey); + CFRelease(sharedSecret); + + return result; +} + +DataResult decryptData(SecKeyRef privateKey, const void* saltData, int saltLen, const void* encData, int encLen, void* context) { + DataResult result = {NULL, 0, 0}; + + // Set up authentication context + LAContext* authContext = [[LAContext alloc] init]; + NSError* authError = nil; + + // Check if biometric authentication is available + if ([authContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { + // Evaluate biometric authentication synchronously + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + __block BOOL authSuccess = NO; + + [authContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:@"Decrypt data using Secure Enclave" + reply:^(BOOL success, NSError * _Nullable error) { + authSuccess = success; + dispatch_semaphore_signal(sema); + }]; + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + + if (!authSuccess) { + result.error = -3; + return result; + } + } + + // Get public key from private key + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + if (!publicKey) { + result.error = -1; + return result; + } + + // Create algorithm parameters with authentication context + CFMutableDictionaryRef params = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(params, kSecUseAuthenticationContext, (__bridge CFTypeRef)authContext); + + // Perform ECDH key agreement with self + CFErrorRef error = NULL; + CFDataRef sharedSecret = SecKeyCopyKeyExchangeResult(privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard, publicKey, params, &error); + CFRelease(params); + + if (error) { + result.error = (int)CFErrorGetCode(error); + CFRelease(error); + CFRelease(publicKey); + return result; + } + + // Decrypt using shared secret + const uint8_t* secretBytes = CFDataGetBytePtr(sharedSecret); + size_t secretLen = CFDataGetLength(sharedSecret); + + // Simple XOR decryption for demonstration + uint8_t* decrypted = malloc(encLen); + for (int i = 0; i < encLen; i++) { + decrypted[i] = ((uint8_t*)encData)[i] ^ secretBytes[i % secretLen]; + } + + result.data = decrypted; + result.len = encLen; + + CFRelease(publicKey); + CFRelease(sharedSecret); + + return result; +} + +void freeKeyResult(KeyResult* result) { + if (result->privateKey) { + CFRelease(result->privateKey); + } + if (result->salt) { + free((void*)result->salt); + } +} + +void freeDataResult(DataResult* result) { + if (result->data) { + free((void*)result->data); + } +} +*/ +import "C" +import ( + "errors" + "unsafe" +) + +type EnclaveKey struct { + privateKey C.SecKeyRef + salt []byte +} + +func NewEnclaveKey(requireBiometric bool) (*EnclaveKey, error) { + result := C.createEnclaveKey(C.bool(requireBiometric)) + defer C.freeKeyResult(&result) + + if result.error != 0 { + return nil, errors.New("failed to create enclave key") + } + + salt := make([]byte, result.saltLen) + copy(salt, (*[1 << 30]byte)(unsafe.Pointer(result.salt))[:result.saltLen:result.saltLen]) + + return &EnclaveKey{ + privateKey: result.privateKey, + salt: salt, + }, nil +} + +func (k *EnclaveKey) Encrypt(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("empty data") + } + if len(k.salt) == 0 { + return nil, errors.New("empty salt") + } + + result := C.encryptData( + k.privateKey, + unsafe.Pointer(&k.salt[0]), + C.int(len(k.salt)), + unsafe.Pointer(&data[0]), + C.int(len(data)), + ) + defer C.freeDataResult(&result) + + if result.error != 0 { + return nil, errors.New("encryption failed") + } + + encrypted := make([]byte, result.len) + copy(encrypted, (*[1 << 30]byte)(unsafe.Pointer(result.data))[:result.len:result.len]) + + return encrypted, nil +} + +func (k *EnclaveKey) Decrypt(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("empty data") + } + if len(k.salt) == 0 { + return nil, errors.New("empty salt") + } + + result := C.decryptData( + k.privateKey, + unsafe.Pointer(&k.salt[0]), + C.int(len(k.salt)), + unsafe.Pointer(&data[0]), + C.int(len(data)), + nil, + ) + defer C.freeDataResult(&result) + + if result.error != 0 { + return nil, errors.New("decryption failed") + } + + decrypted := make([]byte, result.len) + copy(decrypted, (*[1 << 30]byte)(unsafe.Pointer(result.data))[:result.len:result.len]) + + return decrypted, nil +} + +func (k *EnclaveKey) Close() { + if k.privateKey != 0 { + C.CFRelease(C.CFTypeRef(k.privateKey)) + k.privateKey = 0 + } +} \ No newline at end of file diff --git a/internal/macse/enclave_test.go b/internal/macse/enclave_test.go new file mode 100644 index 0000000..b94d1a3 --- /dev/null +++ b/internal/macse/enclave_test.go @@ -0,0 +1,77 @@ +//go:build darwin +// +build darwin + +package macse + +import ( + "bytes" + "testing" +) + +func TestEnclaveKeyEncryption(t *testing.T) { + // Create a new enclave key without requiring biometric + key, err := NewEnclaveKey(false) + if err != nil { + t.Fatalf("Failed to create enclave key: %v", err) + } + defer key.Close() + + // Test data + plaintext := []byte("Hello, Secure Enclave!") + + // Encrypt + encrypted, err := key.Encrypt(plaintext) + if err != nil { + t.Fatalf("Failed to encrypt: %v", err) + } + + // Verify encrypted data is different from plaintext + if bytes.Equal(plaintext, encrypted) { + t.Error("Encrypted data should not equal plaintext") + } + + // Decrypt + decrypted, err := key.Decrypt(encrypted) + if err != nil { + t.Fatalf("Failed to decrypt: %v", err) + } + + // Verify decrypted data matches original + if !bytes.Equal(plaintext, decrypted) { + t.Errorf("Decrypted data does not match original: got %s, want %s", decrypted, plaintext) + } +} + +func TestEnclaveKeyWithBiometric(t *testing.T) { + // This test requires user interaction + // Run with: CGO_ENABLED=1 go test -v -run TestEnclaveKeyWithBiometric + if testing.Short() { + t.Skip("Skipping biometric test in short mode") + } + + key, err := NewEnclaveKey(true) + if err != nil { + t.Logf("Expected failure creating biometric key in test environment: %v", err) + return + } + defer key.Close() + + plaintext := []byte("Biometric protected data") + + encrypted, err := key.Encrypt(plaintext) + if err != nil { + t.Fatalf("Failed to encrypt with biometric key: %v", err) + } + + // Decryption would require biometric authentication + decrypted, err := key.Decrypt(encrypted) + if err != nil { + // This is expected without proper biometric authentication + t.Logf("Expected decryption failure without biometric auth: %v", err) + return + } + + if !bytes.Equal(plaintext, decrypted) { + t.Errorf("Decrypted data does not match original") + } +} \ No newline at end of file