axiom-keychain-diag

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Keychain Diagnostics

钥匙串诊断

Systematic troubleshooting for Security framework failures: uniqueness constraint violations, query mismatches, data protection timing, access group entitlements, disappearing items after updates, and Mac shim behavior differences.
针对Security框架故障的系统性排查:唯一性约束冲突、查询不匹配、数据保护时机问题、访问组权限、更新后项目消失,以及Mac垫片行为差异。

Overview

概述

Core Principle: When keychain operations fail, the problem is usually:
  1. Uniqueness constraint mismatch (errSecDuplicateItem) — 25%
  2. Query attribute confusion (errSecItemNotFound) — 25%
  3. Data protection / background timing (errSecInteractionNotAllowed) — 20%
  4. Access group / entitlement mismatch (errSecMissingEntitlement) — 15%
  5. Mac shim behavior differences — 10%
  6. Lost items after app update (entitlement or App ID prefix change) — 5%
Always dump existing items and compare attributes BEFORE changing keychain code.
核心原则:当钥匙串操作失败时,问题通常分为以下几类:
  1. 唯一性约束不匹配(errSecDuplicateItem)——占25%
  2. 查询属性混淆(errSecItemNotFound)——占25%
  3. 数据保护/后台时机(errSecInteractionNotAllowed)——占20%
  4. 访问组/权限不匹配(errSecMissingEntitlement)——占15%
  5. Mac垫片行为差异——占10%
  6. 应用更新后项目丢失(权限或App ID前缀变更)——占5%
在修改钥匙串代码前,务必先导出所有现有项目并对比属性。

Red Flags

危险信号

Symptoms that indicate keychain-specific issues:
SymptomLikely Cause
errSecDuplicateItem when query returned not foundNon-unique attributes in add query — uniqueness is per-class + primary key attributes, not per your full query
errSecItemNotFound but item was just addedWrong
kSecClass
, erroneous attribute narrowing query, or access group mismatch
errSecInteractionNotAllowed in background
kSecAttrAccessibleWhenUnlocked
(default) + device locked + background execution
errSecMissingEntitlementAccess group not listed in keychain-access-groups entitlement
errSecNoSuchAttrAttribute not supported for item class (e.g.
kSecAttrApplicationTag
on
kSecClassGenericPassword
)
errSecAuthFailed on MacFile-based keychain locked or timed out
Items gone after app updateAccess group or entitlement changed between versions
Items gone after team changeApp ID prefix changed — items keyed to old prefix are inaccessible
SecItemDelete deleted everything
kSecMatchLimit
is irrelevant for delete — it deletes ALL matching items
Keychain works in simulator, fails on deviceSimulator does not enforce data protection — device does
以下症状表明存在钥匙串相关问题:
症状可能原因
查询返回不存在却出现errSecDuplicateItem添加查询中的属性不唯一——唯一性是按类别+主键属性判定,而非你的完整查询
刚添加项目却返回errSecItemNotFound错误的
kSecClass
、查询属性错误缩小范围,或访问组不匹配
后台执行时出现errSecInteractionNotAllowed使用默认的
kSecAttrAccessibleWhenUnlocked
+ 设备已锁定 + 后台执行
errSecMissingEntitlement访问组未在keychain-access-groups权限中列出
errSecNoSuchAttr该属性不支持对应项目类别(如在
kSecClassGenericPassword
上使用
kSecAttrApplicationTag
Mac上出现errSecAuthFailed基于文件的钥匙串已锁定或超时
应用更新后项目消失版本间访问组或权限发生变更
团队变更后项目消失App ID前缀变更——旧前缀下的项目无法访问
SecItemDelete删除了所有内容
kSecMatchLimit
对删除操作无效——它会删除所有匹配的项目
模拟器中正常,设备上失败模拟器不强制执行数据保护——设备会强制执行

Anti-Rationalization

误区纠正

RationalizationWhy It FailsTime Cost
"The wrapper handles it"Wrappers hide uniqueness constraints. When errSecDuplicateItem happens, you can't debug what you can't see. You end up reading the wrapper source.30+ min unwrapping the wrapper
"I'll just delete and re-add"Loses item metadata, breaks iCloud Keychain sync state, and if the delete query is broader than intended, silently deletes other items too.1-2 hours debugging missing credentials
"UserDefaults is fine for this one token"UserDefaults is unencrypted, backed up to iCloud, visible to MDM profiles, and readable via device backup extraction. One security audit catches it.Hours migrating to keychain after rejection
"errSecItemNotFound means it's not there"It means your query didn't match. The item may exist with different attributes than you're searching for. Dump all items to check.30-60 min rewriting add logic when the item already exists
"I'll fix the keychain code after launch"Keychain bugs are silent data loss. Users lose credentials after an update, can't log in, and have no recovery path. You find out from 1-star reviews.Days of emergency patches + user trust damage
错误认知为何不成立时间成本
"封装库会处理好一切"封装库会隐藏唯一性约束。当出现errSecDuplicateItem时,你无法调试看不到的内容,最终还是要去读封装库的源码。30分钟以上的拆封装时间
"我直接删除再重新添加就行"会丢失项目元数据,破坏iCloud钥匙串同步状态;如果删除查询的范围比预期更广,还会静默删除其他项目。1-2小时调试丢失的凭证
"UserDefaults存这个令牌足够了"UserDefaults未加密,会备份到iCloud,对MDM配置文件可见,还能通过设备备份提取读取。一次安全审计就能发现问题。因被拒而迁移到钥匙串的数小时时间
"errSecItemNotFound意味着项目不存在"它仅表示你的查询未匹配到项目。项目可能存在,但属性与你搜索的不同。导出所有项目检查即可确认。30-60分钟重写添加逻辑(当项目实际已存在时)
"我上线后再修复钥匙串代码"钥匙串bug会导致静默数据丢失。用户更新后丢失凭证,无法登录,且没有恢复途径。你会从一星评价中才发现问题。数天的紧急补丁开发 + 用户信任受损

Mandatory First Steps

强制第一步操作

Before changing keychain code, run these diagnostics:
在修改钥匙串代码前,先执行以下诊断步骤:

Step 1: Dump All Items of the Relevant Class

步骤1:导出相关类别的所有项目

swift
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecMatchLimit as String: kSecMatchLimitAll,
    kSecReturnAttributes as String: true,
    kSecReturnRef as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
    for item in items {
        print(item)
    }
}
This reveals every item of that class your app can see — including ones you forgot about.
swift
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecMatchLimit as String: kSecMatchLimitAll,
    kSecReturnAttributes as String: true,
    kSecReturnRef as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
    for item in items {
        print(item)
    }
}
这会显示你的应用能看到的该类别下所有项目——包括你遗忘的项目。

