iOS安全数据存储介绍

保护敏感的用户数据是任何iOS应用程序的基本责任. 无论是存储认证符,加密密钥,还是私人证书,平台都提供了专门的硬件支持解决方案: Keychain . 或属性列表文件不同,Keychain在休息时加密数据并强制实施严格的访问控制. 本条为与Keychain一起实施安全数据存储提供了全面的指南,涵盖了本地安全框架和生产应用的实用最佳做法.

理解iOS 键链

密钥链是一个安全存储容器, 由操作系统管理。 它将小而敏感的项目, 如密码、 密码密钥或证书存储在一个加密的数据库中。 写到密钥链的数据即使在设备被锁定时也受到保护。 密钥能力包括:

  • 休息时使用硬件支撑的AES-256进行催化.
  • 通过设备密码,触摸ID,或FaceID,进入控制.
  • 持续跨应用程序重置(如果配置)和可选的iCloud同步.
  • apps之间的隔离:默认情况下,一个app不能读取另一个app的Keychain项目,除非它们共享一个Keychain访问组.

键链不是为大块块设计的;每个项目都保留在几千字节下。对于较大数据,请考虑使用 API或 框架,同时使用基于文件的加密。

键链服务 API 与第三方图书馆

苹果提供了本地 Keychain Services API (基于C, ), 它很强大, 但动词很强。 您可以直接使用它, 或者采用一个方便Swift的包装器。 流行的第三方库, 如 [ Keychain Access []] SwiftKeychainWrapper [] 减少锅炉板。 但是, 理解基本的API对于调试和您需要精细的对访问政策控制时, 至关重要。 今天,我们将专注于带有Swift的本地API 。

设置密钥链存储

在存储任何信息之前, 您必须决定 [ [FLT: 0]] 关键链项类 [[FLT: 1] 。 通用密码最常见的是 [[FLT: 4]] 。 对于互联网密码或证书, 则有其他类。 每个项目都使用一组属性来引用, 即描述项目的词典( CFDDD词典) 。

基本流量始终遵循这一模式:

  1. 构建带有项目类和属性的查询词典.
  2. 调用适当的函数(,,,]).
  3. 检查返回的 (]] 或错误代码).

写入代码前, 导入安全模块 :

import Security
import Foundation // for Data and String utilities

在键链中存储数据

写入通用密码

要为当前用户保存一个符号(例如 JWT) :

func saveToken(_ token: String, forAccount account: String) -> Bool {
 guard let tokenData = token.data(using: .utf8) else { return false }

 let query: [String: Any] = [
 kSecClass as String: kSecClassGenericPassword,
 kSecAttrAccount as String: account,
 kSecValueData as String: tokenData,
 // Optional: restrict access to when device is unlocked
 kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
 ]

 // Delete any existing item first to avoid duplicates
 SecItemDelete(query as CFDictionary)

 let status = SecItemAdd(query as CFDictionary, nil)
 return status == errSecSuccess
}

