diff --git a/README.md b/README.md index cdefab6e..3af87c35 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,13 @@ This instance can be used to store and retrieve data that can be retrieved by th VALSecureEnclaveValet *mySecureEnclaveValet = [[VALSecureEnclaveValet alloc] initWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence]; ``` -This instance can be used to store and retrieve data in the Secure Enclave (available on iOS 8.0 and later and Mac OS 10.11 and later). Reading or modifying items in this Valet will require the user to confirm their presence via Touch ID on iOS or by entering their device passcode. *If no passcode is set on the device, this instance will be unable to access or store data.* Data is removed from the Secure Enclave when the user removes a passcode from the device. Storing data using VALSecureEnclaveValet is the most secure way to store data on either iOS or Mac OS. +This instance can be used to store and retrieve data in the Secure Enclave (available on iOS 8.0 and later and Mac OS 10.11 and later). Each time data is retrieved from this Valet, the user will be prompted to confirm their presence via Touch ID or by entering their device passcode. *If no passcode is set on the device, this instance will be unable to access or store data.* Data is removed from the Secure Enclave when the user removes a passcode from the device. Storing data using VALSecureEnclaveValet is the most secure way to store data on either iOS or Mac OS. + +```objc +VALSinglePromptSecureEnclaveValet *mySecureEnclaveValet = [[VALSinglePromptSecureEnclaveValet alloc] initWithIdentifier:@"Druidia" accessControl:VALAccessControlUserPresence]; +``` + +This instance also stores and retrieves data in the Secure Enclave, but does not require the user to confirm their presence each time data is retrieved. Instead, the user will be prompted to confirm their presence only on the first data retrieval. A `VALSinglePromptSecureEnclaveValet` instance can be forced to prompt the user on the next data retrieval by calling the instance method `requirePromptOnNextAccess`. ### Migrating Existing Keychain Values into Valet diff --git a/Valet.podspec b/Valet.podspec index ccb5878f..8aa7e66e 100644 --- a/Valet.podspec +++ b/Valet.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Valet' - s.version = '2.3.0' + s.version = '2.4.0' s.license = 'Apache License, Version 2.0' s.summary = 'Valet lets you securely store data in the iOS or OS X Keychain without knowing a thing about how the Keychain works. It\'s easy. We promise.' s.homepage = 'https://github.com/square/Valet' diff --git a/Valet.xcodeproj/project.pbxproj b/Valet.xcodeproj/project.pbxproj index 06190163..50524b7e 100644 --- a/Valet.xcodeproj/project.pbxproj +++ b/Valet.xcodeproj/project.pbxproj @@ -30,6 +30,18 @@ 371150AD1E2962D8004A45D4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 371150AB1E2962D8004A45D4 /* Main.storyboard */; }; 371150AF1E2962D8004A45D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 371150AE1E2962D8004A45D4 /* Assets.xcassets */; }; 371150B21E2962D8004A45D4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 371150B01E2962D8004A45D4 /* LaunchScreen.storyboard */; }; + 371E38081E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */; }; + 371E38091E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */; }; + 371E380A1E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */; }; + 371E380B1E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */; }; + 371E380E1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 371E380F1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 371E38101E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 371E38111E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */ = {isa = PBXBuildFile; fileRef = 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 371E38121E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */ = {isa = PBXBuildFile; fileRef = 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */; }; + 371E38131E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */ = {isa = PBXBuildFile; fileRef = 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */; }; + 371E38141E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */ = {isa = PBXBuildFile; fileRef = 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */; }; + 371E38151E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */ = {isa = PBXBuildFile; fileRef = 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */; }; AC89A3EC1CC82426009A7121 /* ValetTouchIDTestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC89A3EB1CC82426009A7121 /* ValetTouchIDTestAppDelegate.swift */; }; AC89A3EE1CC82738009A7121 /* ValetTouchIDTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC89A3ED1CC82738009A7121 /* ValetTouchIDTestViewController.swift */; }; EA1E1F8F1A8C46090067C991 /* libValet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E1F831A8C46080067C991 /* libValet.a */; }; @@ -102,6 +114,9 @@ 371150AE1E2962D8004A45D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 371150B11E2962D8004A45D4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 371150B31E2962D8004A45D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VALSecureEnclaveValet_Protected.h; sourceTree = ""; }; + 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VALSinglePromptSecureEnclaveValet.h; sourceTree = ""; }; + 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VALSinglePromptSecureEnclaveValet.m; sourceTree = ""; }; 37250B1D1E2DD55D008B4777 /* Valet iOS Test Host App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Valet iOS Test Host App.entitlements"; sourceTree = ""; }; AC89A3EA1CC82426009A7121 /* ValetTouchIDTest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ValetTouchIDTest-Bridging-Header.h"; sourceTree = ""; }; AC89A3EB1CC82426009A7121 /* ValetTouchIDTestAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValetTouchIDTestAppDelegate.swift; sourceTree = ""; }; @@ -241,7 +256,10 @@ EAEEAC231AB7BA0C00EDB6E3 /* VALValet_Protected.h */, EAEEAC191AB7B83300EDB6E3 /* VALValet.m */, EAEEAC1E1AB7B84E00EDB6E3 /* VALSecureEnclaveValet.h */, + 371E38071E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h */, EAEEAC1F1AB7B84E00EDB6E3 /* VALSecureEnclaveValet.m */, + 371E380C1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h */, + 371E380D1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m */, EAEEAC1B1AB7B84000EDB6E3 /* VALSynchronizableValet.h */, EAEEAC1C1AB7B84000EDB6E3 /* VALSynchronizableValet.m */, ); @@ -303,9 +321,11 @@ buildActionMask = 2147483647; files = ( 26E682841BA8B42100EFF4EA /* Valet.h in Headers */, + 371E38101E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */, 26F06A551BA8B53F00E039CD /* VALValet.h in Headers */, 26F06A541BA8B53C00E039CD /* VALSecureEnclaveValet.h in Headers */, 26F06A531BA8B53100E039CD /* VALSynchronizableValet.h in Headers */, + 371E380A1E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */, 26F06A5C1BA8B55D00E039CD /* ValetDefines.h in Headers */, 26F06A561BA8B54400E039CD /* VALValet_Protected.h in Headers */, ); @@ -316,9 +336,11 @@ buildActionMask = 2147483647; files = ( 26F06A571BA8B54E00E039CD /* Valet.h in Headers */, + 371E38111E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */, 26F06A581BA8B55000E039CD /* VALValet.h in Headers */, 26F06A5A1BA8B55300E039CD /* VALSecureEnclaveValet.h in Headers */, 26F06A5B1BA8B55500E039CD /* VALSynchronizableValet.h in Headers */, + 371E380B1E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */, 26F06A591BA8B55100E039CD /* VALValet_Protected.h in Headers */, 26F06A5D1BA8B56000E039CD /* ValetDefines.h in Headers */, ); @@ -329,9 +351,11 @@ buildActionMask = 2147483647; files = ( EAEAA89E1B16818400F7AA98 /* Valet.h in Headers */, + 371E380F1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */, EA7756041C487783009C5C92 /* VALSecureEnclaveValet.h in Headers */, EAEAA8A21B16818E00F7AA98 /* VALValet_Protected.h in Headers */, EAEAA8A11B16818400F7AA98 /* VALSynchronizableValet.h in Headers */, + 371E38091E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */, EAEAA8A31B1681F400F7AA98 /* ValetDefines.h in Headers */, EAEAA89F1B16818400F7AA98 /* VALValet.h in Headers */, ); @@ -342,9 +366,11 @@ buildActionMask = 2147483647; files = ( EAEEAC2A1AB7BD4800EDB6E3 /* VALValet.h in Headers */, + 371E380E1E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.h in Headers */, EAEEAC2B1AB7BD7400EDB6E3 /* VALSecureEnclaveValet.h in Headers */, EAEEAC2C1AB7BD7900EDB6E3 /* VALSynchronizableValet.h in Headers */, EAEAA8AC1B16864D00F7AA98 /* Valet.h in Headers */, + 371E38081E3713CB008A1C62 /* VALSecureEnclaveValet_Protected.h in Headers */, EAEEAC271AB7BC5700EDB6E3 /* VALValet_Protected.h in Headers */, EAEEAC281AB7BC5700EDB6E3 /* ValetDefines.h in Headers */, ); @@ -627,6 +653,7 @@ files = ( 26F06A8D1BA8BC8F00E039CD /* VALValet.m in Sources */, 26F06A8E1BA8BC9000E039CD /* VALSecureEnclaveValet.m in Sources */, + 371E38141E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */, 26F06A8F1BA8BC9200E039CD /* VALSynchronizableValet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -637,6 +664,7 @@ files = ( 26F06A921BA8BC9700E039CD /* VALValet.m in Sources */, 26F06A911BA8BC9500E039CD /* VALSecureEnclaveValet.m in Sources */, + 371E38151E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */, 26F06A901BA8BC9400E039CD /* VALSynchronizableValet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -656,6 +684,7 @@ files = ( EAEEAC1D1AB7B84000EDB6E3 /* VALSynchronizableValet.m in Sources */, EAEEAC1A1AB7B83300EDB6E3 /* VALValet.m in Sources */, + 371E38121E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */, EAEEAC201AB7B84E00EDB6E3 /* VALSecureEnclaveValet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -674,6 +703,7 @@ files = ( EAEAA8A61B16821D00F7AA98 /* VALSynchronizableValet.m in Sources */, EA7756051C4877A3009C5C92 /* VALSecureEnclaveValet.m in Sources */, + 371E38131E3713DF008A1C62 /* VALSinglePromptSecureEnclaveValet.m in Sources */, EAEAA8A41B16821D00F7AA98 /* VALValet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Valet/VALSecureEnclaveValet.h b/Valet/VALSecureEnclaveValet.h index 027351f7..2a5be5d7 100644 --- a/Valet/VALSecureEnclaveValet.h +++ b/Valet/VALSecureEnclaveValet.h @@ -44,11 +44,11 @@ typedef NS_ENUM(NSUInteger, VALAccessControl) { }; -/// Reads and writes keychain elements that are stored on the Secure Enclave (available on iOS 8.0 and later and macOS 10.11 and later) using accessibility attribute VALAccessibilityWhenPasscodeSetThisDeviceOnly. Accessing or modifying these keychain elements will require the user to confirm their presence via Touch ID or passcode entry. If no passcode is set on the device, the below methods will fail. Data is removed from the Secure Enclave when the user removes a passcode from the device. Use the userPrompt methods to display custom text to the user in Apple's Touch ID and passcode entry UI. +/// Reads and writes keychain elements that are stored on the Secure Enclave (available on iOS 8.0 and later and macOS 10.11 and later) using accessibility attribute VALAccessibilityWhenPasscodeSetThisDeviceOnly. Accessing these keychain elements will require the user to confirm their presence via Touch ID or passcode entry. If no passcode is set on the device, the below methods will fail. Data is removed from the Secure Enclave when the user removes a passcode from the device. Use the userPrompt methods to display custom text to the user in Apple's Touch ID and passcode entry UI. /// @version Available on iOS 8 or later, and macOS 10.11 or later. @interface VALSecureEnclaveValet : VALValet -/// @return YES if Secure Enclave storage is supported on the current iOS version (8.0 and later). +/// @return YES if Secure Enclave storage is supported on the current iOS or macOS version (iOS 8.0 and macOS 10.11 and later). + (BOOL)supportsSecureEnclaveKeychainItems; /// Creates a Valet that reads/writes Secure Enclave keychain elements and the specified access control. diff --git a/Valet/VALSecureEnclaveValet.m b/Valet/VALSecureEnclaveValet.m index f1c7418f..aa9b7d70 100644 --- a/Valet/VALSecureEnclaveValet.m +++ b/Valet/VALSecureEnclaveValet.m @@ -19,6 +19,7 @@ // #import "VALSecureEnclaveValet.h" +#import "VALSecureEnclaveValet_Protected.h" #import "VALValet_Protected.h" #import "ValetDefines.h" @@ -262,27 +263,7 @@ - (BOOL)canAccessKeychain; - (BOOL)containsObjectForKey:(nonnull NSString *)key; { - NSDictionary *options = nil; - - // iOS 9 and macOS 10.11 use kSecUseAuthenticationUI, not kSecUseNoAuthenticationUI. -#if ((TARGET_OS_IPHONE && __IPHONE_9_0) || (TARGET_OS_MAC && __MAC_10_11)) - if ([[self class] _iOS9OrLater] || [[self class] _macOSElCapitanOrLater]) { - options = @{ (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIFail }; - } else { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // kSecUseNoAuthenticationUI is deprecated in the iOS 9 SDK, but we still need it on iOS 8. -#if (TARGET_OS_IPHONE && __IPHONE_9_0) - options = @{ (__bridge id)kSecUseNoAuthenticationUI : @YES }; -#endif -#pragma GCC diagnostic pop - } -#else - options = @{ (__bridge id)kSecUseNoAuthenticationUI : @YES }; -#endif - - OSStatus const status = [self containsObjectForKey:key options:options]; - + OSStatus const status = [self containsObjectForKey:key options:nil]; BOOL const keyAlreadyInKeychain = (status == errSecInteractionNotAllowed || status == errSecSuccess); return keyAlreadyInKeychain; } @@ -315,13 +296,7 @@ - (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable N - (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled; { - OSStatus status = errSecSuccess; - NSData *const objectForKey = [self objectForKey:key options:[self _optionsDictionaryForUserPrompt:userPrompt] status:&status]; - if (userCancelled != NULL) { - *userCancelled = (status == errSecUserCanceled || status == errSecAuthFailed); - } - - return objectForKey; + return [self objectForKey:key userPrompt:userPrompt userCancelled:userCancelled options:nil]; } - (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt; @@ -331,16 +306,10 @@ - (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable - (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled; { - OSStatus status = errSecSuccess; - NSString *const stringForKey = [self stringForKey:key options:[self _optionsDictionaryForUserPrompt:userPrompt] status:&status]; - if (userCancelled != NULL) { - *userCancelled = (status == errSecUserCanceled || status == errSecAuthFailed); - } - - return stringForKey; + return [self stringForKey:key userPrompt:userPrompt userCancelled:userCancelled options:nil]; } -#pragma mark - Protected Methods +#pragma mark - VALValet Protected Methods - (BOOL)setObject:(nonnull NSData *)value forKey:(nonnull NSString *)key options:(nullable NSDictionary *)options; { @@ -350,6 +319,68 @@ - (BOOL)setObject:(nonnull NSData *)value forKey:(nonnull NSString *)key options return [super setObject:value forKey:key options:options]; } +- (OSStatus)containsObjectForKey:(nonnull NSString *)key options:(nullable NSDictionary *)options; +{ + NSDictionary *baseOptions = nil; + + // iOS 9 and macOS 10.11 use kSecUseAuthenticationUI, not kSecUseNoAuthenticationUI. +#if ((TARGET_OS_IPHONE && __IPHONE_9_0) || (TARGET_OS_MAC && __MAC_10_11)) + if ([[self class] _iOS9OrLater] || [[self class] _macOSElCapitanOrLater]) { + baseOptions = @{ (__bridge id)kSecUseAuthenticationUI : (__bridge id)kSecUseAuthenticationUIFail }; + } else { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // kSecUseNoAuthenticationUI is deprecated in the iOS 9 SDK, but we still need it on iOS 8. +#if (TARGET_OS_IPHONE && __IPHONE_9_0) + options = @{ (__bridge id)kSecUseNoAuthenticationUI : @YES }; +#endif +#pragma GCC diagnostic pop + } +#else + options = @{ (__bridge id)kSecUseNoAuthenticationUI : @YES }; +#endif + + NSMutableDictionary *const allOptions = [baseOptions mutableCopy]; + [allOptions addEntriesFromDictionary:options]; + return [super containsObjectForKey:key options:allOptions]; +} + +#pragma mark - VALSecureEnclaveValet Protected Methods + +- (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; +{ + OSStatus status = errSecSuccess; + + NSMutableDictionary *const allOptions = [[self _optionsDictionaryForUserPrompt:userPrompt] mutableCopy]; + if (options.count > 0) { + [allOptions addEntriesFromDictionary:options]; + } + + NSData *const objectForKey = [self objectForKey:key options:allOptions status:&status]; + if (userCancelled != NULL) { + *userCancelled = (status == errSecUserCanceled || status == errSecAuthFailed); + } + + return objectForKey; +} + +- (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; +{ + OSStatus status = errSecSuccess; + + NSMutableDictionary *const allOptions = [[self _optionsDictionaryForUserPrompt:userPrompt] mutableCopy]; + if (options.count > 0) { + [allOptions addEntriesFromDictionary:options]; + } + + NSString *const stringForKey = [self stringForKey:key options:allOptions status:&status]; + if (userCancelled != NULL) { + *userCancelled = (status == errSecUserCanceled || status == errSecAuthFailed); + } + + return stringForKey; +} + #pragma mark - Private Methods - (nullable NSDictionary *)_optionsDictionaryForUserPrompt:(nullable NSString *)userPrompt; @@ -437,6 +468,16 @@ - (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable VALCheckCondition(NO, nil, @"VALSecureEnclaveValet unsupported on this SDK"); } +- (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; +{ + VALCheckCondition(NO, nil, @"VALSecureEnclaveValet unsupported on this SDK"); +} + +- (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; +{ + VALCheckCondition(NO, nil, @"VALSecureEnclaveValet unsupported on this SDK"); +} + @end diff --git a/Valet/VALSecureEnclaveValet_Protected.h b/Valet/VALSecureEnclaveValet_Protected.h new file mode 100644 index 00000000..42985879 --- /dev/null +++ b/Valet/VALSecureEnclaveValet_Protected.h @@ -0,0 +1,18 @@ +// +// VALSecureEnclaveValet_Protected.h +// Valet +// +// Created by Dan Federman on 1/23/17. +// Copyright © 2017 Square, Inc. All rights reserved. +// + +#import + + +@interface VALSecureEnclaveValet () + +- (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; + +- (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled options:(nullable NSDictionary *)options; + +@end diff --git a/Valet/VALSinglePromptSecureEnclaveValet.h b/Valet/VALSinglePromptSecureEnclaveValet.h new file mode 100644 index 00000000..21d65d25 --- /dev/null +++ b/Valet/VALSinglePromptSecureEnclaveValet.h @@ -0,0 +1,20 @@ +// +// VALSinglePromptSecureEnclaveValet.h +// Valet +// +// Created by Dan Federman on 1/23/17. +// Copyright © 2017 Square, Inc. All rights reserved. +// + +#import + + +/// Reads and writes keychain elements that are stored on the Secure Enclave (available on iOS 8.0 and later and macOS 10.11 and later) using accessibility attribute VALAccessibilityWhenPasscodeSetThisDeviceOnly. The first access of these keychain elements will require the user to confirm their presence via Touch ID or passcode entry. +/// @see VALSecureEnclaveValet +/// @version Available on iOS 8 or later, and macOS 10.11 or later. +@interface VALSinglePromptSecureEnclaveValet : VALSecureEnclaveValet + +/// Forces a prompt for Touch ID or passcode entry on the next data retrieval from the Secure Enclave. +- (void)requirePromptOnNextAccess; + +@end diff --git a/Valet/VALSinglePromptSecureEnclaveValet.m b/Valet/VALSinglePromptSecureEnclaveValet.m new file mode 100644 index 00000000..3f0a24b9 --- /dev/null +++ b/Valet/VALSinglePromptSecureEnclaveValet.m @@ -0,0 +1,139 @@ +// +// VALSinglePromptSecureEnclaveValet.m +// Valet +// +// Created by Dan Federman on 1/23/17. +// Copyright © 2017 Square, Inc. All rights reserved. +// + +#import "VALSinglePromptSecureEnclaveValet.h" +#import "VALSecureEnclaveValet_Protected.h" +#import "VALValet_Protected.h" + +#import "ValetDefines.h" + + +#if VAL_SECURE_ENCLAVE_SDK_AVAILABLE +#import + + +@interface VALSinglePromptSecureEnclaveValet () + +@property (nonnull, strong, readwrite) LAContext *context; + +@end + + +@implementation VALSinglePromptSecureEnclaveValet + +#pragma mark - Initialization + +- (nullable instancetype)initWithIdentifier:(nonnull NSString *)identifier accessControl:(VALAccessControl)accessControl; +{ + self = [super initWithIdentifier:identifier accessControl:accessControl]; + + if (self != nil) { + _context = [LAContext new]; + } + + return self; +} + +- (nullable instancetype)initWithSharedAccessGroupIdentifier:(nonnull NSString *)sharedAccessGroupIdentifier accessControl:(VALAccessControl)accessControl; +{ + self = [super initWithSharedAccessGroupIdentifier:sharedAccessGroupIdentifier accessControl:accessControl]; + + if (self != nil) { + _context = [LAContext new]; + } + + return self; +} + +#pragma mark - VALValet + +- (BOOL)setObject:(nonnull NSData *)value forKey:(nonnull NSString *)key; +{ + return [self setObject:value forKey:key options:[self _contextOptions]]; +} + +- (nullable NSData *)objectForKey:(nonnull NSString *)key; +{ + return [self objectForKey:key options:[self _contextOptions] status:nil]; +} + +- (BOOL)setString:(nonnull NSString *)string forKey:(nonnull NSString *)key; +{ + return [self setString:string forKey:key options:[self _contextOptions]]; +} + +- (nullable NSString *)stringForKey:(nonnull NSString *)key; +{ + return [self stringForKey:key options:[self _contextOptions] status:nil]; +} + +- (BOOL)containsObjectForKey:(NSString *)key; +{ + OSStatus const status = [self containsObjectForKey:key options:[self _contextOptions]]; + BOOL const keyAlreadyInKeychain = (status == errSecInteractionNotAllowed || status == errSecSuccess); + return keyAlreadyInKeychain; +} + +- (BOOL)removeObjectForKey:(nonnull NSString *)key; +{ + return [self removeObjectForKey:key options:[self _contextOptions]]; +} + +#pragma mark - VALSecureEnclaveValet + +- (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt; +{ + return [self objectForKey:key userPrompt:userPrompt userCancelled:nil options:[self _contextOptions]]; +} + +- (nullable NSData *)objectForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled; +{ + return [self objectForKey:key userPrompt:userPrompt userCancelled:userCancelled options:[self _contextOptions]]; +} + +- (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt; +{ + return [self stringForKey:key userPrompt:userPrompt userCancelled:nil options:[self _contextOptions]]; +} + +- (nullable NSString *)stringForKey:(nonnull NSString *)key userPrompt:(nullable NSString *)userPrompt userCancelled:(nullable inout BOOL *)userCancelled; +{ + return [self stringForKey:key userPrompt:userPrompt userCancelled:userCancelled options:[self _contextOptions]]; +} + +#pragma mark - Public Methods + +- (void)requirePromptOnNextAccess; +{ + VALAtomicSecItemLock(^{ + [self.context invalidate]; + self.context = [LAContext new]; + }); +} + +#pragma mark - Private Methods + +- (nonnull NSDictionary *)_contextOptions; +{ + return @{ (__bridge id)kSecUseAuthenticationContext : self.context }; +} + +@end + +#else // Below this line we're in !VAL_SECURE_ENCLAVE_SDK_AVAILABLE, meaning none of our API is actually usable. Return NO or nil everywhere. + +@implementation VALSinglePromptSecureEnclaveValet + +- (void)requirePromptOnNextAccess; +{ + VALCheckCondition(NO, , @"VALSinglePromptSecureEnclaveValet unsupported on this SDK"); +} + +@end + +#endif // VAL_SECURE_ENCLAVE_SDK_AVAILABLE diff --git a/Valet/VALValet_Protected.h b/Valet/VALValet_Protected.h index 12bf1c73..57a10e72 100644 --- a/Valet/VALValet_Protected.h +++ b/Valet/VALValet_Protected.h @@ -22,6 +22,7 @@ extern NSString * __nonnull VALStringForAccessibility(VALAccessibility accessibility); +extern void VALAtomicSecItemLock(__nonnull dispatch_block_t block); @interface VALValet () diff --git a/Valet/Valet.h b/Valet/Valet.h index c8758be1..18099da7 100644 --- a/Valet/Valet.h +++ b/Valet/Valet.h @@ -21,3 +21,4 @@ #import #import #import +#import diff --git a/ValetTouchIDTest/Base.lproj/ValetSecureElementTestLaunchScreen.xib b/ValetTouchIDTest/Base.lproj/ValetSecureElementTestLaunchScreen.xib index c95c25e7..c915cba8 100644 --- a/ValetTouchIDTest/Base.lproj/ValetSecureElementTestLaunchScreen.xib +++ b/ValetTouchIDTest/Base.lproj/ValetSecureElementTestLaunchScreen.xib @@ -1,9 +1,13 @@ - - + + + + + - + + @@ -15,17 +19,17 @@ - + diff --git a/ValetTouchIDTest/Base.lproj/ValetSecureElementTestMain.storyboard b/ValetTouchIDTest/Base.lproj/ValetSecureElementTestMain.storyboard index 22e6bf80..9062ea0d 100644 --- a/ValetTouchIDTest/Base.lproj/ValetSecureElementTestMain.storyboard +++ b/ValetTouchIDTest/Base.lproj/ValetSecureElementTestMain.storyboard @@ -1,9 +1,13 @@ - - + + + + + - + + @@ -15,75 +19,81 @@ - + + + + - - - + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - diff --git a/ValetTouchIDTest/ValetTouchIDTestViewController.swift b/ValetTouchIDTest/ValetTouchIDTestViewController.swift index 29d96026..e63bf4d0 100644 --- a/ValetTouchIDTest/ValetTouchIDTestViewController.swift +++ b/ValetTouchIDTest/ValetTouchIDTestViewController.swift @@ -26,7 +26,7 @@ final class ValetTouchIDTestViewController : UIViewController // MARK: Properties @IBOutlet var textView : UITextView? - let secureEnclaveValet : VALSecureEnclaveValet + var singlePromptSecureEnclaveValet : VALSinglePromptSecureEnclaveValet let username = "CustomerPresentProof" @@ -34,8 +34,8 @@ final class ValetTouchIDTestViewController : UIViewController required init?(coder aDecoder: NSCoder) { - if let valet = VALSecureEnclaveValet(identifier: "UserPresence", accessControl: VALAccessControl.userPresence) { - self.secureEnclaveValet = valet + if let valet = VALSinglePromptSecureEnclaveValet(identifier: "UserPresence", accessControl: VALAccessControl.userPresence) { + self.singlePromptSecureEnclaveValet = valet } else { return nil; } @@ -50,7 +50,7 @@ final class ValetTouchIDTestViewController : UIViewController @IBAction func setOrUpdateItem(sender: UIResponder) { let stringToSet = "I am here! " + NSUUID().uuidString - let setOrUpdatedItem = secureEnclaveValet.setString(stringToSet, forKey: username) + let setOrUpdatedItem = singlePromptSecureEnclaveValet.setString(stringToSet, forKey: username) updateTextView(messageComponents: #function, (setOrUpdatedItem ? "Success" : "Failure")) } @@ -58,7 +58,7 @@ final class ValetTouchIDTestViewController : UIViewController @IBAction func getItem(sender: UIResponder) { var userCancelled: ObjCBool = false - let password = secureEnclaveValet.string(forKey: username, userPrompt: "Use TouchID to retrieve password", userCancelled:&userCancelled) + let password = singlePromptSecureEnclaveValet.string(forKey: username, userPrompt: "Use TouchID to retrieve password", userCancelled:&userCancelled) var resultString: String if (userCancelled.boolValue) { @@ -75,18 +75,24 @@ final class ValetTouchIDTestViewController : UIViewController @objc(removeItem:) @IBAction func removeItem(sender: UIResponder) { - let removedItem = secureEnclaveValet.removeObject(forKey: username) + let removedItem = singlePromptSecureEnclaveValet.removeObject(forKey: username) updateTextView(messageComponents: #function, (removedItem ? "Success" : "Failure")) } @objc(containsItem:) @IBAction func containsItem(sender: UIResponder) { - let containsItem = secureEnclaveValet.containsObject(forKey: username) + let containsItem = singlePromptSecureEnclaveValet.containsObject(forKey: username) updateTextView(messageComponents: #function, (containsItem ? "YES" : "NO")) } - + @objc(requirePrompt:) + @IBAction func requirePrompt(sender: UIResponder) + { + singlePromptSecureEnclaveValet.requirePromptOnNextAccess() + updateTextView(messageComponents: #function) + } + // MARK: Private private func updateTextView(messageComponents: String...)