Step 2: Compare Attributes Against Your Query

步骤2:对比查询与项目属性

Check each attribute in your add/update/search query against the dump output. Common mismatches:
  • kSecAttrAccount
    vs
    kSecAttrService
    — which one are you using for the key?
  • kSecAttrAccessGroup
    — are you specifying one that differs from the default?
  • Extra attributes narrowing the search (e.g.
    kSecAttrLabel
    you set on add but omit on search)
检查你的添加/更新/搜索查询中的每个属性是否与导出结果一致。常见不匹配情况:
  • kSecAttrAccount
    vs
    kSecAttrService
    ——你用哪个作为键?
  • kSecAttrAccessGroup
    ——你指定的是否与默认值不同?
  • 额外缩小搜索范围的属性(如添加时设置了
    kSecAttrLabel
    但搜索时遗漏)

Step 3: Check Accessibility Class vs Device Lock State

步骤3:检查可访问性类别与设备锁定状态

swift
// In your dump, look for:
// kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked  (default — fails when locked)
// kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock  (survives background)
If the app accesses keychain in background (push notification handlers, background fetch),
WhenUnlocked
will fail on a locked device.
swift
// 在导出结果中查找:
// kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked  (默认值——设备锁定时失败)
// kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock  (支持后台访问)
如果应用在后台访问钥匙串(推送通知处理器、后台刷新),
WhenUnlocked
在设备锁定时会失败。

Step 4: Verify Access Group Entitlements

步骤4:验证访问组权限

bash
codesign -d --entitlements - /path/to/YourApp.app 2>&1 | grep keychain-access-groups
The access group in your query must appear in this list. The default group is
$(AppIdentifierPrefix)$(CFBundleIdentifier)
.
bash
codesign -d --entitlements - /path/to/YourApp.app 2>&1 | grep keychain-access-groups
你查询中的访问组必须出现在此列表中。默认组为
$(AppIdentifierPrefix)$(CFBundleIdentifier)

Decision Trees

决策树

Tree 1: errSecDuplicateItem

决策树1:errSecDuplicateItem

dot
digraph tree1 {
    "errSecDuplicateItem?" [shape=diamond];
    "Dump all items (Step 1)" [shape=box];
    "Item with same primary keys exists?" [shape=diamond];
    "Same kSecAttrAccount + kSecAttrService?" [shape=diamond];

    "Use SecItemUpdate" [shape=box, label="Use SecItemUpdate instead.\nQuery with primary key attrs only.\nPass new values in attributesToUpdate."];
    "Query-before-add" [shape=box, label="Search first, update if found:\nSecItemCopyMatching → exists?\n  yes → SecItemUpdate\n  no → SecItemAdd"];
    "Different account/service" [shape=box, label="Your add query matches an existing\nitem on primary key attributes.\nkSecClassGenericPassword uniqueness:\n  kSecAttrAccount + kSecAttrService\n  + kSecAttrAccessGroup"];
    "Check access group" [shape=box, label="Item exists in a different access\ngroup. Your search missed it but\nadd sees it. Specify kSecAttrAccessGroup\nexplicitly in both operations."];

    "errSecDuplicateItem?" -> "Dump all items (Step 1)";
    "Dump all items (Step 1)" -> "Item with same primary keys exists?" [label="inspect"];
    "Item with same primary keys exists?" -> "Same kSecAttrAccount + kSecAttrService?" [label="yes"];
    "Item with same primary keys exists?" -> "Check access group" [label="no visible match"];
    "Same kSecAttrAccount + kSecAttrService?" -> "Use SecItemUpdate" [label="yes, want to overwrite"];
    "Same kSecAttrAccount + kSecAttrService?" -> "Different account/service" [label="no, different values"];
    "Use SecItemUpdate" -> "Query-before-add" [label="prevent future duplicates"];
}
Uniqueness constraints by class:
ClassPrimary Key Attributes
kSecClassGenericPasswordkSecAttrAccount + kSecAttrService + kSecAttrAccessGroup
kSecClassInternetPasswordkSecAttrAccount + kSecAttrSecurityDomain + kSecAttrServer + kSecAttrProtocol + kSecAttrAuthenticationType + kSecAttrPort + kSecAttrPath
kSecClassCertificatekSecAttrCertificateType + kSecAttrIssuer + kSecAttrSerialNumber
kSecClassKeykSecAttrKeyClass + kSecAttrKeyType + kSecAttrApplicationLabel + kSecAttrApplicationTag + kSecAttrEffectiveKeySize
dot
digraph tree1 {
    "errSecDuplicateItem?" [shape=diamond];
    "Dump all items (Step 1)" [shape=box];
    "Item with same primary keys exists?" [shape=diamond];
    "Same kSecAttrAccount + kSecAttrService?" [shape=diamond];

    "Use SecItemUpdate" [shape=box, label="Use SecItemUpdate instead.\nQuery with primary key attrs only.\nPass new values in attributesToUpdate."];
    "Query-before-add" [shape=box, label="Search first, update if found:\nSecItemCopyMatching → exists?\n  yes → SecItemUpdate\n  no → SecItemAdd"];
    "Different account/service" [shape=box, label="Your add query matches an existing\nitem on primary key attributes.\nkSecClassGenericPassword uniqueness:\n  kSecAttrAccount + kSecAttrService\n  + kSecAttrAccessGroup"];
    "Check access group" [shape=box, label="Item exists in a different access\ngroup. Your search missed it but\nadd sees it. Specify kSecAttrAccessGroup\nexplicitly in both operations."];

    "errSecDuplicateItem?" -> "Dump all items (Step 1)";
    "Dump all items (Step 1)" -> "Item with same primary keys exists?" [label="inspect"];
    "Item with same primary keys exists?" -> "Same kSecAttrAccount + kSecAttrService?" [label="yes"];
    "Item with same primary keys exists?" -> "Check access group" [label="no visible match"];
    "Same kSecAttrAccount + kSecAttrService?" -> "Use SecItemUpdate" [label="yes, want to overwrite"];
    "Same kSecAttrAccount + kSecAttrService?" -> "Different account/service" [label="no, different values"];
    "Use SecItemUpdate" -> "Query-before-add" [label="prevent future duplicates"];
}
按类别划分的唯一性约束
类别主键属性
kSecClassGenericPasswordkSecAttrAccount + kSecAttrService + kSecAttrAccessGroup
kSecClassInternetPasswordkSecAttrAccount + kSecAttrSecurityDomain + kSecAttrServer + kSecAttrProtocol + kSecAttrAuthenticationType + kSecAttrPort + kSecAttrPath
kSecClassCertificatekSecAttrCertificateType + kSecAttrIssuer + kSecAttrSerialNumber
kSecClassKeykSecAttrKeyClass + kSecAttrKeyType + kSecAttrApplicationLabel + kSecAttrApplicationTag + kSecAttrEffectiveKeySize

