TouchID 、FaceID 与KeyChain介绍
LAPublicDefines.h
首先是LAPublicDefines.h
,从名字上来看是公共宏定义类,里面包含了许多定义好的宏,这些宏会在LAContext.h
中用到。
#ifndef LocalAuthentication_LAPublicDefines_h
#define LocalAuthentication_LAPublicDefines_h
// Policies
#define kLAPolicyDeviceOwnerAuthenticationWithBiometrics 1
#define kLAPolicyDeviceOwnerAuthentication 2
// Options
#define kLAOptionUserFallback 1
#define kLAOptionAuthenticationReason 2
// Credential types
#define kLACredentialTypeApplicationPassword 0
// Error codes
#define kLAErrorAuthenticationFailed -1
#define kLAErrorUserCancel -2
#define kLAErrorUserFallback -3
#define kLAErrorSystemCancel -4
#define kLAErrorPasscodeNotSet -5
#define kLAErrorTouchIDNotAvailable -6
#define kLAErrorTouchIDNotEnrolled -7
#define kLAErrorTouchIDLockout -8
#define kLAErrorAppCancel -9
#define kLAErrorInvalidContext -10
#define kLAErrorNotInteractive -1004
#define kLAErrorBiometryNotAvailable kLAErrorTouchIDNotAvailable
#define kLAErrorBiometryNotEnrolled kLAErrorTouchIDNotEnrolled
#define kLAErrorBiometryLockout kLAErrorTouchIDLockout
// Error domain
#define kLAErrorDomain "com.apple.LocalAuthentication"
#endif
LAContext.h API
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LAPublicDefines.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, LAPolicy) {
/*指纹(人脸)识别。验证弹框有两个按钮,第一个是取消按钮,第二个按钮可以自定义标题名称(输入密码)。
只有在第一次指纹验证失败后才会出现第二个按钮,这种方式下的第二个按钮功能需要自己定义。前三次指纹验证失败,指纹验证框不再弹出。
再次重新进入验证,还有两次验证机会,如果还是验证失败,TOUCH ID 被锁住不再继续弹出指纹验证框。以后的每次验证都将会弹出设备密码输入框直至输入正确的设备密码才能重新使用指纹(人脸)识别*/
LAPolicyDeviceOwnerAuthenticationWithBiometrics NS_ENUM_AVAILABLE(10_12_2, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0) = kLAPolicyDeviceOwnerAuthenticationWithBiometrics,
/*指纹(人脸)识别或系统密码验证。
如果Touch ID (Face ID)可用,且已经录入指纹(人脸),则优先调用指纹(人脸)验证。其次是调用系统密码验证,如果没有开启设备密码,则不可以使用这种验证方式。
指纹(人脸)识别验证失败三次将弹出设备密码输入框,如果不进行密码输入,再次进来还可以有两次机会验证指纹(人脸),如果都失败则Touch ID(Face ID)被锁住,以后每次进来验证都是调用系统的设备密码直至输入正确的设备密码才能重新使用指纹(人脸)识别*/
LAPolicyDeviceOwnerAuthentication NS_ENUM_AVAILABLE(10_11, 9_0) = kLAPolicyDeviceOwnerAuthentication
} NS_ENUM_AVAILABLE(10_10, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0);
///复用设备解锁授权最大时间常量
extern const NSTimeInterval LATouchIDAuthenticationMaximumAllowableReuseDuration API_AVAILABLE(macos(10.12), ios(9.0)) API_UNAVAILABLE(watchos, tvos);
NS_CLASS_AVAILABLE(10_10, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0)
@interface LAContext : NSObject
///检查当前设备是否可用TouchID/FaceID
- (BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError * __autoreleasing *)error __attribute__((swift_error(none)));
///验证TouchID/FaceID方法
- (void)evaluatePolicy:(LAPolicy)policy
localizedReason:(NSString *)localizedReason
reply:(void(^)(BOOL success, NSError * __nullable error))reply;
///用来废止这个context
- (void)invalidate NS_AVAILABLE(10_11, 9_0);
typedef NS_ENUM(NSInteger, LACredentialType) {
LACredentialTypeApplicationPassword __TVOS_AVAILABLE(11.0) = kLACredentialTypeApplicationPassword,
} NS_ENUM_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0);
///设置解锁额外加密凭证
- (BOOL)setCredential:(nullable NSData *)credential
type:(LACredentialType)type NS_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(11.0);
///判断加密凭证是否设置成功
- (BOOL)isCredentialSet:(LACredentialType)type NS_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(11.0);
typedef NS_ENUM(NSInteger, LAAccessControlOperation) {
LAAccessControlOperationCreateItem, // 访问控制用于创建新的item
LAAccessControlOperationUseItem, // 访问控制用于使用已存在的item
LAAccessControlOperationCreateKey, // 访问控制用于创建新的密钥
LAAccessControlOperationUseKeySign, // 访问控制用于使用已存在的密钥签名
LAAccessControlOperationUseKeyDecrypt NS_ENUM_AVAILABLE(10_12, 10_0), // 访问控制用于使用已存在的密钥解密
LAAccessControlOperationUseKeyKeyExchange NS_ENUM_AVAILABLE(10_12, 10_0), // 访问控制用于密钥交换
} NS_ENUM_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0);
//更加灵活的安全访问控制
- (void)evaluateAccessControl:(SecAccessControlRef)accessControl
operation:(LAAccessControlOperation)operation
localizedReason:(NSString *)localizedReason
reply:(void(^)(BOOL success, NSError * __nullable error))reply
NS_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_UNAVAILABLE;
//设置验证TouchID时弹出Alert的输入密码按钮的标题
@property (nonatomic, nullable, copy) NSString *localizedFallbackTitle;
//设置验证TouchID时弹出Alert的取消按钮的标题
@property (nonatomic, nullable, copy) NSString *localizedCancelTitle NS_AVAILABLE(10_12, 10_0);
//最大指纹尝试错误次数 (有效iOS8.3 - iOS9.0)
@property (nonatomic, nullable) NSNumber *maxBiometryFailures NS_DEPRECATED_IOS(8_3, 9_0) __WATCHOS_UNAVAILABLE __TVOS_UNAVAILABLE;
@property (nonatomic, nullable, readonly) NSData *evaluatedPolicyDomainState NS_AVAILABLE(10_11, 9_0) __WATCHOS_UNAVAILABLE __TVOS_UNAVAILABLE;
/*该属性表示从设备解锁后多长时间内不需要重新验证的时间,该属性默认值为0,表示不采用设备解锁来授权应用。该属性允许最大的设置时长为5分钟(注:属性值为300,因为是以秒为单位,也可以使用*/
@property (nonatomic) NSTimeInterval touchIDAuthenticationAllowableReuseDuration NS_AVAILABLE(10_12, 9_0) __WATCHOS_UNAVAILABLE __TVOS_UNAVAILABLE;
//
@property (nonatomic, copy) NSString *localizedReason API_AVAILABLE(macos(10.13), ios(11.0)) API_UNAVAILABLE(watchos, tvos);
//允许在非交互模式下进行身份认证。这个是iOS 11新增功能,可以用来解决后台运行时授权处理
@property (nonatomic) BOOL interactionNotAllowed API_AVAILABLE(macos(10.13), ios(11.0)) API_UNAVAILABLE(watchos, tvos);
typedef NS_ENUM(NSInteger, LABiometryType) {
//表示设备不支持生物识别技术
LABiometryTypeNone API_AVAILABLE(macos(10.13.2), ios(11.2)),
LABiometryNone API_DEPRECATED_WITH_REPLACEMENT("LABiometryTypeNone", macos(10.13, 10.13.2), ios(11.0, 11.2)) = LABiometryTypeNone,
//表示当前设备支持指纹识别
LABiometryTypeTouchID,
//表示当前设备支持人脸识别
LABiometryTypeFaceID API_UNAVAILABLE(macos),
} API_AVAILABLE(macos(10.13.2), ios(11.0)) API_UNAVAILABLE(watchos, tvos);
@property (nonatomic, readonly) LABiometryType biometryType API_AVAILABLE(macos(10.13.2), ios(11.0)) API_UNAVAILABLE(watchos, tvos);
@end
NS_ASSUME_NONNULL_END
LAError.h API
包括一个枚举,里面写的是错误的类型,其实就是把上面的kLAError
宏写进这个枚举了
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LAPublicDefines.h>
typedef NS_ENUM(NSInteger, LAError)
{ ///身份验证失败
LAErrorAuthenticationFailed = kLAErrorAuthenticationFailed,
///用户在认证时点击取消
LAErrorUserCancel = kLAErrorUserCancel,
///用户点击输入密码取消指纹验证
LAErrorUserFallback = kLAErrorUserFallback,
///身份认证被系统取消(按下[Home键]或电源键)
LAErrorSystemCancel = kLAErrorSystemCancel,
///用户没有设置TouchID
LAErrorPasscodeNotSet = kLAErrorPasscodeNotSet,
///设备不支持TouchID
LAErrorTouchIDNotAvailable NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotAvailable") = kLAErrorTouchIDNotAvailable,
///用户没有设置手指指纹
LAErrorTouchIDNotEnrolled NS_ENUM_DEPRECATED(10_10, 10_13, 8_0, 11_0, "use LAErrorBiometryNotEnrolled") = kLAErrorTouchIDNotEnrolled,
///连续五次密码错误,FaceID被锁定
LAErrorTouchIDLockout NS_ENUM_DEPRECATED(10_11, 10_13, 9_0, 11_0, "use LAErrorBiometryLockout")
__WATCHOS_DEPRECATED(3.0, 4.0, "use LAErrorBiometryLockout") __TVOS_DEPRECATED(10.0, 11.0, "use LAErrorBiometryLockout") = kLAErrorTouchIDLockout,
///用户不能控制情况下App被挂起 / 在验证中被其他app中断
LAErrorAppCancel NS_ENUM_AVAILABLE(10_11, 9_0) = kLAErrorAppCancel,
///请求验证出错
LAErrorInvalidContext NS_ENUM_AVAILABLE(10_11, 9_0) = kLAErrorInvalidContext,
///
LAErrorBiometryNotAvailable NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryNotAvailable,
///
LAErrorBiometryNotEnrolled NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryNotEnrolled,
///
LAErrorBiometryLockout NS_ENUM_AVAILABLE(10_13, 11_0) __WATCHOS_AVAILABLE(4.0) __TVOS_AVAILABLE(11.0) = kLAErrorBiometryLockout,
///
LAErrorNotInteractive API_AVAILABLE(macos(10.10), ios(8.0), watchos(3.0), tvos(10.0)) = kLAErrorNotInteractive,
} NS_ENUM_AVAILABLE(10_10, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0);
/// LocalAuthentication error domain.
extern NSString *const __nonnull LAErrorDomain
API_AVAILABLE(macos(10.11), ios(8.3), watchos(3.0), tvos(10.0));
FaceID应用
LAContext *context = [[LAContext alloc] init];
context.touchIDAuthenticationAllowableReuseDuration = LATouchIDAuthenticationMaximumAllowableReuseDuration;
NSError *error = nil;
BOOL isSupportFaceID = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
if (isSupportFaceID) {
NSLog(@"FaceID_IS_AVAILABLE");
[context evaluatePolicy:LAPolicyDeviceOwnerAuthentication localizedReason:@"请验证指纹" reply:
^(BOOL success, NSError *authenticationError) {
if (success) {
NSLog(@"FaceID_unlock_Success");
}
else {
NSLog(@"FaceID_unlock_Failure");
}
}];
}
else {
NSLog(@"FaceID_IS_UNAVAILABLE");
}
复用设备解锁授权
如果你的应用想要在设备使用Touch ID / Face ID
解锁后一段时间内自己的App也不需要重新弹出解锁界面,就可以使用LAContext
的touchIDAuthenticationAllowableReuseDuration
属性,该属性表示从设备解锁后多长时间内不需要重新验证的时间,该属性默认值为0,表示不采用设备解锁来授权应用。该属性允许最大的设置时长为5分钟(注:属性值为300,因为是以秒为单位,也可以使用LATouchIDAuthenticationMaximumAllowableReuseDuration
常量值)
控制Keychain(钥匙串)访问权限
在iOS 9之前我们写入到Keychain
的数据,在设备解锁后就能够对keychain
中的数据进行访问,其实这样是不够安全的,特别是在你的App使用第三方SDK的情况底下,很有可能就会去窃取App中的keychain
数据。那么,在iOS 9之后,系统加入了一项新的功能,就是允许应用来控制Keychain的数据访问。它的实现过程是在写入数据时添加一个应用级别的访问密码,后续要访问这个数据除了要设备解锁,还需要有正确的密码才能够访问keychain
中的数据。而这功能正好是集成到了LocalAuthentication
这个框架中,下面我们来探索一下这个功能的用法。
实现keychain
的控制访问,依然还是要依靠LAContext
来实现,我们可以看到在iOS 9之后,这个类型新增了一个方法setCredential:type:
。这个方法的作用就是把访问Keychain
的密码设置到LAContext
对象中,配合LACredentialTypeApplicationPassword
这个类型就能够实现控制访问了。
下面先来看一下实现的示例代码:
OSStatus status = -1;
CFErrorRef error = NULL;
SecAccessControlRef sacr = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlock, kSecAccessControlApplicationPassword, &error);
if (sacr) {
NSString *dataValue = @"要写入的数据"; //要写入的数据
NSString *password = @"123456"; //访问密码
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
LAContext *context = [[LAContext alloc] init];
[context setCredential:passwordData type:LACredentialTypeApplicationPassword];
NSMutableDictionary *saveDictionary = [[NSMutableDictionary alloc] init];
[saveDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[saveDictionary setObject:@"testService" forKey:(__bridge id)kSecAttrService];
[saveDictionary setObject:@"testAccount" forKey:(__bridge id)kSecAttrAccount];
[saveDictionary setObject:[dataValue dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
[saveDictionary setObject:(__bridge id)sacr forKey:(__bridge id)kSecAttrAccessControl];
[saveDictionary setObject:context forKey:(__bridge id)kSecUseAuthenticationContext];
status = SecItemAdd((__bridge CFDictionaryRef)saveDictionary, nil);
if (status == errSecSuccess) {
NSLog(@"存储成功");
}
CFRelease(sacr);
}
OSStatus status = -1;
CFErrorRef error = NULL;
SecAccessControlRef sacr = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleAfterFirstUnlock,
kSecAccessControlApplicationPassword, &error);
if (sacr) {
NSString *password = @"123456"; //访问密码
LAContext *context = [[LAContext alloc] init];
NSData *appPassword = [password dataUsingEncoding:NSUTF8StringEncoding];
[context setCredential:appPassword type:LACredentialTypeApplicationPassword];
NSMutableDictionary *loadDictionary = [[NSMutableDictionary alloc] init];
[loadDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[loadDictionary setObject:@"testService" forKey:(__bridge id)kSecAttrService];
[loadDictionary setObject:@"testAccount" forKey:(__bridge id)kSecAttrAccount];
[loadDictionary setObject:@(YES) forKey:(__bridge id)kSecReturnData];
[loadDictionary setObject:(NSString *)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[loadDictionary setObject:(__bridge id)sacr forKey:(__bridge id)kSecAttrAccessControl];
[loadDictionary setObject:context forKey:(__bridge id)kSecUseAuthenticationContext];
CFDataRef data = NULL;
CFDataRef passwordData = (__bridge CFDataRef)(appPassword);
status = SecItemCopyMatching((CFDictionaryRef)loadDictionary, (CFTypeRef *)&data);
if (status == errSecSuccess) {
if ( 0 < CFDataGetLength(data)) {
CFStringRef string = CFStringCreateWithBytes(kCFAllocatorDefault, CFDataGetBytePtr(passwordData), CFDataGetLength(passwordData), kCFStringEncodingUTF8, FALSE);
if (string) {
NSString *dataString = [[NSString alloc] initWithData:(__bridge NSData *)data encoding:NSUTF8StringEncoding];
NSLog(@"data = %@", dataString);
CFRelease(string);
}
}
}
else {
NSLog(@"密码错误,无法访问");
}
if (data != NULL) {
CFRelease(data);
}
CFRelease(sacr);
}