From 0d6b1d8cd6fe4751ee4ed4bb3f6ea1bdddf5efd9 Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Mon, 6 May 2024 22:11:43 +1000 Subject: [PATCH 1/5] Added support for biometrics (touchID/faceID) --- corefoundation.go | 7 ++++ keychain.go | 103 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/corefoundation.go b/corefoundation.go index b7ee544..7b77faa 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -7,6 +7,7 @@ package keychain #cgo LDFLAGS: -framework CoreFoundation #include +#include // Can't cast a *uintptr to *unsafe.Pointer in Go, and casting // C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to @@ -185,6 +186,12 @@ func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, e switch val := i.(type) { default: return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + case *AuthenticationContext: + // Ignore this, the pointer can't be added to the dictionary + // This value is used within the QueryItemRef functions + continue + case C.SecAccessControlRef: + valueRef = C.CFTypeRef(val) case C.CFTypeRef: valueRef = val case bool: diff --git a/keychain.go b/keychain.go index 7d0a1ac..07b5c43 100644 --- a/keychain.go +++ b/keychain.go @@ -8,10 +8,31 @@ package keychain // Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . /* -#cgo LDFLAGS: -framework CoreFoundation -framework Security +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework LocalAuthentication -framework Security -framework CoreFoundation #include #include +#include + +typedef struct { + int AllowableReuseDuration; +} LAContextOptions; + +LAContext* CreateLAContext(LAContextOptions options) { + LAContext *context = [[LAContext alloc] init]; + context.touchIDAuthenticationAllowableReuseDuration = options.AllowableReuseDuration; + return context; +} + +CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *context) { + CFMutableDictionaryRef newQuery = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); + CFDictionarySetValue(newQuery, kSecUseAuthenticationContext, context); + + // Convert back to CFDictionaryRef + return newQuery; +} + */ import "C" import ( @@ -22,6 +43,25 @@ import ( // Error defines keychain errors type Error int +type AuthenticationContext struct { + ptr *C.LAContext +} + +// LAContextOptions is the options for creating a LAContext +type AuthenticationContextOptions struct { + AllowableReuseDuration int +} + +const ( + nilSecKey C.SecKeyRef = 0 + nilCFData C.CFDataRef = 0 + nilCFString C.CFStringRef = 0 + nilCFDictionary C.CFDictionaryRef = 0 + nilCFError C.CFErrorRef = 0 + nilCFType C.CFTypeRef = 0 + nilSecAccessControl C.SecAccessControlRef = 0 +) + var ( // ErrorUnimplemented corresponds to errSecUnimplemented result code ErrorUnimplemented = Error(C.errSecUnimplemented) @@ -168,13 +208,14 @@ var ( PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort)) // PathKey is for kSecAttrPath PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath)) - // LabelKey is for kSecAttrLabel LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) // AccountKey is for kSecAttrAccount AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) // AccessGroupKey is for kSecAttrAccessGroup AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) + // AccessControlKey is for kSecAttrAccessControl + AccessControlKey = attrKey(C.CFTypeRef(C.kSecAttrAccessControl)) // DataKey is for kSecValueData DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) // DescriptionKey is for kSecAttrDescription @@ -231,6 +272,20 @@ const ( AccessibleAccessibleAlwaysThisDeviceOnly = 7 ) +type AccessControlFlags C.SecAccessControlCreateFlags + +const ( + AccessControlFlagsUserPresence AccessControlFlags = C.kSecAccessControlUserPresence + AccessControlFlagsBiometryAny AccessControlFlags = C.kSecAccessControlBiometryAny + AccessControlFlagsBiometryCurrentSet AccessControlFlags = C.kSecAccessControlBiometryCurrentSet + AccessControlFlagsDevicePasscode AccessControlFlags = C.kSecAccessControlDevicePasscode + AccessControlFlagsWatch AccessControlFlags = C.kSecAccessControlWatch + AccessControlFlagsOr AccessControlFlags = C.kSecAccessControlOr + AccessControlFlagsAnd AccessControlFlags = C.kSecAccessControlAnd + AccessControlFlagsPrivateKeyUsage AccessControlFlags = C.kSecAccessControlPrivateKeyUsage + AccessControlFlagsApplicationPassword AccessControlFlags = C.kSecAccessControlApplicationPassword +) + // MatchLimit is whether to limit results on query type MatchLimit int @@ -353,6 +408,28 @@ func (k *Item) SetAccessGroup(ag string) { k.SetString(AccessGroupKey, ag) } +func CreateAuthenticationContext(options AuthenticationContextOptions) *AuthenticationContext { + return &AuthenticationContext{ptr: C.CreateLAContext(C.LAContextOptions{AllowableReuseDuration: C.int(options.AllowableReuseDuration)})} +} + +func (k *Item) SetAuthenticationContext(context *AuthenticationContext) { + k.attr[AccessControlKey] = context +} + +func (k *Item) SetAccessControl(flags AccessControlFlags) error { + protection := C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly + + var err *C.CFErrorRef + ac := C.SecAccessControlCreateWithFlags(C.kCFAllocatorDefault, C.CFTypeRef(protection), C.SecAccessControlCreateFlags(flags), err) + + if err != nil { + return fmt.Errorf("failed to create access control: %+v", err) + } + + k.attr[AccessControlKey] = ac + return nil +} + // SetSynchronizable sets the synchronizable attribute func (k *Item) SetSynchronizable(sync Synchronizable) { if sync != SynchronizableDefault { @@ -420,6 +497,11 @@ func AddItem(item Item) error { } defer Release(C.CFTypeRef(cfDict)) + context, ok := item.attr[AccessControlKey].(*AuthenticationContext) + if ok { + cfDict = C.AddContextToQuery(cfDict, context.ptr) + } + errCode := C.SecItemAdd(cfDict, nil) err = checkError(errCode) return err @@ -437,6 +519,12 @@ func UpdateItem(queryItem Item, updateItem Item) error { return err } defer Release(C.CFTypeRef(cfDictUpdate)) + + context, ok := queryItem.attr[AccessControlKey].(*AuthenticationContext) + if ok { + cfDict = C.AddContextToQuery(cfDict, context.ptr) + } + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) err = checkError(errCode) return err @@ -474,6 +562,12 @@ func QueryItemRef(item Item) (C.CFTypeRef, error) { defer Release(C.CFTypeRef(cfDict)) var resultsRef C.CFTypeRef + + context, ok := item.attr[AccessControlKey].(*AuthenticationContext) + if ok { + cfDict = C.AddContextToQuery(cfDict, context.ptr) + } + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint if Error(errCode) == ErrorItemNotFound { return 0, nil @@ -599,6 +693,11 @@ func DeleteItem(item Item) error { } defer Release(C.CFTypeRef(cfDict)) + context, ok := item.attr[AccessControlKey].(*AuthenticationContext) + if ok { + cfDict = C.AddContextToQuery(cfDict, context.ptr) + } + errCode := C.SecItemDelete(cfDict) return checkError(errCode) } From 847fd41a29aee87944dd2ccce88bc789f294dead Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Tue, 7 May 2024 11:04:35 +1000 Subject: [PATCH 2/5] Make C functions static so we can import go-backend twice into downstream projects --- corefoundation.go | 4 ++-- keychain.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/corefoundation.go b/corefoundation.go index 7b77faa..c25a99a 100644 --- a/corefoundation.go +++ b/corefoundation.go @@ -20,11 +20,11 @@ package keychain // TODO: Move this file into its own package depended on by go-kext // and this package. -CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { +static CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); } -CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { +static CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); } */ diff --git a/keychain.go b/keychain.go index 07b5c43..1d4f2aa 100644 --- a/keychain.go +++ b/keychain.go @@ -19,13 +19,13 @@ typedef struct { int AllowableReuseDuration; } LAContextOptions; -LAContext* CreateLAContext(LAContextOptions options) { +static LAContext* CreateLAContext(LAContextOptions options) { LAContext *context = [[LAContext alloc] init]; context.touchIDAuthenticationAllowableReuseDuration = options.AllowableReuseDuration; return context; } -CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *context) { +static CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *context) { CFMutableDictionaryRef newQuery = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); CFDictionarySetValue(newQuery, kSecUseAuthenticationContext, context); From 81c4e0fa61bac1c5ddc98e5fe49fdc0cd14a7622 Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Tue, 7 May 2024 22:27:39 +1000 Subject: [PATCH 3/5] Added constraints to ensure new options can't be used in a non app bundle context --- keychain.go | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/keychain.go b/keychain.go index 1d4f2aa..6f746ce 100644 --- a/keychain.go +++ b/keychain.go @@ -9,9 +9,10 @@ package keychain /* #cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework LocalAuthentication -framework Security -framework CoreFoundation +#cgo LDFLAGS: -framework LocalAuthentication -framework Security -framework CoreFoundation -framework Foundation #include +#include #include #include @@ -33,6 +34,26 @@ static CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *conte return newQuery; } +// This ensures that the Data protection keychain is only used within a signed .app bundle +BOOL isAppBinary() { + @autoreleasepool { + // Get the main bundle of the current application + NSBundle *bundle = [NSBundle mainBundle]; + NSString *executablePath = [bundle executablePath]; // Path to the current binary + + // Check if executablePath is within a .app bundle structure + if ([executablePath rangeOfString:@".app/Contents/MacOS"].location != NSNotFound) { + NSString *appBundlePath = [executablePath substringToIndex:[executablePath rangeOfString:@".app/"].location + 4]; + + // Check for the presence of 'embedded.provisionprofile' + NSString *provisionPath = [appBundlePath stringByAppendingPathComponent:@"Contents/embedded.provisionprofile"]; + BOOL provisionExists = [[NSFileManager defaultManager] fileExistsAtPath:provisionPath]; + + return provisionExists; // Confirm both .app structure and provisioning profile + } + } + return NO; // Not within a .app bundle +} */ import "C" import ( @@ -320,6 +341,14 @@ type Item struct { attr map[string]interface{} } +func IsWithinMacAppBundle() bool { + return bool(C.isAppBinary()) +} + +func CanUseDataProtectionKeychain() bool { + return IsWithinMacAppBundle() +} + // SetSecClass sets the security class func (k *Item) SetSecClass(sc SecClass) { k.attr[SecClassKey] = secClassTypeRef[sc] @@ -412,15 +441,22 @@ func CreateAuthenticationContext(options AuthenticationContextOptions) *Authenti return &AuthenticationContext{ptr: C.CreateLAContext(C.LAContextOptions{AllowableReuseDuration: C.int(options.AllowableReuseDuration)})} } -func (k *Item) SetAuthenticationContext(context *AuthenticationContext) { +func (k *Item) SetAuthenticationContext(context *AuthenticationContext) error { + if !CanUseDataProtectionKeychain() { + return fmt.Errorf("SetAuthenticationContext is not available, application must be within a signed .app bundle to access the data protection keychain") + } + k.attr[AccessControlKey] = context + return nil } -func (k *Item) SetAccessControl(flags AccessControlFlags) error { - protection := C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly +func (k *Item) SetAccessControl(flags AccessControlFlags, accessible Accessible) error { + if !CanUseDataProtectionKeychain() { + return fmt.Errorf("SetAccessControl is not available, application must be within a signed .app bundle to access the data protection keychain") + } var err *C.CFErrorRef - ac := C.SecAccessControlCreateWithFlags(C.kCFAllocatorDefault, C.CFTypeRef(protection), C.SecAccessControlCreateFlags(flags), err) + ac := C.SecAccessControlCreateWithFlags(C.kCFAllocatorDefault, C.CFTypeRef(accessible), C.SecAccessControlCreateFlags(flags), err) if err != nil { return fmt.Errorf("failed to create access control: %+v", err) From 993eff4b0c07b47761f99ef9fea87ea3e8097508 Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Tue, 7 May 2024 23:52:11 +1000 Subject: [PATCH 4/5] Added kSecUseDataProtectionKeychain as per recommendation by Apple https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain --- keychain.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/keychain.go b/keychain.go index 6f746ce..b57d0f3 100644 --- a/keychain.go +++ b/keychain.go @@ -247,6 +247,8 @@ var ( CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) // ModificationDateKey is for kSecAttrModificationDate ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) + // UseDataProtectionKeychainKey is for kSecAttrUseDataProtectionKeychain + UseDataProtectionKeychainKey = attrKey(C.CFTypeRef(C.kSecUseDataProtectionKeychain)) ) // Synchronizable is the items synchronizable status @@ -456,7 +458,7 @@ func (k *Item) SetAccessControl(flags AccessControlFlags, accessible Accessible) } var err *C.CFErrorRef - ac := C.SecAccessControlCreateWithFlags(C.kCFAllocatorDefault, C.CFTypeRef(accessible), C.SecAccessControlCreateFlags(flags), err) + ac := C.SecAccessControlCreateWithFlags(C.kCFAllocatorDefault, accessibleTypeRef[accessible], C.SecAccessControlCreateFlags(flags), err) if err != nil { return fmt.Errorf("failed to create access control: %+v", err) @@ -466,6 +468,15 @@ func (k *Item) SetAccessControl(flags AccessControlFlags, accessible Accessible) return nil } +func (k *Item) SetUseDataProtectionKeychain(canUse bool) error { + if !CanUseDataProtectionKeychain() { + return fmt.Errorf("SetUseDataProtectionKeychain is not available, application must be within a signed .app bundle to access the data protection keychain") + } + + k.attr[UseDataProtectionKeychainKey] = canUse + return nil +} + // SetSynchronizable sets the synchronizable attribute func (k *Item) SetSynchronizable(sync Synchronizable) { if sync != SynchronizableDefault { From 41efe171240e9ea4e33fc3644d63cdee0cef0fca Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Wed, 8 May 2024 00:53:35 +1000 Subject: [PATCH 5/5] Fixes amd64<>arm64 conflicts --- keychain.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keychain.go b/keychain.go index b57d0f3..94b0c5e 100644 --- a/keychain.go +++ b/keychain.go @@ -35,7 +35,7 @@ static CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *conte } // This ensures that the Data protection keychain is only used within a signed .app bundle -BOOL isAppBinary() { +int isAppBinary() { @autoreleasepool { // Get the main bundle of the current application NSBundle *bundle = [NSBundle mainBundle]; @@ -49,10 +49,10 @@ BOOL isAppBinary() { NSString *provisionPath = [appBundlePath stringByAppendingPathComponent:@"Contents/embedded.provisionprofile"]; BOOL provisionExists = [[NSFileManager defaultManager] fileExistsAtPath:provisionPath]; - return provisionExists; // Confirm both .app structure and provisioning profile + return provisionExists ? 1 : 0; } } - return NO; // Not within a .app bundle + return 0; // Return 0 if not within a .app bundle } */ import "C" @@ -344,7 +344,7 @@ type Item struct { } func IsWithinMacAppBundle() bool { - return bool(C.isAppBinary()) + return C.isAppBinary() != 0 } func CanUseDataProtectionKeychain() bool {