Tree 2: errSecItemNotFound

决策树2:errSecItemNotFound

dot
digraph tree2 {
    "errSecItemNotFound?" [shape=diamond];
    "Dump all items (Step 1)" [shape=box];
    "Any items returned?" [shape=diamond];
    "Correct kSecClass?" [shape=diamond];
    "Erroneous attribute?" [shape=diamond];

    "Class mismatch" [shape=box, label="Wrong kSecClass in query.\nGenericPassword vs InternetPassword\nis the most common confusion.\nKeys use kSecClassKey."];
    "Narrow query" [shape=box, label="Erroneous attribute narrows\nquery to match nothing.\nRemove attributes one at a time\nuntil item is found.\nCommon: kSecAttrLabel, kSecAttrType"];
    "Access group" [shape=box, label="Item exists in different\naccess group than query.\nCheck kSecAttrAccessGroup\nor omit it to use default."];
    "Data protection" [shape=box, label="Item exists but device is locked\nand item has WhenUnlocked accessibility.\nSee Tree 3."];
    "Not added yet" [shape=box, label="Item was never successfully added.\nCheck return value of SecItemAdd\n— was it errSecSuccess?"];

    "errSecItemNotFound?" -> "Dump all items (Step 1)";
    "Dump all items (Step 1)" -> "Any items returned?" [label="check"];
    "Any items returned?" -> "Not added yet" [label="no items at all"];
    "Any items returned?" -> "Correct kSecClass?" [label="yes, items exist"];
    "Correct kSecClass?" -> "Class mismatch" [label="no"];
    "Correct kSecClass?" -> "Erroneous attribute?" [label="yes"];
    "Erroneous attribute?" -> "Narrow query" [label="yes, extra attrs"];
    "Erroneous attribute?" -> "Access group" [label="no, attrs match"];
    "Access group" -> "Data protection" [label="access group matches too"];
}
dot
digraph tree2 {
    "errSecItemNotFound?" [shape=diamond];
    "Dump all items (Step 1)" [shape=box];
    "Any items returned?" [shape=diamond];
    "Correct kSecClass?" [shape=diamond];
    "Erroneous attribute?" [shape=diamond];

    "Class mismatch" [shape=box, label="Wrong kSecClass in query.\nGenericPassword vs InternetPassword\nis the most common confusion.\nKeys use kSecClassKey."];
    "Narrow query" [shape=box, label="Erroneous attribute narrows\nquery to match nothing.\nRemove attributes one at a time\nuntil item is found.\nCommon: kSecAttrLabel, kSecAttrType"];
    "Access group" [shape=box, label="Item exists in different\naccess group than query.\nCheck kSecAttrAccessGroup\nor omit it to use default."];
    "Data protection" [shape=box, label="Item exists but device is locked\nand item has WhenUnlocked accessibility.\nSee Tree 3."];
    "Not added yet" [shape=box, label="Item was never successfully added.\nCheck return value of SecItemAdd\n— was it errSecSuccess?"];

    "errSecItemNotFound?" -> "Dump all items (Step 1)";
    "Dump all items (Step 1)" -> "Any items returned?" [label="check"];
    "Any items returned?" -> "Not added yet" [label="no items at all"];
    "Any items returned?" -> "Correct kSecClass?" [label="yes, items exist"];
    "Correct kSecClass?" -> "Class mismatch" [label="no"];
    "Correct kSecClass?" -> "Erroneous attribute?" [label="yes"];
    "Erroneous attribute?" -> "Narrow query" [label="yes, extra attrs"];
    "Erroneous attribute?" -> "Access group" [label="no, attrs match"];
    "Access group" -> "Data protection" [label="access group matches too"];
}

