Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Added support for biometrics (Touch ID/Face ID) #107

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions corefoundation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package keychain
#cgo LDFLAGS: -framework CoreFoundation

#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>

// 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
Expand All @@ -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);
}
*/
Expand Down Expand Up @@ -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:
Expand Down
150 changes: 148 additions & 2 deletions keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>
#include <Security/Security.h>
#include <LocalAuthentication/LocalAuthentication.h>

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 (
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down