保护静止的iOS数据:钥匙串

任何保存用户数据的应用都必须注意该数据的安全性和私密性。 正如我们在最近的数据泄露中所看到的那样,如果未能保护用户的存储数据,可能会造成非常严重的后果。 在本教程中,您将学习一些保护用户数据的最佳实践。

在上一篇文章中 ,您学习了如何使用Data Protection API保护文件。 基于文件的保护是安全的大容量数据存储的强大功能。 但是,对于少量信息(例如密钥或密码)进行保护可能会显得过分杀伤力。 对于这些类型的物品,建议使用钥匙串解决方案。

钥匙串服务

钥匙串是存储少量信息(例如,敏感字符串和ID)的好地方,即使用户删除应用程序,这些信息仍会保留。 一个示例可能是您的服务器在注册后返回到应用程序的设备或会话令牌。 无论您称它为秘密字符串还是唯一令牌,钥匙串都将所有这些项目称为密码

有一些流行的用于钥匙串服务的第三方库,例如Strongbox (Swift)和SSKeychain (Objective-C)。 或者,如果您想完全控制自己的代码,则可能希望直接使用Keychain Services API,它是C API。

我将简要解释钥匙串的工作原理。 您可以将钥匙串视为在表上运行查询的典型数据库。 钥匙串API的功能都需要包含查询属性的CFDictionary对象。

钥匙串中的每个条目都有一个服务名称。 该服务名是一个标识符:你想存储或在钥匙串检索任何价值关键 为了只允许特定用户存储钥匙串项目,您通常还需要指定一个帐户名。

因为每个钥匙串函数都使用带有许多相同参数的相似字典进行查询,所以可以通过创建返回此查询字典的辅助函数来避免重复代码。

import Security

//...

class func passwordQuery(service: String, account: String) -> Dictionary
{
    let dictionary = [
        kSecClass as String : kSecClassGenericPassword,
        kSecAttrAccount as String : account,
        kSecAttrService as String : service,
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked //If need access in background, might want to consider kSecAttrAccessibleAfterFirstUnlock
    ] as [String : Any]
    
    return dictionary
}

该代码使用您的帐户和服务名称设置查询Dictionary ,并告诉钥匙串我们将存储密码。

与您可以为单个文件设置保护级别类似(如我们在上 kSecAttrAccessible 文章中讨论的那样 ),您还可以使用kSecAttrAccessible 键为您的钥匙串项目设置保护级别。

新增密码

SecItemAdd()函数将数据添加到钥匙串。 此函数采用一个Data对象,这使其可以存储多种对象。 使用上面创建的密码查询功能,让我们在钥匙串中存储一个字符串。 为此,我们只需要将String转换为Data

@discardableResult class func setPassword(_ password: String, service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        deletePassword(service: service, account: account) //delete password if pass empty string. Could change to pass nil to delete password, etc
        
        if !password.isEmpty
        {
            var dictionary = passwordQuery(service: service, account: account)
            let dataFromString = password.data(using: String.Encoding.utf8, allowLossyConversion: false)
            dictionary[kSecValueData as String] = dataFromString
            status = SecItemAdd(dictionary as CFDictionary, nil)
        }
    }
    return status == errSecSuccess
}

删除密码

为防止重复插入,上面的代码首先删除上一个条目(如果有)。 现在让我们编写该函数。 这是使用SecItemDelete()函数完成的。

@discardableResult class func deletePassword(service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        let dictionary = passwordQuery(service: service, account: account)
        status = SecItemDelete(dictionary as CFDictionary);
    }
    return status == errSecSuccess
}

找回密码

接下来,要从钥匙串中检索条目,请使用SecItemCopyMatching()函数。 它将返回与您的查询匹配的AnyObject

class func password(service: String, account: String) -> String //return empty string if not found, could return an optional
{
    var status : OSStatus = -1
    var resultString = ""
    if !(service.isEmpty) && !(account.isEmpty)
    {
        var passwordData : AnyObject?
        var dictionary = passwordQuery(service: service, account: account)
        dictionary[kSecReturnData as String] = kCFBooleanTrue
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
        status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
        
        if status == errSecSuccess
        {
            if let retrievedData = passwordData as? Data
            {
                resultString = String(data: retrievedData, encoding: String.Encoding.utf8)!
            }
        }
    }
    return resultString
}