Tree 3: errSecInteractionNotAllowed

决策树3:errSecInteractionNotAllowed

dot
digraph tree3 {
    "errSecInteractionNotAllowed?" [shape=diamond];
    "Background execution?" [shape=diamond];
    "Device locked?" [shape=diamond];
    "Check accessibility" [shape=diamond];

    "Change accessibility" [shape=box, label="Migrate item to\nkSecAttrAccessibleAfterFirstUnlock\nor AfterFirstUnlockThisDeviceOnly.\nRequires delete + re-add."];
    "Timing issue" [shape=box, label="App launched in background\nbefore first unlock after reboot.\nDefer keychain access until\nUIApplication.protectedDataDidBecomeAvailable"];
    "Delete trap" [shape=octagon, label="DANGER: Do NOT delete and re-add\njust to change accessibility.\nIf device is locked, the delete\nwill succeed but the add will FAIL\n— you lose the credential."];
    "Not data protection" [shape=box, label="On Mac: file-based keychain\nmay be locked. Check\nsecurity unlock-keychain.\nOr keychain requires user\ninteraction (SecAccessControl)."];
    "Check SecAccessControl" [shape=box, label="If using biometric protection\n(SecAccessControlCreateWithFlags),\nbackground access is impossible.\nStore a separate non-biometric\ncopy for background use."];

    "errSecInteractionNotAllowed?" -> "Background execution?" [label="check context"];
    "Background execution?" -> "Device locked?" [label="yes"];
    "Background execution?" -> "Not data protection" [label="no, foreground"];
    "Device locked?" -> "Check accessibility" [label="yes"];
    "Device locked?" -> "Check SecAccessControl" [label="no, unlocked but still fails"];
    "Check accessibility" -> "Timing issue" [label="WhenUnlocked + after reboot"];
    "Check accessibility" -> "Change accessibility" [label="WhenUnlocked + normal lock"];
    "Change accessibility" -> "Delete trap" [label="WARNING"];
}
dot
digraph tree3 {
    "errSecInteractionNotAllowed?" [shape=diamond];
    "Background execution?" [shape=diamond];
    "Device locked?" [shape=diamond];
    "Check accessibility" [shape=diamond];

    "Change accessibility" [shape=box, label="Migrate item to\nkSecAttrAccessibleAfterFirstUnlock\nor AfterFirstUnlockThisDeviceOnly.\nRequires delete + re-add."];
    "Timing issue" [shape=box, label="App launched in background\nbefore first unlock after reboot.\nDefer keychain access until\nUIApplication.protectedDataDidBecomeAvailable"];
    "Delete trap" [shape=octagon, label="DANGER: Do NOT delete and re-add\njust to change accessibility.\nIf device is locked, the delete\nwill succeed but the add will FAIL\n— you lose the credential."];
    "Not data protection" [shape=box, label="On Mac: file-based keychain\nmay be locked. Check\nsecurity unlock-keychain.\nOr keychain requires user\ninteraction (SecAccessControl)."];
    "Check SecAccessControl" [shape=box, label="If using biometric protection\n(SecAccessControlCreateWithFlags),\nbackground access is impossible.\nStore a separate non-biometric\ncopy for background use."];

    "errSecInteractionNotAllowed?" -> "Background execution?" [label="check context"];
    "Background execution?" -> "Device locked?" [label="yes"];
    "Background execution?" -> "Not data protection" [label="no, foreground"];
    "Device locked?" -> "Check accessibility" [label="yes"];
    "Device locked?" -> "Check SecAccessControl" [label="no, unlocked but still fails"];
    "Check accessibility" -> "Timing issue" [label="WhenUnlocked + after reboot"];
    "Check accessibility" -> "Change accessibility" [label="WhenUnlocked + normal lock"];
    "Change accessibility" -> "Delete trap" [label="WARNING"];
}

Tree 4: errSecMissingEntitlement

决策树4:errSecMissingEntitlement

