0Pricing
Swift Academy · 课时

在钥匙串中存储机密

安全地保存和获取凭据。

在钥匙串中存储机密 是 CoddyKit 上的免费 Swift Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Swift Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Swift Academy 课程共包含 4 节课。

为什么需要钥匙串

密码、令牌和密钥绝不能放在 UserDefaults 或普通文件中,因为这些内容很容易被读取。钥匙串是由 OS 管理的加密数据库,用于存储少量机密数据,并受到硬件和用户密码的保护。它是存放凭据的唯一正确位置。

import Security
// Keychain stores secrets encrypted at rest,
// survives app updates, and gates access by policy.

项目是字典

钥匙串服务 API 基于 C 语言:您需要使用包含 kSec... 常量键的 [String: Any] 查询字典来描述项目。同一种字典结构可重复用于添加、搜索、更新和删除。

import Security
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "user@example.com",
    kSecAttrService as String: "com.example.app"
]
_ = query

项目类别

kSecClass 键用于选择项目类型。kSecClassGenericPassword 用于存储应用令牌和机密数据;kSecClassInternetPassword 用于存储带有主机和协议属性的服务器凭据。大多数应用机密数据都使用通用密码类型。

import Security
// kSecClassGenericPassword   -> tokens, API keys
// kSecClassInternetPassword  -> server logins
// kSecClassKey / Certificate -> crypto material
let cls = kSecClassGenericPassword
_ = cls

添加项目

SecItemAdd 会插入新项目。您需要将数据作为 Data 放在 kSecValueData 下,并提供用于标识项目的属性。它会返回一个 OSStatus;errSecSuccess 表示操作成功。

import Security
func save(_ token: String, account: String) -> Bool {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecValueData as String: Data(token.utf8)
    ]
    return SecItemAdd(query as CFDictionary, nil)
        == errSecSuccess
}

处理重复项目

如果添加项目时其标识属性已经存在,就会返回 errSecDuplicateItem。稳健的保存逻辑会先尝试 SecItemAdd,遇到重复项目后再回退到 SecItemUpdate,这是一种 upsert 模式。

import Security
func upsert(_ data: Data, account: String) -> Bool {
    let base: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account]
    var add = base
    add[kSecValueData as String] = data
    let status = SecItemAdd(add as CFDictionary, nil)
    if status == errSecDuplicateItem {
        return SecItemUpdate(base as CFDictionary,
            [kSecValueData as String: data] as CFDictionary)
            == errSecSuccess
    }
    return status == errSecSuccess
}

读取项目

SecItemCopyMatching 会执行搜索。若要获取机密数据的字节,必须将 kSecReturnData 设置为 true,并将 kSecMatchLimit 设置为 kSecMatchLimitOne。结果会通过输出参数以 CFTypeRef 的形式返回。

import Security
func load(account: String) -> Data? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne]
    var result: CFTypeRef?
    let status = SecItemCopyMatching(
        query as CFDictionary, &result)
    guard status == errSecSuccess else { return nil }
    return result as? Data
}

更新项目

SecItemUpdate 接受两个字典:一个用于查找项目的查询字典,以及一个用于指定要更新属性的字典。只有您列出的属性会发生变化,其他所有内容都会保留。

import Security
func update(_ newData: Data, account: String) -> Bool {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account]
    let attrs: [String: Any] = [
        kSecValueData as String: newData]
    return SecItemUpdate(query as CFDictionary,
        attrs as CFDictionary) == errSecSuccess
}

删除项目

SecItemDelete 会移除匹配的项目。删除不存在的项目会返回 errSecItemNotFound;在注销时清除凭据,可以将这种结果视为成功。

import Security
func delete(account: String) -> Bool {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account]
    let status = SecItemDelete(query as CFDictionary)
    return status == errSecSuccess
        || status == errSecItemNotFound
}

解析 OSStatus

错误以整数形式的 OSStatus 代码表示。SecCopyErrorMessageString 会将其中一个代码转换为人类可读的描述,这在调试保存或读取失败的原因时非常有价值。

import Security
func describe(_ status: OSStatus) -> String {
    return SecCopyErrorMessageString(status, nil)
        as String? ?? "OSStatus \(status)"
}

唯一标识项目

项目通过属性组合进行匹配——对于通用密码,通常是 kSecAttrService 加上 kSecAttrAccount。请选择稳定且专用于本应用的服务字符串,以避免不同机密数据之间发生冲突。

import Security
// Uniqueness for generic passwords usually comes from:
//   service (your bundle id) + account (the username)
let service = "com.example.app.auth"
let account = "current-user"
_ = (service, account)

小型封装

由于原始 API 十分冗长,团队通常会将其封装在一个公开 save、read 和 delete 的小型类型中。这样可以将 kSec 的样板代码集中在一个地方,同时让调用位置保持简洁。

import Security
struct TokenStore {
    let service = "com.example.app.auth"
    func read(_ account: String) -> Data? {
        let q: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne]
        var out: CFTypeRef?
        return SecItemCopyMatching(q as CFDictionary, &out)
            == errSecSuccess ? out as? Data : nil
    }
}

快速检查

请回忆一下,获取已存储机密数据字节的正确方法。

回顾

您已经学习了钥匙串的增删改查:

  • 将凭据存储在加密的钥匙串中,绝不要存储在 UserDefaults 或文件中。
  • 使用 kSec 查询字典描述项目,并选择类似 kSecClassGenericPassword 的类别。
  • SecItemAdd / SecItemCopyMatching / SecItemUpdate / SecItemDelete 覆盖整个生命周期;使用插入或更新操作处理 errSecDuplicateItem。
  • 通过 service + account 标识项目,并解析 OSStatus 以进行调试。

常见问题解答

「在钥匙串中存储机密」课时是免费的吗?

是的 — 「在钥匙串中存储机密」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Swift Academy 课程的其余内容,请升级到 CoddyKit PRO。 Swift Academy 课程共包含 4 节课。

「在钥匙串中存储机密」这节课中我会学到什么?

安全地保存和获取凭据。 你通过在浏览器中直接运行的动手代码来练习 Swift Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Swift Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Swift Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「在钥匙串中存储机密」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Swift Academy 课中编写并运行代码吗?

能。每节 Swift Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 在钥匙串中存储机密
  2. 钥匙串访问控制
  3. 生物识别身份验证
  4. 数据保护与加密
← 返回 Swift Academy