Keychain 共享:访问组、扩展与跨设备同步 原文标题:swift-security-skill/swift-security-expert/references/keychain-sharing.md at main · ivan-magda/swift-security-skill
内容概要总结
本文是 GitHub 仓库 swift-security-skill 中的参考文档 keychain-sharing.md,系统讲解 Apple 平台上 Keychain 凭证共享的访问组(access group)机制。核心观点:访问组是 Apple 平台在应用与扩展之间共享凭证的唯一机制,正确配置需要精确的 Team ID 前缀、每个 target 各自的 entitlement,以及在代码中显式使用 kSecAttrAccessGroup——这三项要求大多数 AI 生成的代码都会弄错。文档详述:系统如何按「keychain 访问组 → 应用标识符(TeamID.BundleID)→ App Groups」的确切顺序拼接出虚拟访问组数组并决定默认组;Keychain Sharing 与 App Groups 两套 entitlement 的差异(格式、Team ID 前缀、作用范围、macOS 限制);正确与错误的 Swift 代码模式(含 -34018 错误);iCloud Keychain 同步(kSecAttrSynchronizable 默认 false、ThisDeviceOnly 无法同步);跨 target 的 entitlement 配置步骤与矩阵;macOS 文件式与数据保护式两套 Keychain 的分裂(kSecUseDataProtectionKeychain);访问组间迁移需 read-add-delete;以及 Keychain 项在卸载后仍保留、团队间转移失效、watchOS 隔离等生命周期边界情形,最后给出错误码表、调试清单与 2024–2026 的变化。
翻译内容
原文内容(English)
展开文件树
keychain-sharing.md
最新提交
历史记录
458 行(323 行代码)· 29.5 KB
keychain-sharing.md
文件元数据与控件
458 行(323 行代码)· 29.5 KB
Keychain 共享:访问组、扩展与跨设备同步
范围:在应用 target、扩展与设备之间共享 keychain 项的访问组(access group)设计与 entitlement 正确性。
Keychain 访问组(access group)是 Apple 平台上在应用与扩展之间共享凭证的唯一机制。正确配置需要精确的 Team ID 前缀、每个 target 各自的 entitlement,以及在代码中显式使用 kSecAttrAccessGroup——这三项要求大多数 AI 生成的代码都会弄错。本参考涵盖访问组机制、两套 entitlement 系统、正确与错误的 Swift 模式、macOS 特定要求、iCloud 同步、平台边界情形与调试策略。所有指导均反映截至 iOS 18、macOS Sequoia 15 及 2025–2026 开发者环境下的当前行为。
权威来源:Apple《Sharing Access to Keychain Items Among a Collection of Apps》文档、TN3137《On Mac Keychain APIs and Implementations》、Apple Platform Security Guide(iCloud Keychain 同步)、Quinn "The Eskimo!" 的 DTS 论坛帖《SecItem: Fundamentals》与《SecItem: Pitfalls and Best Practices》(2025 年 5 月更新)、《Configuring Keychain Sharing》文档。
访问组如何工作
每个应用属于一个或多个访问组——即用于标记哪些进程可以读写特定 keychain 项的字符串标识符。一个应用可以属于多个组,但每个 keychain 项恰好属于一个组。securityd 守护进程在运行时通过将调用进程的 entitlement 与该项的组进行比对来强制执行访问。
系统通过按以下确切顺序拼接三个来源,为每个应用构造一个虚拟的访问组数组:
- 来自 keychain-access-groups entitlement 的 Keychain 访问组
- 应用标识符——自动生成为 TeamID.BundleID(例如 SKMME9E2Y8.com.example.MyApp)
- 来自 com.apple.security.application-groups entitlement 的 App 组(iOS 8+)
这个拼接列表中的第一项成为默认访问组。当调用 SecItemAdd 而未指定 kSecAttrAccessGroup 时,该项会落到该默认组中。当调用 SecItemCopyMatching 而未指定组时,搜索会跨越该应用所属的所有组。这一顺序意味着 keychain 访问组可以成为默认组(它出现在最前),但 App 组永远不能成为默认组,因为应用标识符总是排在它前面。
一个拥有一个 keychain 组和一个 app 组的应用示例:
[SKMME9E2Y8.com.example.SharedItems, ← keychain access group (default)
SKMME9E2Y8.com.example.MyApp, ← application identifier (automatic)
group.com.example.AppSuite] ← app group
共享被限制在单一开发团队内。来自不同开发者团队的应用无法通过访问组共享 keychain 项。每个组标识符上的 Team ID 前缀(通过代码签名的 provisioning profile 强制执行)阻止了跨团队访问。不同开发者的应用共享凭证的唯一途径是通过 iCloud Keychain + Associated Domains(基于网络域名所有权的密码自动填充),那是一种完全不同的机制。
两套 Entitlement、两种格式、不同用途
最常见的开发者错误是把 Keychain Sharing 与 App Groups 混为一谈。它们是各自独立的能力,拥有不同的 entitlement 键、不同的标识符格式和不同的作用范围。
Keychain Sharing(keychain-access-groups)
此 entitlement 仅用于在应用之间共享 keychain 项。标识符以 Team ID 为前缀:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.example.SharedItems</string>
</array>
</dict>
</plist>
$(AppIdentifierPrefix) 构建变量在签名时解析为 Team ID 后跟一个点(例如 SKMME9E2Y8.)。在代码中,需要完全解析后的字符串——"SKMME9E2Y8.com.example.SharedItems"——而不只是 "com.example.SharedItems"。
App Groups(com.apple.security.application-groups)
App Groups 共享的不只是 keychain 项:还包括共享文件容器、UserDefaults(suiteName:) 和 IPC。标识符使用 group. 前缀且没有 Team ID:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.example.AppSuite</string>
</array>
</dict>
</plist>
自 iOS 8 起,app 组名兼作 keychain 访问组——"group.com.example.AppSuite" 可用作 kSecAttrAccessGroup 的值。然而,App Groups 出现在访问组数组的最后,永远不能成为新项的默认组。一个关键的 macOS 注意事项:在 macOS 上 app 组不能用作 keychain 访问组——这是仅限 iOS/iPadOS 的功能。
对比表
| 方面 | Keychain Sharing | App Groups |
|---|---|---|
| Entitlement 键 | keychain-access-groups | com.apple.security.application-groups |
| 格式 | $(AppIdentifierPrefix)com.example.shared | group.com.example.shared |
| Team ID 前缀 | 有(通过构建变量自动) | 无(改用 group. 前缀) |
| 共享内容 | 仅 keychain 项 | 容器、UserDefaults、IPC 及 keychain 项(仅 iOS) |
| 能否为默认组 | 能(若在数组首位) | 不能 |
| macOS keychain 共享 | 能(配合数据保护式 keychain) | 不能 |
两套 entitlement 可以同时使用。如果只需要 keychain 共享,使用 Keychain Sharing。如果 App Groups 已用于共享 UserDefaults 或文件容器,它们可以在 iOS 上顺带用于 keychain 共享——但始终要显式指定 kSecAttrAccessGroup。
代码模式:正确与错误
使用显式访问组存储项
import Security
let teamID = "SKMME9E2Y8"
let accessGroup = "\(teamID).com.example.SharedItems"
let password = "s3cretT0ken".data(using: .utf8)!
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecValueData as String: password
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
print("Keychain add failed: \(status)") // -34018 = missing entitlement
return
}
Team ID 必须是来自 Apple Developer 账号的字面 10 字符字符串,而不是构建变量——$(AppIdentifierPrefix) 只在 entitlement plist 中有效,在 Swift 代码中无效。
不带 Team ID 前缀的访问组(最常见的 AI 错误)
// ❌ WRONG — Missing Team ID prefix
let accessGroup = "com.example.SharedItems"
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup, // Will fail!
kSecValueData as String: password
]
// Returns errSecMissingEntitlement (-34018) on iOS 13+
// Returns errSecItemNotFound (-25300) on older versions
Xcode 的 Keychain Sharing 界面显示的是不带前缀的 com.example.SharedItems,这既误导开发者,也误导 AI 生成器。在代码中,始终需要完整的 TEAMID.com.example.SharedItems 字符串。
应用扩展读取共享的 keychain 项
扩展 target 必须拥有自己的 Keychain Sharing 能力并使用同一个组:
// In a widget extension, share extension, or other app extension
let teamID = "SKMME9E2Y8"
let accessGroup = "\(teamID).com.example.SharedItems"
let readQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(readQuery as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
let token = String(data: data, encoding: .utf8)
// Use the shared token
}
因缺少 entitlement 而失败的扩展
// ❌ This code is syntactically correct, but the extension target is
// missing the Keychain Sharing capability in Xcode → Signing & Capabilities.
// The main app has it, but extensions are SEPARATE executable targets.
// Result: errSecMissingEntitlement (-34018)
每个可执行 target——主应用、widget 扩展、share 扩展、通知扩展——都需要各自的 Keychain Sharing entitlement。Framework 没有 entitlement;只有链接它们的 target 才有。在 Xcode 中:选择扩展 target → Signing & Capabilities → + Capability → Keychain Sharing → 添加相同的组名。
使用 kSecAttrSynchronizable 的 iCloud Keychain 同步
let syncQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: "\(teamID).com.example.SharedItems",
kSecAttrSynchronizable as String: kCFBooleanTrue!,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
kSecValueData as String: password
]
let status = SecItemAdd(syncQuery as CFDictionary, nil)
- 可同步项不能使用以 ThisDeviceOnly 结尾的 kSecAttrAccessible 值——该项将永远无法同步。尝试这样做会静默地无法跨设备同步。
- 查询可同步项时,要包含 kSecAttrSynchronizable: true 或 kSecAttrSynchronizableAny——否则搜索会排除它们。
- 用户必须在所有目标设备上启用 iCloud Keychain 并登录同一个 Apple ID。
- 同步与设备内共享是正交的:一个项可以既处于共享访问组中,又可跨设备同步。
// ✅ Query that finds both sync and non-sync items
let findQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
kSecReturnData as String: true
]
假设项默认会同步
// ❌ WRONG — This item will NOT sync to iCloud Keychain.
// kSecAttrSynchronizable defaults to false when omitted.
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecValueData as String: password
// No kSecAttrSynchronizable → stays on this device only
]
iCloud Keychain 同步严格按项选择加入。省略 kSecAttrSynchronizable 或将其设为 false 意味着该项只存在于当前设备上。已同步的项受益于端到端加密——Apple 无法解密数据。
跨 Target 的 Entitlement 设置
扩展是独立的沙盒化可执行 target,不会从包含它们的应用继承能力。
Xcode 配置步骤
- 选择主应用 target → Signing & Capabilities → + Capability → Keychain Sharing。
- 添加所需的组标识符(例如 com.example.shared)。Xcode 会在 entitlement 文件中自动加上 Team ID 前缀。
- 对每个扩展 target 重复——选择扩展 target,添加 Keychain Sharing,添加完全相同的组标识符。
- 对于 App Groups:为每个 target 添加 App Groups 能力,并使用相同的 group. 标识符。
所需 Entitlement 矩阵
| Target | keychain-access-groups | application-groups | 备注 |
|---|---|---|---|
| 主应用 | TEAMID.com.example.shared | group.com.example.appsuite | 首个条目定义默认组 |
| Share 扩展 | TEAMID.com.example.shared | group.com.example.appsuite | 必须完全一致 |
| Widget 扩展 | TEAMID.com.example.shared | group.com.example.appsuite | 独立签名与配置 |
| 通知扩展 | TEAMID.com.example.shared | group.com.example.appsuite | 适用相同规则 |
macOS 的 Keychain 分裂
macOS 维护两套完全独立的 keychain 实现,将它们混淆是无尽 bug 的来源。根据 Apple 的 TN3137:
- 文件式 keychain(File-based keychain)——可追溯到 Mac OS X 的遗留系统。使用访问控制列表(SecAccess),将项存储在 .keychain-db 文件中,是 macOS 上 SecItem API 调用的默认目标。不支持 iCloud Keychain、生物识别、Secure Enclave 密钥或访问组。
- 数据保护式 keychain(Data protection keychain)——起源于 iOS,于 10.9 通过 iCloud Keychain 来到 macOS。使用 keychain 访问组 + SecAccessControl,支持 iCloud 同步、Touch ID/Face ID 和 Secure Enclave。仅在用户登录上下文中可用——launchd 守护进程无法使用它。
使用 kSecUseDataProtectionKeychain 的跨平台 macOS 支持
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: "\(teamID).com.example.SharedItems",
kSecUseDataProtectionKeychain as String: true,
kSecValueData as String: password
]
let status = SecItemAdd(query as CFDictionary, nil)
在 macOS 上,除非以数据保护式 keychain 为目标,kSecAttrAccessGroup 会被静默忽略。将 kSecUseDataProtectionKeychain 设为 true 即选择加入 iOS 风格的 keychain 行为。在 iOS、tvOS 和 watchOS 上该键被忽略(这些平台始终使用数据保护式)。
在 macOS 上以数据保护式 keychain 为目标有两种方式:将 kSecUseDataProtectionKeychain 设为 true,或将 kSecAttrSynchronizable 设为 true(这同时会启用 iCloud 同步)。Mac Catalyst 和 Mac 上的 iOS 应用专门使用数据保护式——在那里该标志被忽略。
| 平台/运行时 | 默认 keychain | 支持访问组 | 所需标志 |
|---|---|---|---|
| iOS/iPadOS | 数据保护式 | 是 | 无 |
| Mac Catalyst | 数据保护式 | 是 | 无 |
| macOS (AppKit) | 遗留文件式 | 默认否 | kSecUseDataProtectionKeychain: true |
Apple 的 TN3137 指出文件式 keychain「正在走向弃用」。SecKeychainCreate 在 macOS 12 SDK 中被弃用。新代码应专门以数据保护式为目标,唯一例外是缺少用户上下文的 launchd 守护进程。
在访问组之间迁移项
对于已存在的 keychain 项,kSecAttrAccessGroup 是不可变的——无法通过 SecItemUpdate 更改。迁移需要一个 read-add-delete(读取-添加-删除)序列:
- 读取:通过 SecItemCopyMatching 从原始访问组获取完整项。
- 添加:用新的 kSecAttrAccessGroup 调用 SecItemAdd。
- 删除:只有在 SecItemAdd 返回 errSecSuccess 之后,才通过 SecItemDelete 删除原始项。
如果添加操作失败,原始项保持不变,从而防止数据丢失。此模式是安全的,因为它在新副本得到确认之前从不删除。
生命周期边界情形
Keychain 项在应用卸载后仍然保留
此行为未见于文档,但自 iOS 早期以来一直保持一致。Apple 曾在 iOS 10.3 beta 中尝试在移除应用时删除 keychain 项,但因兼容性问题在发布前撤回。Quinn "The Eskimo!" 警告此行为可能在不通知的情况下改变。如果应用 A 与应用 B 之间存在共享 keychain 项,删除应用 A 会使所有共享项对应用 B 保持完好。即使删除共享组中的所有应用,也不会移除孤立项——只有恢复出厂设置才能可靠地清除它们。
用于检测全新安装(因为 UserDefaults 会在卸载时被抹除)的一种常见变通方法:
func clearKeychainOnFreshInstall() {
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if !hasLaunchedBefore {
// Scope deletion to specific service/group to avoid nuking shared items
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService"
]
SecItemDelete(query as CFDictionary)
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
}
}
关于完整的版本化迁移方法与全新安装检测模式,请参阅 migration-legacy-stores.md § First-Launch Keychain Cleanup。
要点:上面的模式处理的是基本的共享上下文情形;规范文件涵盖多版本迁移协调、安全删除顺序与 CI 影响。
团队之间的应用转移会破坏 keychain 访问
项绑定到原始 Team ID。如果应用被转移到另一个开发者账号,存储在旧 Team ID 下的 keychain 项将变得不可访问。推荐的变通方法:将应用转移回来,发布一个把 keychain 数据导出/迁移到外部存储的更新,然后再转移回去。
跨开发者共享无法通过访问组实现
通过代码签名的 provisioning profile 强制实施的 Team ID 前缀,阻止来自不同团队的应用访问彼此的 keychain 项。跨开发者凭证共享需要 iCloud Keychain + Associated Domains(基于网络域名所有权的密码自动填充)。
平台特定模式
watchOS
watchOS 2+ 运行一个独立的 keychain,不通过访问组与配对的 iPhone 的 keychain 相连。在 iPhone 与 Watch 之间共享凭证需要 iCloud Keychain 同步(kSecAttrSynchronizable: true,自 watchOS 6.2 起可用)或 WatchConnectivity 数据传输。对于 watchOS 应用,请将 Keychain Sharing 添加到 WatchKit Extension target,而不是 WatchKit App target。
Widget 扩展(WidgetKit)
Widget 扩展遵循与所有应用扩展相同的规则——独立地为 widget 扩展 target 添加 Keychain Sharing 或 App Groups 能力。Widget 通常需要认证令牌来发起网络请求。将这些令牌存储在共享 keychain 组中,而不是 UserDefaults(suiteName:),后者缺乏 keychain 级加密。App Group 共享容器只使用标准文件系统加密(NSFileProtectionCompleteUntilFirstUserAuthentication),因此对于敏感凭证,keychain 是更安全的选择。
构建与分发考量
entitlement 格式与 Team ID 前缀规则在所有构建配置中保持一致:开发、Ad Hoc、TestFlight 和 App Store 分发。Team ID 是开发者账号固有的,不会在配置之间改变。
然而,每种分发类型的具体 provisioning profile 决定了允许哪些 entitlement,并嵌入正确的 AppIdentifierPrefix。请核实每种构建类型的 provisioning profile 是否正确授权了所需的访问组。
遗留账号注意事项:大多数现代账号使用 Team ID 作为 App ID 前缀,但遗留账号(2011 年 6 月之前)可能有与 Team ID 不同的按应用前缀。有报告称,向一个 target 而非另一个添加诸如 Associated Domains 之类的能力会改变前缀,导致 -34018 错误。确保共享某个 keychain 组的所有 target 拥有完全相同的能力。
当 Keychain 共享出问题时的调试
关键错误码
| 代码 | 常量 | 含义 |
|---|---|---|
| 0 | errSecSuccess | 操作成功 |
| -25299 | errSecDuplicateItem | 项已存在;应改用 SecItemUpdate |
| -25300 | errSecItemNotFound | 未找到匹配;在 iOS 13 之前对未授权组也会返回 |
| -34018 | errSecMissingEntitlement | 应用缺少指定访问组的 entitlement |
| -25308 | errSecInteractionNotAllowed | 设备已锁定且项需要 WhenUnlocked 访问 |
| -50 | errSecParam | 参数无效(缺少 kSecClass、值类型错误) |
自 iOS 13 起,查询未授权的访问组会返回明确的 errSecMissingEntitlement(-34018),而不是含糊的 errSecItemNotFound。这使得在现代操作系统版本上的调试显著更容易。
调试清单
- 核实已构建二进制上的 entitlement——而非 .entitlements 源文件:
codesign -d --entitlements :- /path/to/YourApp.app
codesign -d --entitlements :- /path/to/YourExtension.appex
比较 keychain-access-groups 数组——它们必须包含一个共同的组。
- 检查 provisioning profile:
security cms -D -i YourApp.app/embedded.mobileprovision
核实 keychain-access-groups、com.apple.security.application-groups 和 com.apple.developer.team-identifier 存在且正确。
- 在实体设备上测试。iOS 模拟器不使用真实的 provisioning profile,可能无法暴露 entitlement 问题。模拟器中的 Keychain Sharing 行为可能与设备行为不同。
- 监控系统日志。打开 Console.app,选择已连接的设备,筛选 "keychain",并复现问题。当 entitlement 检查失败时,系统会记录明确的消息,指出缺失的组。
- 检查所有共享 target 之间的 App ID 前缀是否匹配——尤其是当任何 target 启用了不同的能力时。
测试矩阵
| 场景 | 主应用 | Share 扩展 | Widget 扩展 | 预期 |
|---|---|---|---|---|
| 在 TeamID.com.example.shared 中写入/读取 | Pass | Pass | Pass | 所有 target 看到同一项 |
| 在 group.com.example.appsuite 中写入/读取 | Pass | Pass | Pass | 仅在指定 kSecAttrAccessGroup 时 |
| iCloud 同步(非 ThisDeviceOnly) | Pass | N/A | N/A | 项出现在第二台设备上 |
| 扩展中缺少 entitlement | N/A | Fail | N/A | -34018 或 -25300 |
安全威胁模型说明
- 端到端加密:已同步的 iCloud Keychain 项是端到端加密的;Apple 无法解密它们。
- 恶意设备风险:加入用户 iCloud 账号的设备可能访问或污染已同步的 keychain 项。始终将机密范围最小化,并验证从共享或同步 keychain 中取回的数据。
- 过度共享风险:放置在共享访问组中的项对该组内所有应用可读。使用尽可能窄的访问组——不要在不需要相同凭证的应用之间共享访问组。
- 孤立项:共享组中所有应用被卸载后,keychain 项仍留在设备上直到恢复出厂设置。存储高度敏感数据时请考虑这一点。
2024–2026 年的变化
核心 SecItem API 没有变化。iOS 17、18 或 macOS 14/15 中没有引入新的 keychain 共享专用 API。Apple 仍未提供 Swift 原生的 keychain 封装;基于 C 的 Security framework 仍是唯一的官方接口。
iOS 18 和 macOS Sequoia(WWDC 2024)引入的 Passwords 应用为管理密码、passkey 和验证码提供了专门的面向用户界面。它是 iCloud Keychain 之上的一层 UI——不影响 SecItem API 或访问组机制。
Passkey 增强在 WWDC 2024–2025 期间持续进行,包括自动 passkey 升级和凭证导入/导出 API(ASCredentialExportManager)。这些在凭证管理器层面运作,不引入新的 keychain 共享机制。
kSecAttrAccessibleAlways 和 kSecAttrAccessibleAlwaysThisDeviceOnly 自 iOS 12 起仍然被弃用。请使用 kSecAttrAccessibleAfterFirstUnlock 或更严格的 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly。
交叉引用
- keychain-fundamentals.md —— SecItem CRUD 模式、macOS 上的 kSecUseDataProtectionKeychain、查询字典构造
- keychain-access-control.md —— 共享项的可访问性常量、ThisDeviceOnly 与可同步的含义
- keychain-item-classes.md —— 复合主键以及 kSecAttrAccessGroup 如何与各 kSecClass 交互
- common-anti-patterns.md —— 反模式 #5(缺少 kSecAttrAccessible),在共享上下文中会加剧
- credential-storage-patterns.md —— 应用与扩展之间的 OAuth 令牌共享
结论
Apple 平台上的 Keychain 共享是一个精确的、由 entitlement 驱动的系统,微小的配置错误——缺少 Team ID 前缀、未向扩展 target 添加能力、macOS 上忘记 kSecUseDataProtectionKeychain——都会产生隐晦的错误且没有运行时警告。访问组数组的三来源拼接顺序以令开发者措手不及的方式决定了默认组与搜索范围。
三条规则可以预防大多数问题:在代码中始终包含完整的 Team ID 前缀(TEAMID.com.example.shared,绝不要只用 com.example.shared);向每个需要访问的可执行 target 添加 Keychain Sharing,而不只是主应用;在 macOS 上将 kSecUseDataProtectionKeychain 设为 true 以获得与 iOS 一致的行为。对于 iCloud 同步,请记住 kSecAttrSynchronizable 默认为 false,且查询必须显式选择加入才能找到可同步项。
小结清单
- 代码中的 Team ID 前缀——Swift 中的访问组字符串必须使用完全解析的 TEAMID.com.example.shared 格式;$(AppIdentifierPrefix) 只在 entitlement plist 中有效。
- 按 target 的 entitlement——每个可执行 target(主应用、每个扩展)都必须在 Xcode 中独立添加 Keychain Sharing 能力,并使用相同的组标识符。
- Keychain Sharing 与 App Groups——它们是各自独立的 entitlement,格式不同(带 Team ID 前缀的 keychain-access-groups 与带 group. 前缀的 com.apple.security.application-groups)。App Groups 在 macOS 上不能充当 keychain 访问组。
- 默认访问组意识——拼接后的访问组数组(keychain 组 → 应用标识符 → app 组)中的第一个条目成为默认组。App Groups 永远不能成为默认组。
- 显式 kSecAttrAccessGroup——在 SecItemAdd 和 SecItemCopyMatching 调用中始终指定访问组。在添加时省略它会使用默认组(可能出乎意料);在查询时省略它会搜索所有组(可能缓慢或过于宽泛)。
- iCloud 同步是选择加入的——kSecAttrSynchronizable 默认为 false。同步需要非 ThisDeviceOnly 的可访问性,且查询必须包含 kSecAttrSynchronizable: true 或 kSecAttrSynchronizableAny 才能找到已同步项。
- macOS 数据保护式 keychain——在所有 macOS SecItem 调用上设置 kSecUseDataProtectionKeychain: true。没有它,kSecAttrAccessGroup 会被静默忽略,并改用遗留文件式 keychain。
- 项在卸载后保留——Keychain 项在应用删除后仍存活。使用 UserDefaults 标志检测全新安装并清理陈旧项。小心限定删除范围,以免清除共享项。
- kSecAttrAccessGroup 不可变——在组之间移动项需要 read-add-delete 序列,而不是更新。
- 核实已构建二进制的 entitlement——对已构建的 .app/.appex 使用 codesign -d --entitlements :- 来确认 entitlement,而非源 .entitlements 文件。在实体设备上测试;模拟器可能无法暴露 entitlement 问题。
- watchOS 是隔离的——Apple Watch 有一个独立的 keychain,不通过访问组相连。使用 iCloud Keychain 同步或 WatchConnectivity 进行跨设备凭证共享。
You signed in with another tab or window. Reload to refresh your session.
You signed out in another tab or window. Reload to refresh your session.
You switched accounts on another tab or window. Reload to refresh your session.
Dismiss alert
Expand file tree
keychain-sharing.md
Latest commit
History
458 lines (323 loc) · 29.5 KB
keychain-sharing.md
File metadata and controls
458 lines (323 loc) · 29.5 KB
Keychain Sharing: Access Groups, Extensions, and Cross-Device Sync
Scope: Access-group design and entitlement correctness for sharing keychain items across app targets, extensions, and devices.
Keychain access groups are the sole mechanism for sharing credentials between apps and extensions on Apple platforms. Correct configuration requires exact Team ID prefixes, per-target entitlements, and explicit kSecAttrAccessGroup usage in code — three requirements that most AI-generated code gets wrong. This reference covers access group mechanics, the two entitlement systems, correct and incorrect Swift patterns, macOS-specific requirements, iCloud sync, platform edge cases, and debugging strategies. All guidance reflects current behavior through iOS 18, macOS Sequoia 15, and the 2025–2026 developer landscape.
Authoritative sources: Apple "Sharing Access to Keychain Items Among a Collection of Apps" documentation, TN3137 "On Mac Keychain APIs and Implementations," Apple Platform Security Guide (iCloud Keychain syncing), Quinn "The Eskimo!" DTS forum posts "SecItem: Fundamentals" and "SecItem: Pitfalls and Best Practices" (updated May 2025), Configuring Keychain Sharing documentation.
How Access Groups Work
Every app belongs to one or more access groups — string identifiers that tag which processes can read and write specific keychain items. An app can belong to many groups, but each keychain item belongs to exactly one. The securityd daemon enforces access by checking the calling process's entitlements against the item's group at runtime.
The system constructs a virtual array of access groups for each app by concatenating three sources in this exact order:
- Keychain access groups from the keychain-access-groups entitlement
- Application identifier — automatically generated as TeamID.BundleID (e.g., SKMME9E2Y8.com.example.MyApp)
- App groups from the com.apple.security.application-groups entitlement (iOS 8+)
The first item in this concatenated list becomes the default access group. When SecItemAdd is called without specifying kSecAttrAccessGroup, the item lands in that default group. When SecItemCopyMatching is called without specifying a group, the search spans all groups the app belongs to. This ordering means a keychain access group can be the default (it appears first), but an app group can never be the default because the application identifier always precedes it.
Example for an app with one keychain group and one app group:
[SKMME9E2Y8.com.example.SharedItems, ← keychain access group (default)
SKMME9E2Y8.com.example.MyApp, ← application identifier (automatic)
group.com.example.AppSuite] ← app group
Sharing is restricted to a single development team. Apps from different developer teams cannot share keychain items through access groups. The Team ID prefix on every group identifier, enforced through code-signed provisioning profiles, prevents cross-team access. The only way different developers' apps can share credentials is through iCloud Keychain + Associated Domains (password autofill based on web domain ownership), which is an entirely different mechanism.
Two Entitlements, Two Formats, Different Purposes
The most common developer mistake is confusing Keychain Sharing with App Groups. These are separate capabilities with different entitlement keys, different identifier formats, and different scopes.
Keychain Sharing (keychain-access-groups)
This entitlement exists solely for sharing keychain items between apps. Identifiers are prefixed with the Team ID:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.example.SharedItems</string>
</array>
</dict>
</plist>
The $(AppIdentifierPrefix) build variable resolves at signing time to the Team ID followed by a dot (e.g., SKMME9E2Y8.). In code, the fully resolved string is required — "SKMME9E2Y8.com.example.SharedItems" — not just "com.example.SharedItems".
App Groups (com.apple.security.application-groups)
App Groups share more than keychain items: shared file containers, UserDefaults(suiteName:), and IPC. The identifier uses a group. prefix with no Team ID:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.example.AppSuite</string>
</array>
</dict>
</plist>
Since iOS 8, app group names double as keychain access groups — "group.com.example.AppSuite" can be used as the kSecAttrAccessGroup value. However, App Groups appear last in the access group array and can never be the default group for new items. A critical macOS caveat: app groups cannot be used as keychain access groups on macOS — this is an iOS/iPadOS-only feature.
Comparison Table
| Aspect | Keychain Sharing | App Groups |
|---|---|---|
| Entitlement key | keychain-access-groups | com.apple.security.application-groups |
| Format | $(AppIdentifierPrefix)com.example.shared | group.com.example.shared |
| Team ID prefix | Yes (automatic via build variable) | No (group. prefix instead) |
| Shares | Keychain items only | Containers, UserDefaults, IPC, and keychain items (iOS only) |
| Can be default group | Yes (if first in array) | No |
| macOS keychain sharing | Yes (with data protection keychain) | No |
Both entitlements can be used simultaneously. If only keychain sharing is needed, use Keychain Sharing. If App Groups are already in use for shared UserDefaults or file containers, they can piggyback for keychain sharing on iOS — but always specify kSecAttrAccessGroup explicitly.
Code Patterns: Correct and Incorrect
Storing an item with an explicit access group
import Security
let teamID = "SKMME9E2Y8"
let accessGroup = "\(teamID).com.example.SharedItems"
let password = "s3cretT0ken".data(using: .utf8)!
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecValueData as String: password
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
print("Keychain add failed: \(status)") // -34018 = missing entitlement
return
}
The Team ID must be the literal 10-character string from the Apple Developer account, not a build variable — $(AppIdentifierPrefix) only works in entitlements plists, not in Swift code.
Access group without Team ID prefix (most common AI mistake)
// ❌ WRONG — Missing Team ID prefix
let accessGroup = "com.example.SharedItems"
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup, // Will fail!
kSecValueData as String: password
]
// Returns errSecMissingEntitlement (-34018) on iOS 13+
// Returns errSecItemNotFound (-25300) on older versions
Xcode's Keychain Sharing UI shows com.example.SharedItems without the prefix, which misleads developers and AI generators alike. In code, the full TEAMID.com.example.SharedItems string is always required.
App extension reading a shared keychain item
The extension target must have its own Keychain Sharing capability with the same group:
// In a widget extension, share extension, or other app extension
let teamID = "SKMME9E2Y8"
let accessGroup = "\(teamID).com.example.SharedItems"
let readQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: accessGroup,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(readQuery as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
let token = String(data: data, encoding: .utf8)
// Use the shared token
}
Extension that fails because it lacks the entitlement
// ❌ This code is syntactically correct, but the extension target is
// missing the Keychain Sharing capability in Xcode → Signing & Capabilities.
// The main app has it, but extensions are SEPARATE executable targets.
// Result: errSecMissingEntitlement (-34018)
Each executable target — main app, widget extension, share extension, notification extension — needs its own Keychain Sharing entitlement. Frameworks do not have entitlements; only the targets linking them do. In Xcode: select the extension target → Signing & Capabilities → + Capability → Keychain Sharing → add the same group name.
iCloud Keychain sync with kSecAttrSynchronizable
let syncQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: "\(teamID).com.example.SharedItems",
kSecAttrSynchronizable as String: kCFBooleanTrue!,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
kSecValueData as String: password
]
let status = SecItemAdd(syncQuery as CFDictionary, nil)
- Synchronizable items cannot use kSecAttrAccessible values ending in ThisDeviceOnly — the item would never sync. Attempting this silently fails to sync across devices.
- When querying for synchronizable items, include kSecAttrSynchronizable: true or kSecAttrSynchronizableAny — otherwise the search excludes them.
- The user must have iCloud Keychain enabled and be signed into the same Apple ID on all target devices.
- Synchronization is orthogonal to on-device sharing: an item can be both in a shared access group and synchronizable across devices.
// ✅ Query that finds both sync and non-sync items
let findQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrSynchronizable as String: kSecAttrSynchronizableAny,
kSecReturnData as String: true
]
Assuming items sync by default
// ❌ WRONG — This item will NOT sync to iCloud Keychain.
// kSecAttrSynchronizable defaults to false when omitted.
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecValueData as String: password
// No kSecAttrSynchronizable → stays on this device only
]
iCloud Keychain sync is strictly opt-in per item. Omitting kSecAttrSynchronizable or setting it to false means the item exists only on the current device. Synchronized items benefit from end-to-end encryption — Apple cannot decrypt the data.
Cross-Target Entitlements Setup
Extensions are separate sandboxed executable targets that do not inherit capabilities from their containing app.
Xcode Configuration Steps
- Select the main application target → Signing & Capabilities → + Capability → Keychain Sharing.
- Add the desired group identifier (e.g., com.example.shared). Xcode auto-prefixes with Team ID in the entitlements file.
- Repeat for every extension target — select the extension target, add Keychain Sharing, add the exact same group identifier.
- For App Groups: add the App Groups capability to each target and use the same group. identifier.
Required Entitlements Matrix
| Target | keychain-access-groups | application-groups | Notes |
|---|---|---|---|
| Main app | TEAMID.com.example.shared | group.com.example.appsuite | First entry defines default group |
| Share extension | TEAMID.com.example.shared | group.com.example.appsuite | Must match exactly |
| Widget extension | TEAMID.com.example.shared | group.com.example.appsuite | Independent signing and provisioning |
| Notification ext. | TEAMID.com.example.shared | group.com.example.appsuite | Same rules apply |
The macOS Keychain Split
macOS maintains two completely separate keychain implementations, and confusing them is a source of endless bugs. Per Apple's TN3137:
File-based keychain — the legacy system dating back to Mac OS X. Uses Access Control Lists (SecAccess), stores items in .keychain-db files, and is the default target for SecItem API calls on macOS. Does not support iCloud Keychain, biometrics, Secure Enclave keys, or access groups.
Data protection keychain — originated on iOS and arrived on macOS via iCloud Keychain in 10.9. Uses keychain access groups + SecAccessControl, supports iCloud sync, Touch ID/Face ID, and Secure Enclave. Available only in user-login contexts — launchd daemons cannot use it.
Cross-platform macOS support with kSecUseDataProtectionKeychain
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService",
kSecAttrAccount as String: "user@example.com",
kSecAttrAccessGroup as String: "\(teamID).com.example.SharedItems",
kSecUseDataProtectionKeychain as String: true,
kSecValueData as String: password
]
let status = SecItemAdd(query as CFDictionary, nil)
On macOS, kSecAttrAccessGroup is silently ignored unless the data protection keychain is targeted. Setting kSecUseDataProtectionKeychain to true opts into iOS-style keychain behavior. On iOS, tvOS, and watchOS this key is ignored (those platforms always use data protection).
Two ways to target the data protection keychain on macOS: set kSecUseDataProtectionKeychain to true, or set kSecAttrSynchronizable to true (which also enables iCloud sync). Mac Catalyst and iOS Apps on Mac use data protection exclusively — the flag is ignored there.
| Platform/Runtime | Default keychain | Access groups supported | Required flag |
|---|---|---|---|
| iOS/iPadOS | Data Protection | Yes | None |
| Mac Catalyst | Data Protection | Yes | None |
| macOS (AppKit) | Legacy file-based | No (by default) | kSecUseDataProtectionKeychain: true |
Apple's TN3137 states the file-based keychain is "on the road to deprecation." SecKeychainCreate was deprecated in the macOS 12 SDK. New code should target data protection exclusively, with the sole exception of launchd daemons that lack a user context.
Migrating Items Between Access Groups
kSecAttrAccessGroup is immutable for an existing keychain item — it cannot be changed via SecItemUpdate. Migration requires a read-add-delete sequence:
- Read: Retrieve the complete item from its original access group via SecItemCopyMatching.
- Add: Call SecItemAdd with the new kSecAttrAccessGroup.
- Delete: Only after SecItemAdd returns errSecSuccess, delete the original item via SecItemDelete.
If the add operation fails, the original item remains untouched, preventing data loss. This pattern is safe because it never deletes until the new copy is confirmed.
Lifecycle Edge Cases
Keychain items persist after app uninstall
This behavior is undocumented but has been consistent since iOS's early days. Apple attempted to delete keychain items on app removal in iOS 10.3 beta but rolled it back before release due to compatibility issues. Quinn "The Eskimo!" has warned this behavior could change without notice. If shared keychain items exist between App A and App B, deleting App A leaves all shared items intact for App B. Even deleting all apps in a shared group does not remove orphaned items — only a factory reset clears them reliably.
A common workaround for detecting fresh installs (since UserDefaults are wiped on uninstall):
func clearKeychainOnFreshInstall() {
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if !hasLaunchedBefore {
// Scope deletion to specific service/group to avoid nuking shared items
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.authService"
]
SecItemDelete(query as CFDictionary)
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
}
}
For the complete versioned migration approach and fresh-install detection pattern, see migration-legacy-stores.md § First-Launch Keychain Cleanup.
Key point: The pattern above handles the basic sharing-context case; the canonical file covers multi-version migration coordination, safe deletion ordering, and CI implications.
App transfers between teams break keychain access
Items are tied to the original Team ID. If an app is transferred to another developer account, keychain items stored under the old Team ID become inaccessible. Recommended workaround: transfer the app back, release an update that exports/migrates keychain data to an external store, then transfer again.
Cross-developer sharing is impossible via access groups
The Team ID prefix enforcement, through code-signed provisioning profiles, prevents apps from different teams from accessing each other's keychain items. Cross-developer credential sharing requires iCloud Keychain + Associated Domains (password autofill based on web domain ownership).
Platform-Specific Patterns
watchOS
watchOS 2+ runs a separate keychain not connected to the paired iPhone's keychain through access groups. Sharing credentials between iPhone and Watch requires either iCloud Keychain sync (kSecAttrSynchronizable: true, available since watchOS 6.2) or WatchConnectivity data transfer. For watchOS apps, add Keychain Sharing to the WatchKit Extension target, not the WatchKit App target.
Widget Extensions (WidgetKit)
Widget extensions follow the same rules as all app extensions — add Keychain Sharing or App Groups capabilities to the widget extension target independently. Widgets commonly need auth tokens for network requests. Store these in the shared keychain group rather than UserDefaults(suiteName:), which lacks keychain-level encryption. App Group shared containers use only standard filesystem encryption (NSFileProtectionCompleteUntilFirstUserAuthentication), making the keychain the more secure choice for sensitive credentials.
Build and Distribution Considerations
The entitlement format and Team ID prefix rules are consistent across all build configurations: development, Ad Hoc, TestFlight, and App Store distribution. The Team ID is inherent to the developer account and does not change between configurations.
However, the specific provisioning profile for each distribution type dictates which entitlements are allowed and embeds the correct AppIdentifierPrefix. Verify that the provisioning profile for each build type correctly authorizes the required access groups.
Legacy account caveat: Most modern accounts use the Team ID as the App ID prefix, but legacy accounts (pre-June 2011) may have per-app prefixes that differ from the Team ID. Adding capabilities like Associated Domains to one target but not another has been reported to change the prefix, causing -34018 errors. Ensure all targets sharing a keychain group have identical capabilities.
Debugging When Keychain Sharing Breaks
Essential Error Codes
| Code | Constant | Meaning |
|---|---|---|
| 0 | errSecSuccess | Operation succeeded |
| -25299 | errSecDuplicateItem | Item exists; use SecItemUpdate instead |
| -25300 | errSecItemNotFound | No match found; also returned pre-iOS 13 for unauthorized groups |
| -34018 | errSecMissingEntitlement | App lacks entitlement for the specified access group |
| -25308 | errSecInteractionNotAllowed | Device locked and item requires WhenUnlocked access |
| -50 | errSecParam | Invalid parameter (missing kSecClass, wrong value types) |
Starting with iOS 13, querying an unauthorized access group returns the explicit errSecMissingEntitlement (-34018) instead of the ambiguous errSecItemNotFound. This makes debugging significantly easier on modern OS versions.
Debugging Checklist
- Verify entitlements on the built binary — not the .entitlements source file:
codesign -d --entitlements :- /path/to/YourApp.app
codesign -d --entitlements :- /path/to/YourExtension.appex
Compare the keychain-access-groups arrays — they must contain a common group.
- Inspect the provisioning profile:
security cms -D -i YourApp.app/embedded.mobileprovision
Verify that keychain-access-groups, com.apple.security.application-groups, and com.apple.developer.team-identifier are present and correct.
- Test on a physical device. The iOS Simulator does not use real provisioning profiles and may not surface entitlement issues. Keychain Sharing behavior in the Simulator can differ from device behavior.
- Monitor system logs. Open Console.app, select the connected device, filter for "keychain", and reproduce the issue. The system logs explicit messages when an entitlement check fails, identifying the missing group.
- Check for App ID prefix mismatches across all sharing targets — especially if any target has different capabilities enabled.
Test Matrix
| Scenario | Main App | Share Ext | Widget Ext | Expected |
|---|---|---|---|---|
| Write/read in TeamID.com.example.shared | Pass | Pass | Pass | All targets see same item |
| Write/read in group.com.example.appsuite | Pass | Pass | Pass | Only when kSecAttrAccessGroup specified |
| iCloud sync (non-ThisDeviceOnly) | Pass | N/A | N/A | Item appears on second device |
| Missing entitlement in extension | N/A | Fail | N/A | -34018 or -25300 |
Security Threat Model Notes
- End-to-end encryption: Synchronized iCloud Keychain items are encrypted end-to-end; Apple cannot decrypt them.
- Malicious device risk: A device joined to the user's iCloud account could potentially access or poison synchronized keychain items. Always scope secrets minimally and validate data retrieved from shared or synchronized keychains.
- Over-sharing risk: Items placed in a shared access group are readable by all apps in that group. Use the narrowest possible access group — do not share an access group across apps that do not need the same credentials.
- Orphaned items: After all apps in a shared group are uninstalled, keychain items remain on-device until factory reset. Consider this when storing highly sensitive data.
What Changed in 2024–2026
The core SecItem API has not changed. No new keychain-sharing-specific APIs were introduced in iOS 17, 18, or macOS 14/15. Apple still has not shipped a Swift-native keychain wrapper; the C-based Security framework remains the only official interface.
The Passwords app introduced in iOS 18 and macOS Sequoia (WWDC 2024) provides a dedicated user-facing interface for managing passwords, passkeys, and verification codes. This is a UI layer over iCloud Keychain — it does not affect the SecItem API or access group mechanics.
Passkey enhancements continued through WWDC 2024–2025, including automatic passkey upgrades and credential import/export APIs (ASCredentialExportManager). These operate at the credential-manager level and do not introduce new keychain-sharing mechanisms.
kSecAttrAccessibleAlways and kSecAttrAccessibleAlwaysThisDeviceOnly remain deprecated since iOS 12. Use kSecAttrAccessibleAfterFirstUnlock or the more restrictive kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
Cross-References
- keychain-fundamentals.md — SecItem CRUD patterns, kSecUseDataProtectionKeychain on macOS, query dictionary construction
- keychain-access-control.md — Accessibility constants for shared items, ThisDeviceOnly vs syncable implications
- keychain-item-classes.md — Composite primary keys and how kSecAttrAccessGroup interacts with each kSecClass
- common-anti-patterns.md — Anti-pattern #5 (missing kSecAttrAccessible), which compounds in shared contexts
- credential-storage-patterns.md — OAuth token sharing between app and extensions
Conclusion
Keychain sharing on Apple platforms is a precise, entitlement-driven system where small configuration errors — a missing Team ID prefix, a capability not added to an extension target, a forgotten kSecUseDataProtectionKeychain on macOS — produce cryptic errors with no runtime warnings. The access group array's three-source concatenation order determines defaults and search scope in ways that catch developers off guard.
Three rules prevent most issues: always include the full Team ID prefix in code (TEAMID.com.example.shared, never just com.example.shared); add Keychain Sharing to every executable target that needs access, not just the main app; and set kSecUseDataProtectionKeychain to true on macOS for iOS-consistent behavior. For iCloud sync, remember that kSecAttrSynchronizable defaults to false and that queries must explicitly opt in to find synchronizable items.
Summary Checklist
- Team ID prefix in code — Access group strings in Swift must use the fully resolved TEAMID.com.example.shared format; $(AppIdentifierPrefix) only works in entitlements plists.
- Per-target entitlements — Every executable target (main app, each extension) must independently have the Keychain Sharing capability added in Xcode with the same group identifier.
- Keychain Sharing vs App Groups — These are separate entitlements with different formats (keychain-access-groups with Team ID prefix vs com.apple.security.application-groups with group. prefix). App Groups cannot serve as keychain access groups on macOS.
- Default access group awareness — The first entry in the concatenated access group array (keychain groups → app identifier → app groups) becomes the default. App Groups can never be the default.
- Explicit kSecAttrAccessGroup — Always specify the access group in both SecItemAdd and SecItemCopyMatching calls. Omitting it on add uses the default group (which may be unexpected); omitting it on query searches all groups (which may be slow or overly broad).
- iCloud sync is opt-in — kSecAttrSynchronizable defaults to false. Sync requires non-ThisDeviceOnly accessibility, and queries must include kSecAttrSynchronizable: true or kSecAttrSynchronizableAny to find synced items.
- macOS data protection keychain — Set kSecUseDataProtectionKeychain: true on all macOS SecItem calls. Without it, kSecAttrAccessGroup is silently ignored and the legacy file-based keychain is used.
- Items persist after uninstall — Keychain items survive app deletion. Use a UserDefaults flag to detect fresh installs and clean up stale items. Scope deletion carefully to avoid nuking shared items.
- kSecAttrAccessGroup is immutable — Moving an item between groups requires a read-add-delete sequence, not an update.
- Verify built binary entitlements — Use codesign -d --entitlements :- on the built .app/.appex to confirm entitlements, not the source .entitlements file. Test on physical devices; the Simulator may not surface entitlement issues.
- watchOS is isolated — The Apple Watch has a separate keychain not connected via access groups. Use iCloud Keychain sync or WatchConnectivity for cross-device credential sharing.
You can’t perform that action at this time.