在此代码中,我们将kSecReturnData参数设置为kCFBooleanTrue kSecReturnData表示将返回项目的实际数据。 另一种选择是返回项目的属性( kSecReturnAttributes )。 密钥采用CFBoolean类型,其中包含常量kCFBooleanTruekCFBooleanFalse 我们将kSecMatchLimit设置为kSecMatchLimitOne以便仅返回在钥匙串中找到的第一项,而不是无限数量的结果。

公钥和私钥

推荐使用钥匙串存储公共和私有密钥对象,例如,如果您的应用程序可以使用EC或RSA SecKey对象并且需要存储它们。

主要区别在于,我们可以告诉它存储密钥,而不是告诉钥匙串存储密码。 实际上,我们可以通过设置存储密钥的类型来确定具体的类型,例如公开密钥或私有密钥。 所有需要做的就是使查询帮助器功能适应所需的键类型。

密钥通常使用诸如com.mydomain.mykey之类的反向域标记而不是服务和帐户名来标识(因为公共密钥在不同公司或实体之间公开共享)。 我们将获取服务和帐户字符串,并将它们转换为标签Data对象。 例如,上面适用于存储RSA Private SecKey代码如下所示:

class func keyQuery(service: String, account: String) -> Dictionary
{
    let tagString = "com.mydomain." + service + "." + account
    let tag = tagString.data(using: .utf8)! //Store it as Data, not as a String
    let dictionary = [
        kSecClass as String : kSecClassKey,
        kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
        kSecAttrKeyClass as String : kSecAttrKeyClassPrivate,
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked,
        kSecAttrApplicationTag as String : tag
        ] as [String : Any]
    
    return dictionary
}

@discardableResult class func setKey(_ key: SecKey, service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        deleteKey(service: service, account:account)
        var dictionary = keyQuery(service: service, account: account)
        dictionary[kSecValueRef as String] = key
        status = SecItemAdd(dictionary as CFDictionary, nil);
    }
    return status == errSecSuccess
}

@discardableResult class func deleteKey(service: String, account: String) -> Bool
{
    var status : OSStatus = -1
    if !(service.isEmpty) && !(account.isEmpty)
    {
        let dictionary = keyQuery(service: service, account: account)
        status = SecItemDelete(dictionary as CFDictionary);
    }
    return status == errSecSuccess
}

class func key(service: String, account: String) -> SecKey?
{
    var item: CFTypeRef?
    if !(service.isEmpty) && !(account.isEmpty)
    {
        var dictionary = keyQuery(service: service, account: account)
        dictionary[kSecReturnRef as String] = kCFBooleanTrue
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
        SecItemCopyMatching(dictionary as CFDictionary, &item);
    }
    return item as! SecKey?
}

应用密码

仅在设备解锁后,用kSecAttrAccessibleWhenUnlocked标志保护的项目kSecAttrAccessibleWhenUnlocked解锁,但它首先取决于用户是否设置了密码或Touch ID。

applicationPassword凭证允许使用附加密码保护钥匙串中的项目。 这样,如果用户没有设置密码或Touch ID,则物品仍将是安全的,并且如果它们确实设置了密码,则将增加额外的安全性。

例如,在您的应用通过服务器验证后,服务器可以通过HTTPS返回密码,以解锁钥匙串项。 这是提供该额外密码的首选方式。 不建议以二进制形式对密码进行硬编码。

另一种情况是从应用程序中用户提供的密码中检索其他密码。 但是,这需要更多的工作才能正确保护(使用PBKDF2 )。 在下一个教程中,我们将介绍如何保护用户提供的密码。

应用程序密码的另一种用途是用于存储敏感密钥,例如,您不想因为用户尚未设置密码而公开该密钥。

applicationPassword仅在iOS 9及更高版本上可用,因此如果您要定位较低的iOS版本,则需要不使用applicationPassword的后备。 要使用该代码,您需要在桥接头中添加以下内容:

#import 
#import 

以下代码为查询Dictionary设置密码。

