跳过内容

iOS 本地身份验证

概述

在本地身份验证期间,应用程序会根据存储在设备本地的凭据对用户进行身份验证。 换句话说,用户通过提供有效的 PIN 码、密码或生物特征(如面部或指纹)“解锁”应用程序或某些内部功能层,并通过引用本地数据进行验证。 通常,这样做是为了让用户更方便地恢复与远程服务的现有会话,或者作为一种升级身份验证的方式来保护某些关键功能。

正如之前在“移动应用身份验证架构”一章中所述:测试人员应注意,始终应在远程端点或基于加密原语强制执行本地身份验证。 如果身份验证过程中没有返回任何数据,攻击者可以轻松绕过本地身份验证。

有多种方法可用于将本地身份验证集成到应用程序中。 本地身份验证框架提供了一组 API,供开发人员将身份验证对话框扩展到用户。 在连接到远程服务的上下文中,可以(并且推荐)利用钥匙串来实现本地身份验证。

iOS 上的指纹身份验证被称为Touch ID。 指纹 ID 传感器由SecureEnclave 安全协处理器操作,并且不会将指纹数据暴露给系统的任何其他部分。 除了 Touch ID 之外,Apple 还引入了Face ID:它允许基于面部识别进行身份验证。 两者都在应用程序级别使用类似的 API,但存储和检索数据的实际方法(例如,面部数据或指纹相关数据)是不同的。

开发人员有两种选择来整合 Touch ID/Face ID 身份验证

  • LocalAuthentication.framework是一个高级 API,可用于通过 Touch ID 对用户进行身份验证。 应用程序无法访问与注册指纹相关的任何数据,并且仅在身份验证成功时收到通知。
  • Security.framework是一个较低级别的 API,用于访问钥匙串服务。 如果您的应用程序需要使用生物特征身份验证来保护某些秘密数据,这是一个安全的选择,因为访问控制是在系统级别管理的,并且不容易被绕过。 Security.framework有一个 C API,但有几个开源包装器可用,使访问钥匙串就像访问 NSUserDefaults 一样简单。 Security.frameworkLocalAuthentication.framework的基础; Apple 建议尽可能默认使用更高级别的 API。

请注意,使用LocalAuthentication.frameworkSecurity.framework都将是一种可以被攻击者绕过的控制,因为它只返回一个布尔值,没有数据可以继续使用。 详情请参见Don't touch me that way, by David Lindner et al

本地身份验证框架

本地身份验证框架提供了从用户请求密码或 Touch ID 身份验证的工具。 开发人员可以通过利用LAContext类的函数evaluatePolicy来显示和使用身份验证提示。

两种可用的策略定义了可接受的身份验证形式

  • deviceOwnerAuthentication(Swift) 或LAPolicyDeviceOwnerAuthentication(Objective-C):如果可用,系统会提示用户执行 Touch ID 身份验证。 如果未激活 Touch ID,则改为请求设备密码。 如果未启用设备密码,则策略评估失败。

  • deviceOwnerAuthenticationWithBiometrics (Swift) 或LAPolicyDeviceOwnerAuthenticationWithBiometrics(Objective-C):身份验证仅限于生物识别技术,系统会提示用户使用 Touch ID。

evaluatePolicy函数返回一个布尔值,指示用户是否已成功通过身份验证。

Apple 开发人员网站提供了SwiftObjective-C的代码示例。 Swift 中的典型实现如下所示。

let context = LAContext()
var error: NSError?

guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
    // Could not evaluate policy; look at error and present an appropriate message to user
}

context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Please, pass authorization to enter this area") { success, evaluationError in
    guard success else {
        // User did not authenticate successfully, look at evaluationError and take appropriate action
    }

    // User authenticated successfully, take appropriate action
}

使用钥匙串服务进行本地身份验证

