MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI 与Keychain钥匙串访问

2023-01-302.9k 阅读

SwiftUI 基础介绍

SwiftUI 是苹果公司为开发 iOS、iPadOS、macOS、watchOS 和 tvOS 应用程序而推出的声明式用户界面 (UI) 框架。它允许开发者通过描述用户界面的结构和外观,而不是像传统方式那样通过命令式代码来构建 UI。例如,创建一个简单的按钮在 SwiftUI 中可以这样写:

Button("点击我") {
    print("按钮被点击了")
}

这种方式简洁明了,大大提高了开发效率,并且易于维护和扩展。SwiftUI 采用了视图、视图修饰符和容器视图的概念。视图是构成用户界面的基本元素,比如文本、按钮、图像等。视图修饰符用于修改视图的外观和行为,例如设置文本颜色、按钮样式等。容器视图则可以包含多个子视图,用于组织和布局界面。例如,VStack 是一个垂直排列子视图的容器视图:

VStack {
    Text("第一行文本")
    Text("第二行文本")
}

Keychain 钥匙串概述

Keychain 是苹果操作系统提供的一种安全存储机制,用于保存敏感信息,如密码、证书、密钥等。它为应用程序提供了一个安全的地方来存储这些信息,只有授权的应用程序才能访问。Keychain 存储的数据会随着用户的 iCloud 账户同步,这意味着在用户的不同设备上都可以访问这些数据。例如,用户在 iPhone 上保存的登录密码,在 iPad 上也可以通过 Keychain 访问到。Keychain 的安全性基于操作系统的安全机制,数据在存储和传输过程中都经过加密处理。它还与用户的设备密码或 Touch ID / Face ID 集成,进一步增强了安全性。

在 SwiftUI 应用中使用 Keychain 访问

引入必要的框架

在 SwiftUI 项目中使用 Keychain,首先需要引入 Security 框架。在 Xcode 项目中,打开 ViewController.swift 文件(如果是 SwiftUI 项目,也可以在 AppDelegate.swift 或者相关视图文件中操作),在文件顶部添加以下导入语句:

import Security

封装 Keychain 访问方法

为了方便在 SwiftUI 应用中使用 Keychain,我们可以封装一些方法。创建一个新的 Swift 文件,比如 KeychainManager.swift,并在其中定义如下类和方法:

class KeychainManager {
    static let shared = KeychainManager()
    
    private init() {}
    