if #available(iOS 9.0, *)
{
    //Use this in place of kSecAttrAccessible for the query
    var error: Unmanaged?
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags.applicationPassword, &error)
    if accessControl != nil
    {
        dictionary[kSecAttrAccessControl as String] = accessControl
    }
    
    let localAuthenticationContext = LAContext.init()
    let theApplicationPassword = "passwordFromServer".data(using:String.Encoding.utf8)!
    localAuthenticationContext.setCredential(theApplicationPassword, type: LACredentialType.applicationPassword)
    dictionary[kSecUseAuthenticationContext as String] = localAuthenticationContext
}

请注意,我们设置kSecAttrAccessControlDictionary 用它代替了kSecAttrAccessible ,它是先前在我们的passwordQuery方法中设置的。 如果您尝试同时使用两者,则会收到OSStatus -50错误。

用户认证

从iOS 8开始,您可以将数据存储在钥匙串中,只有在用户使用Touch ID或密码在设备上成功通过身份验证后,才能访问数据。 当用户进行身份验证时,如果设置了Touch ID,Touch ID将具有优先权,否则将显示密码屏幕。 保存到钥匙串将不需要用户进行身份验证,但是将检索数据。

您可以通过提供设置为.userPresence的访问控制对象来将钥匙串项设置为需要用户认证。 如果未设置密码,则任何带有.userPresence钥匙串请求.userPresence将失败。

if #available(iOS 8.0, *)
{
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil)
    if accessControl != nil
    {
        dictionary[kSecAttrAccessControl as String] = accessControl
    }
}

当您要确保正确的人正在使用您的应用程序时,此功能非常有用。 例如,对于用户而言,在能够登录银行应用程序之前进行身份验证非常重要。 这样可以保护未锁定设备的用户,从而无法访问银行。

另外,如果您的应用程序没有服务器端组件,则可以使用此功能执行设备端身份验证。

对于负载查询,您可以提供用户为什么需要进行身份验证的描述。

dictionary[kSecUseOperationPrompt as String] = "Authenticate to retrieve x"

使用SecItemCopyMatching()检索数据时,该函数将显示身份验证UI并等待用户使用Touch ID或输入密码。 由于SecItemCopyMatching()将在用户完成身份验证之前一直阻塞,因此您需要从后台线程调用该函数,以允许主UI线程保持响应状态。

DispatchQueue.global().async 
{
    status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
    if status == errSecSuccess
    {
        if let retrievedData = passwordData as? Data
        {
            DispatchQueue.main.async 
            {
                //... do the rest of the work back on the main thread
            }   
        }
    }
}

同样,我们在查询Dictionary上设置kSecAttrAccessControl 您将需要删除先前在我们的passwordQuery方法中设置的kSecAttrAccessible 一次使用两者将导致OSStatus -50错误。

结论

在本文中,您浏览了Keychain Services API。 除了我们在上一篇文章中看到的Data Protection API之外,使用此库也是保护数据的最佳实践的一部分。

但是,如果用户在设备上没有密码或Touch ID,则两种框架都不会加密。 由于“钥匙串服务”和“ 数据保护” API经常被iOS应用程序使用,因此它们有时会成为攻击者的目标,尤其是在越狱设备上。 如果您的应用程序无法使用高度敏感的信息,则这可能是可以接受的风险。 尽管iOS一直在不断更新框架的安全性,但我们仍然处于用户的摆布之下,他们使用强大的密码来更新操作系统,而不会越狱他们的设备。

钥匙串用于存储较小的数据,并且您可能需要保护独立于设备身份验证的大量数据。 虽然iOS更新增加了一些很棒的新功能,例如应用程序密码,但您可能仍需要支持较低的iOS版本,并且仍具有强大的安全性。 由于某些原因,您可能想要自己加密数据。

本系列的最后一篇文章介绍了使用AES加密自己加密数据的方法,虽然这是一种更高级的方法,但是它使您可以完全控制数据的加密方式和时间。

翻译自: https://code.tutsplus.com/tutorials/securing-ios-data-at-rest--cms-28528

你可能感兴趣的:(保护静止的iOS数据:钥匙串)