iOS 钥匙串 API 可以(并且应该)用于实现本地身份验证。 在此过程中,应用程序将秘密身份验证令牌或识别用户的另一段秘密数据存储在钥匙串中。 为了向远程服务进行身份验证,用户必须使用其密码或指纹解锁钥匙串以获取秘密数据。

钥匙串允许使用特殊的SecAccessControl属性保存项目,该属性将允许仅在用户通过 Touch ID 身份验证(或密码,如果属性参数允许此类回退)后才能从钥匙串访问该项目。

在以下示例中,我们将字符串“test_strong_password”保存到钥匙串。 只能在设置密码的情况下(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly参数)并且仅在当前注册的手指进行 Touch ID 身份验证后(SecAccessControlCreateFlags.biometryCurrentSet参数)才能在当前设备上访问该字符串

Swift

// 1. Create the AccessControl object that will represent authentication settings

var error: Unmanaged<CFError>?

guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                                                          SecAccessControlCreateFlags.biometryCurrentSet,
                                                          &error) else {
    // failed to create AccessControl object

    return
}

// 2. Create the keychain services query. Pay attention that kSecAttrAccessControl is mutually exclusive with kSecAttrAccessible attribute

var query: [String: Any] = [:]

query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrLabel as String] = "com.me.myapp.password" as CFString
query[kSecAttrAccount as String] = "OWASP Account" as CFString
query[kSecValueData as String] = "test_strong_password".data(using: .utf8)! as CFData
query[kSecAttrAccessControl as String] = accessControl

// 3. Save the item

let status = SecItemAdd(query as CFDictionary, nil)

if status == noErr {
    // successfully saved
} else {
    // error while saving
}

// 4. Now we can request the saved item from the keychain. Keychain services will present the authentication dialog to the user and return data or nil depending on whether a suitable fingerprint was provided or not.

// 5. Create the query
var query = [String: Any]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecAttrAccount as String] = "My Name" as CFString
query[kSecAttrLabel as String] = "com.me.myapp.password" as CFString
query[kSecUseOperationPrompt as String] = "Please, pass authorisation to enter this area" as CFString

// 6. Get the item
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
    SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}

if status == noErr {
    let password = String(data: queryResult as! Data, encoding: .utf8)!
    // successfully received password
} else {
    // authorization not passed
}

Objective-C

// 1. Create the AccessControl object that will represent authentication settings
CFErrorRef *err = nil;

SecAccessControlRef sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    kSecAccessControlUserPresence,
    err);

// 2. Create the keychain services query. Pay attention that kSecAttrAccessControl is mutually exclusive with kSecAttrAccessible attribute
NSDictionary* query = @{
    (_ _bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrLabel: @"com.me.myapp.password",
    (__bridge id)kSecAttrAccount: @"OWASP Account",
    (__bridge id)kSecValueData: [@"test_strong_password" dataUsingEncoding:NSUTF8StringEncoding],
    (__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacRef
};

// 3. Save the item
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

if (status == noErr) {
    // successfully saved
} else {
    // error while saving
}

// 4. Now we can request the saved item from the keychain. Keychain services will present the authentication dialog to the user and return data or nil depending on whether a suitable fingerprint was provided or not.

// 5. Create the query
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecReturnData: @YES,
    (__bridge id)kSecAttrAccount: @"My Name1",
    (__bridge id)kSecAttrLabel: @"com.me.myapp.password",
    (__bridge id)kSecUseOperationPrompt: @"Please, pass authorisation to enter this area" };

// 6. Get the item
CFTypeRef queryResult = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &queryResult);

if (status == noErr){
    NSData* resultData = ( __bridge_transfer NSData* )queryResult;
    NSString* password = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", password);
} else {
    NSLog(@"Something went wrong");
}

关于钥匙串中密钥临时性的说明

与 macOS 和 Android 不同,iOS 不支持钥匙串中项目可访问性的临时性:当进入钥匙串时没有额外的安全检查时(例如,设置了kSecAccessControlUserPresence或类似选项),一旦设备解锁,密钥就可以访问。