    func save(data: Data, forKey key: String) -> OSStatus {
        let query: [String : Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        SecItemDelete(query as CFDictionary)
        
        return SecItemAdd(query as CFDictionary, nil)
    }
    
    func load(forKey key: String) -> Data? {
        let query: [String : Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        if status == errSecSuccess, let data = result as? Data {
            return data
        }
        
        return nil
    }
    
    func delete(forKey key: String) -> OSStatus {
        let query: [String : Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key
        ]
        
        return SecItemDelete(query as CFDictionary)
    }
}

在上述代码中,save 方法用于将数据保存到 Keychain 中。它首先构建一个查询字典,指定要保存的数据类型为通用密码(kSecClassGenericPassword),设置账户名为传入的 key,并将数据赋值给 kSecValueData。然后,它会尝试删除已有的相同 key 的数据(如果存在),最后添加新的数据。load 方法用于从 Keychain 中加载数据。它构建一个查询字典,指定要获取的数据类型和账户名,并设置返回数据的选项。如果查询成功,它将返回加载的数据。delete 方法用于从 Keychain 中删除指定 key 的数据。它构建一个简单的查询字典,指定数据类型和账户名,然后调用 SecItemDelete 方法删除数据。

在 SwiftUI 视图中使用 Keychain 方法

假设我们有一个 SwiftUI 视图,需要保存和读取用户的登录密码。在视图文件中,可以这样使用 KeychainManager

import SwiftUI

struct LoginView: View {
    @State private var password: String = ""
    
    var body: some View {
        VStack {
            SecureField("密码", text: $password)
            Button("保存密码") {
                let passwordData = password.data(using:.utf8)!
                let status = KeychainManager.shared.save(data: passwordData, forKey: "user_login_password")
                if status == errSecSuccess {
                    print("密码保存成功")
                } else {
                    print("密码保存失败,错误码:\(status)")
                }
            }
            Button("读取密码") {
                if let savedData = KeychainManager.shared.load(forKey: "user_login_password") {
                    if let savedPassword = String(data: savedData, encoding:.utf8) {
                        print("读取到的密码:\(savedPassword)")
                    }
                } else {
                    print("未找到保存的密码")
                }
            }
        }
    }
}

在这个 LoginView 中,我们有一个 SecureField 用于输入密码,两个按钮分别用于保存和读取密码。当点击 “保存密码” 按钮时,它将获取输入的密码并转换为 Data 类型,然后调用 KeychainManagersave 方法保存密码。如果保存成功,会打印提示信息;如果失败,会打印错误码。当点击 “读取密码” 按钮时,它会调用 KeychainManagerload 方法读取密码。如果读取成功,会将数据转换为字符串并打印;如果失败,会打印提示信息。

Keychain 访问的安全性考量

访问权限设置

在设置 Keychain 数据的访问权限时,需要谨慎选择。例如,kSecAttrAccessibleWhenUnlockedThisDeviceOnly 表示数据只能在设备解锁时访问,并且只在当前设备上可用。这种设置适用于一些对安全性要求较高,且不需要在不同设备间同步的数据。如果数据需要在不同设备间同步,可以选择 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 或其他合适的选项,但这样会在一定程度上降低安全性。在选择访问权限时,要综合考虑应用程序的功能需求和数据的敏感程度。如果应用程序处理的是金融相关的敏感信息,如银行账户密码,应尽量选择更严格的访问权限。

数据加密与保护

虽然 Keychain 本身对数据进行了加密存储,但开发者在将数据存入 Keychain 之前,也可以考虑对数据进行额外的加密处理。例如,可以使用对称加密算法(如 AES)对数据进行加密,然后将加密后的数据存入 Keychain。这样,即使 Keychain 中的数据被非法获取,没有解密密钥也无法获取原始数据。在使用额外加密时,要妥善保管加密密钥。可以将密钥存储在 Keychain 中,但要注意设置合适的访问权限,确保密钥的安全性。同时,要注意加密和解密过程的性能开销,避免对应用程序的性能产生过大影响。

防止 Keychain 滥用

在应用程序中,要严格限制对 Keychain 的访问。只有在真正需要保存或读取敏感信息时才进行操作,并且要对传入的参数进行严格验证。例如,在保存数据时,要确保 key 参数是合法且唯一的,避免因错误的 key 导致数据覆盖或混乱。另外,要防止恶意代码通过注入等方式非法访问 Keychain。可以通过代码签名、权限检查等方式来增强应用程序的安全性,防止 Keychain 被滥用。

Keychain 与 SwiftUI 应用的集成优化

结合 SwiftUI 的数据绑定

SwiftUI 的数据绑定机制可以与 Keychain 访问更好地集成。例如,可以将 Keychain 中保存的数据绑定到 SwiftUI 的视图模型中。假设我们有一个用户设置视图,其中的某些设置项保存在 Keychain 中。我们可以创建一个视图模型类,在其中定义与 Keychain 交互的方法,并将数据绑定到视图:

class SettingsViewModel: ObservableObject {
    @Published var notificationSetting: Bool = false
    
    init() {
        if let data = KeychainManager.shared.load(forKey: "notification_setting") {
            if let setting = try? JSONDecoder().decode(Bool.self, from: data) {
                notificationSetting = setting
            }
        }
    }
    
    func saveNotificationSetting() {
        let data = try? JSONEncoder().encode(notificationSetting)
        if let data = data {
            KeychainManager.shared.save(data: data, forKey: "notification_setting")
        }
    }
}

在视图中,可以这样使用这个视图模型:

struct SettingsView: View {
    @ObservedObject var viewModel = SettingsViewModel()
    
    var body: some View {
        VStack {
            Toggle("开启通知", isOn: $viewModel.notificationSetting)
               .onChange(of: viewModel.notificationSetting) { _ in
                    viewModel.saveNotificationSetting()
                }
        }
    }
}

在这个例子中,SettingsViewModel 类管理着通知设置的数据,并与 Keychain 进行交互。@Published 属性包装器使得 notificationSetting 的变化能够通知到视图。在 SettingsView 中,Toggle 开关与 viewModel.notificationSetting 进行绑定,并且当开关状态改变时,会调用 viewModel.saveNotificationSetting 方法将新的设置保存到 Keychain 中。

异步操作优化

Keychain 访问方法如 saveloaddelete 可能会涉及一些 I/O 操作,在某些情况下可能会比较耗时。为了避免阻塞主线程,影响用户体验,可以将这些操作放到异步队列中执行。例如,修改 KeychainManagerload 方法为异步版本:

func loadAsync(forKey key: String, completion: @escaping (Data?) -> Void) {
    DispatchQueue.global(qos:.userInitiated).async {
        let query: [String : Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: kCFBooleanTrue,
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        if status == errSecSuccess, let data = result as? Data {
            DispatchQueue.main.async {
                completion(data)
            }
        } else {
            DispatchQueue.main.async {
                completion(nil)
            }
        }
    }
}

在视图中使用这个异步方法时,可以这样处理:

struct ContentView: View {
    @State private var loadedData: Data?
    
    var body: some View {
        VStack {
            Button("异步加载数据") {
                KeychainManager.shared.loadAsync(forKey: "example_key") { data in
                    loadedData = data
                    if let data = data, let text = String(data: data, encoding:.utf8) {
                        print("异步加载到的数据:\(text)")
                    } else {
                        print("异步加载数据失败")
                    }
                }
            }
        }
    }
}

在这个例子中,loadAsync 方法将 Keychain 加载操作放到全局队列中执行,完成后在主线程中调用 completion 闭包,这样可以确保 UI 的流畅性,不会因为 Keychain 操作而卡顿。

处理 Keychain 访问错误

常见错误类型及原因

在使用 Keychain 访问时,可能会遇到各种错误。常见的错误包括 errSecItemNotFound,这表示在 Keychain 中未找到指定 key 的数据。通常是因为之前没有保存过相关数据,或者 key 拼写错误等原因导致。另一个常见错误是 errSecAuthFailed,这通常表示访问 Keychain 的权限不足。可能是因为应用程序没有正确的授权,或者 Keychain 的访问权限设置过于严格。还有 errSecDuplicateItem 错误,这表示尝试保存的数据已经存在于 Keychain 中,并且不允许重复保存。这种情况可能发生在没有先检查数据是否存在就直接尝试保存的情况下。

错误处理策略

对于 errSecItemNotFound 错误,在应用程序逻辑中可以进行相应的提示,例如提示用户相关数据未保存,或者引导用户进行数据保存操作。对于 errSecAuthFailed 错误,应用程序可以尝试请求用户授权,例如提示用户开启相关权限。如果权限是由于 Keychain 访问设置问题导致的,应用程序可以提供一些帮助信息,引导用户正确设置 Keychain 访问权限。对于 errSecDuplicateItem 错误,可以在保存数据之前先调用 load 方法检查数据是否已经存在,如果存在则根据应用程序的需求进行更新操作或者提示用户数据已存在。在处理 Keychain 访问错误时,要确保错误信息对用户友好,避免用户因为不理解错误而产生困扰。同时,要记录错误日志,以便开发者进行调试和分析,找出错误产生的根本原因并进行修复。

与其他安全机制结合使用

与生物识别技术结合

SwiftUI 应用可以将 Keychain 与生物识别技术(如 Touch ID 或 Face ID)结合使用,进一步增强安全性。例如,在用户登录时,除了验证用户名和密码(从 Keychain 中读取),还可以要求用户通过生物识别进行身份验证。在 iOS 中,可以使用 LocalAuthentication 框架来实现生物识别功能。首先,引入 LocalAuthentication 框架:

import LocalAuthentication

然后,在需要进行生物识别验证的地方,例如登录按钮的点击事件中,可以这样实现:

func authenticateUser() {
    let context = LAContext()
    var error: NSError?
    
    if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
        let reason = "请使用生物识别解锁应用"
        
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
            DispatchQueue.main.async {
                if success {
                    // 生物识别成功,继续后续操作,如验证 Keychain 中的密码
                    if let passwordData = KeychainManager.shared.load(forKey: "user_login_password") {
                        if let password = String(data: passwordData, encoding:.utf8), password == "correct_password" {
                            print("用户登录成功")
                        } else {
                            print("密码错误")
                        }
                    } else {
                        print("未找到保存的密码")
                    }
                } else {
                    let errorMessage = authenticationError?.localizedDescription ?? "生物识别失败"
                    print(errorMessage)
                }
            }
        }
    } else {
        let errorMessage = error?.localizedDescription ?? "设备不支持生物识别"
        print(errorMessage)
    }
}

在上述代码中,首先检查设备是否支持生物识别,然后进行生物识别验证。如果验证成功,再从 Keychain 中读取密码进行进一步验证。这样可以确保只有授权的用户能够访问应用程序的敏感功能。

与应用内加密机制结合

除了 Keychain 本身的加密,应用程序还可以在内部实现额外的加密机制。例如,在保存数据到 Keychain 之前,先使用加密算法对数据进行加密。可以使用 CryptoKit 框架进行加密操作。假设我们要保存用户的一些敏感文本信息,首先引入 CryptoKit 框架:

import CryptoKit

然后,在保存数据到 Keychain 之前进行加密:

func encryptData(data: String) -> Data? {
    let plaintext = data.data(using:.utf8)!
    let key = SymmetricKey(size:.bits256)
    let sealedBox = try? AES.GCM.seal(plaintext, using: key)
    
    if let sealedBox = sealedBox {
        var combinedData = key.withUnsafeBytes { Data($0) }
        combinedData.append(sealedBox.combined)
        return combinedData
    }
    
    return nil
}

在读取数据时,进行解密操作:

func decryptData(data: Data) -> String? {
    let keyData = data.prefix(SymmetricKey.size(.bits256))
    let encryptedData = data.dropFirst(SymmetricKey.size(.bits256))
    
    guard let key = try? SymmetricKey(data: keyData),
          let sealedBox = try? AES.GCM.SealedBox(combined: encryptedData) else {
        return nil
    }
    
    let decrypted = try? AES.GCM.open(sealedBox, using: key)
    
    if let decrypted = decrypted, let text = String(data: decrypted, encoding:.utf8) {
        return text
    }
    
    return nil
}

在保存和读取数据到 Keychain 时,使用这些加密和解密方法:

// 保存加密后的数据到 Keychain
if let encryptedData = encryptData(data: "sensitive_text") {
    KeychainManager.shared.save(data: encryptedData, forKey: "encrypted_sensitive_text")
}

// 从 Keychain 读取并解密数据
if let savedData = KeychainManager.shared.load(forKey: "encrypted_sensitive_text") {
    if let decryptedText = decryptData(data: savedData) {
        print("解密后的数据:\(decryptedText)")
    }
}

通过这种方式,即使 Keychain 中的数据被泄露,没有解密密钥也无法获取原始的敏感信息,进一步提高了数据的安全性。

Keychain 访问在不同平台的差异

iOS 与 macOS

在 iOS 和 macOS 上,Keychain 的基本功能和使用方法相似,但也存在一些差异。在 iOS 上,Keychain 通常与设备的安全机制紧密集成,例如与 Touch ID、Face ID 结合使用。而在 macOS 上,Keychain 更多地与用户账户密码等集成。在访问权限方面,iOS 对 Keychain 的访问权限设置相对更严格,以保护用户设备上的数据安全。例如,在 iOS 上,应用程序需要明确声明对 Keychain 的访问权限,并在某些情况下需要用户授权。而在 macOS 上,虽然也有访问权限控制,但在某些场景下可能相对宽松一些。在数据同步方面,iOS 设备可以通过 iCloud 同步 Keychain 数据,而 macOS 上的 Keychain 数据同步可能需要通过其他方式,如 macOS 自带的同步机制或第三方工具。

watchOS 与 tvOS

watchOS 和 tvOS 上的 Keychain 访问也有其特点。在 watchOS 上,由于设备的特性,应用程序对 Keychain 的使用场景相对有限。通常用于保存与手表相关的一些敏感信息,如手表解锁密码或与配对 iPhone 通信的密钥等。watchOS 上的 Keychain 数据访问速度可能相对较慢,因为设备资源有限。在 tvOS 上,Keychain 主要用于保存与电视应用相关的认证信息,如流媒体服务的登录凭证等。与 iOS 和 macOS 相比,tvOS 上的 Keychain 访问可能受到更多的系统限制,以确保用户在电视上的操作安全。例如,tvOS 可能对应用程序访问 Keychain 的频率和数据量进行限制,防止恶意应用滥用 Keychain 资源。

在开发跨平台应用时,需要根据不同平台的特点,合理调整 Keychain 的使用方式和访问策略,以确保应用程序在各个平台上都能安全、稳定地运行。

实际应用场景案例分析

金融类应用

以一款移动银行应用为例,用户的登录密码、交易密码等敏感信息需要安全存储。通过将这些密码保存到 Keychain 中,并结合生物识别技术(如 Face ID 或 Touch ID)进行身份验证,可以大大提高应用的安全性。当用户登录时,首先通过生物识别验证用户身份,然后从 Keychain 中读取保存的密码进行验证。在进行交易时,同样先验证用户身份,再从 Keychain 中获取交易密码进行授权。此外,金融类应用可能还会保存一些加密密钥,用于对交易数据进行加密和解密。这些密钥也可以安全地存储在 Keychain 中,确保只有授权的应用程序部分能够访问和使用。通过合理使用 Keychain 和其他安全机制,金融类应用可以为用户提供安全可靠的服务。

社交类应用

社交类应用可能需要保存用户的登录令牌(token),以保持用户的登录状态。将登录令牌保存到 Keychain 中,可以防止令牌被非法获取和滥用。当用户打开应用时,应用程序从 Keychain 中读取登录令牌,并使用它来验证用户身份,自动登录到社交平台。此外,一些社交应用可能允许用户设置隐私密码,用于保护特定的聊天记录或个人资料。这些隐私密码也可以存储在 Keychain 中,只有用户输入正确的密码才能访问相关内容。通过这种方式,社交类应用可以更好地保护用户的隐私和账号安全。

云存储类应用

云存储类应用需要保存用户的云服务账号密码或访问密钥。将这些信息存储在 Keychain 中,可以确保即使设备丢失或被盗,他人也无法轻易获取用户的云存储账号信息。当用户首次登录云存储应用时,输入账号密码,应用程序将密码保存到 Keychain 中,并在后续的自动登录过程中从 Keychain 中读取密码进行验证。此外,云存储应用可能还会使用 Keychain 保存一些加密密钥,用于对本地存储的文件进行加密,确保用户的数据在本地设备上也得到安全保护。这样,用户可以放心地使用云存储应用来存储和管理自己的重要文件。

通过这些实际应用场景案例可以看出,Keychain 在不同类型的应用中都发挥着重要的安全存储作用,结合 SwiftUI 的开发优势,可以为用户提供更加安全、便捷的应用体验。在实际开发中,开发者需要根据应用的具体需求和安全要求,合理设计 Keychain 的使用方式和与其他安全机制的结合方式,确保应用程序的安全性和稳定性。