Remove *PasswordWithAuthentication and replace with accessControl option

This commit is contained in:
Joel Arvidsson 2018-02-26 12:31:06 +01:00
parent 37ea15ae5e
commit add90c5769
8 changed files with 105 additions and 299 deletions

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { import {
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
SegmentedControlIOS,
StyleSheet, StyleSheet,
Text, Text,
TextInput, TextInput,
@ -11,12 +12,16 @@ import {
import * as Keychain from 'react-native-keychain'; import * as Keychain from 'react-native-keychain';
const ACCESS_CONTROL_OPTIONS = ['None', 'Passcode', 'Password'];
const ACCESS_CONTROL_MAP = [null, Keychain.ACCESS_CONTROL.DEVICE_PASSCODE, Keychain.ACCESS_CONTROL.APPLICATION_PASSWORD, Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET]
export default class KeychainExample extends Component { export default class KeychainExample extends Component {
state = { state = {
username: '', username: '',
password: '', password: '',
status: '', status: '',
biometryType: null, biometryType: null,
accessControl: null,
}; };
componentDidMount() { componentDidMount() {
@ -25,24 +30,13 @@ export default class KeychainExample extends Component {
}); });
} }
async save() { async save(accessControl) {
try { try {
if (this.state.biometryType) { await Keychain.setGenericPassword(
await Keychain.setPasswordWithAuthentication( this.state.username,
this.state.username, this.state.password,
this.state.password, { accessControl: this.state.accessControl }
{ );
accessControl:
Keychain.ACCESS_CONTROL.TOUCH_ID_ANY_OR_DEVICE_PASSCODE,
authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS,
}
);
} else {
await Keychain.setGenericPassword(
this.state.username,
this.state.password
);
}
this.setState({ status: 'Credentials saved!' }); this.setState({ status: 'Credentials saved!' });
} catch (err) { } catch (err) {
this.setState({ status: 'Could not save credentials, ' + err }); this.setState({ status: 'Could not save credentials, ' + err });
@ -51,13 +45,7 @@ export default class KeychainExample extends Component {
async load() { async load() {
try { try {
const credentials = await (this.state.biometryType const credentials = await Keychain.getGenericPassword();
? Keychain.getPasswordWithAuthentication({
accessControl:
Keychain.ACCESS_CONTROL.TOUCH_ID_ANY_OR_DEVICE_PASSCODE,
authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS,
})
: Keychain.getGenericPassword());
if (credentials) { if (credentials) {
this.setState({ ...credentials, status: 'Credentials loaded!' }); this.setState({ ...credentials, status: 'Credentials loaded!' });
} else { } else {
@ -113,14 +101,24 @@ export default class KeychainExample extends Component {
underlineColorAndroid="transparent" underlineColorAndroid="transparent"
/> />
</View> </View>
{Platform.OS === 'ios' && (
<View style={styles.field}>
<Text style={styles.label}>Access Control</Text>
<SegmentedControlIOS
selectedIndex={0}
values={this.state.biometryType ? [...ACCESS_CONTROL_OPTIONS, this.state.biometryType] : ACCESS_CONTROL_OPTIONS}
onChange={({ nativeEvent }) => {
this.setState({
accessControl: ACCESS_CONTROL_MAP[nativeEvent.selectedSegmentIndex],
});
}}
/>
</View>
)}
{!!this.state.status && ( {!!this.state.status && (
<Text style={styles.status}>{this.state.status}</Text> <Text style={styles.status}>{this.state.status}</Text>
)} )}
{!!this.state.biometryType && (
<Text style={styles.biometryType}>
Supported biometry: {this.state.biometryType}
</Text>
)}
<View style={styles.buttons}> <View style={styles.buttons}>
<TouchableHighlight <TouchableHighlight
onPress={() => this.save()} onPress={() => this.save()}
@ -156,12 +154,11 @@ export default class KeychainExample extends Component {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: '#F5FCFF', backgroundColor: '#F5FCFF',
}, },
content: { content: {
width: 250, marginHorizontal: 20,
}, },
title: { title: {
fontSize: 28, fontSize: 28,

View File

@ -11,26 +11,9 @@
#import <React/RCTBundleURLProvider.h> #import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h> #import <React/RCTRootView.h>
#import <RNKeychain/RNKeychainAuthenticationListener.h>
@interface AppDelegate() <RNKeychainAuthenticationListener>
@end
@implementation AppDelegate @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 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ {
NSURL *jsCodeLocation; NSURL *jsCodeLocation;

View File

@ -127,12 +127,12 @@ Keychain
| Key | Applies to | Description | Default | | Key | Applies to | Description | Default |
|---|---|---|---| |---|---|---|---|
|**`accessControl`**|`PasswordWithAuthentication`|This dictates how a keychain item may be used, see possible values in `Keychain.ACCESS_CONTROL`. |*`Keychain.ACCESS_CONTROL.TOUCH_ID_CURRENT_SET_OR_DEVICE_PASSCODE`*| |**`accessControl`**|`setGenericPassword`, `setInternetCredentials`|This dictates how a keychain item may be used, see possible values in `Keychain.ACCESS_CONTROL`. |*None*|
|**`accessible`**|`GenericPassword`, `InternetCredentials`|This dictates when a keychain item is accessible, see possible values in `Keychain.ACCESSIBLE`. |*`Keychain.ACCESSIBLE.WHEN_UNLOCKED`*| |**`accessible`**|`setGenericPassword`, `setInternetCredentials`|This dictates when a keychain item is accessible, see possible values in `Keychain.ACCESSIBLE`. |*`Keychain.ACCESSIBLE.WHEN_UNLOCKED`*|
|**`accessGroup`**|`GenericPassword`, `InternetCredentials`, `PasswordWithAuthentication`|In which App Group to share the keychain. Requires additional setup with entitlements. |*None*| |**`accessGroup`**|`setGenericPassword`, `setInternetCredentials`|In which App Group to share the keychain. Requires additional setup with entitlements. |*None*|
|**`authenticationPrompt`**|`PasswordWithAuthentication`|What to prompt the user when unlocking the keychain with biometry or device password. |`Authenticate to retrieve secret!`| |**`authenticationPrompt`**|`getGenericPassword`, `getInternetCredentials`|What to prompt the user when unlocking the keychain with biometry or device password. |`Authenticate to retrieve secret`|
|**`authenticationType`**|`canImplyAuthentication`|Policies specifying which forms of authentication are acceptable. |`Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS`| |**`authenticationType`**|`canImplyAuthentication`|Policies specifying which forms of authentication are acceptable. |`Keychain.AUTHENTICATION_TYPE.DEVICE_PASSCODE_OR_BIOMETRICS`|
|**`service`**|`GenericPassword`, `PasswordWithAuthentication`|Reverse domain name qualifier for the service associated with password. |*App bundle ID*| |**`service`**|`setGenericPassword`, `getGenericPassword`|Reverse domain name qualifier for the service associated with password. |*App bundle ID*|
#### `Keychain.ACCESS_CONTROL` enum #### `Keychain.ACCESS_CONTROL` enum

View File

@ -10,11 +10,7 @@
5D82368F1B0CE3CB005A9EF3 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 5D82368F1B0CE3CB005A9EF3 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; };
5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D8236901B0CE3D6005A9EF3 /* Security.framework */; }; 5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D8236901B0CE3D6005A9EF3 /* Security.framework */; };
5DE632D52043423E004F9598 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632D42043423E004F9598 /* LocalAuthentication.framework */; }; 5DE632D52043423E004F9598 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632D42043423E004F9598 /* LocalAuthentication.framework */; };
5DE632D720434276004F9598 /* RNKeychainAuthenticationListener.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */; };
5DE632D920434291004F9598 /* RNKeychainAuthenticationListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */; };
5DE632DB204342AE004F9598 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632DA204342AE004F9598 /* Security.framework */; }; 5DE632DB204342AE004F9598 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632DA204342AE004F9598 /* Security.framework */; };
5DE632DC204342B5004F9598 /* RNKeychainAuthenticationListener.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */; };
5DE632DE204342C3004F9598 /* RNKeychainAuthenticationListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */; };
6478986B1F38BFA100DA1C12 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 6478986B1F38BFA100DA1C12 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -25,7 +21,6 @@
dstPath = "include/$(PRODUCT_NAME)"; dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
5DE632D720434276004F9598 /* RNKeychainAuthenticationListener.h in Copy Headers */,
); );
name = "Copy Headers"; name = "Copy Headers";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -36,7 +31,6 @@
dstPath = "include/$(PRODUCT_NAME)"; dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
5DE632DC204342B5004F9598 /* RNKeychainAuthenticationListener.h in Copy Headers */,
); );
name = "Copy Headers"; name = "Copy Headers";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -46,10 +40,9 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
5D82366F1B0CE05B005A9EF3 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; }; 5D82366F1B0CE05B005A9EF3 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; };
5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNKeychainManager.h; sourceTree = "<group>"; }; 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>"; }; 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNKeychainManager.m; sourceTree = "<group>"; tabWidth = 2; };
5D8236901B0CE3D6005A9EF3 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 5D8236901B0CE3D6005A9EF3 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
5DE632D42043423E004F9598 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; 5DE632D42043423E004F9598 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; };
5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNKeychainAuthenticationListener.h; sourceTree = "<group>"; };
5DE632DA204342AE004F9598 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.2.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 5DE632DA204342AE004F9598 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.2.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; };
6478985F1F38BF9100DA1C12 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; }; 6478985F1F38BF9100DA1C12 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -97,7 +90,6 @@
5D82368A1B0CE2A6005A9EF3 /* RNKeychainManager */ = { 5D82368A1B0CE2A6005A9EF3 /* RNKeychainManager */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5DE632D62043426A004F9598 /* RNKeychainAuthenticationListener.h */,
5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */, 5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */,
5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */, 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */,
); );
@ -121,7 +113,6 @@
isa = PBXHeadersBuildPhase; isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5DE632D920434291004F9598 /* RNKeychainAuthenticationListener.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -129,7 +120,6 @@
isa = PBXHeadersBuildPhase; isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5DE632DE204342C3004F9598 /* RNKeychainAuthenticationListener.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -1,22 +0,0 @@
//
// 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

@ -15,8 +15,6 @@
#import <LocalAuthentication/LAContext.h> #import <LocalAuthentication/LAContext.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "RNKeychainAuthenticationListener.h"
@implementation RNKeychainManager @implementation RNKeychainManager
@synthesize bridge = _bridge; @synthesize bridge = _bridge;
@ -148,43 +146,69 @@ LAPolicy authPolicy(NSDictionary *options)
return LAPolicyDeviceOwnerAuthentication; return LAPolicyDeviceOwnerAuthentication;
} }
SecAccessControlCreateFlags secureAccessControl(NSDictionary *options) SecAccessControlCreateFlags accessControlValue(NSDictionary *options)
{ {
if (options && options[kAccessControlType]) { if (options && options[kAccessControlType] && [options[kAccessControlType] isKindOfClass:[NSString class]]) {
if ([ options[kAccessControlType] isEqualToString: kAccessControlUserPresence ]) { if ([options[kAccessControlType] isEqualToString: kAccessControlUserPresence]) {
return kSecAccessControlUserPresence; return kSecAccessControlUserPresence;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlBiometryAny ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlBiometryAny]) {
return kSecAccessControlTouchIDAny; return kSecAccessControlTouchIDAny;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlBiometryCurrentSet ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlBiometryCurrentSet]) {
return kSecAccessControlTouchIDCurrentSet; return kSecAccessControlTouchIDCurrentSet;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlDevicePasscode ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlDevicePasscode]) {
return kSecAccessControlDevicePasscode; return kSecAccessControlDevicePasscode;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlBiometryAnyOrDevicePasscode ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlBiometryAnyOrDevicePasscode]) {
return kSecAccessControlTouchIDAny|kSecAccessControlOr|kSecAccessControlDevicePasscode; return kSecAccessControlTouchIDAny|kSecAccessControlOr|kSecAccessControlDevicePasscode;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlBiometryCurrentSetOrDevicePasscode ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlBiometryCurrentSetOrDevicePasscode]) {
return kSecAccessControlTouchIDCurrentSet|kSecAccessControlOr|kSecAccessControlDevicePasscode; return kSecAccessControlTouchIDCurrentSet|kSecAccessControlOr|kSecAccessControlDevicePasscode;
} }
else if ([ options[kAccessControlType] isEqualToString: kAccessControlApplicationPassword ]) { else if ([options[kAccessControlType] isEqualToString: kAccessControlApplicationPassword]) {
return kSecAccessControlApplicationPassword; return kSecAccessControlApplicationPassword;
} }
} }
return kSecAccessControlTouchIDCurrentSet|kSecAccessControlOr|kSecAccessControlDevicePasscode; return 0;
} }
//LAPolicyDeviceOwnerAuthenticationWithBiometrics | LAPolicyDeviceOwnerAuthentication - (void)insertKeychainEntry:(NSDictionary *)attributes withOptions:(NSDictionary * __nullable)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject
- (void)insertKeychainEntry:(NSDictionary *)attributes withAccessGroup:(NSString * __nullable)accessGroup resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject
{ {
if (accessGroup != nil) { NSString *accessGroup = accessGroupValue(options);
NSMutableDictionary *mAttributes = attributes.mutableCopy; CFStringRef accessible = accessibleValue(options);
mAttributes[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup; SecAccessControlCreateFlags accessControl = accessControlValue(options);
attributes = [NSDictionary dictionaryWithDictionary:mAttributes];
NSMutableDictionary *mAttributes = attributes.mutableCopy;
if (accessControl) {
NSError *aerr = nil;
BOOL canAuthenticate = [[LAContext new] canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&aerr];
if (aerr || !canAuthenticate) {
return rejectWithError(reject, aerr);
}
CFErrorRef error = NULL;
SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
accessible,
accessControl,
&error);
if (error) {
return rejectWithError(reject, aerr);
}
mAttributes[(__bridge NSString *)kSecAttrAccessControl] = (__bridge id)sacRef;
} else {
mAttributes[(__bridge NSString *)kSecAttrAccessible] = (__bridge id)accessible;
} }
if (accessGroup != nil) {
mAttributes[(__bridge NSString *)kSecAttrAccessGroup] = accessGroup;
}
attributes = [NSDictionary dictionaryWithDictionary:mAttributes];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef) attributes, NULL); OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef) attributes, NULL);
@ -223,7 +247,7 @@ SecAccessControlCreateFlags secureAccessControl(NSDictionary *options)
return SecItemDelete((__bridge CFDictionaryRef) query); return SecItemDelete((__bridge CFDictionaryRef) query);
} }
#pragma mark - Proposed functionality - RCT_EXPORT_METHOD #pragma mark - RNKeychain
#if TARGET_OS_IOS #if TARGET_OS_IOS
RCT_EXPORT_METHOD(canCheckAuthentication:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXPORT_METHOD(canCheckAuthentication:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@ -261,129 +285,28 @@ RCT_EXPORT_METHOD(getSupportedBiometryType:(RCTPromiseResolveBlock)resolve rejec
} }
#endif #endif
RCT_EXPORT_METHOD(setPasswordWithAuthentication:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *service = serviceValue(options);
NSError *aerr = nil;
BOOL canAuthenticate = [[LAContext new] canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&aerr];
if (aerr || !canAuthenticate) {
return rejectWithError(reject, aerr);
}
[self deletePasswordsForService:service];
CFErrorRef error = NULL;
SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, //kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
secureAccessControl(options),
&error);
if (error) {
// ok: failed
return rejectWithError(reject, aerr);
}
NSDictionary *attributes = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
(__bridge NSString *)kSecAttrService: service,
(__bridge NSString *)kSecAttrAccount: username,
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding],
(__bridge NSString *)kSecAttrAccessControl: (__bridge id)sacRef
};
[self insertKeychainEntry:attributes withAccessGroup:accessGroupValue(options) resolver:resolve rejecter:reject];
}
RCT_EXPORT_METHOD(getPasswordWithAuthentication:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *service = serviceValue(options);
NSString *promptMessage = @"Authenticate to retrieve secret!";
if (options && options[kAuthenticationPromptMessage]) {
promptMessage = options[kAuthenticationPromptMessage];
}
NSDictionary *query = @{
(__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
};
// 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) query, (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) RCT_EXPORT_METHOD(setGenericPasswordForOptions:(NSDictionary *)options withUsername:(NSString *)username withPassword:(NSString *)password resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{ {
NSString *service = serviceValue(options); NSString *service = serviceValue(options);
NSDictionary *attributes = attributes = @{
[self deletePasswordsForService:service];
// Create dictionary of parameters to add
NSDictionary *attributes = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
(__bridge NSString *)kSecAttrService: service, (__bridge NSString *)kSecAttrService: service,
(__bridge NSString *)kSecAttrAccount: username, (__bridge NSString *)kSecAttrAccount: username,
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding], (__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding]
(__bridge NSString *)kSecAttrAccessible: (__bridge id)accessibleValue(options)
}; };
[self insertKeychainEntry:attributes withAccessGroup:accessGroupValue(options) resolver:resolve rejecter:reject]; [self deletePasswordsForService:service];
[self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject];
} }
RCT_EXPORT_METHOD(getGenericPasswordForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXPORT_METHOD(getGenericPasswordForOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{ {
NSString *service = serviceValue(options); NSString *service = serviceValue(options);
NSString *authenticationPrompt = @"Authenticate to retrieve secret";
// secure compatibility with TouchId / Passcode secured stored items if (options && options[kAuthenticationPromptMessage]) {
// http://stackoverflow.com/questions/42339000/ksecuseauthenticationuiskip-how-to-use-it authenticationPrompt = options[kAuthenticationPromptMessage];
// Silently skip any items that require user authentication. Only use this value with the SecItemCopyMatching function. }
NSDictionary *query = @{ NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword), (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),
@ -391,7 +314,7 @@ RCT_EXPORT_METHOD(getGenericPasswordForOptions:(NSDictionary *)options resolver:
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne, (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne,
(__bridge NSString *)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUISkip (__bridge NSString *)kSecUseOperationPrompt: authenticationPrompt
}; };
// Look up service in the keychain // Look up service in the keychain
@ -443,26 +366,20 @@ RCT_EXPORT_METHOD(setInternetCredentialsForServer:(NSString *)server withUsernam
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword),
(__bridge NSString *)kSecAttrServer: server, (__bridge NSString *)kSecAttrServer: server,
(__bridge NSString *)kSecAttrAccount: username, (__bridge NSString *)kSecAttrAccount: username,
(__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding], (__bridge NSString *)kSecValueData: [password dataUsingEncoding:NSUTF8StringEncoding]
(__bridge NSString *)kSecAttrAccessible: (__bridge id)accessibleValue(options)
}; };
[self insertKeychainEntry:attributes withAccessGroup:accessGroupValue(options) resolver:resolve rejecter:reject]; [self insertKeychainEntry:attributes withOptions:options resolver:resolve rejecter:reject];
} }
RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXPORT_METHOD(getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{ {
// 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.
NSDictionary *query = @{ NSDictionary *query = @{
(__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword),
(__bridge NSString *)kSecAttrServer: server, (__bridge NSString *)kSecAttrServer: server,
(__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue,
(__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne, (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne
(__bridge NSString *)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUISkip
}; };
// Look up server in the keychain // Look up server in the keychain
@ -502,7 +419,6 @@ RCT_EXPORT_METHOD(resetInternetCredentialsForServer:(NSString *)server withOptio
} }
return resolve(@(YES)); return resolve(@(YES));
} }
#if TARGET_OS_IOS #if TARGET_OS_IOS

View File

@ -41,12 +41,6 @@ type SecAccessible =
| 'AccessibleAfterFirstUnlockThisDeviceOnly' | 'AccessibleAfterFirstUnlockThisDeviceOnly'
| 'AccessibleAlwaysThisDeviceOnly'; | 'AccessibleAlwaysThisDeviceOnly';
type Options = {
accessGroup?: string,
accessible?: SecAccessible,
service?: string,
};
type SecAccessControl = type SecAccessControl =
| 'UserPresence' | 'UserPresence'
| 'BiometryAny' | 'BiometryAny'
@ -58,9 +52,10 @@ type SecAccessControl =
type LAPolicy = 'Authentication' | 'AuthenticationWithBiometrics'; type LAPolicy = 'Authentication' | 'AuthenticationWithBiometrics';
type SecureOptions = { type Options = {
accessControl?: SecAccessControl, accessControl?: SecAccessControl,
accessGroup?: string, accessGroup?: string,
accessible?: SecAccessible,
authenticationPrompt?: string, authenticationPrompt?: string,
authenticationType?: LAPolicy, authenticationType?: LAPolicy,
service?: string, service?: string,
@ -72,7 +67,7 @@ type SecureOptions = {
* @param {object} options LAPolicy option, iOS only * @param {object} options LAPolicy option, iOS only
* @return {Promise} Resolves to `true` when supported, otherwise `false` * @return {Promise} Resolves to `true` when supported, otherwise `false`
*/ */
export function canImplyAuthentication(options?: SecureOptions): Promise { export function canImplyAuthentication(options?: Options): Promise {
if (RNKeychainManager.canCheckAuthentication) { if (RNKeychainManager.canCheckAuthentication) {
return Promise.reject( return Promise.reject(
new Error(`canImplyAuthentication() is not supported on this platform`) new Error(`canImplyAuthentication() is not supported on this platform`)
@ -93,50 +88,6 @@ export function getSupportedBiometryType(): Promise {
return RNKeychainManager.getSupportedBiometryType(); return RNKeychainManager.getSupportedBiometryType();
} }
/**
* Saves the `username` and `password` combination securely - needs authentication to retrieve it.
* @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 setPasswordWithAuthentication(
username: string,
password: string,
options?: SecureOptions
): Promise {
if (Platform.OS !== 'ios') {
return Promise.reject(
new Error(
`setPasswordWithAuthentication() is not supported on ${Platform.OS} yet`
)
);
}
return RNKeychainManager.setPasswordWithAuthentication(
options,
username,
password
);
}
/**
* 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 getPasswordWithAuthentication(
options?: SecureOptions
): Promise {
if (Platform.OS !== 'ios') {
return Promise.reject(
new Error(
`getPasswordWithAuthentication() is not supported on ${Platform.OS} yet`
)
);
}
return RNKeychainManager.getPasswordWithAuthentication(options);
}
/** /**
* Saves the `username` and `password` combination for `server`. * Saves the `username` and `password` combination for `server`.
* @param {string} server URL to server. * @param {string} server URL to server.
@ -185,7 +136,7 @@ export function resetInternetCredentials(
return RNKeychainManager.resetInternetCredentialsForServer(server, options); return RNKeychainManager.resetInternetCredentialsForServer(server, options);
} }
function getOptionsArgument(serviceOrOptions?: string | KeychainOptions) { function getOptionsArgument(serviceOrOptions?: string | Options) {
if (Platform.OS !== 'ios') { if (Platform.OS !== 'ios') {
return typeof serviceOrOptions === 'object' return typeof serviceOrOptions === 'object'
? serviceOrOptions.service ? serviceOrOptions.service
@ -206,7 +157,7 @@ function getOptionsArgument(serviceOrOptions?: string | KeychainOptions) {
export function setGenericPassword( export function setGenericPassword(
username: string, username: string,
password: string, password: string,
serviceOrOptions?: string | KeychainOptions serviceOrOptions?: string | Options
): Promise { ): Promise {
return RNKeychainManager.setGenericPasswordForOptions( return RNKeychainManager.setGenericPasswordForOptions(
getOptionsArgument(serviceOrOptions), getOptionsArgument(serviceOrOptions),
@ -221,7 +172,7 @@ export function setGenericPassword(
* @return {Promise} Resolves to `{ service, username, password }` when successful * @return {Promise} Resolves to `{ service, username, password }` when successful
*/ */
export function getGenericPassword( export function getGenericPassword(
serviceOrOptions?: string | KeychainOptions serviceOrOptions?: string | Options
): Promise { ): Promise {
return RNKeychainManager.getGenericPasswordForOptions( return RNKeychainManager.getGenericPasswordForOptions(
getOptionsArgument(serviceOrOptions) getOptionsArgument(serviceOrOptions)
@ -234,7 +185,7 @@ export function getGenericPassword(
* @return {Promise} Resolves to `true` when successful * @return {Promise} Resolves to `true` when successful
*/ */
export function resetGenericPassword( export function resetGenericPassword(
serviceOrOptions?: string | KeychainOptions serviceOrOptions?: string | Options
): Promise { ): Promise {
return RNKeychainManager.resetGenericPasswordForOptions( return RNKeychainManager.resetGenericPasswordForOptions(
getOptionsArgument(serviceOrOptions) getOptionsArgument(serviceOrOptions)

View File

@ -11,7 +11,7 @@ declare module 'react-native-keychain' {
password: string; password: string;
} }
export interface SecureOptions { export interface Options {
accessControl?: string; accessControl?: string;
accessGroup?: string; accessGroup?: string;
authenticationPrompt?: string; authenticationPrompt?: string;
@ -20,26 +20,17 @@ declare module 'react-native-keychain' {
} }
function canImplyAuthentication( function canImplyAuthentication(
options?: SecureOptions options?: Options
): Promise<boolean>; ): Promise<boolean>;
function getSupportedBiometryType( function getSupportedBiometryType(
): Promise<string>; ): Promise<string>;
function setSecurePassword(
username: string,
password: string,
options?: SecureOptions
): Promise<boolean>;
function getSecurePassword(
options?: SecureOptions
): Promise<boolean | {service: string, username: string, password: string}>;
function setInternetCredentials( function setInternetCredentials(
server: string, server: string,
username: string, username: string,
password: string password: string,
options?: Options
): Promise<void>; ): Promise<void>;
function getInternetCredentials( function getInternetCredentials(
@ -53,15 +44,15 @@ declare module 'react-native-keychain' {
function setGenericPassword( function setGenericPassword(
username: string, username: string,
password: string, password: string,
service?: string options?: Options
): Promise<boolean>; ): Promise<boolean>;
function getGenericPassword( function getGenericPassword(
service?: string options?: Options
): Promise<boolean | {service: string, username: string, password: string}>; ): Promise<boolean | {service: string, username: string, password: string}>;
function resetGenericPassword( function resetGenericPassword(
service?: string options?: Options
): Promise<boolean> ): Promise<boolean>
function requestSharedWebCredentials ( function requestSharedWebCredentials (