axiom-keychain-diag について
Apple Security framework と Keychain の障害を調べる開発者向けトラブルシューティングガイドです。よくある症状を確認手順と判断フローに整理し、権限、データ保護、macOS の挙動差を扱います。
主な機能
- 01Keychain の一般的なエラーコード診断
- 02項目属性とクエリ不一致の確認
- 03アクセシビリティとバックグラウンド時刻の調査
- 04アクセスグループの権限確認
- 05macOS の Keychain 動作比較
活用シーン
- 01errSecDuplicateItem や errSecItemNotFound の原因調査
- 02バックグラウンド実行中の Keychain 障害調査
- 03アプリ更新後に項目が消える原因の特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-keychain-diagインストール後、Claude Code で/axiom-keychain-diagを入力して読み込まれたか確認します。
詳細
キーチェーン診断
Security フレームワークの障害を体系的にトラブルシューティングします。重複制約違反、クエリの不一致、データ保護のタイミング、アクセスグループのエンタイトルメント、アップデート後に項目が消える問題、Mac のシムの動作の違いを扱います。
概要
基本原則:キーチェーン操作が失敗する場合、問題は通常、次のいずれかです。
- 重複制約の不一致(errSecDuplicateItem)— 25%
- クエリ属性の混同(errSecItemNotFound)— 25%
- データ保護/バックグラウンド実行のタイミング(errSecInteractionNotAllowed)— 20%
- アクセスグループ/エンタイトルメントの不一致(errSecMissingEntitlement)— 15%
- Mac のシムの動作の違い — 10%
- アプリのアップデート後に項目が失われる(エンタイトルメントまたは App ID プレフィックスの変更)— 5%
キーチェーンのコードを変更する前に、必ず既存の項目をダンプし、属性を比較してください。
注意すべき兆候
キーチェーン固有の問題を示す症状:
| 症状 | 考えられる原因 |
|---|---|
| クエリで見つからなかったのに errSecDuplicateItem が発生する | 追加クエリの属性が一意ではありません。一意性はクエリ全体ではなく、クラスと主キー属性の組み合わせで決まります |
| 追加したばかりの項目に対して errSecItemNotFound が発生する | kSecClass が誤っている、属性で検索を誤って絞り込んでいる、またはアクセスグループが一致していません |
| バックグラウンドで errSecInteractionNotAllowed が発生する | kSecAttrAccessibleWhenUnlocked(デフォルト)+デバイスがロック中+バックグラウンド実行 |
| errSecMissingEntitlement が発生する | アクセスグループが keychain-access-groups エンタイトルメントに含まれていません |
| errSecNoSuchAttr が発生する | 属性が項目クラスでサポートされていません(例:kSecClassGenericPassword での kSecAttrApplicationTag) |
| Mac で errSecAuthFailed が発生する | ファイルベースのキーチェーンがロックされているか、タイムアウトしています |
| アプリのアップデート後に項目が消える | バージョン間でアクセスグループまたはエンタイトルメントが変更されています |
| チームの変更後に項目が消える | App ID プレフィックスが変更され、以前のプレフィックスに紐づいた項目にアクセスできません |
| SecItemDelete ですべて削除される | kSecMatchLimit は削除では無関係です。一致する項目がすべて削除されます |
| シミュレーターでは動作するが、デバイスでは失敗する | シミュレーターはデータ保護を強制しませんが、デバイスでは強制されます |
自己正当化を避ける
| 自己正当化 | うまくいかない理由 | 時間コスト |
|---|---|---|
| 「ラッパーが処理してくれる」 | ラッパーは重複制約を隠します。errSecDuplicateItem が発生したとき、見えないものはデバッグできません。結局、ラッパーのソースを読むことになります。 | ラッパーを解析するのに30分以上 |
| 「削除して追加し直せばいい」 | 項目のメタデータが失われ、iCloud キーチェーンの同期状態が壊れます。また、削除クエリが意図より広いと、ほかの項目まで気づかないまま削除されます。 | 認証情報が見つからない問題のデバッグに1~2時間 |
| 「このトークンだけなら UserDefaults で問題ない」 | UserDefaults は暗号化されず、iCloud にバックアップされ、MDM プロファイルから参照でき、デバイスのバックアップ抽出によって読み取れます。セキュリティ監査で必ず指摘されます。 | 審査で却下された後、キーチェーンへ移行するのに数時間 |
| 「errSecItemNotFound は項目が存在しないという意味だ」 | クエリに一致しなかったという意味です。検索条件とは異なる属性で項目が存在する可能性があります。確認するため、すべての項目をダンプしてください。 | 項目がすでに存在するのに追加ロジックを書き直すため、30~60分 |
| 「リリース後にキーチェーンのコードを直せばいい」 | キーチェーンのバグは、気づかれにくいデータ損失につながります。アップデート後にユーザーは認証情報を失い、ログインできず、復旧手段もありません。低評価のレビューで初めて気づきます。 | 緊急パッチに数日+ユーザーの信頼を損なう |
必須の初手
キーチェーンのコードを変更する前に、次の診断を実行してください。
ステップ1:対象クラスの項目をすべてダンプする
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)
}
}
アプリから参照できる、そのクラスのすべての項目が表示されます。忘れていた項目も含まれます。
ステップ2:クエリと属性を比較する
追加/更新/検索クエリ内の各属性を、ダンプ結果と照合します。よくある不一致:
kSecAttrAccountとkSecAttrService— キーにはどちらを使用していますか?kSecAttrAccessGroup— デフォルトとは異なるアクセスグループを指定していますか?- 検索を絞り込む余分な属性(例:追加時には設定しているが、検索時には指定していない
kSecAttrLabel)
ステップ 3:アクセス可能クラスとデバイスのロック状態を確認する
// In your dump, look for:
// kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked (default — fails when locked)
// kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock (survives background)
アプリがバックグラウンドでキーチェーンにアクセスする場合(プッシュ通知ハンドラー、バックグラウンドフェッチなど)、デバイスがロックされていると WhenUnlocked では失敗します。
ステップ 4:アクセスグループのエンタイトルメントを確認する
codesign -d --entitlements - /path/to/YourApp.app 2>&1 | grep keychain-access-groups
クエリ内のアクセスグループが、このリストに含まれている必要があります。デフォルトのグループは $(AppIdentifierPrefix)$(CFBundleIdentifier) です。
判断ツリー
ツリー 1:errSecDuplicateItem
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"];
}
クラスごとの一意性制約:
| クラス | 主キー属性 |
|---|---|
| kSecClassGenericPassword | kSecAttrAccount + kSecAttrService + kSecAttrAccessGroup |
| kSecClassInternetPassword | kSecAttrAccount + kSecAttrSecurityDomain + kSecAttrServer + kSecAttrProtocol + kSecAttrAuthenticationType + kSecAttrPort + kSecAttrPath |
| kSecClassCertificate | kSecAttrCertificateType + kSecAttrIssuer + kSecAttrSerialNumber |
| kSecClassKey | kSecAttrKeyClass + kSecAttrKeyType + kSecAttrApplicationLabel + kSecAttrApplicationTag + kSecAttrEffectiveKeySize |
ツリー 2:errSecItemNotFound
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"];
}
ツリー 3:errSecInteractionNotAllowed
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"];
}
ツリー 4:errSecMissingEntitlement
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"];
}
ツリー 5:アプリのアップデート後にキーチェーンの項目が失われる
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"];
}
ツリー 6:Mac 固有の問題
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"];
}
ツリー 7:errSecNoSuchAttr
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"];
}
クイックリファレンステーブル
| 症状 | 確認項目 | 修正方法 |
|---|---|---|
| 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 を明示的に指定する |
高負荷シナリオ
シナリオ 1:「アップデート後にユーザーがログインできない — トークンを削除して保存し直すだけでいい」
背景:バージョン 2.1 では、Keychain Sharing のエンタイトルメントが変更されました。2.0 からアップデートしたユーザーは認証トークンを失います。サポートへの問い合わせが殺到しています。
プレッシャー:「古い項目を削除して、初回起動時に新しい項目を保存するだけでいい。」
実情:アクセスグループが変更されたため、古い項目にはアクセスできません。SecItemDelete でも見つけられません。「削除して再追加する」方法は何もせずに終わります。一方、本当の解決策は、エンタイトルメントで古いアクセスグループを復元して既存の項目を再び読み取れるようにし、その後新しいグループに移行することです。
正しい対応:古いアクセスグループを Keychain Sharing エンタイトルメントに追加し直します。初回起動時に古いグループから読み取り、新しいグループに書き込み、古いグループから削除します。2.1.1 としてリリースします。
反論用テンプレート:「削除も機能しません。古い項目は、もう読み取れない古いアクセスグループにあります。項目を読み取って移行できるよう、古いアクセスグループをエンタイトルメントに追加し直す必要があります。これは再設計ではなく、30 分でできる修正です。」
シナリオ 2:「プッシュハンドラーで errSecInteractionNotAllowed が発生 — AfterFirstUnlock に変えるだけでいい」
背景:バックグラウンドのプッシュ通知ハンドラーが、API を呼び出すためにキーチェーンから認証トークンを読み取ります。デバイスがロックされていると errSecInteractionNotAllowed が発生します。
プレッシャー:「アクセシビリティを AfterFirstUnlock に変えるだけでいい。すぐに直せます。」
実情:アクセシビリティを変更するには、古い項目を削除して、新しいアクセシビリティクラスで新しい項目を追加する必要があります。デバイスがロックされているときにプッシュハンドラー内でこれを行うと、削除は成功します(データを読み取らないため)が、追加は失敗します(AfterFirstUnlock でも初回ロック解除が必要で、デバイスを再起動したばかりの場合は、まだ初回ロック解除が行われていないためです)。その結果、ユーザーの認証情報を削除してしまいます。
正しい対応:フォアグラウンドのコード(アプリの起動時、protectedDataDidBecomeAvailable)でアクセシビリティを変更します。バックグラウンド実行パスでキーチェーン項目を移行してはいけません。
反論用テンプレート:「プッシュハンドラー内ではアクセシビリティを変更できません。削除は成功しますが、デバイスを再起動してロック解除していない場合、再追加に失敗する可能性があります。次回のアプリ起動時にフォアグラウンドで移行し、それまではプッシュハンドラーの失敗を適切に処理する必要があります。」
シナリオ 3:「キーチェーンラッパーがすべて処理してくれる — それを使うだけでいい」
背景:チームはサードパーティ製のキーチェーンラッパー(KeychainAccess、Valet など)を使っています。ラッパーの「アップサート」メソッドを使っているにもかかわらず、errSecDuplicateItem が繰り返し発生します。
プレッシャー:「ラッパーのドキュメントには重複を処理すると書いてあります。ラッパーのバグに違いありません。」
実情:ラッパーのアップサートは、検索してから追加するか、検索してから更新します。しかし、検索属性が項目クラスの一意性制約と一致しない場合、検索では見つからない一方、追加では既存の項目の主キーに引っかかります。誤った属性を使う検索は、ラッパーでは修正できません。項目の一意性を決める要素を理解し、ラッパーの設定を一致させる必要があります。
正しい対応:すべての項目をダンプして(ステップ 1)、何が存在するか確認します。ラッパーの検索属性を、項目クラスの一意性制約テーブルと照合します。主キー属性で検索するよう、ラッパーの設定を修正します。
反論用テンプレート:「ラッパーは正しく動作しています。キーチェーンの一意性制約に合っていないのは、こちらの設定です。既存の項目をダンプして、検索条件と照合させてください。これは 10 分でできる診断です。」
チェックリスト
キーチェーンの問題が修正されたと宣言する前に:
- 関連するクラスのすべての項目をダンプし、存在する項目を把握した
- kSecClass が項目の種類(GenericPassword、InternetPassword、Key)と一致することを確認した
- 一意性制約に関わる主キー属性を確認した
- kSecAttrAccessible が実行コンテキスト(フォアグラウンドとバックグラウンド)に適していることを確認した
- エンタイトルメントのアクセスグループがクエリと一致することを確認した
- デバイスでテストした(シミュレーターだけではなく、シミュレーターはデータ保護を無視するため)
- バックグラウンドのシナリオでは、デバイスの再起動後およびロック中にもテストした
- アクセシビリティを移行する場合、移行はフォアグラウンドでのみ実行され、バックグラウンドでは実行されないことを確認した
- アプリ間で共有する場合、両方のアプリが Keychain Sharing で同じアクセスグループを使用していることを確認した
リソース
ドキュメント:/security/keychain_services, /security/keychain_services/keychain_items, /security/errSecDuplicateItem, /security/errSecItemNotFound, /security/errSecInteractionNotAllowed
参考資料:Quinn "The Eskimo" — SecItem Pitfalls and Best Practices (Apple Developer Forums)、Keychain Items Fundamentals (Apple TN3137)
スキル:axiom-keychain, axiom-keychain-ref