@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,620 +1,500 @@
|
|
|
1
1
|
# Keychain Fundamentals
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
>
|
|
7
|
-
> **Apple Documentation:** [Keychain Services](https://sosumi.ai/documentation/security/keychain_services), [TN3137](https://sosumi.ai/documentation/technotes/tn3137-on-mac-keychains), Quinn "The Eskimo!" DTS posts: "SecItem: Fundamentals" and "SecItem: Pitfalls and Best Practices"
|
|
3
|
+
Every other reference in this skill builds on the material here: how the four
|
|
4
|
+
`SecItem` functions behave, what their dictionaries may contain, how to read
|
|
5
|
+
their status codes, and how to keep them away from the main thread.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
Primary sources: TN3137 ("On Mac keychain APIs and implementations"), the
|
|
8
|
+
Keychain Services pages in Apple's documentation, and the Apple Developer Forums posts by
|
|
9
|
+
Quinn "The Eskimo!" (DTS) titled "SecItem: Fundamentals" and "SecItem: Pitfalls
|
|
10
|
+
and Best Practices".
|
|
10
11
|
|
|
11
12
|
## Contents
|
|
12
13
|
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [Uniqueness
|
|
16
|
-
- [
|
|
17
|
-
- [Reading
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- [
|
|
26
|
-
- [Two-Tier Encryption and Query Cost](#two-tier-encryption-and-query-cost)
|
|
27
|
-
- [Query Specificity](#query-specificity)
|
|
28
|
-
- [App Launch Performance](#app-launch-performance)
|
|
29
|
-
- [Batch Operations](#batch-operations)
|
|
30
|
-
- [macOS Keychain Routing (TN3137)](#macos-keychain-routing-tn3137)
|
|
31
|
-
- [Accessibility and Data Protection Classes](#accessibility-and-data-protection-classes)
|
|
32
|
-
- [Cross-References](#cross-references)
|
|
33
|
-
- [Authoritative References](#authoritative-references)
|
|
34
|
-
- [Implementation Notes](#implementation-notes)
|
|
35
|
-
- [Summary Checklist](#summary-checklist)
|
|
14
|
+
- [How the Keychain Is Built](#how-the-keychain-is-built)
|
|
15
|
+
- [Dictionary Contracts](#dictionary-contracts)
|
|
16
|
+
- [Uniqueness](#uniqueness)
|
|
17
|
+
- [Saving: Add, Then Update on Duplicate](#saving-add-then-update-on-duplicate)
|
|
18
|
+
- [Reading](#reading)
|
|
19
|
+
- [One Place That Builds Queries](#one-place-that-builds-queries)
|
|
20
|
+
- [OSStatus Codes](#osstatus-codes)
|
|
21
|
+
- [An Actor Around the Keychain (iOS 17+ / macOS 14+)](#an-actor-around-the-keychain-ios-17--macos-14)
|
|
22
|
+
- [Performance](#performance)
|
|
23
|
+
- [macOS: Two Keychains (TN3137)](#macos-two-keychains-tn3137)
|
|
24
|
+
- [Accessibility at a Glance](#accessibility-at-a-glance)
|
|
25
|
+
- [Checklist](#checklist)
|
|
26
|
+
- [Related Files](#related-files)
|
|
36
27
|
|
|
37
|
-
##
|
|
28
|
+
## How the Keychain Is Built
|
|
38
29
|
|
|
39
|
-
The
|
|
30
|
+
The API is four C functions, one per CRUD verb. None of them touches the
|
|
31
|
+
database directly: each call travels over IPC to `securityd`, the daemon
|
|
32
|
+
that owns an encrypted SQLite store. Because the caller waits for that round
|
|
33
|
+
trip, every call blocks its thread. Keep them off `@MainActor`.
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
Items are protected in two tiers, both AES-256-GCM:
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
- the searchable attributes sit under a metadata key that is shared by the
|
|
38
|
+
table and kept cached on the Application Processor;
|
|
39
|
+
- a per-row secret key encrypts `kSecValueData`, and unwrapping it needs a trip
|
|
40
|
+
to the Secure Enclave.
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
That split explains most of the performance advice further down.
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
## Dictionary Contracts
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
2. **Item attributes** - `kSecAttrAccount`, `kSecAttrService`, etc.
|
|
51
|
-
3. **Search properties** - `kSecMatchLimit`
|
|
52
|
-
4. **Return type properties** - `kSecReturnData`, `kSecReturnAttributes`, `kSecReturnRef`, `kSecReturnPersistentRef`
|
|
53
|
-
5. **Value type properties** - `kSecValueData`, `kSecValueRef`
|
|
46
|
+
Quinn groups the keys you can put in a SecItem dictionary into five kinds:
|
|
54
47
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
48
|
+
| Group | Examples |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Item class | `kSecClass` |
|
|
51
|
+
| Attributes | `kSecAttrAccount`, `kSecAttrService`, ... |
|
|
52
|
+
| Search | `kSecMatchLimit` |
|
|
53
|
+
| Return type | `kSecReturnData`, `kSecReturnAttributes`, `kSecReturnRef`, `kSecReturnPersistentRef` |
|
|
54
|
+
| Value type | `kSecValueData`, `kSecValueRef` |
|
|
61
55
|
|
|
62
|
-
|
|
56
|
+
Each function accepts a different subset (all available since iOS 2.0):
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
| Function | Accepts | Return keys | Default match limit |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `SecItemAdd(_:_:)` | class, attributes, values | optional | n/a |
|
|
61
|
+
| `SecItemCopyMatching(_:_:)` | all five groups | needed to get anything back | `kSecMatchLimitOne` |
|
|
62
|
+
| `SecItemUpdate(_:_:)` | a query, plus a second dictionary of changes | not allowed | `kSecMatchLimitAll` |
|
|
63
|
+
| `SecItemDelete(_:)` | a query | not allowed | `kSecMatchLimitAll` |
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
The delete default deserves attention: a query that names only the class, or
|
|
66
|
+
only the service, removes every item it matches.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Give every call its own freshly built dictionary instead of reusing one. Leftover keys break the contract: a `kSecReturnData` inside an add
|
|
69
|
+
dictionary, or a `kSecClass` inside an update dictionary, fails with
|
|
70
|
+
`errSecParam` (-50).
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
## Uniqueness
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
A generic password is identified by the combination of `kSecAttrAccount`,
|
|
75
|
+
`kSecAttrService`, `kSecAttrAccessGroup` and `kSecAttrSynchronizable`.
|
|
76
|
+
`kSecAttrGeneric`, `kSecAttrLabel` and `kSecAttrDescription` play no part in
|
|
77
|
+
that identity.
|
|
73
78
|
|
|
74
|
-
|
|
79
|
+
This produces a confusing pair of results: a query that filters on a
|
|
80
|
+
non-identifying attribute such as `kSecAttrGeneric` can report
|
|
81
|
+
`errSecItemNotFound`, while an add with the same service and account still
|
|
82
|
+
reports `errSecDuplicateItem`.
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
Internet passwords are identified by account, server, protocol, authentication
|
|
85
|
+
type, port, path, security domain, access group and synchronizable. See
|
|
86
|
+
[keychain-item-classes.md](keychain-item-classes.md) for every class.
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
`SecItemUpdate` cannot change `kSecAttrAccount` or `kSecClass`. To rename an
|
|
89
|
+
account or change class, delete the old item and add a new one.
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
## Saving: Add, Then Update on Duplicate
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
❌ **Naive add that silently fails on duplicate:**
|
|
93
|
+
The bug that shows up most in generated keychain code is a `SecItemAdd` whose
|
|
94
|
+
result is never read. The first save works; the second returns
|
|
95
|
+
`errSecDuplicateItem` (-25299), nothing is written, and the app keeps running
|
|
96
|
+
with the stale value.
|
|
87
97
|
|
|
88
98
|
```swift
|
|
89
|
-
//
|
|
90
|
-
func
|
|
91
|
-
let
|
|
99
|
+
// Wrong: status ignored, so every save after the first silently does nothing.
|
|
100
|
+
func storeWrong(_ pin: Data) {
|
|
101
|
+
let item: [CFString: Any] = [
|
|
92
102
|
kSecClass: kSecClassGenericPassword,
|
|
93
|
-
kSecAttrService: "com.example.
|
|
94
|
-
kSecAttrAccount:
|
|
95
|
-
kSecValueData:
|
|
103
|
+
kSecAttrService: "com.example.lockbox",
|
|
104
|
+
kSecAttrAccount: "vault-pin",
|
|
105
|
+
kSecValueData: pin
|
|
96
106
|
]
|
|
97
|
-
SecItemAdd(
|
|
98
|
-
// If item exists → errSecDuplicateItem (-25299) - password never saved
|
|
107
|
+
SecItemAdd(item as CFDictionary, nil)
|
|
99
108
|
}
|
|
100
109
|
```
|
|
101
110
|
|
|
102
|
-
✅ **Correct add-or-update with exhaustive OSStatus handling:**
|
|
103
|
-
|
|
104
111
|
```swift
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let baseQuery: [CFString: Any] = [
|
|
112
|
+
func store(_ pin: Data) throws {
|
|
113
|
+
let identity: [CFString: Any] = [
|
|
108
114
|
kSecClass: kSecClassGenericPassword,
|
|
109
|
-
kSecAttrService: "com.example.
|
|
110
|
-
kSecAttrAccount:
|
|
115
|
+
kSecAttrService: "com.example.lockbox",
|
|
116
|
+
kSecAttrAccount: "vault-pin"
|
|
111
117
|
]
|
|
118
|
+
var insert = identity
|
|
119
|
+
insert[kSecValueData] = pin
|
|
120
|
+
insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
116
|
-
|
|
117
|
-
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
118
|
-
|
|
119
|
-
switch addStatus {
|
|
122
|
+
let added = SecItemAdd(insert as CFDictionary, nil)
|
|
123
|
+
switch added {
|
|
120
124
|
case errSecSuccess:
|
|
121
125
|
return
|
|
122
|
-
|
|
123
126
|
case errSecDuplicateItem:
|
|
124
|
-
|
|
125
|
-
let
|
|
126
|
-
|
|
127
|
-
baseQuery as CFDictionary,
|
|
128
|
-
updates as CFDictionary
|
|
129
|
-
)
|
|
130
|
-
guard updateStatus == errSecSuccess else {
|
|
131
|
-
throw KeychainError(status: updateStatus)
|
|
132
|
-
}
|
|
133
|
-
|
|
127
|
+
let changes: [CFString: Any] = [kSecValueData: pin]
|
|
128
|
+
let updated = SecItemUpdate(identity as CFDictionary, changes as CFDictionary)
|
|
129
|
+
guard updated == errSecSuccess else { throw KeychainError(status: updated) }
|
|
134
130
|
case errSecInteractionNotAllowed:
|
|
135
|
-
|
|
136
|
-
throw KeychainError(status: addStatus)
|
|
137
|
-
|
|
131
|
+
throw KeychainError(status: added)
|
|
138
132
|
default:
|
|
139
|
-
throw KeychainError(status:
|
|
133
|
+
throw KeychainError(status: added)
|
|
140
134
|
}
|
|
141
135
|
}
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- **Separate dictionaries** for add vs. update - the update dictionary contains only the attributes to change, never `kSecClass` or search properties.
|
|
147
|
-
- **`errSecInteractionNotAllowed`** (-25308) means the device is locked and data protection prevents access. Never delete items in response to this error; the item is valid but temporarily inaccessible.
|
|
148
|
-
- **Prefer update over delete-then-add** - update preserves persistent references and avoids the race condition window between delete and add.
|
|
138
|
+
Points to carry into any variant:
|
|
149
139
|
|
|
150
|
-
|
|
140
|
+
- The changes dictionary lists only what should change. It never contains
|
|
141
|
+
`kSecClass` or the search attributes.
|
|
142
|
+
- `errSecInteractionNotAllowed` (-25308) tells you the item exists but cannot be
|
|
143
|
+
opened now: the device is locked, or the item needs UI that cannot appear. The item is fine. Deleting it here destroys
|
|
144
|
+
good data.
|
|
145
|
+
- Updating in place beats deleting and re-adding: persistent references to the
|
|
146
|
+
item stay valid, and there is no moment where the item is missing for a
|
|
147
|
+
concurrent reader. Delete and re-add only when the access control itself has
|
|
148
|
+
to change (see [keychain-access-control.md](keychain-access-control.md)).
|
|
151
149
|
|
|
152
|
-
## Reading
|
|
150
|
+
## Reading
|
|
153
151
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
❌ **Query that returns no data because `kSecReturnData` is missing:**
|
|
152
|
+
Next in frequency: a `SecItemCopyMatching` that sets no `kSecReturn*` key. The call succeeds and the result is empty.
|
|
157
153
|
|
|
158
154
|
```swift
|
|
159
|
-
//
|
|
160
|
-
func
|
|
161
|
-
let
|
|
155
|
+
// Wrong: no return key, so `out` is nil and the cast always fails.
|
|
156
|
+
func fetchWrong() -> Data? {
|
|
157
|
+
let lookup: [CFString: Any] = [
|
|
162
158
|
kSecClass: kSecClassGenericPassword,
|
|
163
|
-
kSecAttrService: "com.example.
|
|
164
|
-
kSecAttrAccount:
|
|
159
|
+
kSecAttrService: "com.example.lockbox",
|
|
160
|
+
kSecAttrAccount: "vault-pin",
|
|
165
161
|
kSecMatchLimit: kSecMatchLimitOne
|
|
166
|
-
// BUG: Missing kSecReturnData: true
|
|
167
162
|
]
|
|
168
|
-
var
|
|
169
|
-
SecItemCopyMatching(
|
|
170
|
-
return
|
|
163
|
+
var out: CFTypeRef?
|
|
164
|
+
SecItemCopyMatching(lookup as CFDictionary, &out)
|
|
165
|
+
return out as? Data
|
|
171
166
|
}
|
|
172
167
|
```
|
|
173
168
|
|
|
174
|
-
✅ **Correct query with proper return flags and exhaustive error handling:**
|
|
175
|
-
|
|
176
169
|
```swift
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let query: [CFString: Any] = [
|
|
170
|
+
func fetch() throws -> Data? {
|
|
171
|
+
let lookup: [CFString: Any] = [
|
|
180
172
|
kSecClass: kSecClassGenericPassword,
|
|
181
|
-
kSecAttrService: "com.example.
|
|
182
|
-
kSecAttrAccount:
|
|
173
|
+
kSecAttrService: "com.example.lockbox",
|
|
174
|
+
kSecAttrAccount: "vault-pin",
|
|
183
175
|
kSecMatchLimit: kSecMatchLimitOne,
|
|
184
|
-
kSecReturnData: true
|
|
176
|
+
kSecReturnData: true
|
|
185
177
|
]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
189
|
-
|
|
178
|
+
var out: CFTypeRef?
|
|
179
|
+
let status = SecItemCopyMatching(lookup as CFDictionary, &out)
|
|
190
180
|
switch status {
|
|
191
181
|
case errSecSuccess:
|
|
192
|
-
guard let
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
return data
|
|
196
|
-
|
|
182
|
+
guard let bytes = out as? Data else { throw KeychainError(status: errSecParam) }
|
|
183
|
+
return bytes
|
|
197
184
|
case errSecItemNotFound:
|
|
198
|
-
return nil
|
|
199
|
-
|
|
185
|
+
return nil
|
|
200
186
|
case errSecInteractionNotAllowed:
|
|
201
187
|
throw KeychainError(status: status)
|
|
202
|
-
|
|
203
188
|
default:
|
|
204
189
|
throw KeychainError(status: status)
|
|
205
190
|
}
|
|
206
191
|
}
|
|
207
192
|
```
|
|
208
193
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
The `CFTypeRef` type depends entirely on which return flags and match limits are set:
|
|
212
|
-
|
|
213
|
-
```text
|
|
214
|
-
kSecReturnData only + kSecMatchLimitOne → Data
|
|
215
|
-
kSecReturnAttributes + kSecMatchLimitOne → [String: Any]
|
|
216
|
-
kSecReturnData + Attrs + kSecMatchLimitOne → [String: Any] (data under kSecValueData key)
|
|
217
|
-
kSecReturnRef + kSecMatchLimitOne → SecKey / SecCertificate / SecIdentity
|
|
218
|
-
kSecReturnPersistentRef + kSecMatchLimitOne → Data (opaque handle)
|
|
219
|
-
Any combination + kSecMatchLimitAll → Array of the above type
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
**Note:** Combining `kSecReturnData` with `kSecMatchLimitAll` may be restricted for password classes on some OS versions. For listing items, prefer `kSecReturnAttributes` or `kSecReturnRef` with `kSecMatchLimitAll`, then fetch data per-item as needed.
|
|
223
|
-
|
|
224
|
-
### String Keys vs. kSec\* Constants
|
|
225
|
-
|
|
226
|
-
Never use raw string literals (`"svce"`, `"class"`) instead of `kSec*` constants. The constants are `CFString` values with specific internal representations. Two equally valid dictionary key styles exist:
|
|
227
|
-
|
|
228
|
-
```swift
|
|
229
|
-
// Style A: CFString keys (fewer casts at definition, cast once at call site)
|
|
230
|
-
let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword]
|
|
231
|
-
SecItemAdd(query as CFDictionary, nil)
|
|
194
|
+
What lands in `out` depends on the return keys and the match limit:
|
|
232
195
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
196
|
+
| Return keys | `kSecMatchLimitOne` gives | `kSecMatchLimitAll` gives |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| Data | `Data` | array of `Data` |
|
|
199
|
+
| Attributes | `[String: Any]` | array of dictionaries |
|
|
200
|
+
| Data + Attributes | dictionary, secret under `kSecValueData` | array of dictionaries |
|
|
201
|
+
| Ref | `SecKey`, `SecCertificate` or `SecIdentity` | array of refs |
|
|
202
|
+
| PersistentRef | opaque `Data` | array of `Data` |
|
|
237
203
|
|
|
238
|
-
|
|
204
|
+
Some OS versions refuse `kSecReturnData` combined with `kSecMatchLimitAll` for
|
|
205
|
+
password classes. List items with attributes or refs, then fetch data for the
|
|
206
|
+
one you need.
|
|
239
207
|
|
|
240
|
-
|
|
208
|
+
Use the `kSec*` constants, never their string values such as `"svce"` or
|
|
209
|
+
`"class"`. Dictionaries typed `[CFString: Any]` work, and so do `[String: Any]` ones
|
|
210
|
+
whose keys are cast with `as String`; pick one style per codebase. This file uses `[CFString: Any]` because it is
|
|
211
|
+
shorter.
|
|
241
212
|
|
|
242
|
-
##
|
|
213
|
+
## One Place That Builds Queries
|
|
243
214
|
|
|
244
|
-
|
|
215
|
+
A small builder keeps every query shape in one auditable spot, so a missing
|
|
216
|
+
return flag or a typo in an attribute shows up in one review instead of twenty.
|
|
245
217
|
|
|
246
218
|
```swift
|
|
247
|
-
enum
|
|
248
|
-
static func
|
|
249
|
-
|
|
250
|
-
account: String
|
|
251
|
-
service: String
|
|
219
|
+
enum KeychainQuery {
|
|
220
|
+
static func make(
|
|
221
|
+
itemClass: CFString = kSecClassGenericPassword,
|
|
222
|
+
account: String,
|
|
223
|
+
service: String,
|
|
252
224
|
accessGroup: String? = nil,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
) -> [
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return
|
|
225
|
+
wantData: Bool = false,
|
|
226
|
+
wantAttributes: Bool = false,
|
|
227
|
+
limit: CFString = kSecMatchLimitOne
|
|
228
|
+
) -> [CFString: Any] {
|
|
229
|
+
var q: [CFString: Any] = [
|
|
230
|
+
kSecClass: itemClass,
|
|
231
|
+
kSecAttrAccount: account,
|
|
232
|
+
kSecAttrService: service,
|
|
233
|
+
kSecMatchLimit: limit
|
|
234
|
+
]
|
|
235
|
+
if let accessGroup { q[kSecAttrAccessGroup] = accessGroup }
|
|
236
|
+
if wantData { q[kSecReturnData] = true }
|
|
237
|
+
if wantAttributes { q[kSecReturnAttributes] = true }
|
|
238
|
+
return q
|
|
267
239
|
}
|
|
268
240
|
}
|
|
269
241
|
```
|
|
270
242
|
|
|
271
|
-
|
|
243
|
+
Drop `kSecMatchLimit` and the return flags before handing a builder result to
|
|
244
|
+
`SecItemAdd`, `SecItemUpdate` or `SecItemDelete`.
|
|
272
245
|
|
|
273
|
-
|
|
246
|
+
## OSStatus Codes
|
|
274
247
|
|
|
275
|
-
|
|
248
|
+
A nonzero status is not automatically a failure to report. Treat each one on
|
|
249
|
+
its merits:
|
|
276
250
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
| `-25244` | `errSecNoSuchAttr` | Attribute not supported (data protection keychain) | Check for unsupported attributes |
|
|
288
|
-
|
|
289
|
-
Map raw codes to a domain-specific Swift error:
|
|
251
|
+
| Code | Constant | Response |
|
|
252
|
+
| --- | --- | --- |
|
|
253
|
+
| 0 | `errSecSuccess` | Continue. |
|
|
254
|
+
| -25299 | `errSecDuplicateItem` | Switch to `SecItemUpdate`. |
|
|
255
|
+
| -25300 | `errSecItemNotFound` | Return nil; on delete, count it as success. |
|
|
256
|
+
| -25308 | `errSecInteractionNotAllowed` | Try again later (device locked). Never delete. |
|
|
257
|
+
| -128 | `errSecUserCanceled` | The user dismissed the prompt; tell the UI. |
|
|
258
|
+
| -25293 | `errSecAuthFailed` | Authentication failed or the protected item is no longer usable. |
|
|
259
|
+
| -50 | `errSecParam` | A programming error in the dictionary; fix the query. |
|
|
260
|
+
| -25244 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
|
|
290
261
|
|
|
291
262
|
```swift
|
|
292
263
|
struct KeychainError: Error, CustomStringConvertible {
|
|
293
264
|
let status: OSStatus
|
|
294
|
-
|
|
295
265
|
var description: String {
|
|
296
|
-
let
|
|
297
|
-
return "
|
|
266
|
+
let text = SecCopyErrorMessageString(status, nil) as String? ?? "unknown"
|
|
267
|
+
return "Keychain status \(status): \(text)"
|
|
298
268
|
}
|
|
299
269
|
}
|
|
300
270
|
```
|
|
301
271
|
|
|
302
|
-
|
|
272
|
+
Logs may record the shape of a query (class, service, which flags were set)
|
|
273
|
+
and the status code. They never record `kSecValueData`, tokens or key bytes.
|
|
303
274
|
|
|
304
|
-
|
|
275
|
+
## An Actor Around the Keychain (iOS 17+ / macOS 14+)
|
|
305
276
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
Every `SecItem*` function blocks the calling thread due to IPC to `securityd` and potential Secure Enclave round-trips. For biometry-protected items, the block can last several seconds during user authentication (WWDC 2014 Session 711).
|
|
309
|
-
|
|
310
|
-
❌ **`@MainActor` keychain access that blocks the UI:**
|
|
277
|
+
Reads of biometric-protected items wait for the user and can take several
|
|
278
|
+
seconds (WWDC 2014 Session 711). Doing that on the main actor freezes the UI.
|
|
311
279
|
|
|
312
280
|
```swift
|
|
313
|
-
//
|
|
281
|
+
// Wrong: the main actor waits on securityd and, for protected items, on the user.
|
|
314
282
|
@MainActor
|
|
315
|
-
class
|
|
316
|
-
@Published var
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
let
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
kSecReturnData: true,
|
|
324
|
-
kSecMatchLimit: kSecMatchLimitOne
|
|
325
|
-
]
|
|
326
|
-
var result: CFTypeRef?
|
|
327
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
328
|
-
if status == errSecSuccess, let data = result as? Data {
|
|
329
|
-
self.token = String(data: data, encoding: .utf8) ?? ""
|
|
330
|
-
}
|
|
331
|
-
// UI frozen for entire duration of securityd IPC + potential SE round-trip
|
|
283
|
+
final class ProfileModel: ObservableObject {
|
|
284
|
+
@Published var apiKey: String?
|
|
285
|
+
func load() {
|
|
286
|
+
var out: CFTypeRef?
|
|
287
|
+
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
|
288
|
+
kSecAttrAccount: "api", kSecReturnData: true]
|
|
289
|
+
_ = SecItemCopyMatching(q as CFDictionary, &out)
|
|
290
|
+
apiKey = (out as? Data).flatMap { String(decoding: $0, as: UTF8.self) }
|
|
332
291
|
}
|
|
333
292
|
}
|
|
334
293
|
```
|
|
335
294
|
|
|
336
|
-
✅ **Actor-isolated keychain manager with full CRUD:**
|
|
337
|
-
|
|
338
295
|
```swift
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
static let shared = KeychainManager()
|
|
342
|
-
|
|
296
|
+
actor SecretStore {
|
|
297
|
+
static let shared = SecretStore()
|
|
343
298
|
private let service: String
|
|
344
299
|
|
|
345
|
-
init(service: String = Bundle.main.bundleIdentifier ?? "
|
|
300
|
+
init(service: String = Bundle.main.bundleIdentifier ?? "app.secrets") {
|
|
346
301
|
self.service = service
|
|
347
302
|
}
|
|
348
303
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
) throws {
|
|
354
|
-
let baseQuery: [CFString: Any] = [
|
|
355
|
-
kSecClass: kSecClassGenericPassword,
|
|
356
|
-
kSecAttrService: service,
|
|
357
|
-
kSecAttrAccount: key
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
var addQuery = baseQuery
|
|
361
|
-
addQuery[kSecValueData] = data
|
|
362
|
-
addQuery[kSecAttrAccessible] = accessibility
|
|
363
|
-
|
|
364
|
-
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
365
|
-
|
|
366
|
-
switch addStatus {
|
|
367
|
-
case errSecSuccess:
|
|
368
|
-
return
|
|
369
|
-
case errSecDuplicateItem:
|
|
370
|
-
let updates: [CFString: Any] = [kSecValueData: data]
|
|
371
|
-
let updateStatus = SecItemUpdate(
|
|
372
|
-
baseQuery as CFDictionary,
|
|
373
|
-
updates as CFDictionary
|
|
374
|
-
)
|
|
375
|
-
guard updateStatus == errSecSuccess else {
|
|
376
|
-
throw KeychainError(status: updateStatus)
|
|
377
|
-
}
|
|
378
|
-
case errSecInteractionNotAllowed:
|
|
379
|
-
throw KeychainError(status: addStatus)
|
|
380
|
-
default:
|
|
381
|
-
throw KeychainError(status: addStatus)
|
|
382
|
-
}
|
|
304
|
+
private func identity(_ account: String) -> [CFString: Any] {
|
|
305
|
+
[kSecClass: kSecClassGenericPassword,
|
|
306
|
+
kSecAttrService: service,
|
|
307
|
+
kSecAttrAccount: account]
|
|
383
308
|
}
|
|
384
309
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
var result: CFTypeRef?
|
|
397
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
398
|
-
|
|
399
|
-
switch status {
|
|
400
|
-
case errSecSuccess:
|
|
401
|
-
return result as? Data
|
|
402
|
-
case errSecItemNotFound:
|
|
403
|
-
return nil
|
|
404
|
-
case errSecInteractionNotAllowed:
|
|
405
|
-
throw KeychainError(status: status)
|
|
406
|
-
default:
|
|
407
|
-
throw KeychainError(status: status)
|
|
408
|
-
}
|
|
310
|
+
func put(_ value: Data, for account: String,
|
|
311
|
+
accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) throws {
|
|
312
|
+
var insert = identity(account)
|
|
313
|
+
insert[kSecValueData] = value
|
|
314
|
+
insert[kSecAttrAccessible] = accessible
|
|
315
|
+
let status = SecItemAdd(insert as CFDictionary, nil)
|
|
316
|
+
if status == errSecSuccess { return }
|
|
317
|
+
guard status == errSecDuplicateItem else { throw KeychainError(status: status) }
|
|
318
|
+
let changed = SecItemUpdate(identity(account) as CFDictionary,
|
|
319
|
+
[kSecValueData: value] as CFDictionary)
|
|
320
|
+
guard changed == errSecSuccess else { throw KeychainError(status: changed) }
|
|
409
321
|
}
|
|
410
322
|
|
|
411
|
-
|
|
323
|
+
func get(_ account: String) throws -> Data? {
|
|
324
|
+
var q = identity(account)
|
|
325
|
+
q[kSecReturnData] = true
|
|
326
|
+
q[kSecMatchLimit] = kSecMatchLimitOne
|
|
327
|
+
var out: CFTypeRef?
|
|
328
|
+
let status = SecItemCopyMatching(q as CFDictionary, &out)
|
|
329
|
+
if status == errSecItemNotFound { return nil }
|
|
330
|
+
guard status == errSecSuccess else { throw KeychainError(status: status) }
|
|
331
|
+
return out as? Data
|
|
332
|
+
}
|
|
412
333
|
|
|
413
|
-
func
|
|
414
|
-
let
|
|
415
|
-
kSecClass: kSecClassGenericPassword,
|
|
416
|
-
kSecAttrService: service,
|
|
417
|
-
kSecAttrAccount: key
|
|
418
|
-
]
|
|
419
|
-
let status = SecItemDelete(query as CFDictionary)
|
|
334
|
+
func remove(_ account: String) throws {
|
|
335
|
+
let status = SecItemDelete(identity(account) as CFDictionary)
|
|
420
336
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
|
421
337
|
throw KeychainError(status: status)
|
|
422
338
|
}
|
|
423
339
|
}
|
|
424
340
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
]
|
|
434
|
-
|
|
435
|
-
var result: CFTypeRef?
|
|
436
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
437
|
-
|
|
438
|
-
switch status {
|
|
439
|
-
case errSecSuccess:
|
|
440
|
-
guard let items = result as? [[String: Any]] else { return [] }
|
|
441
|
-
return items.compactMap { $0[kSecAttrAccount as String] as? String }
|
|
442
|
-
case errSecItemNotFound:
|
|
443
|
-
return []
|
|
444
|
-
default:
|
|
341
|
+
func accounts() throws -> [String] {
|
|
342
|
+
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
|
343
|
+
kSecAttrService: service,
|
|
344
|
+
kSecMatchLimit: kSecMatchLimitAll,
|
|
345
|
+
kSecReturnAttributes: true]
|
|
346
|
+
var out: CFTypeRef?
|
|
347
|
+
let status = SecItemCopyMatching(q as CFDictionary, &out)
|
|
348
|
+
if status == errSecItemNotFound { return [] }
|
|
349
|
+
guard status == errSecSuccess, let rows = out as? [[String: Any]] else {
|
|
445
350
|
throw KeychainError(status: status)
|
|
446
351
|
}
|
|
352
|
+
return rows.compactMap { $0[kSecAttrAccount as String] as? String }
|
|
447
353
|
}
|
|
448
354
|
}
|
|
449
355
|
```
|
|
450
356
|
|
|
451
|
-
|
|
357
|
+
`remove` is idempotent. `accounts()` asks for attributes only, so it never pays
|
|
358
|
+
for the Secure Enclave unwrap of each secret.
|
|
359
|
+
|
|
360
|
+
From SwiftUI, the view model awaits the actor and the main actor is free while
|
|
361
|
+
the keychain works:
|
|
452
362
|
|
|
453
363
|
```swift
|
|
454
364
|
@MainActor
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
func
|
|
459
|
-
|
|
460
|
-
// Crosses actor boundary - suspends, does NOT block MainActor
|
|
461
|
-
let data = try await KeychainManager.shared.load(for: "authToken")
|
|
462
|
-
isAuthenticated = data != nil
|
|
463
|
-
} catch {
|
|
464
|
-
isAuthenticated = false
|
|
465
|
-
}
|
|
365
|
+
@Observable
|
|
366
|
+
final class SettingsModel {
|
|
367
|
+
var hasKey = false
|
|
368
|
+
func refresh() async {
|
|
369
|
+
hasKey = (try? await SecretStore.shared.get("api")) != nil
|
|
466
370
|
}
|
|
467
371
|
}
|
|
468
372
|
```
|
|
469
373
|
|
|
470
|
-
|
|
374
|
+
Why an actor rather than a queue:
|
|
471
375
|
|
|
472
|
-
|
|
|
473
|
-
|
|
|
474
|
-
|
|
|
475
|
-
|
|
|
476
|
-
|
|
|
477
|
-
|
|
|
478
|
-
| Swift 6
|
|
376
|
+
| | Actor | GCD serial queue |
|
|
377
|
+
| --- | --- | --- |
|
|
378
|
+
| Isolation | Checked by the compiler | Up to the developer |
|
|
379
|
+
| Serialization | Done by the runtime | Done by the queue, if used consistently |
|
|
380
|
+
| Control flow | Straight-line `async`/`await` | Nested completion handlers |
|
|
381
|
+
| `Sendable` | Enforced | Annotate `@Sendable` by hand |
|
|
382
|
+
| Swift 6 | Native | Needs care to pass strict checking |
|
|
479
383
|
|
|
480
|
-
|
|
384
|
+
For iOS 13 through 16, a private serial queue does the same job:
|
|
481
385
|
|
|
482
386
|
```swift
|
|
483
|
-
class
|
|
484
|
-
private let queue = DispatchQueue(label: "
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
func load(key: String, completion: @escaping (Result<Data?, Error>) -> Void) {
|
|
387
|
+
final class LegacySecretStore {
|
|
388
|
+
private let queue = DispatchQueue(label: "secrets.io", qos: .userInitiated)
|
|
389
|
+
func get(_ account: String, completion: @escaping @Sendable (Result<Data?, Error>) -> Void) {
|
|
488
390
|
queue.async {
|
|
489
|
-
|
|
391
|
+
let result = Result { try readItem(account: account) }
|
|
490
392
|
DispatchQueue.main.async { completion(result) }
|
|
491
393
|
}
|
|
492
394
|
}
|
|
493
395
|
}
|
|
494
396
|
```
|
|
495
397
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
-
|
|
506
|
-
|
|
507
|
-
For
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
398
|
+
Here `readItem(account:)` stands for a synchronous read such as `fetch()` in
|
|
399
|
+
the Reading section.
|
|
400
|
+
The completion is `@Sendable` because it crosses two queues; without it Swift 6
|
|
401
|
+
reports sending `completion` as a data race.
|
|
402
|
+
|
|
403
|
+
## Performance
|
|
404
|
+
|
|
405
|
+
- Returning attributes only uses the cached metadata key. It is quick and needs
|
|
406
|
+
no Secure Enclave work.
|
|
407
|
+
- Returning data needs the per-row key from the Secure Enclave, so it costs
|
|
408
|
+
more.
|
|
409
|
+
- For a list screen, fetch attributes or refs, and fetch data only for the row
|
|
410
|
+
the user picks.
|
|
411
|
+
- A query with nothing but `kSecClass` and `kSecMatchLimitAll` scans the whole
|
|
412
|
+
table. Adding service and account lets `securityd` use its index.
|
|
413
|
+
- At launch every read costs IPC plus enclave latency, and items protected with
|
|
414
|
+
a `WhenUnlocked` class can be unavailable if the app was launched before first
|
|
415
|
+
unlock (background refresh, VoIP pushes).
|
|
416
|
+
- Do not call SecItem synchronously in
|
|
417
|
+
`application(_:didFinishLaunchingWithOptions:)`. Read the item when a feature
|
|
418
|
+
first needs it.
|
|
419
|
+
- Passwords cannot be written in bulk. `SecItemAdd` with `kSecUseItemList` can
|
|
420
|
+
add several certificates or keys at once, but not password items. Reading in
|
|
421
|
+
bulk means `kSecMatchLimitAll`.
|
|
422
|
+
|
|
423
|
+
## macOS: Two Keychains (TN3137)
|
|
424
|
+
|
|
425
|
+
A Mac has two keychain implementations:
|
|
426
|
+
|
|
427
|
+
- the legacy **file-based keychain**, which SecItem uses unless told otherwise;
|
|
428
|
+
- the **data protection keychain**, which matches iOS.
|
|
429
|
+
|
|
430
|
+
The file-based one ignores attributes it does not understand without saying
|
|
431
|
+
so, applies `kSecMatchLimit` defaults inconsistently, and returns different
|
|
432
|
+
types from `SecItemAdd`.
|
|
433
|
+
|
|
434
|
+
Code opts into the data protection keychain with
|
|
435
|
+
`kSecUseDataProtectionKeychain: true`, available from macOS 10.15, or by setting
|
|
436
|
+
`kSecAttrSynchronizable: true`. It is required for iCloud Keychain sync,
|
|
437
|
+
biometric protection and storing Secure Enclave keys. New code should always
|
|
438
|
+
target it. Mac Catalyst apps and iOS apps running on a Mac use it
|
|
439
|
+
automatically.
|
|
538
440
|
|
|
539
441
|
```swift
|
|
540
|
-
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
query[kSecUseDataProtectionKeychain] = true
|
|
548
|
-
#endif
|
|
442
|
+
func macSafe(_ query: [CFString: Any]) -> [CFString: Any] {
|
|
443
|
+
var q = query
|
|
444
|
+
#if os(macOS)
|
|
445
|
+
q[kSecUseDataProtectionKeychain] = true
|
|
446
|
+
#endif
|
|
447
|
+
return q
|
|
448
|
+
}
|
|
549
449
|
```
|
|
550
450
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
## Accessibility
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
|
560
|
-
|
|
|
561
|
-
| `
|
|
562
|
-
| `
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
3. **`kSecReturnData` + `kSecMatchLimitAll`:** Some OS versions and keychain implementations have restrictions around returning data for multiple password-class matches. The safer pattern is to use `kSecReturnRef` or `kSecReturnAttributes` with `LimitAll`, then fetch data per item. See the Return Type Cheat Sheet.
|
|
603
|
-
|
|
604
|
-
---
|
|
605
|
-
|
|
606
|
-
## Summary Checklist
|
|
607
|
-
|
|
608
|
-
Before shipping keychain code, verify:
|
|
609
|
-
|
|
610
|
-
1. **OSStatus checked on every call** - exhaustive `switch` covering at minimum `errSecSuccess`, `errSecDuplicateItem`, `errSecItemNotFound`, `errSecInteractionNotAllowed`; no ignored return values
|
|
611
|
-
2. **Add-or-update pattern implemented** - `SecItemAdd` catches `-25299` and falls back to `SecItemUpdate`; duplicate saves never crash or silently fail
|
|
612
|
-
3. **Return flags explicitly set** - every `SecItemCopyMatching` call includes at least one `kSecReturn*` flag; no "success but nil" bugs
|
|
613
|
-
4. **CFTypeRef cast matches flags** - cast type corresponds to the combination of return flags and match limit (see Return Type Cheat Sheet)
|
|
614
|
-
5. **Zero SecItem calls on `@MainActor`** - all keychain access isolated in a dedicated `actor` (iOS 17+) or serial `DispatchQueue` (iOS 13-16)
|
|
615
|
-
6. **Fresh dictionaries per call** - no dictionary reuse across SecItem functions; add dict, query dict, and update dict are separate
|
|
616
|
-
7. **kSec\* constants used** - no raw string literals for dictionary keys; using either `[CFString: Any]` or `[String: Any]` with `as String` casts
|
|
617
|
-
8. **Queries are specific** - `kSecAttrService` + `kSecAttrAccount` included for GenericPassword; `kSecMatchLimitOne` used unless enumeration is needed
|
|
618
|
-
9. **Delete treats not-found as success** - `errSecItemNotFound` on delete is a valid postcondition, not an error
|
|
619
|
-
10. **macOS targets data protection keychain** - `kSecUseDataProtectionKeychain: true` set for macOS targets (automatic for Catalyst/iOS-on-Mac)
|
|
620
|
-
11. **errSecInteractionNotAllowed handled non-destructively** - device-locked state triggers retry-later logic, never delete-and-recreate
|
|
451
|
+
When a macOS keychain bug appears, first confirm which backend the call went
|
|
452
|
+
to. An attribute the file-based keychain quietly drops will produce
|
|
453
|
+
`errSecNoSuchAttr` (-25244) on the data protection keychain.
|
|
454
|
+
|
|
455
|
+
## Accessibility at a Glance
|
|
456
|
+
|
|
457
|
+
| Constant | Readable | In backups | Typical use |
|
|
458
|
+
| --- | --- | --- | --- |
|
|
459
|
+
| `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | while unlocked | no | the default for most secrets |
|
|
460
|
+
| `kSecAttrAccessibleAfterFirstUnlock` | from first unlock until restart | yes | tokens read in the background |
|
|
461
|
+
| `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | unlocked and a passcode is set | no | the most sensitive data (OWASP recommendation) |
|
|
462
|
+
| `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | after the first unlock, until reboot | no | device-bound items read in the background |
|
|
463
|
+
|
|
464
|
+
`kSecAttrAccessibleAlways` was deprecated in iOS 12 and is not supported on
|
|
465
|
+
Apple silicon Macs. Do not use it. The full model is in
|
|
466
|
+
[keychain-access-control.md](keychain-access-control.md).
|
|
467
|
+
|
|
468
|
+
Choosing among them: `WhenPasscodeSetThisDeviceOnly` is the strongest, but its
|
|
469
|
+
items are deleted if the user removes the passcode. `WhenUnlockedThisDeviceOnly`
|
|
470
|
+
is a safe default for foreground use. The actor above defaults to
|
|
471
|
+
`AfterFirstUnlockThisDeviceOnly` because it assumes background callers.
|
|
472
|
+
|
|
473
|
+
## Checklist
|
|
474
|
+
|
|
475
|
+
1. Every SecItem call switches over its status, covering success, duplicate,
|
|
476
|
+
not found and interaction-not-allowed.
|
|
477
|
+
2. Saves fall back to `SecItemUpdate` on -25299.
|
|
478
|
+
3. Every `SecItemCopyMatching` sets at least one `kSecReturn*` key.
|
|
479
|
+
4. The cast of the `CFTypeRef` result matches the return keys and match limit.
|
|
480
|
+
5. No SecItem call runs on `@MainActor` (actor on iOS 17+, serial queue on
|
|
481
|
+
iOS 13 to 16).
|
|
482
|
+
6. Add, query and update dictionaries are built fresh for each call.
|
|
483
|
+
7. Keys are `kSec*` constants, not string literals.
|
|
484
|
+
8. Queries are specific: service and account for generic passwords,
|
|
485
|
+
`kSecMatchLimitOne` unless listing.
|
|
486
|
+
9. Delete treats `errSecItemNotFound` as success.
|
|
487
|
+
10. macOS code sets `kSecUseDataProtectionKeychain: true`.
|
|
488
|
+
11. `errSecInteractionNotAllowed` never leads to deletion.
|
|
489
|
+
|
|
490
|
+
## Related Files
|
|
491
|
+
|
|
492
|
+
- [keychain-item-classes.md](keychain-item-classes.md), [keychain-access-control.md](keychain-access-control.md),
|
|
493
|
+
[biometric-authentication.md](biometric-authentication.md), [secure-enclave.md](secure-enclave.md)
|
|
494
|
+
- [credential-storage-patterns.md](credential-storage-patterns.md), [keychain-sharing.md](keychain-sharing.md),
|
|
495
|
+
[testing-security-code.md](testing-security-code.md), [common-anti-patterns.md](common-anti-patterns.md)
|
|
496
|
+
|
|
497
|
+
Further reading: the Keychain Services documentation; TN3137; Quinn's two SecItem
|
|
498
|
+
forum posts; the keychain data protection chapter of the Apple Platform
|
|
499
|
+
Security Guide; WWDC 2014 session 711, titled Keychain and Authentication
|
|
500
|
+
with Touch ID; WWDC 2019 session 516, titled What's New in Authentication.
|