@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1
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 +21 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +285 -285
- package/package.json +3 -3
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +69 -69
- 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 +345 -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 +107 -121
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -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 +335 -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 +277 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -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 +274 -384
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
- 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 +228 -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 +302 -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 +236 -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/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -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/skill-creator/template.md +7 -1
- 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 +302 -241
- 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 +304 -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 +183 -162
- 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 +411 -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 +375 -491
- 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 +397 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -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 +201 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
- 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 +160 -315
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,620 +1,504 @@
|
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 13+ / macOS 10.15+)](#an-actor-around-the-keychain-ios-13--macos-1015)
|
|
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)
|
|
27
|
+
|
|
28
|
+
## How the Keychain Is Built
|
|
29
|
+
|
|
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`.
|
|
34
|
+
|
|
35
|
+
Items are protected in two tiers, both AES-256-GCM:
|
|
36
|
+
|
|
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.
|
|
41
|
+
|
|
42
|
+
That split explains most of the performance advice further down.
|
|
43
|
+
|
|
44
|
+
## Dictionary Contracts
|
|
45
|
+
|
|
46
|
+
Quinn groups the keys you can put in a SecItem dictionary into five kinds:
|
|
47
|
+
|
|
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` |
|
|
55
|
+
|
|
56
|
+
Each function accepts a different subset (all available since iOS 2.0):
|
|
57
|
+
|
|
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` |
|
|
64
|
+
|
|
65
|
+
The delete default deserves attention: a query that names only the class, or
|
|
66
|
+
only the service, removes every item it matches.
|
|
67
|
+
|
|
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).
|
|
71
|
+
|
|
72
|
+
## Uniqueness
|
|
73
|
+
|
|
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.
|
|
78
|
+
|
|
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`.
|
|
83
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
`SecItemUpdate` cannot change `kSecClass`: the class belongs to the query,
|
|
89
|
+
and the attributes dictionary accepts only real keychain attributes. To change
|
|
90
|
+
class, delete the old item and add a new one. A primary-key attribute such as
|
|
91
|
+
`kSecAttrAccount` can be updated; the update fails with `errSecDuplicateItem`
|
|
92
|
+
only when another item already has the resulting primary key.
|
|
93
|
+
|
|
94
|
+
## Saving: Add, Then Update on Duplicate
|
|
95
|
+
|
|
96
|
+
The bug that shows up most in generated keychain code is a `SecItemAdd` whose
|
|
97
|
+
result is never read. The first save works; the second returns
|
|
98
|
+
`errSecDuplicateItem` (-25299), nothing is written, and the app keeps running
|
|
99
|
+
with the stale value.
|
|
87
100
|
|
|
88
101
|
```swift
|
|
89
|
-
//
|
|
90
|
-
func
|
|
91
|
-
let
|
|
102
|
+
// Wrong: status ignored, so every save after the first silently does nothing.
|
|
103
|
+
func storeWrong(_ pin: Data) {
|
|
104
|
+
let item: [CFString: Any] = [
|
|
92
105
|
kSecClass: kSecClassGenericPassword,
|
|
93
|
-
kSecAttrService: "com.example.
|
|
94
|
-
kSecAttrAccount:
|
|
95
|
-
kSecValueData:
|
|
106
|
+
kSecAttrService: "com.example.lockbox",
|
|
107
|
+
kSecAttrAccount: "vault-pin",
|
|
108
|
+
kSecValueData: pin
|
|
96
109
|
]
|
|
97
|
-
SecItemAdd(
|
|
98
|
-
// If item exists → errSecDuplicateItem (-25299) - password never saved
|
|
110
|
+
SecItemAdd(item as CFDictionary, nil)
|
|
99
111
|
}
|
|
100
112
|
```
|
|
101
113
|
|
|
102
|
-
✅ **Correct add-or-update with exhaustive OSStatus handling:**
|
|
103
|
-
|
|
104
114
|
```swift
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
let baseQuery: [CFString: Any] = [
|
|
115
|
+
func store(_ pin: Data) throws {
|
|
116
|
+
let identity: [CFString: Any] = [
|
|
108
117
|
kSecClass: kSecClassGenericPassword,
|
|
109
|
-
kSecAttrService: "com.example.
|
|
110
|
-
kSecAttrAccount:
|
|
118
|
+
kSecAttrService: "com.example.lockbox",
|
|
119
|
+
kSecAttrAccount: "vault-pin"
|
|
111
120
|
]
|
|
121
|
+
var insert = identity
|
|
122
|
+
insert[kSecValueData] = pin
|
|
123
|
+
insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
addQuery[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
116
|
-
|
|
117
|
-
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
118
|
-
|
|
119
|
-
switch addStatus {
|
|
125
|
+
let added = SecItemAdd(insert as CFDictionary, nil)
|
|
126
|
+
switch added {
|
|
120
127
|
case errSecSuccess:
|
|
121
128
|
return
|
|
122
|
-
|
|
123
129
|
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
|
-
|
|
130
|
+
let changes: [CFString: Any] = [kSecValueData: pin]
|
|
131
|
+
let updated = SecItemUpdate(identity as CFDictionary, changes as CFDictionary)
|
|
132
|
+
guard updated == errSecSuccess else { throw KeychainError(status: updated) }
|
|
134
133
|
case errSecInteractionNotAllowed:
|
|
135
|
-
|
|
136
|
-
throw KeychainError(status: addStatus)
|
|
137
|
-
|
|
134
|
+
throw KeychainError(status: added)
|
|
138
135
|
default:
|
|
139
|
-
throw KeychainError(status:
|
|
136
|
+
throw KeychainError(status: added)
|
|
140
137
|
}
|
|
141
138
|
}
|
|
142
139
|
```
|
|
143
140
|
|
|
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.
|
|
149
|
-
|
|
150
|
-
---
|
|
141
|
+
Points to carry into any variant:
|
|
151
142
|
|
|
152
|
-
|
|
143
|
+
- The changes dictionary lists only what should change. It never contains
|
|
144
|
+
`kSecClass` or the search attributes.
|
|
145
|
+
- `errSecInteractionNotAllowed` (-25308) tells you the item exists but cannot be
|
|
146
|
+
opened now: the device is locked, or the item needs UI that cannot appear. The item is fine. Deleting it here destroys
|
|
147
|
+
good data.
|
|
148
|
+
- Updating in place beats deleting and re-adding: persistent references to the
|
|
149
|
+
item stay valid, and there is no moment where the item is missing for a
|
|
150
|
+
concurrent reader. Delete and re-add only when the access control itself has
|
|
151
|
+
to change (see [keychain-access-control.md](keychain-access-control.md)).
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
## Reading
|
|
155
154
|
|
|
156
|
-
|
|
155
|
+
Next in frequency: a `SecItemCopyMatching` that sets no `kSecReturn*` key. The call succeeds and the result is empty.
|
|
157
156
|
|
|
158
157
|
```swift
|
|
159
|
-
//
|
|
160
|
-
func
|
|
161
|
-
let
|
|
158
|
+
// Wrong: no return key, so `out` is nil and the cast always fails.
|
|
159
|
+
func fetchWrong() -> Data? {
|
|
160
|
+
let lookup: [CFString: Any] = [
|
|
162
161
|
kSecClass: kSecClassGenericPassword,
|
|
163
|
-
kSecAttrService: "com.example.
|
|
164
|
-
kSecAttrAccount:
|
|
162
|
+
kSecAttrService: "com.example.lockbox",
|
|
163
|
+
kSecAttrAccount: "vault-pin",
|
|
165
164
|
kSecMatchLimit: kSecMatchLimitOne
|
|
166
|
-
// BUG: Missing kSecReturnData: true
|
|
167
165
|
]
|
|
168
|
-
var
|
|
169
|
-
SecItemCopyMatching(
|
|
170
|
-
return
|
|
166
|
+
var out: CFTypeRef?
|
|
167
|
+
SecItemCopyMatching(lookup as CFDictionary, &out)
|
|
168
|
+
return out as? Data
|
|
171
169
|
}
|
|
172
170
|
```
|
|
173
171
|
|
|
174
|
-
✅ **Correct query with proper return flags and exhaustive error handling:**
|
|
175
|
-
|
|
176
172
|
```swift
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let query: [CFString: Any] = [
|
|
173
|
+
func fetch() throws -> Data? {
|
|
174
|
+
let lookup: [CFString: Any] = [
|
|
180
175
|
kSecClass: kSecClassGenericPassword,
|
|
181
|
-
kSecAttrService: "com.example.
|
|
182
|
-
kSecAttrAccount:
|
|
176
|
+
kSecAttrService: "com.example.lockbox",
|
|
177
|
+
kSecAttrAccount: "vault-pin",
|
|
183
178
|
kSecMatchLimit: kSecMatchLimitOne,
|
|
184
|
-
kSecReturnData: true
|
|
179
|
+
kSecReturnData: true
|
|
185
180
|
]
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
189
|
-
|
|
181
|
+
var out: CFTypeRef?
|
|
182
|
+
let status = SecItemCopyMatching(lookup as CFDictionary, &out)
|
|
190
183
|
switch status {
|
|
191
184
|
case errSecSuccess:
|
|
192
|
-
guard let
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
return data
|
|
196
|
-
|
|
185
|
+
guard let bytes = out as? Data else { throw KeychainError(status: errSecParam) }
|
|
186
|
+
return bytes
|
|
197
187
|
case errSecItemNotFound:
|
|
198
|
-
return nil
|
|
199
|
-
|
|
188
|
+
return nil
|
|
200
189
|
case errSecInteractionNotAllowed:
|
|
201
190
|
throw KeychainError(status: status)
|
|
202
|
-
|
|
203
191
|
default:
|
|
204
192
|
throw KeychainError(status: status)
|
|
205
193
|
}
|
|
206
194
|
}
|
|
207
195
|
```
|
|
208
196
|
|
|
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)
|
|
197
|
+
What lands in `out` depends on the return keys and the match limit:
|
|
232
198
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
199
|
+
| Return keys | `kSecMatchLimitOne` gives | `kSecMatchLimitAll` gives |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| Data | `Data` | array of `Data` |
|
|
202
|
+
| Attributes | `[String: Any]` | array of dictionaries |
|
|
203
|
+
| Data + Attributes | dictionary, secret under `kSecValueData` | array of dictionaries |
|
|
204
|
+
| Ref | `SecKey`, `SecCertificate` or `SecIdentity` | array of refs |
|
|
205
|
+
| PersistentRef | opaque `Data` | array of `Data` |
|
|
237
206
|
|
|
238
|
-
|
|
207
|
+
Some OS versions refuse `kSecReturnData` combined with `kSecMatchLimitAll` for
|
|
208
|
+
password classes. List items with attributes or refs, then fetch data for the
|
|
209
|
+
one you need.
|
|
239
210
|
|
|
240
|
-
|
|
211
|
+
Use the `kSec*` constants, never their string values such as `"svce"` or
|
|
212
|
+
`"class"`. Dictionaries typed `[CFString: Any]` work, and so do `[String: Any]` ones
|
|
213
|
+
whose keys are cast with `as String`; pick one style per codebase. This file uses `[CFString: Any]` because it is
|
|
214
|
+
shorter.
|
|
241
215
|
|
|
242
|
-
##
|
|
216
|
+
## One Place That Builds Queries
|
|
243
217
|
|
|
244
|
-
|
|
218
|
+
A small builder keeps every query shape in one auditable spot, so a missing
|
|
219
|
+
return flag or a typo in an attribute shows up in one review instead of twenty.
|
|
245
220
|
|
|
246
221
|
```swift
|
|
247
|
-
enum
|
|
248
|
-
static func
|
|
249
|
-
|
|
250
|
-
account: String
|
|
251
|
-
service: String
|
|
222
|
+
enum KeychainQuery {
|
|
223
|
+
static func make(
|
|
224
|
+
itemClass: CFString = kSecClassGenericPassword,
|
|
225
|
+
account: String,
|
|
226
|
+
service: String,
|
|
252
227
|
accessGroup: String? = nil,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
) -> [
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return
|
|
228
|
+
wantData: Bool = false,
|
|
229
|
+
wantAttributes: Bool = false,
|
|
230
|
+
limit: CFString = kSecMatchLimitOne
|
|
231
|
+
) -> [CFString: Any] {
|
|
232
|
+
var q: [CFString: Any] = [
|
|
233
|
+
kSecClass: itemClass,
|
|
234
|
+
kSecAttrAccount: account,
|
|
235
|
+
kSecAttrService: service,
|
|
236
|
+
kSecMatchLimit: limit
|
|
237
|
+
]
|
|
238
|
+
if let accessGroup { q[kSecAttrAccessGroup] = accessGroup }
|
|
239
|
+
if wantData { q[kSecReturnData] = true }
|
|
240
|
+
if wantAttributes { q[kSecReturnAttributes] = true }
|
|
241
|
+
return q
|
|
267
242
|
}
|
|
268
243
|
}
|
|
269
244
|
```
|
|
270
245
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
---
|
|
246
|
+
Drop `kSecMatchLimit` and the return flags before handing a builder result to
|
|
247
|
+
`SecItemAdd`, `SecItemUpdate` or `SecItemDelete`.
|
|
274
248
|
|
|
275
|
-
## OSStatus
|
|
249
|
+
## OSStatus Codes
|
|
276
250
|
|
|
277
|
-
|
|
251
|
+
A nonzero status is not automatically a failure to report. Treat each one on
|
|
252
|
+
its merits:
|
|
278
253
|
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
|
|
|
288
|
-
|
|
289
|
-
Map raw codes to a domain-specific Swift error:
|
|
254
|
+
| Code | Constant | Response |
|
|
255
|
+
| --- | --- | --- |
|
|
256
|
+
| 0 | `errSecSuccess` | Continue. |
|
|
257
|
+
| -25299 | `errSecDuplicateItem` | Switch to `SecItemUpdate`. |
|
|
258
|
+
| -25300 | `errSecItemNotFound` | Return nil; on delete, count it as success. |
|
|
259
|
+
| -25308 | `errSecInteractionNotAllowed` | Try again later (device locked). Never delete. |
|
|
260
|
+
| -128 | `errSecUserCanceled` | The user dismissed the prompt; tell the UI. |
|
|
261
|
+
| -25293 | `errSecAuthFailed` | Authentication failed or the protected item is no longer usable. |
|
|
262
|
+
| -50 | `errSecParam` | A programming error in the dictionary; fix the query. |
|
|
263
|
+
| -25303 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
|
|
290
264
|
|
|
291
265
|
```swift
|
|
292
266
|
struct KeychainError: Error, CustomStringConvertible {
|
|
293
267
|
let status: OSStatus
|
|
294
|
-
|
|
295
268
|
var description: String {
|
|
296
|
-
let
|
|
297
|
-
return "
|
|
269
|
+
let text = SecCopyErrorMessageString(status, nil) as String? ?? "unknown"
|
|
270
|
+
return "Keychain status \(status): \(text)"
|
|
298
271
|
}
|
|
299
272
|
}
|
|
300
273
|
```
|
|
301
274
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
---
|
|
305
|
-
|
|
306
|
-
## Actor-Isolated Keychain Manager (iOS 17+ / macOS 14+)
|
|
275
|
+
Logs may record the shape of a query (class, service, which flags were set)
|
|
276
|
+
and the status code. They never record `kSecValueData`, tokens or key bytes.
|
|
307
277
|
|
|
308
|
-
|
|
278
|
+
## An Actor Around the Keychain (iOS 13+ / macOS 10.15+)
|
|
309
279
|
|
|
310
|
-
|
|
280
|
+
Reads of biometric-protected items wait for the user and can take several
|
|
281
|
+
seconds (WWDC 2014 Session 711). Doing that on the main actor freezes the UI.
|
|
311
282
|
|
|
312
283
|
```swift
|
|
313
|
-
//
|
|
284
|
+
// Wrong: the main actor waits on securityd and, for protected items, on the user.
|
|
314
285
|
@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
|
|
286
|
+
final class ProfileModel: ObservableObject {
|
|
287
|
+
@Published var apiKey: String?
|
|
288
|
+
func load() {
|
|
289
|
+
var out: CFTypeRef?
|
|
290
|
+
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
|
291
|
+
kSecAttrAccount: "api", kSecReturnData: true]
|
|
292
|
+
_ = SecItemCopyMatching(q as CFDictionary, &out)
|
|
293
|
+
apiKey = (out as? Data).flatMap { String(decoding: $0, as: UTF8.self) }
|
|
332
294
|
}
|
|
333
295
|
}
|
|
334
296
|
```
|
|
335
297
|
|
|
336
|
-
✅ **Actor-isolated keychain manager with full CRUD:**
|
|
337
|
-
|
|
338
298
|
```swift
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
static let shared = KeychainManager()
|
|
342
|
-
|
|
299
|
+
actor SecretStore {
|
|
300
|
+
static let shared = SecretStore()
|
|
343
301
|
private let service: String
|
|
344
302
|
|
|
345
|
-
init(service: String = Bundle.main.bundleIdentifier ?? "
|
|
303
|
+
init(service: String = Bundle.main.bundleIdentifier ?? "app.secrets") {
|
|
346
304
|
self.service = service
|
|
347
305
|
}
|
|
348
306
|
|
|
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
|
-
}
|
|
307
|
+
private func identity(_ account: String) -> [CFString: Any] {
|
|
308
|
+
[kSecClass: kSecClassGenericPassword,
|
|
309
|
+
kSecAttrService: service,
|
|
310
|
+
kSecAttrAccount: account]
|
|
383
311
|
}
|
|
384
312
|
|
|
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
|
-
}
|
|
313
|
+
func put(_ value: Data, for account: String,
|
|
314
|
+
accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) throws {
|
|
315
|
+
var insert = identity(account)
|
|
316
|
+
insert[kSecValueData] = value
|
|
317
|
+
insert[kSecAttrAccessible] = accessible
|
|
318
|
+
let status = SecItemAdd(insert as CFDictionary, nil)
|
|
319
|
+
if status == errSecSuccess { return }
|
|
320
|
+
guard status == errSecDuplicateItem else { throw KeychainError(status: status) }
|
|
321
|
+
let changed = SecItemUpdate(identity(account) as CFDictionary,
|
|
322
|
+
[kSecValueData: value] as CFDictionary)
|
|
323
|
+
guard changed == errSecSuccess else { throw KeychainError(status: changed) }
|
|
409
324
|
}
|
|
410
325
|
|
|
411
|
-
|
|
326
|
+
func get(_ account: String) throws -> Data? {
|
|
327
|
+
var q = identity(account)
|
|
328
|
+
q[kSecReturnData] = true
|
|
329
|
+
q[kSecMatchLimit] = kSecMatchLimitOne
|
|
330
|
+
var out: CFTypeRef?
|
|
331
|
+
let status = SecItemCopyMatching(q as CFDictionary, &out)
|
|
332
|
+
if status == errSecItemNotFound { return nil }
|
|
333
|
+
guard status == errSecSuccess else { throw KeychainError(status: status) }
|
|
334
|
+
return out as? Data
|
|
335
|
+
}
|
|
412
336
|
|
|
413
|
-
func
|
|
414
|
-
let
|
|
415
|
-
kSecClass: kSecClassGenericPassword,
|
|
416
|
-
kSecAttrService: service,
|
|
417
|
-
kSecAttrAccount: key
|
|
418
|
-
]
|
|
419
|
-
let status = SecItemDelete(query as CFDictionary)
|
|
337
|
+
func remove(_ account: String) throws {
|
|
338
|
+
let status = SecItemDelete(identity(account) as CFDictionary)
|
|
420
339
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
|
421
340
|
throw KeychainError(status: status)
|
|
422
341
|
}
|
|
423
342
|
}
|
|
424
343
|
|
|
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:
|
|
344
|
+
func accounts() throws -> [String] {
|
|
345
|
+
let q: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
|
346
|
+
kSecAttrService: service,
|
|
347
|
+
kSecMatchLimit: kSecMatchLimitAll,
|
|
348
|
+
kSecReturnAttributes: true]
|
|
349
|
+
var out: CFTypeRef?
|
|
350
|
+
let status = SecItemCopyMatching(q as CFDictionary, &out)
|
|
351
|
+
if status == errSecItemNotFound { return [] }
|
|
352
|
+
guard status == errSecSuccess, let rows = out as? [[String: Any]] else {
|
|
445
353
|
throw KeychainError(status: status)
|
|
446
354
|
}
|
|
355
|
+
return rows.compactMap { $0[kSecAttrAccount as String] as? String }
|
|
447
356
|
}
|
|
448
357
|
}
|
|
449
358
|
```
|
|
450
359
|
|
|
451
|
-
|
|
360
|
+
`remove` is idempotent. `accounts()` asks for attributes only, so it never pays
|
|
361
|
+
for the Secure Enclave unwrap of each secret.
|
|
362
|
+
|
|
363
|
+
From SwiftUI, the view model awaits the actor and the main actor is free while
|
|
364
|
+
the keychain works:
|
|
452
365
|
|
|
453
366
|
```swift
|
|
454
367
|
@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
|
-
}
|
|
368
|
+
@Observable
|
|
369
|
+
final class SettingsModel {
|
|
370
|
+
var hasKey = false
|
|
371
|
+
func refresh() async {
|
|
372
|
+
hasKey = (try? await SecretStore.shared.get("api")) != nil
|
|
466
373
|
}
|
|
467
374
|
}
|
|
468
375
|
```
|
|
469
376
|
|
|
470
|
-
|
|
377
|
+
Why an actor rather than a queue:
|
|
471
378
|
|
|
472
|
-
|
|
|
473
|
-
|
|
|
474
|
-
|
|
|
475
|
-
|
|
|
476
|
-
|
|
|
477
|
-
|
|
|
478
|
-
| Swift 6
|
|
379
|
+
| | Actor | GCD serial queue |
|
|
380
|
+
| --- | --- | --- |
|
|
381
|
+
| Isolation | Checked by the compiler | Up to the developer |
|
|
382
|
+
| Serialization | Done by the runtime | Done by the queue, if used consistently |
|
|
383
|
+
| Control flow | Straight-line `async`/`await` | Nested completion handlers |
|
|
384
|
+
| `Sendable` | Enforced | Annotate `@Sendable` by hand |
|
|
385
|
+
| Swift 6 | Native | Needs care to pass strict checking |
|
|
479
386
|
|
|
480
|
-
|
|
387
|
+
In completion-handler code that has not moved to Swift concurrency, a private
|
|
388
|
+
serial queue does the same job:
|
|
481
389
|
|
|
482
390
|
```swift
|
|
483
|
-
class
|
|
484
|
-
private let queue = DispatchQueue(label: "
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
func load(key: String, completion: @escaping (Result<Data?, Error>) -> Void) {
|
|
391
|
+
final class LegacySecretStore {
|
|
392
|
+
private let queue = DispatchQueue(label: "secrets.io", qos: .userInitiated)
|
|
393
|
+
func get(_ account: String, completion: @escaping @Sendable (Result<Data?, Error>) -> Void) {
|
|
488
394
|
queue.async {
|
|
489
|
-
|
|
395
|
+
let result = Result { try readItem(account: account) }
|
|
490
396
|
DispatchQueue.main.async { completion(result) }
|
|
491
397
|
}
|
|
492
398
|
}
|
|
493
399
|
}
|
|
494
400
|
```
|
|
495
401
|
|
|
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
|
-
|
|
402
|
+
Here `readItem(account:)` stands for a synchronous read such as `fetch()` in
|
|
403
|
+
the Reading section.
|
|
404
|
+
The completion is `@Sendable` because it crosses two queues; without it Swift 6
|
|
405
|
+
reports sending `completion` as a data race.
|
|
406
|
+
|
|
407
|
+
## Performance
|
|
408
|
+
|
|
409
|
+
- Returning attributes only uses the cached metadata key. It is quick and needs
|
|
410
|
+
no Secure Enclave work.
|
|
411
|
+
- Returning data needs the per-row key from the Secure Enclave, so it costs
|
|
412
|
+
more.
|
|
413
|
+
- For a list screen, fetch attributes or refs, and fetch data only for the row
|
|
414
|
+
the user picks.
|
|
415
|
+
- A query with nothing but `kSecClass` and `kSecMatchLimitAll` scans the whole
|
|
416
|
+
table. Adding service and account lets `securityd` use its index.
|
|
417
|
+
- At launch every read costs IPC plus enclave latency, and items protected with
|
|
418
|
+
a `WhenUnlocked` class can be unavailable if the app was launched before first
|
|
419
|
+
unlock (background refresh, VoIP pushes).
|
|
420
|
+
- Do not call SecItem synchronously in
|
|
421
|
+
`application(_:didFinishLaunchingWithOptions:)`. Read the item when a feature
|
|
422
|
+
first needs it.
|
|
423
|
+
- Passwords cannot be written in bulk. `SecItemAdd` with `kSecUseItemList` can
|
|
424
|
+
add several certificates or keys at once, but not password items. Reading in
|
|
425
|
+
bulk means `kSecMatchLimitAll`.
|
|
426
|
+
|
|
427
|
+
## macOS: Two Keychains (TN3137)
|
|
428
|
+
|
|
429
|
+
A Mac has two keychain implementations:
|
|
430
|
+
|
|
431
|
+
- the legacy **file-based keychain**, which SecItem uses unless told otherwise;
|
|
432
|
+
- the **data protection keychain**, which matches iOS.
|
|
433
|
+
|
|
434
|
+
The file-based one ignores attributes it does not understand without saying
|
|
435
|
+
so, applies `kSecMatchLimit` defaults inconsistently, and returns different
|
|
436
|
+
types from `SecItemAdd`.
|
|
437
|
+
|
|
438
|
+
Code opts into the data protection keychain with
|
|
439
|
+
`kSecUseDataProtectionKeychain: true`, available from macOS 10.15, or by setting
|
|
440
|
+
`kSecAttrSynchronizable: true`. It is required for iCloud Keychain sync,
|
|
441
|
+
biometric protection and storing Secure Enclave keys. New code should always
|
|
442
|
+
target it. Mac Catalyst apps and iOS apps running on a Mac use it
|
|
443
|
+
automatically.
|
|
538
444
|
|
|
539
445
|
```swift
|
|
540
|
-
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
query[kSecUseDataProtectionKeychain] = true
|
|
548
|
-
#endif
|
|
446
|
+
func macSafe(_ query: [CFString: Any]) -> [CFString: Any] {
|
|
447
|
+
var q = query
|
|
448
|
+
#if os(macOS)
|
|
449
|
+
q[kSecUseDataProtectionKeychain] = true
|
|
450
|
+
#endif
|
|
451
|
+
return q
|
|
452
|
+
}
|
|
549
453
|
```
|
|
550
454
|
|
|
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
|
|
455
|
+
When a macOS keychain bug appears, first confirm which backend the call went
|
|
456
|
+
to. An attribute the file-based keychain quietly drops will produce
|
|
457
|
+
`errSecNoSuchAttr` (-25303) on the data protection keychain.
|
|
458
|
+
|
|
459
|
+
## Accessibility at a Glance
|
|
460
|
+
|
|
461
|
+
| Constant | Readable | In backups | Typical use |
|
|
462
|
+
| --- | --- | --- | --- |
|
|
463
|
+
| `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | while unlocked | no | the default for most secrets |
|
|
464
|
+
| `kSecAttrAccessibleAfterFirstUnlock` | from first unlock until restart | yes | tokens read in the background |
|
|
465
|
+
| `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | unlocked and a passcode is set | no | the most sensitive data (OWASP recommendation) |
|
|
466
|
+
| `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | after the first unlock, until reboot | no | device-bound items read in the background |
|
|
467
|
+
|
|
468
|
+
`kSecAttrAccessibleAlways` was deprecated in iOS 12 and is not supported on
|
|
469
|
+
Apple silicon Macs. Do not use it. The full model is in
|
|
470
|
+
[keychain-access-control.md](keychain-access-control.md).
|
|
471
|
+
|
|
472
|
+
Choosing among them: `WhenPasscodeSetThisDeviceOnly` is the strongest, but its
|
|
473
|
+
items are deleted if the user removes the passcode. `WhenUnlockedThisDeviceOnly`
|
|
474
|
+
is a safe default for foreground use. The actor above defaults to
|
|
475
|
+
`AfterFirstUnlockThisDeviceOnly` because it assumes background callers.
|
|
476
|
+
|
|
477
|
+
## Checklist
|
|
478
|
+
|
|
479
|
+
1. Every SecItem call switches over its status, covering success, duplicate,
|
|
480
|
+
not found and interaction-not-allowed.
|
|
481
|
+
2. Saves fall back to `SecItemUpdate` on -25299.
|
|
482
|
+
3. Every `SecItemCopyMatching` sets at least one `kSecReturn*` key.
|
|
483
|
+
4. The cast of the `CFTypeRef` result matches the return keys and match limit.
|
|
484
|
+
5. No SecItem call runs on `@MainActor` (an actor, available from iOS 13, or
|
|
485
|
+
a private serial queue in completion-handler code).
|
|
486
|
+
6. Add, query and update dictionaries are built fresh for each call.
|
|
487
|
+
7. Keys are `kSec*` constants, not string literals.
|
|
488
|
+
8. Queries are specific: service and account for generic passwords,
|
|
489
|
+
`kSecMatchLimitOne` unless listing.
|
|
490
|
+
9. Delete treats `errSecItemNotFound` as success.
|
|
491
|
+
10. macOS code sets `kSecUseDataProtectionKeychain: true`.
|
|
492
|
+
11. `errSecInteractionNotAllowed` never leads to deletion.
|
|
493
|
+
|
|
494
|
+
## Related Files
|
|
495
|
+
|
|
496
|
+
- [keychain-item-classes.md](keychain-item-classes.md), [keychain-access-control.md](keychain-access-control.md),
|
|
497
|
+
[biometric-authentication.md](biometric-authentication.md), [secure-enclave.md](secure-enclave.md)
|
|
498
|
+
- [credential-storage-patterns.md](credential-storage-patterns.md), [keychain-sharing.md](keychain-sharing.md),
|
|
499
|
+
[testing-security-code.md](testing-security-code.md), [common-anti-patterns.md](common-anti-patterns.md)
|
|
500
|
+
|
|
501
|
+
Further reading: the Keychain Services documentation; TN3137; Quinn's two SecItem
|
|
502
|
+
forum posts; the keychain data protection chapter of the Apple Platform
|
|
503
|
+
Security Guide; WWDC 2014 session 711, titled Keychain and Authentication
|
|
504
|
+
with Touch ID; WWDC 2019 session 516, titled What's New in Authentication.
|