dot
digraph tree4 {
    "errSecMissingEntitlement?" [shape=diamond];
    "Using explicit access group?" [shape=diamond];
    "Check entitlements (Step 4)" [shape=box];
    "Group in entitlements?" [shape=diamond];

    "Add to entitlements" [shape=box, label="Xcode > Target >\nSigning & Capabilities >\nKeychain Sharing >\nAdd access group"];
    "Prefix mismatch" [shape=box, label="Access group must use\nApp ID prefix (Team ID or\nApp ID prefix from portal).\n$(AppIdentifierPrefix)com.your.group\nNOT just com.your.group"];
    "Shared group config" [shape=box, label="For shared keychain between apps:\n1. Same Team ID\n2. Same access group string\n3. Both apps list group in\n   Keychain Sharing capability"];
    "Default group" [shape=box, label="If not specifying access group,\ndefault is AppIdentifierPrefix +\nbundle ID. Verify your app's\nprefix hasn't changed."];

    "errSecMissingEntitlement?" -> "Using explicit access group?" [label="check query"];
    "Using explicit access group?" -> "Check entitlements (Step 4)" [label="yes"];
    "Using explicit access group?" -> "Default group" [label="no"];
    "Check entitlements (Step 4)" -> "Group in entitlements?" [label="inspect"];
    "Group in entitlements?" -> "Prefix mismatch" [label="no, group missing"];
    "Group in entitlements?" -> "Shared group config" [label="yes but still fails"];
    "Prefix mismatch" -> "Add to entitlements" [label="fix"];
}
dot
digraph tree4 {
    "errSecMissingEntitlement?" [shape=diamond];
    "Using explicit access group?" [shape=diamond];
    "Check entitlements (Step 4)" [shape=box];
    "Group in entitlements?" [shape=diamond];

    "Add to entitlements" [shape=box, label="Xcode > Target >\nSigning & Capabilities >\nKeychain Sharing >\nAdd access group"];
    "Prefix mismatch" [shape=box, label="Access group must use\nApp ID prefix (Team ID or\nApp ID prefix from portal).\n$(AppIdentifierPrefix)com.your.group\nNOT just com.your.group"];
    "Shared group config" [shape=box, label="For shared keychain between apps:\n1. Same Team ID\n2. Same access group string\n3. Both apps list group in\n   Keychain Sharing capability"];
    "Default group" [shape=box, label="If not specifying access group,\ndefault is AppIdentifierPrefix +\nbundle ID. Verify your app's\nprefix hasn't changed."];

    "errSecMissingEntitlement?" -> "Using explicit access group?" [label="check query"];
    "Using explicit access group?" -> "Check entitlements (Step 4)" [label="yes"];
    "Using explicit access group?" -> "Default group" [label="no"];
    "Check entitlements (Step 4)" -> "Group in entitlements?" [label="inspect"];
    "Group in entitlements?" -> "Prefix mismatch" [label="no, group missing"];
    "Group in entitlements?" -> "Shared group config" [label="yes but still fails"];
    "Prefix mismatch" -> "Add to entitlements" [label="fix"];
}

Tree 5: Lost Keychain Items After App Update

决策树5:应用更新后钥匙串项目丢失

dot
digraph tree5 {
    "Items gone after update?" [shape=diamond];
    "Access group changed?" [shape=diamond];
    "App ID prefix changed?" [shape=diamond];
    "Entitlements file changed?" [shape=diamond];

    "Restore access group" [shape=box, label="Add the OLD access group back\nto Keychain Sharing entitlement.\nItems are keyed to the group\nthey were created with."];
    "Prefix migration" [shape=box, label="App ID prefix change means\nnew items are under new prefix.\nOld items are under old prefix.\nAdd both prefixes to entitlements\nor migrate items at first launch."];
    "Entitlement restore" [shape=box, label="If Keychain Sharing was removed,\nthe default access group changed.\nRe-add Keychain Sharing with\nthe original group name."];
    "Query change" [shape=box, label="Check if the query attributes\nchanged between versions.\nDump items (Step 1) to verify\nitems still exist under old attrs."];

    "Items gone after update?" -> "Access group changed?" [label="check entitlements diff"];
    "Access group changed?" -> "Restore access group" [label="yes"];
    "Access group changed?" -> "App ID prefix changed?" [label="no"];
    "App ID prefix changed?" -> "Prefix migration" [label="yes, team transfer"];
    "App ID prefix changed?" -> "Entitlements file changed?" [label="no"];
    "Entitlements file changed?" -> "Entitlement restore" [label="yes"];
    "Entitlements file changed?" -> "Query change" [label="no, entitlements identical"];
}
dot
digraph tree5 {
    "Items gone after update?" [shape=diamond];
    "Access group changed?" [shape=diamond];
    "App ID prefix changed?" [shape=diamond];
    "Entitlements file changed?" [shape=diamond];

    "Restore access group" [shape=box, label="Add the OLD access group back\nto Keychain Sharing entitlement.\nItems are keyed to the group\nthey were created with."];
    "Prefix migration" [shape=box, label="App ID prefix change means\nnew items are under new prefix.\nOld items are under old prefix.\nAdd both prefixes to entitlements\nor migrate items at first launch."];
    "Entitlement restore" [shape=box, label="If Keychain Sharing was removed,\nthe default access group changed.\nRe-add Keychain Sharing with\nthe original group name."];
    "Query change" [shape=box, label="Check if the query attributes\nchanged between versions.\nDump items (Step 1) to verify\nitems still exist under old attrs."];

    "Items gone after update?" -> "Access group changed?" [label="check entitlements diff"];
    "Access group changed?" -> "Restore access group" [label="yes"];
    "Access group changed?" -> "App ID prefix changed?" [label="no"];
    "App ID prefix changed?" -> "Prefix migration" [label="yes, team transfer"];
    "App ID prefix changed?" -> "Entitlements file changed?" [label="no"];
    "Entitlements file changed?" -> "Entitlement restore" [label="yes"];
    "Entitlements file changed?" -> "Query change" [label="no, entitlements identical"];
}

Tree 6: Mac-Specific Issues

决策树6:Mac特定问题

