diff --git a/corefoundation.go b/corefoundation.go index b7ee544..c25a99a 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 @@ -19,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); } */ @@ -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..94b0c5e 100644 --- a/keychain.go +++ b/keychain.go @@ -8,10 +8,52 @@ 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 -framework Foundation #include +#include #include +#include + +typedef struct { + int AllowableReuseDuration; +} LAContextOptions; + +static LAContext* CreateLAContext(LAContextOptions options) { + LAContext *context = [[LAContext alloc] init]; + context.touchIDAuthenticationAllowableReuseDuration = options.AllowableReuseDuration; + return context; +} + +static CFDictionaryRef AddContextToQuery(CFDictionaryRef query, LAContext *context) { + CFMutableDictionaryRef newQuery = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); + CFDictionarySetValue(newQuery, kSecUseAuthenticationContext, context); + + // Convert back to CFDictionaryRef + return newQuery; +} + +// This ensures that the Data protection keychain is only used within a signed .app bundle +int 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 ? 1 : 0; + } + } + return 0; // Return 0 if not within a .app bundle +} */ import "C" import ( @@ -22,6 +64,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 +229,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 @@ -185,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 @@ -231,6 +295,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 @@ -265,6 +343,14 @@ type Item struct { attr map[string]interface{} } +func IsWithinMacAppBundle() bool { + return C.isAppBinary() != 0 +} + +func CanUseDataProtectionKeychain() bool { + return IsWithinMacAppBundle() +} + // SetSecClass sets the security class func (k *Item) SetSecClass(sc SecClass) { k.attr[SecClassKey] = secClassTypeRef[sc] @@ -353,6 +439,44 @@ 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) 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, 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, accessibleTypeRef[accessible], C.SecAccessControlCreateFlags(flags), err) + + if err != nil { + return fmt.Errorf("failed to create access control: %+v", err) + } + + k.attr[AccessControlKey] = ac + 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 { @@ -420,6 +544,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 +566,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 +609,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 +740,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) }