prototype secure enclave interface
This commit is contained in:
parent
e53161188c
commit
d4f557631b
313
internal/macse/enclave.go
Normal file
313
internal/macse/enclave.go
Normal file
@ -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 <Foundation/Foundation.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import <LocalAuthentication/LocalAuthentication.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
77
internal/macse/enclave_test.go
Normal file
77
internal/macse/enclave_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user