dot
digraph tree6 {
    "Mac keychain issue?" [shape=diamond];
    "Catalyst or native?" [shape=diamond];
    "File-based keychain?" [shape=diamond];

    "Shim behavior" [shape=box, label="Mac Catalyst uses iOS-style\ndata-protection keychain by default.\nkSecUseDataProtectionKeychain = true\nis automatic on Catalyst.\nFile-based keychain quirks don't apply."];
    "Native Mac" [shape=box, label="Native macOS apps default to\nfile-based keychain unless you set\nkSecUseDataProtectionKeychain = true.\nFile-based has different:\n- kSecMatchLimit defaults\n- Locking behavior\n- Access control prompts"];
    "Match limit" [shape=box, label="File-based keychain default:\nkSecMatchLimit = kSecMatchLimitAll\nData-protection keychain default:\nkSecMatchLimit = kSecMatchLimitOne\nAlways set explicitly."];
    "Lock timeout" [shape=box, label="File-based keychain locks after\ntimeout (default: sleep + 5 min idle).\nerrSecAuthFailed = locked keychain.\nsecurity unlock-keychain to test."];
    "Use data protection" [shape=box, label="For cross-platform code,\nset kSecUseDataProtectionKeychain = true\non macOS. This gives iOS-identical\nbehavior on macOS 10.15+."];

    "Mac keychain issue?" -> "Catalyst or native?" [label="check target"];
    "Catalyst or native?" -> "Shim behavior" [label="Catalyst"];
    "Catalyst or native?" -> "File-based keychain?" [label="native macOS"];
    "File-based keychain?" -> "Match limit" [label="unexpected result count"];
    "File-based keychain?" -> "Lock timeout" [label="errSecAuthFailed"];
    "File-based keychain?" -> "Use data protection" [label="want iOS-identical behavior"];
    "Shim behavior" -> "Native Mac" [label="opted out of shim"];
}
dot
digraph tree6 {
    "Mac keychain issue?" [shape=diamond];
    "Catalyst or native?" [shape=diamond];
    "File-based keychain?" [shape=diamond];

    "Shim behavior" [shape=box, label="Mac Catalyst uses iOS-style\ndata-protection keychain by default.\nkSecUseDataProtectionKeychain = true\nis automatic on Catalyst.\nFile-based keychain quirks don't apply."];
    "Native Mac" [shape=box, label="Native macOS apps default to\nfile-based keychain unless you set\nkSecUseDataProtectionKeychain = true.\nFile-based has different:\n- kSecMatchLimit defaults\n- Locking behavior\n- Access control prompts"];
    "Match limit" [shape=box, label="File-based keychain default:\nkSecMatchLimit = kSecMatchLimitAll\nData-protection keychain default:\nkSecMatchLimit = kSecMatchLimitOne\nAlways set explicitly."];
    "Lock timeout" [shape=box, label="File-based keychain locks after\ntimeout (default: sleep + 5 min idle).\nerrSecAuthFailed = locked keychain.\nsecurity unlock-keychain to test."];
    "Use data protection" [shape=box, label="For cross-platform code,\nset kSecUseDataProtectionKeychain = true\non macOS. This gives iOS-identical\nbehavior on macOS 10.15+."];

    "Mac keychain issue?" -> "Catalyst or native?" [label="check target"];
    "Catalyst or native?" -> "Shim behavior" [label="Catalyst"];
    "Catalyst or native?" -> "File-based keychain?" [label="native macOS"];
    "File-based keychain?" -> "Match limit" [label="unexpected result count"];
    "File-based keychain?" -> "Lock timeout" [label="errSecAuthFailed"];
    "File-based keychain?" -> "Use data protection" [label="want iOS-identical behavior"];
    "Shim behavior" -> "Native Mac" [label="opted out of shim"];
}

Tree 7: errSecNoSuchAttr

决策树7:errSecNoSuchAttr

dot
digraph tree7 {
    "errSecNoSuchAttr?" [shape=diamond];
    "Check attr vs class" [shape=box, label="Not all attributes work\nwith all item classes.\nDump item to see which\nattributes it actually has."];
    "Common mistakes" [shape=diamond];

    "Tag on password" [shape=box, label="kSecAttrApplicationTag is for\nkSecClassKey only.\nFor passwords, use\nkSecAttrAccount or kSecAttrService."];
    "Label mismatch" [shape=box, label="kSecAttrLabel behavior differs:\n- Passwords: free-form string\n- Keys: computed from key data\n- Certs: computed from subject\nSetting it may be silently ignored."];
    "Description on key" [shape=box, label="kSecAttrDescription is for\nkSecClassGenericPassword and\nkSecClassInternetPassword only.\nNot available on keys or certs."];

    "errSecNoSuchAttr?" -> "Check attr vs class" [label="first"];
    "Check attr vs class" -> "Common mistakes" [label="identify"];
    "Common mistakes" -> "Tag on password" [label="kSecAttrApplicationTag + password"];
    "Common mistakes" -> "Label mismatch" [label="kSecAttrLabel unexpected behavior"];
    "Common mistakes" -> "Description on key" [label="kSecAttrDescription + key/cert"];
}
dot
digraph tree7 {
    "errSecNoSuchAttr?" [shape=diamond];
    "Check attr vs class" [shape=box, label="Not all attributes work\nwith all item classes.\nDump item to see which\nattributes it actually has."];
    "Common mistakes" [shape=diamond];

    "Tag on password" [shape=box, label="kSecAttrApplicationTag is for\nkSecClassKey only.\nFor passwords, use\nkSecAttrAccount or kSecAttrService."];
    "Label mismatch" [shape=box, label="kSecAttrLabel behavior differs:\n- Passwords: free-form string\n- Keys: computed from key data\n- Certs: computed from subject\nSetting it may be silently ignored."];
    "Description on key" [shape=box, label="kSecAttrDescription is for\nkSecClassGenericPassword and\nkSecClassInternetPassword only.\nNot available on keys or certs."];

    "errSecNoSuchAttr?" -> "Check attr vs class" [label="first"];
    "Check attr vs class" -> "Common mistakes" [label="identify"];
    "Common mistakes" -> "Tag on password" [label="kSecAttrApplicationTag + password"];
    "Common mistakes" -> "Label mismatch" [label="kSecAttrLabel unexpected behavior"];
    "Common mistakes" -> "Description on key" [label="kSecAttrDescription + key/cert"];
}

Quick Reference Table

快速参考表