要点:

  • 作为主键;选择一个独特的字符串(例如用户ID或像]这样的常数).
  • ] 当项目可以读取时控制。使用[ 来进行最佳安全;它防止了iCloud备份,并限制了对当前设备的访问。
  • 我们先拨打,然后再添加以避免重复项目。或者,您可以使用。

添加访问控制( 图形或密码)

对于高度敏感数据,在读取前需要触摸ID或Face ID:

let accessControl = SecAccessControlCreateWithFlags(
 nil,
 kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
 .userPresence, // requires passcode, Face ID, or Touch ID
 nil
)

let query: [String: Any] = [
 kSecClass as String: kSecClassGenericPassword,
 kSecAttrAccount as String: account,
 kSecValueData as String: tokenData,
 kSecAttrAccessControl as String: accessControl as Any
]
SecItemAdd(query as CFDictionary, nil)

现在, 任何对此项目的调用都会触发生物鉴别或密码提示。 请使用本地认证的 [[FLT: 22]] 来优雅地处理用户交互 。

从密钥链获取数据

要读取存储的符号 :

func retrieveToken(forAccount account: String) -> String? {
 let query: [String: Any] = [
 kSecClass as String: kSecClassGenericPassword,
 kSecAttrAccount as String: account,
 kSecReturnData as String: true,
 kSecMatchLimit as String: kSecMatchLimitOne
 ]

 var item: CFTypeRef?
 let status = SecItemCopyMatching(query as CFDictionary, &item)

 guard status == errSecSuccess,
 let data = item as? Data,
 let token = String(data: data, encoding: .utf8) else {
 return nil
 }
 return token
}

设定 [[FLT: 24] 以获取数据。 使用 [[FLT: 26]] 以获取一个结果。 如果您省略了限制, API 可以返回一个数组 。

重要性: 当使用访问控制(生物量学)时,如果用户取消,则[]的呼叫可能返回。将这个案例分开处理,永远不要回到纯文本存储中。

更新和删除密钥链项目

更新现有项目

使用[],而不是删除和重新添加:

func updateToken(_ newToken: String, forAccount account: String) -> Bool {
 guard let newData = newToken.data(using: .utf8) else { return false }

 let query: [String: Any] = [
 kSecClass as String: kSecClassGenericPassword,
 kSecAttrAccount as String: account
 ]

 let attributesToUpdate: [String: Any] = [
 kSecValueData as String: newData
 ]

 let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
 return status == errSecSuccess
}

这比删除+添加更有效,并避免潜在的种族条件.

删除项目

func deleteItem(forAccount account: String) -> Bool {
 let query: [String: Any] = [
 kSecClass as String: kSecClassGenericPassword,
 kSecAttrAccount as String: account
 ]
 let status = SecItemDelete(query as CFDictionary)
 return status == errSecSuccess
}

使用共享的密钥链时, 请注意不要删除属于共享相同访问组的其他应用程序的项目 。

访问控制和无障碍属性

常数定义了当键链项可以读取时。选择仍然满足您的应用需要的最限制性选项:

AttributeMeaning
kSecAttrAccessibleWhenUnlockedAvailable only while device is unlocked (default).
kSecAttrAccessibleAfterFirstUnlockAvailable after device boots and is unlocked once. Allows background access.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyRequires a passcode to be set. Strictest option—prevents access even after unlock if passcode is removed.
kSecAttrAccessibleWhenUnlockedThisDeviceOnlySame as WhenUnlocked but does not back up to iCloud, and cannot be restored to another device.

对于大多数应用程序, 在安全和可用性之间达成正确的平衡。如果需要读取背景中的项目(例如背景刷新符),则必须使用(并接受数据的保护程度略低)。

处理错误和常见陷阱

函数返回 。 总是检查并妥善处理故障。 常见错误 :

  • (–25300) – 没有项目匹配查询.
  • (–25299) — — 具有相同主键的项目已经存在(如果你没有先删除的话) 。
  • (–128) – 用户取消了生物鉴别快报.
  • (–25293) – 认证失败或生物鉴别技术不可用.

永远不要忽略一个非成功状态。 优雅的降解: 显示错误消息或重试, 但绝不将敏感数据存储在键链之外作为倒置。 您可以使用 [[FLT: 47]] 在尝试访问前检查生物鉴别可用性 。

最佳做法和生产考虑因素

  • 每个用户或每个物品类型使用独特的,描述性的账户名称[,以避免碰撞.
  • ]总是指定一个可访问属性;否则,系统默认(]])适用,这可能不理想.
  • 当用户登录]时—— 清除密钥链数据—— 对所有已知账户进行输入并删除项目.
  • 仅在自己的应用程序共享时使用密钥链访问组。避免使用宽组 。
  • 永远不要在键链使用或代之以数据库中存储非敏感数据(如用户偏好)。
  • 考虑使用与 用于先进的情景(大型海洋观测系统催化器)。
  • 在真设备上进行测试;模拟器使用一个与硬件支撑存储不同的行为的软件键链.

使用 SwiftUI 和 Async/Await 的密钥链

对于现代应用,将键链操作包在演员或自动安全类中,以避免阻断主线程. 示例使用 :

actor KeychainManager {
 func saveToken(_ token: String, for account: String) async -> Bool {
 // same implementation as above, but now it's safe to call from any context
 return saveToken(token, forAccount: account)
 }
}

如果您使用生物鉴别技术, 调用在等待用户交互时可能会阻断线程。 在键链调用前, 请用 [ [FLT: 55] 方法将线程排入背景队列, 或者更好 。

结论

The iOS Keychain is the correct place to store small, sensitive pieces of data. By using the native Keychain Services API, you gain direct control over encryption, accessibility, and authentication policies. Always pair your Keychain usage with solid error handling and remember to clear data when appropriate. For further reading, refer to the Apple Keychain Service Documentation and the Keychain Concepts overview. Adopting these practices will help you ship iOS apps that respect user privacy and withstand security scrutiny.