diff --git a/KeychainExample/app.js b/KeychainExample/app.js index becefb1..2f68afe 100644 --- a/KeychainExample/app.js +++ b/KeychainExample/app.js @@ -19,7 +19,7 @@ export default class KeychainExample extends Component { save() { Keychain - .setGenericPassword(this.state.username, this.state.password) + .setSecurePassword('myService', this.state.username, this.state.password) .then(() => { this.setState({ status: 'Credentials saved!' }); }) @@ -30,7 +30,7 @@ export default class KeychainExample extends Component { load() { Keychain - .getGenericPassword() + .getSecurePassword('myService') .then((credentials) => { if (credentials) { this.setState({ ...credentials, status: 'Credentials loaded!' }); diff --git a/KeychainExample/ios/KeychainExample.xcodeproj/project.pbxproj b/KeychainExample/ios/KeychainExample.xcodeproj/project.pbxproj index 160e76c..9318785 100644 --- a/KeychainExample/ios/KeychainExample.xcodeproj/project.pbxproj +++ b/KeychainExample/ios/KeychainExample.xcodeproj/project.pbxproj @@ -1123,7 +1123,9 @@ "$(SRCROOT)/../node_modules/react-native-keychain/RNKeychainManager", ); INFOPLIST_FILE = KeychainExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1144,7 +1146,9 @@ "$(SRCROOT)/../node_modules/react-native-keychain/RNKeychainManager", ); INFOPLIST_FILE = KeychainExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/KeychainExample/ios/KeychainExample/AppDelegate.m b/KeychainExample/ios/KeychainExample/AppDelegate.m index 2112a27..3ba5994 100644 --- a/KeychainExample/ios/KeychainExample/AppDelegate.m +++ b/KeychainExample/ios/KeychainExample/AppDelegate.m @@ -11,9 +11,26 @@ #import #import +#import + +@interface AppDelegate() + +@end @implementation AppDelegate +@synthesize willPromptForAuthentication = _willPromptForAuthentication; + +- (void)setWillPromptForAuthentication:(BOOL)willPromptForAuthentication { + _willPromptForAuthentication = willPromptForAuthentication; + + if (willPromptForAuthentication) { + NSLog(@"APPDELEGATE::: will prompt TouchId"); + } else { + NSLog(@"APPDELEGATE::: ended prompt TouchId"); + } +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; diff --git a/RNKeychain.xcodeproj/project.pbxproj b/RNKeychain.xcodeproj/project.pbxproj index e074281..1b5b85b 100644 --- a/RNKeychain.xcodeproj/project.pbxproj +++ b/RNKeychain.xcodeproj/project.pbxproj @@ -9,19 +9,21 @@ /* Begin PBXBuildFile section */ 5D82368F1B0CE3CB005A9EF3 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D8236901B0CE3D6005A9EF3 /* Security.framework */; }; - 6478986A1F38BF9D00DA1C12 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 647898691F38BF9D00DA1C12 /* Security.framework */; }; - 6478986B1F38BFA100DA1C12 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; + B2AF06371E97BF10006435CD /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2AF06361E97BF10006435CD /* LocalAuthentication.framework */; }; + B2AF063D1E97DE25006435CD /* RNKeychainAuthenticationListener.h in Headers */ = {isa = PBXBuildFile; fileRef = B22856161E950BB300CCF753 /* RNKeychainAuthenticationListener.h */; }; + B2AF063F1E97DF85006435CD /* RNKeychainAuthenticationListener.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = B22856161E950BB300CCF753 /* RNKeychainAuthenticationListener.h */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ - 5D82366D1B0CE05B005A9EF3 /* Copy Files */ = { + 5D82366D1B0CE05B005A9EF3 /* Copy Headers */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; + dstPath = include/RNKeychain; dstSubfolderSpec = 16; files = ( + B2AF063F1E97DF85006435CD /* RNKeychainAuthenticationListener.h in Copy Headers */, ); - name = "Copy Files"; + name = "Copy Headers"; runOnlyForDeploymentPostprocessing = 0; }; 6478985D1F38BF9100DA1C12 /* CopyFiles */ = { @@ -40,8 +42,8 @@ 5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNKeychainManager.h; sourceTree = ""; }; 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNKeychainManager.m; sourceTree = ""; }; 5D8236901B0CE3D6005A9EF3 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; - 6478985F1F38BF9100DA1C12 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 647898691F38BF9D00DA1C12 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.2.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; + B22856161E950BB300CCF753 /* RNKeychainAuthenticationListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNKeychainAuthenticationListener.h; sourceTree = ""; }; + B2AF06361E97BF10006435CD /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +51,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B2AF06371E97BF10006435CD /* LocalAuthentication.framework in Frameworks */, 5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -69,7 +72,7 @@ children = ( 5D82368A1B0CE2A6005A9EF3 /* RNKeychainManager */, 5D8236701B0CE05B005A9EF3 /* Products */, - 647898681F38BF9C00DA1C12 /* Frameworks */, + B2AF06351E97BF10006435CD /* Frameworks */, ); sourceTree = ""; wrapsLines = 0; @@ -88,21 +91,32 @@ children = ( 5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */, 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */, + B22856161E950BB300CCF753 /* RNKeychainAuthenticationListener.h */, ); path = RNKeychainManager; sourceTree = ""; }; - 647898681F38BF9C00DA1C12 /* Frameworks */ = { + B2AF06351E97BF10006435CD /* Frameworks */ = { isa = PBXGroup; children = ( - 5D8236901B0CE3D6005A9EF3 /* Security.framework */, - 647898691F38BF9D00DA1C12 /* Security.framework */, + B2AF06361E97BF10006435CD /* LocalAuthentication.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + B2AF063C1E97DE1E006435CD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B2AF063D1E97DE25006435CD /* RNKeychainAuthenticationListener.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 5D82366E1B0CE05B005A9EF3 /* RNKeychain */ = { isa = PBXNativeTarget; @@ -110,7 +124,8 @@ buildPhases = ( 5D82366B1B0CE05B005A9EF3 /* Sources */, 5D82366C1B0CE05B005A9EF3 /* Frameworks */, - 5D82366D1B0CE05B005A9EF3 /* Copy Files */, + B2AF063C1E97DE1E006435CD /* Headers */, + 5D82366D1B0CE05B005A9EF3 /* Copy Headers */, ); buildRules = ( ); diff --git a/RNKeychainManager/RNKeychainAuthenticationListener.h b/RNKeychainManager/RNKeychainAuthenticationListener.h new file mode 100644 index 0000000..214f07d --- /dev/null +++ b/RNKeychainManager/RNKeychainAuthenticationListener.h @@ -0,0 +1,22 @@ +// +// TouchIdPromptListener.h +// RNKeychain +// +// Created by Steffen Blümm on 05/04/17. +// Copyright © 2017 Joel Arvidsson. All rights reserved. +// + +#import + +/** + This is a protocol to be implemented by the AppDelegate in case + the AppDelegate takes precautions to obfuscate the screen when + the app resigns active state. + Thus the AppDelegate can avoid to obfuscating the screen when + the TouchId-prompt is brought up by the OS + */ +@protocol RNKeychainAuthenticationListener + +@property (nonatomic, assign) BOOL willPromptForAuthentication; + +@end diff --git a/RNKeychainManager/RNKeychainManager.m b/RNKeychainManager/RNKeychainManager.m index bf4962f..4fa50c0 100644 --- a/RNKeychainManager/RNKeychainManager.m +++ b/RNKeychainManager/RNKeychainManager.m @@ -12,6 +12,11 @@ #import #import +#import +#import + +#import "RNKeychainAuthenticationListener.h" + @implementation RNKeychainManager @synthesize bridge = _bridge; @@ -104,6 +109,196 @@ NSString *serviceValue(NSDictionary *options) return [[NSBundle mainBundle] bundleIdentifier]; } +#pragma mark - Proposed functionality - Helpers + +#define kAuthenticationType @"authenticationType" +#define kBiometrics @"AuthenticationWithBiometrics" + +#define kAccessControlType @"accessControl" +#define kAccessControlUserPresence @"UserPresence" +#define kAccessControlTouchIDAny @"TouchIDAny" +#define kAccessControlTouchIDCurrentSet @"TouchIDCurrentSet" +#define kAccessControlDevicePasscode @"DevicePasscode" +#define kAccessControlTouchIDAnyOrDevicePasscode @"TouchIDAnyOrDevicePasscode" +#define kAccessControlTouchIDCurrentSetOrDevicePasscode @"TouchIDCurrentSetOrDevicePasscode" + + +#define kCustomPromptMessage @"customPrompt" + +LAPolicy authPolicy(NSDictionary *options) +{ + if (options && options[kAuthenticationType]) { + if ([ options[kAuthenticationType] isEqualToString:kBiometrics ]) { + return LAPolicyDeviceOwnerAuthenticationWithBiometrics; + } + } + return LAPolicyDeviceOwnerAuthentication; +} + +SecAccessControlCreateFlags secureAccessControl(NSDictionary *options) +{ + if (options && options[kAccessControlType]) { + if ([ options[kAccessControlType] isEqualToString: kAccessControlUserPresence ]) { + return kSecAccessControlUserPresence; + } + else if ([ options[kAccessControlType] isEqualToString: kAccessControlTouchIDAny ]) { + return kSecAccessControlTouchIDAny; + } + else if ([ options[kAccessControlType] isEqualToString: kAccessControlTouchIDCurrentSet ]) { + return kSecAccessControlTouchIDCurrentSet; + } + else if ([ options[kAccessControlType] isEqualToString: kAccessControlDevicePasscode ]) { + return kSecAccessControlDevicePasscode; + } + else if ([ options[kAccessControlType] isEqualToString: kAccessControlTouchIDAnyOrDevicePasscode ]) { + return kSecAccessControlTouchIDAny|kSecAccessControlOr|kSecAccessControlDevicePasscode; + } + else if ([ options[kAccessControlType] isEqualToString: kAccessControlTouchIDCurrentSetOrDevicePasscode ]) { + return kSecAccessControlTouchIDCurrentSet|kSecAccessControlOr|kSecAccessControlDevicePasscode; + } + } + return kSecAccessControlTouchIDCurrentSet|kSecAccessControlOr|kSecAccessControlDevicePasscode; +} + +//LAPolicyDeviceOwnerAuthenticationWithBiometrics | LAPolicyDeviceOwnerAuthentication + +#pragma mark - Proposed functionality - RCT_EXPORT_METHOD + +RCT_EXPORT_METHOD(canCheckAuthentication:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + LAPolicy policyToEvaluate = authPolicy(options); + + NSError *aerr = nil; + BOOL canBeProtected = [self canCheckAuthentication:policyToEvaluate error:&aerr ]; + + if (aerr || !canBeProtected) { + return rejectWithError(reject, aerr); + } else { + return resolve(@(YES)); + } +} + +- (BOOL) canCheckAuthentication:(LAPolicy)policyToEvaluate error:(NSError **)err { + return [[[ LAContext alloc] init ] canEvaluatePolicy:policyToEvaluate error:err ]; +} + +RCT_EXPORT_METHOD(setSecurePasswordForService:(NSString *)service withUsername:(NSString *)username withPassword:(NSString *)password withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + // Delete old entry for that key if Available + NSError *aerr = nil; + BOOL canAuthenticate = [ self canCheckAuthentication:LAPolicyDeviceOwnerAuthentication error:&aerr ]; + if (aerr || !canAuthenticate) { + return rejectWithError(reject, aerr); + } + + NSMutableDictionary *dict = @{ (__bridge NSString *)kSecClass : (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService: service, + (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue + }.mutableCopy; + + OSStatus osStatus = SecItemDelete((__bridge CFDictionaryRef) dict); + + // make new entry + dict = @{ (__bridge NSString *)kSecClass : (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService : service, + (__bridge NSString *)kSecAttrAccount : username + }.mutableCopy; + + CFErrorRef error = NULL; + SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, //kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + secureAccessControl(options), + &error); + + if (error) { + // ok: failed + return rejectWithError(reject, aerr); + } + + NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding]; + [ dict setObject:(__bridge id)sacRef forKey:kSecAttrAccessControl ]; + + [ dict setObject:passwordData forKey:kSecValueData ]; + + // Try to save to keychain + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef) dict, NULL); + + dispatch_async(dispatch_get_main_queue(), ^{ + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } else { + return resolve(@(YES)); + } + }); + + }); +} + +RCT_EXPORT_METHOD(getSecurePasswordForService:(NSString *)service withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *promptMessage = @"Authenticate to retrieve secret!"; + if (options && options[kCustomPromptMessage]) { + promptMessage = options[kCustomPromptMessage]; + } + + NSMutableDictionary *dict = @{ (__bridge NSString *)kSecClass : (__bridge id)(kSecClassGenericPassword), + (__bridge NSString *)kSecAttrService : service, + (__bridge NSString *)kSecReturnAttributes : (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecReturnData : (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecMatchLimit : (__bridge NSString *)kSecMatchLimitOne, + (__bridge NSString *)kSecUseOperationPrompt : promptMessage + }.mutableCopy; + + // Notify AppDelegate + dispatch_async(dispatch_get_main_queue(), ^{ + [ self notifyAuthenticationListener: YES ]; + }); + + // Look up password for service in the keychain + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __block NSDictionary* found = nil; + CFTypeRef foundTypeRef = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) dict, (CFTypeRef*)&foundTypeRef); + + dispatch_async(dispatch_get_main_queue(), ^{ + [ self notifyAuthenticationListener: NO ]; + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil]; + return rejectWithError(reject, error); + } + + found = (__bridge NSDictionary*)(foundTypeRef); + if (!found) { + return resolve(@(NO)); + } + + // Found + NSString* username = (NSString *) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString* password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + return resolve(@{ + @"service": service, + @"username": username, + @"password": password + }); + }); + + }); +} + +- (void) notifyAuthenticationListener:(BOOL)willPresent { + id appDelegate = [ UIApplication sharedApplication ].delegate; + + if ([ appDelegate conformsToProtocol:@protocol(RNKeychainAuthenticationListener) ]) { + ((id)appDelegate).willPromptForAuthentication = willPresent; + } +} + +#pragma mark - RNKeychain + RCT_EXPORT_METHOD(setGenericPasswordForOptions:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString *service = serviceValue(options); @@ -148,6 +343,11 @@ RCT_EXPORT_METHOD(getGenericPasswordForOptions:(NSDictionary *)options resolver: if (options && options[@"accessGroup"]) { [dict setObject:options[@"accessGroup"] forKey:kSecAttrAccessGroup]; } + + // secure compatibility with TouchId / Passcode secured stored items + // http://stackoverflow.com/questions/42339000/ksecuseauthenticationuiskip-how-to-use-it + // Silently skip any items that require user authentication. Only use this value with the SecItemCopyMatching function. + [ dict setObject:kSecUseAuthenticationUISkip forKey:kSecUseAuthenticationUI ]; // Look up server in the keychain NSDictionary* found = nil; @@ -237,6 +437,11 @@ RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString *)server withOptions if (options && options[@"accessGroup"]) { [dict setObject:options[@"accessGroup"] forKey:kSecAttrAccessGroup]; } + + // secure compatibility with TouchId / Passcode secured stored items + // http://stackoverflow.com/questions/42339000/ksecuseauthenticationuiskip-how-to-use-it + // Silently skip any items that require user authentication. Only use this value with the SecItemCopyMatching function. + [ dict setObject:kSecUseAuthenticationUISkip forKey:kSecUseAuthenticationUI ]; // Look up server in the keychain NSDictionary *found = nil; diff --git a/index.js b/index.js index 4908d1b..acb51f6 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,65 @@ type Options = { service?: string; }; +type SecAccessControl = + | 'UserPresence' + | 'TouchIDAny' + | 'TouchIDCurrentSet' + | 'DevicePasscode' + | 'TouchIDAnyOrDevicePasscode' + | 'TouchIDCurrentSetOrDevicePasscode' + + type LAPolicy = + | 'Authentication' + | 'AuthenticationWithBiometrics' + +type SecureOptions = { + customPrompt?: string; + authenticationType?: LAPolicy; + accessControl?: SecAccessControl; +}; + +/** + * Inquire if the type of local authentication policy (LAPolicy) is supported + * on this device with the device settings the user chose. + * @param {object} options LAPolicy option, iOS only + * @return {Promise} Resolves to `true` when successful + */ +export function canImplyAuthentication( + options?: SecureOptions +): Promise { + return RNKeychainManager.canCheckAuthentication(options); +} + +/** + * Saves the `username` and `password` combination for `service` securely - needs authentication to retrieve it. + * @param {string} service Associated service. + * @param {string} username Associated username or e-mail to be saved. + * @param {string} password Associated password to be saved. + * @param {object} options Keychain options, iOS only + * @return {Promise} Resolves to `true` when successful + */ +export function setSecurePassword( + service: string, + username: string, + password: string, + options?: SecureOptions +): Promise { + return RNKeychainManager.setSecurePasswordForService(service, username, password, options); +} + +/** + * Fetches login combination for `service` - demands for authentication if necessary. + * @param {string|object} serviceOrOptions Reverse domain name qualifier for the service, defaults to `bundleId` or an options object. + * @return {Promise} Resolves to `{ service, username, password }` when successful + */ +export function getSecurePassword( + service: string, + options?: SecureOptions +): Promise { + return RNKeychainManager.getSecurePasswordForService(service, options); +} + /** * Saves the `username` and `password` combination for `server`. * @param {string} server URL to server. diff --git a/typings/react-native-keychain.d.ts b/typings/react-native-keychain.d.ts index 28cf122..28e79ad 100644 --- a/typings/react-native-keychain.d.ts +++ b/typings/react-native-keychain.d.ts @@ -11,6 +11,28 @@ declare module 'react-native-keychain' { password: string; } + export interface SecureOptions { + customPrompt?: string; + authenticationType?: string; + accessControl?: string; + } + + function canImplyAuthentication( + options?: SecureOptions + ): Promise; + + function setSecurePassword( + service: string, + username: string, + password: string, + options?: SecureOptions + ): Promise; + + function getSecurePassword( + service: string, + options?: SecureOptions + ): Promise; + function setInternetCredentials( server: string, username: string,