SymptomCheckFix
errSecDuplicateItemDump items (Step 1), compare primary key attrsUse SecItemUpdate or query-before-add pattern
errSecItemNotFoundDump items, verify kSecClass + attributes matchRemove erroneous attributes, fix class
errSecInteractionNotAllowed in backgroundCheck kSecAttrAccessible valueMigrate to AfterFirstUnlock (delete + re-add while unlocked)
errSecInteractionNotAllowed after rebootCheck if first unlock happenedDefer access until protectedDataDidBecomeAvailable
errSecMissingEntitlement
codesign -d --entitlements -
for access groups
Add group to Keychain Sharing capability
errSecNoSuchAttrCheck attribute compatibility with item classUse correct attribute for the class
errSecAuthFailed on MacCheck if file-based keychain is locked
security unlock-keychain
or use data-protection keychain
Items gone after updateDiff entitlements between versionsRestore old access group, migrate items
Items gone after team changeCheck App ID prefix changeAdd both prefixes to entitlements
Delete removed too many itemsReview delete query specificityAlways specify all primary key attrs in delete query
Works in simulator, fails on deviceCheck accessibility classSimulator ignores data protection — test on device
Inconsistent Mac vs iOS behaviorCheck kSecUseDataProtectionKeychainSet to true for consistent cross-platform behavior
Query returns wrong itemCheck kSecMatchLimitAlways set explicitly — defaults differ by keychain type
Biometric item fails in backgroundCheck SecAccessControl flagsStore separate non-biometric copy for background
SecItemAdd returns errSecSuccess but search failsCheck if access groups differ between add and searchSpecify kSecAttrAccessGroup explicitly in both
症状检查项修复方案
errSecDuplicateItem导出项目(步骤1),对比主键属性使用SecItemUpdate或先查询再添加的模式
errSecItemNotFound导出项目,验证kSecClass + 属性是否匹配移除错误属性,修正类别
后台出现errSecInteractionNotAllowed检查kSecAttrAccessible值迁移到AfterFirstUnlock(仅在设备解锁时删除并重新添加)
重启后出现errSecInteractionNotAllowed检查是否已完成首次解锁延迟访问直到protectedDataDidBecomeAvailable
errSecMissingEntitlement执行
codesign -d --entitlements -
查看访问组
将组添加到Keychain Sharing权限中
errSecNoSuchAttr检查属性与项目类别的兼容性使用该类别支持的正确属性
Mac上出现errSecAuthFailed检查基于文件的钥匙串是否已锁定执行
security unlock-keychain
或使用数据保护钥匙串
应用更新后项目消失对比版本间的权限差异恢复旧访问组,迁移项目
团队变更后项目消失检查App ID前缀是否变更在权限中添加新旧两个前缀
删除操作移除了过多项目检查删除查询的特异性始终在删除查询中指定所有主键属性
模拟器正常,设备上失败检查可访问性类别模拟器忽略数据保护——务必在设备上测试
Mac与iOS行为不一致检查kSecUseDataProtectionKeychain设置为true以实现跨平台一致行为
查询返回错误项目检查kSecMatchLimit始终显式设置——默认值因钥匙串类型而异
生物识别项目在后台失败检查SecAccessControl标志存储单独的非生物识别副本用于后台访问
SecItemAdd返回errSecSuccess但搜索失败检查添加与搜索的访问组是否不同在两个操作中都显式指定kSecAttrAccessGroup

Pressure Scenarios

压力场景

Scenario 1: "Users can't log in after the update — just clear and re-store the token"

场景1:"用户更新后无法登录——直接清除并重新存储令牌"

Context: Version 2.1 shipped with a Keychain Sharing entitlement change. Users updating from 2.0 lose their auth tokens. Support tickets are flooding in.
Pressure: "Just delete the old item and store a new one on first launch."
Reality: The old item is inaccessible because the access group changed — SecItemDelete can't find it either. The "delete and re-add" approach silently does nothing. Meanwhile, the real fix is restoring the old access group in entitlements so existing items are readable again, then migrating to the new group.
Correct action: Add the old access group back to the Keychain Sharing entitlement. On first launch, read from old group, write to new group, delete from old group. Ship as 2.1.1.
Push-back template: "The delete won't work either — the old items are under the old access group that we can no longer read. We need to add the old access group back to our entitlements so we can read and migrate those items. This is a 30-minute fix, not a redesign."
背景:2.1版本发布时修改了Keychain Sharing权限。从2.0版本更新的用户丢失了他们的认证令牌,支持工单蜂拥而至。
压力:"首次启动时直接删除旧项目并存储新令牌就行。"
实际情况:旧项目因访问组变更而无法访问——SecItemDelete也找不到它。"删除再添加"的方法会静默失败。真正的修复方案是在权限中恢复旧访问组,使现有项目可被读取,然后迁移到新组。
正确操作:将旧访问组重新添加到Keychain Sharing权限中。首次启动时,从旧组读取,写入新组,再从旧组删除。发布2.1.1版本。
反驳模板:"删除操作也无效——旧项目在我们无法再读取的旧访问组下。我们需要把旧访问组重新添加到权限中,这样才能读取并迁移这些项目。这是30分钟的修复,不需要重新设计。"

Scenario 2: "errSecInteractionNotAllowed in push handler — just change to AfterFirstUnlock"

场景2:"推送处理器中出现errSecInteractionNotAllowed——直接改成AfterFirstUnlock"

