iOS Keychain 使用指南

目录

  1. 什么是 Keychain?
  2. 为什么使用 Keychain?
  3. Keychain 核心概念
  4. Keychain 操作指南
  5. Keychain 共享(App Groups)
  6. 最佳实践与注意事项
  7. 常见问题解决

什么是 Keychain?

Keychain 是 iOS 和 macOS 提供的安全存储服务,用于保存敏感信息:

  • 用户密码
  • 加密密钥
  • 证书
  • 支付信息
  • 其他敏感数据

与 UserDefaults 不同,Keychain 数据:

  • 加密存储在设备上
  • 应用卸载后仍然保留(除非手动删除)
  • 受系统级安全保护

为什么使用 Keychain?

  1. 安全性:硬件加密保护(Secure Enclave)
  2. 持久性:应用卸载后数据不会丢失
  3. 跨应用共享:通过 App Groups 实现
  4. 符合隐私要求:安全存储用户凭证
  5. 生物识别集成:支持 Face ID/Touch ID 验证

Keychain 核心概念

Keychain 项结构

每个 Keychain 条目包含:

  • kSecClass:数据类型(密码/证书/密钥等)
  • kSecAttrAccount:账户标识(通常用户名)
  • kSecAttrService:服务标识(通常 bundle ID)
  • kSecValueData:加密存储的实际数据
  • kSecAttrAccessible:访问策略(见下表)

访问策略(Accessibility)

选项 描述
kSecAttrAccessibleWhenUnlocked 设备解锁时可访问(推荐)
kSecAttrAccessibleAfterFirstUnlock 设备首次解锁后可访问
kSecAttrAccessibleWhenPasscodeSet 设备设置密码时可访问
kSecAttrAccessibleWhenUnlockedThisDeviceOnly 仅当前设备解锁可访问(不备份)

常用 Keychain 类(kSecClass)

  • kSecClassGenericPassword:通用密码(最常用)
  • kSecClassInternetPassword:互联网密码
  • kSecClassCertificate:证书
  • kSecClassKey:加密密钥

Keychain 操作指南

提示:实际开发建议封装 Keychain 工具类(示例代码

添加数据到 Keychain

import Security

func addItemToKeychain(account: String, password: String) -> Bool {
    guard let passwordData = password.data(using: .utf8) else { return false }
    
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: account,
        kSecAttrService: Bundle.main.bundleIdentifier!,
        kSecValueData: passwordData,
        kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked
    ]
    
    let status = SecItemAdd(query as CFDictionary, nil)
    return status == errSecSuccess
}

查询 Keychain 数据

func getPasswordFromKeychain(account: String) -> String? {
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: account,
        kSecAttrService: Bundle.main.bundleIdentifier!,
        kSecReturnData: true,
        kSecMatchLimit: kSecMatchLimitOne
    ]
    
    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
    
    guard status == errSecSuccess,
          let data = result as? Data else { return nil }
    
    return String(data: data, encoding: .utf8)
}

更新 Keychain 数据

func updateKeychainItem(account: String, newPassword: String) -> Bool {
    guard let newPasswordData = newPassword.data(using: .utf8) else { return false }
    
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: account,
        kSecAttrService: Bundle.main.bundleIdentifier!
    ]
    
    let attributes: [CFString: Any] = [
        kSecValueData: newPasswordData
    ]
    
    let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    return status == errSecSuccess
}

删除 Keychain 数据

func deleteKeychainItem(account: String) -> Bool {
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: account,
        kSecAttrService: Bundle.main.bundleIdentifier!
    ]
    
    let status = SecItemDelete(query as CFDictionary)
    return status == errSecSuccess
}

Keychain 共享(App Groups)

实现同一开发者账号下的多个应用共享 Keychain 数据:

  1. 开启 App Groups

    • 在 Xcode 项目中:
      • 添加 Capability → App Groups
      • 创建新的 App Group ID(格式:group.com.yourcompany.appgroup
  2. 使用共享 Keychain

    let sharedGroup = "group.com.yourcompany.appgroup"
    
    let query: [CFString: Any] = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: "sharedAccount",
        kSecAttrService: "SharedService",
        kSecAttrAccessGroup: sharedGroup, // 关键设置
        kSecValueData: data
    ]
    

最佳实践与注意事项

  1. 合理选择访问策略

    • 使用 kSecAttrAccessibleWhenUnlocked 作为默认选项
    • 敏感数据使用 ThisDeviceOnly 防止备份
  2. 错误处理

    let status = SecItemAdd(...)
    if status == errSecDuplicateItem {
        // 处理重复项
    } else if status == errSecAuthFailed {
        // 认证失败
    }
    
  3. 数据组织

    • 使用 kSecAttrService 区分数据类型
    • 避免存储大体积数据(Keychain 不是数据库)
  4. 生物识别集成

    let context = LAContext()
    context.touchIDAuthenticationAllowableReuseDuration = 10
    
    let query: [CFString: Any] = [
        kSecUseAuthenticationContext: context,
        // 其他参数...
    ]
    
  5. 线程安全

    • Keychain 操作是同步的
    • 在后台队列执行耗时操作

常见问题解决

错误代码对照表

错误代码 描述
errSecSuccess (0) 操作成功
errSecDuplicateItem (-25299) 项已存在
errSecItemNotFound (-25300) 项不存在
errSecAuthFailed (-25293) 认证失败

调试技巧

  1. 重置模拟器 Keychain:

    # 终端执行
    xcrun simctl erase all
    
  2. 查看 Keychain 内容:

    • 使用 Keychain Access 应用(macOS)
    • 过滤器选择 “iCloud” 或 “登录”
  3. 真机测试时:

    • 确保开启 Keychain 共享能力
    • 检查 App Group 配置

完整工具类示例

import Security

struct KeychainManager {
    static let service = Bundle.main.bundleIdentifier!
    
    enum KeychainError: Error {
        case duplicateItem
        case unknown(OSStatus)
    }
    
    static func save(_ data: Data, account: String) throws {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            kSecValueData: data,
            kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked
        ]
        
        let status = SecItemAdd(query as CFDictionary, nil)
        
        guard status != errSecDuplicateItem else {
            throw KeychainError.duplicateItem
        }
        
        guard status == errSecSuccess else {
            throw KeychainError.unknown(status)
        }
    }
    
    static func get(account: String) -> Data? {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service,
            kSecReturnData: true,
            kSecMatchLimit: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        return status == errSecSuccess ? result as? Data : nil
    }
    
    static func delete(account: String) -> Bool {
        let query: [CFString: Any] = [
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: account,
            kSecAttrService: service
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

通过本指南,您应该掌握了 Keychain 的核心概念、操作方法和最佳实践。Keychain 是 iOS 安全体系中至关重要的组件,正确使用它可以显著提升您应用的 security posture。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