Protect the data stored in keychain by TouchId or Passcode (#65)

* First draft of implementing secured storage support (TouchId or Passcode)

* minor improvements

* improving the implementation; Support for AppDelegate-notification

* minor changes and improvements

* provding requested constant as usability feature; added documentation to canImplyAuthentication

* updating .d.ts-file

* when fetching stored items using the traditional modality (not TouchId or Passcode protected) ignore any items that need authentication.
This commit is contained in:
Steff 2018-02-25 15:55:17 +01:00 committed by Joel Arvidsson
parent 55681fa8e8
commit 172368f2fd
8 changed files with 358 additions and 14 deletions

View File

@ -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!' });

View File

@ -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",

View File

@ -11,9 +11,26 @@
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <RNKeychain/RNKeychainAuthenticationListener.h>
@interface AppDelegate() <RNKeychainAuthenticationListener>
@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;

View File

@ -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 = "<group>"; };
5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNKeychainManager.m; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
wrapsLines = 0;
@ -88,21 +91,32 @@
children = (
5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */,
5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */,
B22856161E950BB300CCF753 /* RNKeychainAuthenticationListener.h */,
);
path = RNKeychainManager;
sourceTree = "<group>";
};
647898681F38BF9C00DA1C12 /* Frameworks */ = {
B2AF06351E97BF10006435CD /* Frameworks */ = {
isa = PBXGroup;
children = (
5D8236901B0CE3D6005A9EF3 /* Security.framework */,
647898691F38BF9D00DA1C12 /* Security.framework */,
B2AF06361E97BF10006435CD /* LocalAuthentication.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* 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 = (
);

View File

@ -0,0 +1,22 @@
//
// TouchIdPromptListener.h
// RNKeychain
//
// Created by Steffen Blümm on 05/04/17.
// Copyright © 2017 Joel Arvidsson. All rights reserved.
//
#import <Foundation/Foundation.h>
/**
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 <NSObject>
@property (nonatomic, assign) BOOL willPromptForAuthentication;
@end

View File

@ -12,6 +12,11 @@
#import <React/RCTBridge.h>
#import <React/RCTUtils.h>
#import <LocalAuthentication/LAContext.h>
#import <UIKit/UIKit.h>
#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<UIApplicationDelegate> appDelegate = [ UIApplication sharedApplication ].delegate;
if ([ appDelegate conformsToProtocol:@protocol(RNKeychainAuthenticationListener) ]) {
((id<RNKeychainAuthenticationListener>)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;

View File

@ -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.

View File

@ -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<boolean>;
function setSecurePassword(
service: string,
username: string,
password: string,
options?: SecureOptions
): Promise<boolean>;
function getSecurePassword(
service: string,
options?: SecureOptions
): Promise<boolean | {service: string, username: string, password: string}>;
function setInternetCredentials(
server: string,
username: string,