Context: Background push notification handler reads an auth token from keychain to call an API. Fails with errSecInteractionNotAllowed when device is locked.
Pressure: "Just change the accessibility to AfterFirstUnlock. Quick fix."
Reality: Changing accessibility requires deleting the old item and adding a new one with the new accessibility class. If you do this in the push handler while the device is locked, the delete succeeds (it doesn't read data) but the add fails (AfterFirstUnlock still requires first unlock, and if the device just rebooted, first unlock hasn't happened). You just deleted the user's credential.
Correct action: Change accessibility in foreground code (app launch,
protectedDataDidBecomeAvailable
). Never migrate keychain items in background execution paths.
Push-back template: "We can't change accessibility in the push handler — the delete works but the re-add can fail if the device rebooted without unlocking. We need to migrate in the foreground on next app launch, and handle the push handler failure gracefully until then."
背景:后台推送通知处理器从钥匙串读取认证令牌以调用API,设备锁定时出现errSecInteractionNotAllowed。
压力:"直接把可访问性改成AfterFirstUnlock就行,快速修复。"
实际情况:修改可访问性需要删除旧项目并添加新的可访问性类别的项目。如果在推送处理器中执行此操作且设备已锁定,删除会成功(不需要读取数据)但添加会失败(AfterFirstUnlock仍需首次解锁,如果设备刚重启,首次解锁还未完成)。你会丢失用户的凭证。
正确操作:在前台代码中修改可访问性(应用启动、
protectedDataDidBecomeAvailable
时)。绝不要在后台执行路径中迁移钥匙串项目。
反驳模板:"我们不能在推送处理器中修改可访问性——删除会成功但重新添加可能在设备重启未解锁时失败。我们需要在下次应用启动时在前台迁移,并在此之前优雅处理推送处理器的失败情况。"

Scenario 3: "The keychain wrapper handles all this — just use it"

场景3:"钥匙串封装库会处理所有问题——直接用它就行"

Context: Team uses a third-party keychain wrapper (KeychainAccess, Valet, etc.). errSecDuplicateItem keeps happening despite the wrapper's "upsert" method.
Pressure: "The wrapper documentation says it handles duplicates. Must be a bug in the wrapper."
Reality: The wrapper's upsert does query-then-add or query-then-update. But if your query attributes don't match the uniqueness constraints of the item class, the search returns not-found while the add hits the existing item's primary key. The wrapper can't fix a query that uses the wrong attributes. You need to understand what makes items unique and ensure your wrapper configuration matches.
Correct action: Dump all items (Step 1) to see what exists. Compare the wrapper's query attributes against the item class uniqueness constraints table. Fix the wrapper configuration to query on primary key attributes.
Push-back template: "The wrapper works correctly — it's our configuration that doesn't match the keychain's uniqueness constraints. Let me dump the existing items and compare against our query. This is a 10-minute diagnosis."
背景:团队使用第三方钥匙串封装库(KeychainAccess、Valet等)。尽管封装库有"upsert"方法,errSecDuplicateItem仍持续出现。
压力:"封装库文档说它会处理重复项,肯定是封装库的bug。"
实际情况:封装库的upsert是先查询再添加或更新。但如果你的查询属性与项目类别的唯一性约束不匹配,搜索会返回不存在但添加会命中现有项目的主键。封装库无法修复使用错误属性的查询。你需要理解项目的唯一性判定规则,并确保封装库配置与之匹配。
正确操作:导出所有项目(步骤1)查看实际存在的项目。对比封装库的查询属性与项目类别唯一性约束表。修正封装库的配置使其查询主键属性。
反驳模板:"封装库本身没问题——是我们的配置与钥匙串的唯一性约束不匹配。让我导出现有项目并对比查询条件,这是10分钟的诊断工作。"

Checklist

检查清单

Before declaring a keychain issue fixed:
  • Dumped all items of relevant class — understand what exists
  • Verified kSecClass matches the item type (GenericPassword vs InternetPassword vs Key)
  • Checked primary key attributes for uniqueness constraints
  • Confirmed kSecAttrAccessible suits the execution context (foreground vs background)
  • Verified access group in entitlements matches query
  • Tested on device (not just simulator — simulator ignores data protection)
  • Tested after device reboot + lock for background scenarios
  • If migrating accessibility: migration runs in foreground only, never background
  • If sharing between apps: both apps have same access group in Keychain Sharing
在宣布钥匙串问题修复前,请完成以下检查:
  • 导出相关类别的所有项目——了解实际存在的内容
  • 验证kSecClass与项目类型匹配(GenericPassword vs InternetPassword vs Key)
  • 检查主键属性是否符合唯一性约束
  • 确认kSecAttrAccessible适合执行上下文(前台 vs 后台)
  • 验证权限中的访问组与查询一致
  • 在设备上测试(不只是模拟器——模拟器忽略数据保护)
  • 测试设备重启+锁定后的后台场景
  • 如果迁移可访问性:仅在前台执行迁移,绝不在后台
  • 如果在应用间共享:两个应用都在Keychain Sharing中列出相同的访问组

Resources

资源

Docs: /security/keychain_services, /security/keychain_services/keychain_items, /security/errSecDuplicateItem, /security/errSecItemNotFound, /security/errSecInteractionNotAllowed
Reference: Quinn "The Eskimo" — SecItem Pitfalls and Best Practices (Apple Developer Forums), Keychain Items Fundamentals (Apple TN3137)
Skills: axiom-keychain, axiom-keychain-ref
文档:/security/keychain_services, /security/keychain_services/keychain_items, /security/errSecDuplicateItem, /security/errSecItemNotFound, /security/errSecInteractionNotAllowed
参考资料:Quinn "The Eskimo" — SecItem陷阱与最佳实践(Apple开发者论坛),钥匙串项目基础(Apple TN3137)
技能:axiom-keychain, axiom-keychain-ref