@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
package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md
CHANGED
|
@@ -1,543 +1,421 @@
|
|
|
1
1
|
# Keychain Access Control
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two independent settings decide whether a read succeeds:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **When** the item can be decrypted: `kSecAttrAccessible` picks the data
|
|
6
|
+
protection class, which decides whether the class key is in memory given the
|
|
7
|
+
device state (locked, unlocked, not yet unlocked since boot).
|
|
8
|
+
- **How** the user must prove presence at the moment of access:
|
|
9
|
+
`SecAccessControl` adds biometric, passcode or application-password
|
|
10
|
+
requirements.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
A read has to satisfy both. Getting either wrong produces the keychain failures
|
|
13
|
+
that reach production most often: background reads that return nothing, items
|
|
14
|
+
that vanish after a device migration, and credentials that are readable at rest
|
|
15
|
+
when they should not be.
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
Sources: Apple Platform Security Guide (2024 to 2026 editions), Keychain
|
|
18
|
+
Services documentation, TN3137, the `SecAccessControl` documentation, WWDC
|
|
19
|
+
2014 Session 711, WWDC 2015 Session 706, and OWASP MASTG.
|
|
10
20
|
|
|
11
21
|
## Contents
|
|
12
22
|
|
|
13
|
-
- [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
-
- [Available Flags](#available-flags)
|
|
19
|
-
- [Flag Compatibility Matrix](#flag-compatibility-matrix)
|
|
20
|
-
- [Composing Constraints](#composing-constraints)
|
|
21
|
-
- [The Cardinal Rule: Never Set Both Attributes](#the-cardinal-rule-never-set-both-attributes)
|
|
22
|
-
- [Decision Matrix: Choosing the Right Accessibility Level](#decision-matrix-choosing-the-right-accessibility-level)
|
|
23
|
-
- [`WhenPasscodeSetThisDeviceOnly` - Data that should self-destruct](#whenpasscodesetthisdeviceonly-data-that-should-self-destruct)
|
|
24
|
-
- [`WhenUnlockedThisDeviceOnly` - Standard device-bound credentials](#whenunlockedthisdeviceonly-standard-device-bound-credentials)
|
|
25
|
-
- [`AfterFirstUnlockThisDeviceOnly` - Background operations (most common for services)](#afterfirstunlockthisdeviceonly-background-operations-most-common-for-services)
|
|
26
|
-
- [`AfterFirstUnlock` - Background + backup migration](#afterfirstunlock-background-backup-migration)
|
|
27
|
-
- [Dual-Item Strategy for Mixed Contexts](#dual-item-strategy-for-mixed-contexts)
|
|
28
|
-
- [Common AI-Generated Mistakes](#common-ai-generated-mistakes)
|
|
29
|
-
- [Mistake 1: Omitting `kSecAttrAccessible` (inheriting the wrong default)](#mistake-1-omitting-ksecattraccessible-inheriting-the-wrong-default)
|
|
30
|
-
- [Mistake 2: Using deprecated `kSecAttrAccessibleAlways`](#mistake-2-using-deprecated-ksecattraccessiblealways)
|
|
31
|
-
- [Mistake 3: Not handling `ThisDeviceOnly` item loss after device migration](#mistake-3-not-handling-thisdeviceonly-item-loss-after-device-migration)
|
|
32
|
-
- [Mistake 4: Biometric flags on background-accessible protection levels](#mistake-4-biometric-flags-on-background-accessible-protection-levels)
|
|
33
|
-
- [Mistake 5: Conflicting flags without logical operator](#mistake-5-conflicting-flags-without-logical-operator)
|
|
23
|
+
- [When: The Accessibility Constants](#when-the-accessibility-constants)
|
|
24
|
+
- [How: SecAccessControl Flags](#how-secaccesscontrol-flags)
|
|
25
|
+
- [Accessibility and Access Control Are Exclusive](#accessibility-and-access-control-are-exclusive)
|
|
26
|
+
- [Choosing a Combination](#choosing-a-combination)
|
|
27
|
+
- [Mistakes in Generated Code](#mistakes-in-generated-code)
|
|
34
28
|
- [Code Patterns](#code-patterns)
|
|
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
|
-
### Lock-State Spectrum Explained
|
|
85
|
-
|
|
86
|
-
After a device restart, the system is in **Before First Unlock (BFU)** state. Only items with the deprecated `Always` class are supposed to be accessible. Even `AfterFirstUnlock` items are locked.
|
|
87
|
-
|
|
88
|
-
Once the user enters their passcode, the device enters **After First Unlock (AFU)** state. `AfterFirstUnlock` class keys load into memory and remain there through subsequent lock/unlock cycles until the next restart. `WhenUnlocked` class keys are available only during active unlocked periods and discarded each time the device locks.
|
|
89
|
-
|
|
90
|
-
> **iOS 15+ caveat - app pre-warming:** iOS can launch your process before first unlock for faster app startup. This means even `AfterFirstUnlock` items may be temporarily unavailable during pre-warm. Check `UIApplication.shared.isProtectedDataAvailable` before accessing keychain items, and defer if it returns `false`.
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## The "How" Layer: SecAccessControl Flags
|
|
95
|
-
|
|
96
|
-
`SecAccessControl` adds runtime authentication requirements on top of data-at-rest protection. It is created via `SecAccessControlCreateWithFlags`, which embeds the accessibility level inside the control object:
|
|
29
|
+
- [macOS: kSecUseDataProtectionKeychain](#macos-ksecusedataprotectionkeychain)
|
|
30
|
+
- [File Protection Sidebar](#file-protection-sidebar)
|
|
31
|
+
- [Error Codes](#error-codes)
|
|
32
|
+
- [Version Timeline](#version-timeline)
|
|
33
|
+
- [Testing](#testing)
|
|
34
|
+
- [Checklist](#checklist)
|
|
35
|
+
- [Related Files](#related-files)
|
|
36
|
+
|
|
37
|
+
## When: The Accessibility Constants
|
|
38
|
+
|
|
39
|
+
Every item is encrypted under a class key. Class keys derive from the device's
|
|
40
|
+
hardware UID and, for most classes, from the passcode as well. If an add leaves
|
|
41
|
+
`kSecAttrAccessible` out, the item gets `kSecAttrAccessibleWhenUnlocked`, which
|
|
42
|
+
means nothing running in the background can read it.
|
|
43
|
+
|
|
44
|
+
| Constant | Readable when | Leaves the device? | Notes |
|
|
45
|
+
| --- | --- | --- | --- |
|
|
46
|
+
| `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` (iOS 8+) | unlocked, and a passcode is currently set | never | `SecItemAdd` fails if there is no passcode. Removing the passcode deletes every such item for good. No backup, no iCloud sync, not in the escrow keybag, and no migratable variant. |
|
|
47
|
+
| `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` | unlocked | never | Not in backups, not migrated. |
|
|
48
|
+
| `kSecAttrAccessibleWhenUnlocked` | unlocked | encrypted backups | The default. Corresponds to `NSFileProtectionComplete`. With Require Passcode set to Immediately, the class key is discarded about ten seconds after lock. |
|
|
49
|
+
| `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | from the first unlock after boot until restart, including while locked | never | The choice for background work that should stay on this device. |
|
|
50
|
+
| `kSecAttrAccessibleAfterFirstUnlock` | same as above | encrypted backups | Apple uses it for Wi-Fi passwords, mail accounts and iCloud tokens. Corresponds to `NSFileProtectionCompleteUntilFirstUserAuthentication`. |
|
|
51
|
+
| `kSecAttrAccessibleAlwaysThisDeviceOnly` | always | never | Deprecated in iOS 12 and macOS 10.14; the intent was announced at WWDC 2015 Session 706. |
|
|
52
|
+
| `kSecAttrAccessibleAlways` | always | encrypted backups | Deprecated at the same time. Protected by the UID only, the same strength as `NSFileProtectionNone`. |
|
|
53
|
+
|
|
54
|
+
The two `Always` constants still compile, but what they do depends on the OS
|
|
55
|
+
version (they may be treated as `AfterFirstUnlock`). Move existing items to
|
|
56
|
+
`AfterFirstUnlock` or something stricter, and add a CI lint that rejects the
|
|
57
|
+
constants.
|
|
58
|
+
|
|
59
|
+
Summary:
|
|
60
|
+
|
|
61
|
+
- Still readable while locked: the `AfterFirstUnlock` pair (and the deprecated
|
|
62
|
+
`Always` pair).
|
|
63
|
+
- Carried in backups: `WhenUnlocked`, `AfterFirstUnlock`, `Always`.
|
|
64
|
+
|
|
65
|
+
### Device states
|
|
66
|
+
|
|
67
|
+
- After a restart the device is in **Before First Unlock** (BFU). Nothing but
|
|
68
|
+
the `Always` classes can be read, including `AfterFirstUnlock` items.
|
|
69
|
+
- Once the user enters the passcode the device is **After First Unlock** (AFU).
|
|
70
|
+
`AfterFirstUnlock` class keys stay loaded through later lock cycles until the
|
|
71
|
+
next restart. `WhenUnlocked` keys are dropped at every lock.
|
|
72
|
+
- From iOS 15, the system may pre-warm an app, launching its process before the
|
|
73
|
+
first unlock. Even `AfterFirstUnlock` items can be unreadable then. Check
|
|
74
|
+
`UIApplication.shared.isProtectedDataAvailable` and postpone the read when it
|
|
75
|
+
is `false` (or wait for `protectedDataDidBecomeAvailableNotification`).
|
|
76
|
+
|
|
77
|
+
## How: SecAccessControl Flags
|
|
97
78
|
|
|
98
79
|
```swift
|
|
99
80
|
func SecAccessControlCreateWithFlags(
|
|
100
|
-
_ allocator: CFAllocator?,
|
|
101
|
-
_ protection: CFTypeRef,
|
|
81
|
+
_ allocator: CFAllocator?,
|
|
82
|
+
_ protection: CFTypeRef,
|
|
102
83
|
_ flags: SecAccessControlCreateFlags,
|
|
103
84
|
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
|
|
104
85
|
) -> SecAccessControl?
|
|
105
86
|
```
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
**Authentication constraints:**
|
|
110
|
-
|
|
111
|
-
- **`.userPresence`** (iOS 8+) - Biometry OR passcode. Does not require biometry enrollment; auto-falls back to passcode. Equivalent to `[.biometryAny, .or, .devicePasscode]` but handles no-biometry gracefully.
|
|
112
|
-
- **`.biometryAny`** (iOS 11.3+, was `.touchIDAny`) - Requires biometric authentication. Item **survives** enrollment changes (new fingerprints, Face ID re-enrollment).
|
|
113
|
-
- **`.biometryCurrentSet`** (iOS 11.3+, was `.touchIDCurrentSet`) - Requires biometric authentication. Item **invalidated** on enrollment changes. Most secure biometric option - blocks an attacker who enrolls their own biometrics.
|
|
114
|
-
- **`.devicePasscode`** (iOS 9+) - Requires device passcode entry only.
|
|
115
|
-
|
|
116
|
-
**Logical combinators:**
|
|
117
|
-
|
|
118
|
-
- **`.or`** - At least one constraint must be satisfied.
|
|
119
|
-
- **`.and`** - All constraints must be satisfied.
|
|
120
|
-
|
|
121
|
-
**Additional:**
|
|
88
|
+
Pass `nil` for the allocator. `protection` is one of the accessibility
|
|
89
|
+
constants above; it becomes part of the access-control object.
|
|
122
90
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
| `.
|
|
131
|
-
| `.biometryAny` | No | Foreground secrets | `errSecAuthFailed` if no biometrics enrolled |
|
|
132
|
-
| `.biometryCurrentSet` | No | `WhenPasscodeSetTDO` for highest security | Auth fails on enrollment change |
|
|
133
|
-
| `.devicePasscode` | No | Compliance flows | `-25308` without UI |
|
|
134
|
-
| `.privateKeyUsage` | Yes (for key ops) | Secure Enclave keys | - |
|
|
135
|
-
| `.applicationPassword` | Yes (if password cached) | Niche models | Password lifecycle management |
|
|
136
|
-
|
|
137
|
-
### Composing Constraints
|
|
138
|
-
|
|
139
|
-
Since `SecAccessControlCreateFlags` is an `OptionSet`, compose with array literal syntax:
|
|
140
|
-
|
|
141
|
-
```swift
|
|
142
|
-
// Biometry OR passcode - most common pattern
|
|
143
|
-
let flags: SecAccessControlCreateFlags = [.biometryCurrentSet, .or, .devicePasscode]
|
|
144
|
-
|
|
145
|
-
// Biometry AND passcode - both required (rare, high security)
|
|
146
|
-
let flags: SecAccessControlCreateFlags = [.biometryAny, .and, .devicePasscode]
|
|
147
|
-
|
|
148
|
-
// Biometry OR passcode, plus application password encryption
|
|
149
|
-
let flags: SecAccessControlCreateFlags = [.biometryAny, .or, .devicePasscode, .applicationPassword]
|
|
150
|
-
```
|
|
91
|
+
| Flag | Since | Requires | Works in the background |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| `.userPresence` | iOS 8 | biometry or the passcode; falls back to the passcode by itself and works with nothing enrolled. Close to `[.biometryAny, .or, .devicePasscode]` but tolerant of devices without biometry. | no |
|
|
94
|
+
| `.biometryAny` | iOS 11.3 (was `.touchIDAny`) | any enrolled finger or face; survives enrollment changes | no |
|
|
95
|
+
| `.biometryCurrentSet` | iOS 11.3 (was `.touchIDCurrentSet`) | the enrollment that existed when the item was stored; any change invalidates the item. The strongest choice, since an attacker who adds their own face or finger gains nothing. | no |
|
|
96
|
+
| `.devicePasscode` | iOS 9 | the passcode only | no |
|
|
97
|
+
| `.privateKeyUsage` | iOS 9 | needed for any Secure Enclave private key operation (signing, key agreement) | yes, for key operations |
|
|
98
|
+
| `.applicationPassword` | iOS 9 | a password supplied by the app, mixed into key derivation; an additional encryption layer rather than a gate | yes, if the password is cached |
|
|
151
99
|
|
|
152
|
-
|
|
100
|
+
`.or` accepts any one of the listed requirements; `.and` demands all of them.
|
|
153
101
|
|
|
154
|
-
|
|
102
|
+
What misuse looks like:
|
|
155
103
|
|
|
156
|
-
|
|
104
|
+
| Flag | Failure |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `.userPresence` | -25308 when read in the background |
|
|
107
|
+
| `.biometryAny` | `errSecAuthFailed` when nothing is enrolled |
|
|
108
|
+
| `.biometryCurrentSet` | authentication fails for good after an enrollment change |
|
|
109
|
+
| `.devicePasscode` | -25308 when no UI can be shown |
|
|
157
110
|
|
|
158
|
-
|
|
111
|
+
Usual pairings: `.userPresence` with a foreground `WhenUnlocked` item;
|
|
112
|
+
`.biometryCurrentSet` with `WhenPasscodeSetThisDeviceOnly`; `.devicePasscode`
|
|
113
|
+
for flows that a compliance rule requires; `.privateKeyUsage` on Secure
|
|
114
|
+
Enclave keys.
|
|
159
115
|
|
|
160
|
-
|
|
161
|
-
// ❌ WRONG - sets accessibility twice, causes errSecParam (-50)
|
|
162
|
-
var error: Unmanaged<CFError>?
|
|
163
|
-
let access = SecAccessControlCreateWithFlags(
|
|
164
|
-
nil,
|
|
165
|
-
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, // ← accessibility set HERE
|
|
166
|
-
[.biometryCurrentSet, .or, .devicePasscode],
|
|
167
|
-
&error
|
|
168
|
-
)!
|
|
169
|
-
|
|
170
|
-
let query: [String: Any] = [
|
|
171
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
172
|
-
kSecAttrAccount as String: "credential",
|
|
173
|
-
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, // ❌ CONFLICT
|
|
174
|
-
kSecAttrAccessControl as String: access, // ← already contains accessibility
|
|
175
|
-
kSecValueData as String: secretData
|
|
176
|
-
]
|
|
177
|
-
// SecItemAdd returns errSecParam (-50)
|
|
178
|
-
```
|
|
116
|
+
`SecAccessControlCreateFlags` is an `OptionSet`:
|
|
179
117
|
|
|
180
118
|
```swift
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
nil,
|
|
185
|
-
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
186
|
-
[.biometryCurrentSet, .or, .devicePasscode],
|
|
187
|
-
&error
|
|
188
|
-
) else { throw KeychainError.accessControlCreationFailed(error?.takeRetainedValue()) }
|
|
189
|
-
|
|
190
|
-
let query: [String: Any] = [
|
|
191
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
192
|
-
kSecAttrAccount as String: "credential",
|
|
193
|
-
kSecAttrAccessControl as String: access, // Contains accessibility + auth flags
|
|
194
|
-
kSecValueData as String: secretData
|
|
195
|
-
]
|
|
119
|
+
let strongWithRecovery: SecAccessControlCreateFlags = [.biometryCurrentSet, .or, .devicePasscode]
|
|
120
|
+
let both: SecAccessControlCreateFlags = [.biometryAny, .and, .devicePasscode]
|
|
121
|
+
let layered: SecAccessControlCreateFlags = [.biometryAny, .or, .devicePasscode, .applicationPassword]
|
|
196
122
|
```
|
|
197
123
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
## Decision Matrix: Choosing the Right Accessibility Level
|
|
201
|
-
|
|
202
|
-
### `WhenPasscodeSetThisDeviceOnly` - Data that should self-destruct
|
|
203
|
-
|
|
204
|
-
Use for your most sensitive credentials. Pair with `.biometryCurrentSet` via `SecAccessControl`. Accept the tradeoff: items are permanently destroyed on passcode removal and never survive device migration. Your app **must** handle item absence gracefully and guide users through re-authentication.
|
|
205
|
-
|
|
206
|
-
**Use cases:** Banking session tokens, password manager vault keys, healthcare credentials, E2E encryption private keys.
|
|
207
|
-
|
|
208
|
-
### `WhenUnlockedThisDeviceOnly` - Standard device-bound credentials
|
|
209
|
-
|
|
210
|
-
Credentials that should be device-bound but don't need passcode-deletion behavior. Re-authenticate after device migration.
|
|
211
|
-
|
|
212
|
-
**Use cases:** OAuth access tokens (refreshable), app-specific API keys, cached credentials, device registration tokens.
|
|
213
|
-
|
|
214
|
-
### `AfterFirstUnlockThisDeviceOnly` - Background operations (most common for services)
|
|
215
|
-
|
|
216
|
-
The correct choice for **any keychain item accessed by background code** - push notification handlers, WidgetKit timeline providers, background fetch, VPN extensions, notification service extensions. Device-bound.
|
|
217
|
-
|
|
218
|
-
**Use cases:** Push notification decryption keys, VPN credentials, background sync tokens, watch connectivity tokens.
|
|
219
|
-
|
|
220
|
-
### `AfterFirstUnlock` - Background + backup migration
|
|
221
|
-
|
|
222
|
-
Same background accessibility, plus items migrate with encrypted backups. Use when background access and device-transfer continuity are both needed.
|
|
124
|
+
Two authentication flags with no `.or` or `.and` between them make
|
|
125
|
+
`SecAccessControlCreateWithFlags` return `nil` with `errSecParam` (-50).
|
|
223
126
|
|
|
224
|
-
|
|
127
|
+
Any item whose flags involve biometry makes the system show Face ID on devices
|
|
128
|
+
that have it, so the app's `Info.plist` must contain
|
|
129
|
+
`NSFaceIDUsageDescription`. See
|
|
130
|
+
[biometric-authentication.md](biometric-authentication.md).
|
|
225
131
|
|
|
226
|
-
|
|
132
|
+
## Accessibility and Access Control Are Exclusive
|
|
227
133
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
## Common AI-Generated Mistakes
|
|
233
|
-
|
|
234
|
-
### Mistake 1: Omitting `kSecAttrAccessible` (inheriting the wrong default)
|
|
235
|
-
|
|
236
|
-
The most pervasive error. AI code generators produce keychain wrappers that never set `kSecAttrAccessible`, inheriting `WhenUnlocked`. Works during development (device unlocked while testing), fails in production when background extensions execute while locked - `errSecInteractionNotAllowed` (-25308), often silently swallowed.
|
|
237
|
-
|
|
238
|
-
```swift
|
|
239
|
-
// ❌ WRONG - omits kSecAttrAccessible, defaults to WhenUnlocked
|
|
240
|
-
let query: [String: Any] = [
|
|
241
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
242
|
-
kSecAttrAccount as String: "authToken",
|
|
243
|
-
kSecAttrService as String: "com.example.app",
|
|
244
|
-
kSecValueData as String: tokenData
|
|
245
|
-
// Missing: kSecAttrAccessible - background extensions WILL fail with -25308
|
|
246
|
-
]
|
|
247
|
-
```
|
|
134
|
+
An add dictionary carries `kSecAttrAccessible` or `kSecAttrAccessControl`,
|
|
135
|
+
never both. The pair is rejected with `errSecParam` (-50). With access control,
|
|
136
|
+
the accessibility value lives inside the `SecAccessControl` object.
|
|
248
137
|
|
|
249
138
|
```swift
|
|
250
|
-
//
|
|
251
|
-
let
|
|
139
|
+
// Wrong: accessibility given twice, and the access control force-unwrapped.
|
|
140
|
+
let acl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
141
|
+
.userPresence, nil)!
|
|
142
|
+
let clash: [String: Any] = [
|
|
252
143
|
kSecClass as String: kSecClassGenericPassword,
|
|
253
|
-
kSecAttrAccount as String: "
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
kSecValueData as String: tokenData
|
|
144
|
+
kSecAttrAccount as String: "journal-key",
|
|
145
|
+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
146
|
+
kSecAttrAccessControl as String: acl
|
|
257
147
|
]
|
|
258
148
|
```
|
|
259
149
|
|
|
260
|
-
### Mistake 2: Using deprecated `kSecAttrAccessibleAlways`
|
|
261
|
-
|
|
262
|
-
Compiles with a warning on iOS 12+, runs at runtime - arguably worse than a hard failure. No meaningful lock-state protection.
|
|
263
|
-
|
|
264
|
-
```swift
|
|
265
|
-
// ❌ WRONG - deprecated since iOS 12
|
|
266
|
-
let query: [String: Any] = [
|
|
267
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
268
|
-
kSecAttrAccessible as String: kSecAttrAccessibleAlways, // ⚠️ Deprecated
|
|
269
|
-
kSecValueData as String: tokenData
|
|
270
|
-
]
|
|
271
|
-
// Replacement: kSecAttrAccessibleAfterFirstUnlock
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Mistake 3: Not handling `ThisDeviceOnly` item loss after device migration
|
|
275
|
-
|
|
276
|
-
Items with `ThisDeviceOnly` are cryptographically bound to the hardware UID. They are excluded from all backups, iCloud sync, and Quick Start device-to-device migration. After restoring to a new device, these items silently disappear - `errSecItemNotFound` (-25300). AI-generated code rarely implements re-authentication flows for this scenario.
|
|
277
|
-
|
|
278
|
-
### Mistake 4: Biometric flags on background-accessible protection levels
|
|
279
|
-
|
|
280
|
-
Setting `.biometryCurrentSet` with `kSecAttrAccessibleAfterFirstUnlock` is technically valid at the API level but creates a **logical contradiction**: `AfterFirstUnlock` implies background access while locked, but biometric auth requires an interactive prompt. Result: `errSecInteractionNotAllowed` in background contexts, defeating the purpose.
|
|
281
|
-
|
|
282
|
-
### Mistake 5: Conflicting flags without logical operator
|
|
283
|
-
|
|
284
|
-
Combining `.biometryCurrentSet` and `.devicePasscode` without `.or` or `.and` causes `SecAccessControlCreateWithFlags` to return `nil` / `errSecParam` (-50).
|
|
285
|
-
|
|
286
|
-
```swift
|
|
287
|
-
// ❌ WRONG - missing logical operator
|
|
288
|
-
let access = SecAccessControlCreateWithFlags(
|
|
289
|
-
nil,
|
|
290
|
-
kSecAttrAccessibleWhenUnlocked,
|
|
291
|
-
[.biometryCurrentSet, .devicePasscode], // Missing .or or .and
|
|
292
|
-
&error
|
|
293
|
-
)
|
|
294
|
-
// Returns nil, error contains errSecParam
|
|
295
|
-
```
|
|
296
|
-
|
|
297
150
|
```swift
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
151
|
+
func journalKeyEntry(_ bytes: Data) throws -> [String: Any] {
|
|
152
|
+
var cfError: Unmanaged<CFError>?
|
|
153
|
+
guard let acl = SecAccessControlCreateWithFlags(
|
|
154
|
+
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, &cfError) else {
|
|
155
|
+
throw (cfError?.takeRetainedValue() as Error?) ?? KeychainError(status: errSecParam)
|
|
156
|
+
}
|
|
157
|
+
return [
|
|
158
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
159
|
+
kSecAttrService as String: "com.example.journal",
|
|
160
|
+
kSecAttrAccount as String: "journal-key",
|
|
161
|
+
kSecAttrAccessControl as String: acl,
|
|
162
|
+
kSecValueData as String: bytes
|
|
163
|
+
]
|
|
164
|
+
}
|
|
305
165
|
```
|
|
306
166
|
|
|
307
|
-
|
|
167
|
+
## Choosing a Combination
|
|
168
|
+
|
|
169
|
+
| Setting | For | Consequence to design for | Examples |
|
|
170
|
+
| --- | --- | --- | --- |
|
|
171
|
+
| `WhenPasscodeSetThisDeviceOnly` + `.biometryCurrentSet` | the most sensitive data | Items disappear if the passcode is removed and never move to a new device; the app must cope with the item being gone and re-authenticate. | banking session tokens, password-vault keys, health-record credentials, end-to-end private keys |
|
|
172
|
+
| `WhenUnlockedThisDeviceOnly` | device-bound data that should survive passcode changes | Re-authenticate after migration. | refreshable OAuth access tokens, app API keys, cached credentials, device registration tokens |
|
|
173
|
+
| `AfterFirstUnlockThisDeviceOnly` | anything read by background code: push handlers, WidgetKit timeline providers, background fetch, VPN and notification service extensions | Readable while locked, so weaker at rest. | push payload decryption keys, VPN credentials, background sync tokens, watch connectivity tokens |
|
|
174
|
+
| `AfterFirstUnlock` | background access that should also survive a restore | Travels in encrypted backups. | enterprise VPN credentials, mail credentials, Wi-Fi passwords |
|
|
175
|
+
|
|
176
|
+
When one credential needs both background access and biometric protection in
|
|
177
|
+
the foreground, keep two items: one `AfterFirstUnlockThisDeviceOnly` with no
|
|
178
|
+
presence flags for background code, and one `WhenUnlockedThisDeviceOnly` with a
|
|
179
|
+
biometric access control for the foreground path.
|
|
180
|
+
|
|
181
|
+
## Mistakes in Generated Code
|
|
182
|
+
|
|
183
|
+
1. **No accessibility value.** The item defaults to `WhenUnlocked`. Everything
|
|
184
|
+
works during development on an unlocked phone; in the background the read
|
|
185
|
+
returns -25308, which is often swallowed. Background items should say
|
|
186
|
+
`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` explicitly.
|
|
187
|
+
2. **`kSecAttrAccessibleAlways`.** On iOS 12 and later it compiles with a
|
|
188
|
+
deprecation warning and runs, offering no protection tied to the lock state.
|
|
189
|
+
Replace it with `kSecAttrAccessibleAfterFirstUnlock`.
|
|
190
|
+
3. **Forgetting what `ThisDeviceOnly` means.** Such items are tied to the
|
|
191
|
+
hardware UID and are left out of backups, iCloud Keychain and Quick Start
|
|
192
|
+
transfers. After a restore onto a new device the read returns
|
|
193
|
+
`errSecItemNotFound` (-25300). Build the re-authentication path.
|
|
194
|
+
4. **`.biometryCurrentSet` on an `AfterFirstUnlock` item.** The API accepts it,
|
|
195
|
+
but the two settings contradict each other: the class promises background
|
|
196
|
+
availability and the flag demands a user. Background reads fail with
|
|
197
|
+
`errSecInteractionNotAllowed`.
|
|
198
|
+
5. **Two authentication flags with no operator.** `nil` and `errSecParam`.
|
|
308
199
|
|
|
309
200
|
## Code Patterns
|
|
310
201
|
|
|
311
|
-
|
|
202
|
+
### Top-sensitivity secret
|
|
312
203
|
|
|
313
|
-
|
|
204
|
+
Saving follows add, then update on duplicate. Updating the data of an item that
|
|
205
|
+
has an access control requires the user to authenticate, so pass an
|
|
206
|
+
`LAContext` for the prompt. `SecItemUpdate` cannot change an item's access
|
|
207
|
+
control, so a change of policy is the one case that deletes and re-adds.
|
|
314
208
|
|
|
315
209
|
```swift
|
|
316
|
-
|
|
317
|
-
var error: Unmanaged<CFError>?
|
|
318
|
-
guard let accessControl = SecAccessControlCreateWithFlags(
|
|
319
|
-
nil,
|
|
320
|
-
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
|
321
|
-
[.biometryCurrentSet, .or, .devicePasscode],
|
|
322
|
-
&error
|
|
323
|
-
) else {
|
|
324
|
-
throw KeychainError.accessControlCreationFailed(error?.takeRetainedValue())
|
|
325
|
-
}
|
|
210
|
+
import LocalAuthentication
|
|
326
211
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
kSecAttrService as String: service,
|
|
331
|
-
kSecAttrAccessControl as String: accessControl,
|
|
332
|
-
kSecValueData as String: data
|
|
333
|
-
]
|
|
334
|
-
|
|
335
|
-
let status = SecItemAdd(query as CFDictionary, nil)
|
|
336
|
-
switch status {
|
|
337
|
-
case errSecSuccess: return
|
|
338
|
-
case errSecDuplicateItem:
|
|
339
|
-
// Must delete + re-add: SecItemUpdate cannot change SecAccessControl
|
|
340
|
-
let searchQuery: [String: Any] = [
|
|
212
|
+
enum VaultKeyStore {
|
|
213
|
+
static var base: [String: Any] {
|
|
214
|
+
[
|
|
341
215
|
kSecClass as String: kSecClassGenericPassword,
|
|
342
|
-
|
|
343
|
-
|
|
216
|
+
kSecAttrService as String: "com.example.vault",
|
|
217
|
+
kSecAttrAccount as String: "master"
|
|
344
218
|
]
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static func policy() throws -> SecAccessControl {
|
|
222
|
+
var cfError: Unmanaged<CFError>?
|
|
223
|
+
guard let acl = SecAccessControlCreateWithFlags(
|
|
224
|
+
nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
|
225
|
+
[.biometryCurrentSet, .or, .devicePasscode], &cfError) else {
|
|
226
|
+
throw (cfError?.takeRetainedValue() as Error?) ?? KeychainError(status: errSecParam)
|
|
227
|
+
}
|
|
228
|
+
return acl
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static func save(_ key: Data, context: LAContext) throws {
|
|
232
|
+
var insert = base
|
|
233
|
+
insert[kSecAttrAccessControl as String] = try policy()
|
|
234
|
+
insert[kSecValueData as String] = key
|
|
235
|
+
let added = SecItemAdd(insert as CFDictionary, nil)
|
|
236
|
+
guard added == errSecDuplicateItem else {
|
|
237
|
+
guard added == errSecSuccess else { throw KeychainError(status: added) }
|
|
238
|
+
return
|
|
348
239
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
240
|
+
var match = base
|
|
241
|
+
match[kSecUseAuthenticationContext as String] = context
|
|
242
|
+
let changed = SecItemUpdate(match as CFDictionary,
|
|
243
|
+
[kSecValueData as String: key] as CFDictionary)
|
|
244
|
+
guard changed == errSecSuccess else { throw KeychainError(status: changed) }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static func replacePolicy(keeping key: Data) throws {
|
|
248
|
+
let removed = SecItemDelete(base as CFDictionary)
|
|
249
|
+
guard removed == errSecSuccess || removed == errSecItemNotFound else {
|
|
250
|
+
throw KeychainError(status: removed)
|
|
352
251
|
}
|
|
353
|
-
|
|
354
|
-
|
|
252
|
+
var insert = base
|
|
253
|
+
insert[kSecAttrAccessControl as String] = try policy()
|
|
254
|
+
insert[kSecValueData as String] = key
|
|
255
|
+
let added = SecItemAdd(insert as CFDictionary, nil)
|
|
256
|
+
guard added == errSecSuccess else { throw KeychainError(status: added) }
|
|
355
257
|
}
|
|
356
258
|
}
|
|
357
259
|
```
|
|
358
260
|
|
|
359
|
-
|
|
261
|
+
`replacePolicy` exists only because access control cannot be updated in place.
|
|
262
|
+
Read the value (with authentication) before calling it, so nothing is lost.
|
|
263
|
+
`base` is computed: a `[String: Any]` is not `Sendable`, so a `static let` of
|
|
264
|
+
it is a Swift 6 error.
|
|
360
265
|
|
|
361
|
-
###
|
|
266
|
+
### Token read by background code
|
|
362
267
|
|
|
363
268
|
```swift
|
|
364
|
-
func
|
|
365
|
-
let
|
|
269
|
+
func saveSyncToken(_ token: Data) throws {
|
|
270
|
+
let match: [String: Any] = [
|
|
366
271
|
kSecClass as String: kSecClassGenericPassword,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
|
370
|
-
kSecValueData as String: token
|
|
272
|
+
kSecAttrService as String: "com.example.sync",
|
|
273
|
+
kSecAttrAccount as String: "bg-token"
|
|
371
274
|
]
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
kSecAttrService as String: service
|
|
382
|
-
]
|
|
383
|
-
let updateStatus = SecItemUpdate(searchQuery as CFDictionary, updateAttrs as CFDictionary)
|
|
384
|
-
guard updateStatus == errSecSuccess else {
|
|
385
|
-
throw KeychainError.fromStatus(updateStatus)
|
|
386
|
-
}
|
|
387
|
-
default:
|
|
388
|
-
throw KeychainError.fromStatus(status)
|
|
389
|
-
}
|
|
275
|
+
var insert = match
|
|
276
|
+
insert[kSecValueData as String] = token
|
|
277
|
+
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
278
|
+
let added = SecItemAdd(insert as CFDictionary, nil)
|
|
279
|
+
if added == errSecSuccess { return }
|
|
280
|
+
guard added == errSecDuplicateItem else { throw KeychainError(status: added) }
|
|
281
|
+
let changed = SecItemUpdate(match as CFDictionary,
|
|
282
|
+
[kSecValueData as String: token] as CFDictionary)
|
|
283
|
+
guard changed == errSecSuccess else { throw KeychainError(status: changed) }
|
|
390
284
|
}
|
|
391
285
|
```
|
|
392
286
|
|
|
393
|
-
###
|
|
287
|
+
### What goes wrong in an extension
|
|
394
288
|
|
|
395
289
|
```swift
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
var result: AnyObject?
|
|
408
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
409
|
-
// status == errSecInteractionNotAllowed (-25308) when device is locked
|
|
410
|
-
guard status == errSecSuccess, let data = result as? Data else {
|
|
411
|
-
return nil // ❌ Silent failure - no logging, no error propagation
|
|
412
|
-
}
|
|
413
|
-
return String(data: data, encoding: .utf8)
|
|
290
|
+
// Wrong: the item was stored with the default WhenUnlocked class. A widget or
|
|
291
|
+
// notification service extension running while the phone is locked gets -25308,
|
|
292
|
+
// and this code turns that into a silent nil with no log.
|
|
293
|
+
func tokenForWidget() -> Data? {
|
|
294
|
+
var out: CFTypeRef?
|
|
295
|
+
let q: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
|
296
|
+
kSecAttrAccount as String: "widget-token",
|
|
297
|
+
kSecReturnData as String: true]
|
|
298
|
+
guard SecItemCopyMatching(q as CFDictionary, &out) == errSecSuccess else { return nil }
|
|
299
|
+
return out as? Data
|
|
414
300
|
}
|
|
415
301
|
```
|
|
416
302
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
## macOS: `kSecUseDataProtectionKeychain`
|
|
303
|
+
## macOS: kSecUseDataProtectionKeychain
|
|
420
304
|
|
|
421
|
-
|
|
305
|
+
Unless told otherwise, SecItem on macOS works on the legacy file-based keychain
|
|
306
|
+
at `~/Library/Keychains/login.keychain-db`. Without
|
|
307
|
+
`kSecUseDataProtectionKeychain: true`:
|
|
422
308
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
-
|
|
426
|
-
-
|
|
427
|
-
- Secure Enclave integration is unavailable
|
|
428
|
-
- Biometric protection (Touch ID) won't function
|
|
309
|
+
- `SecAccessControl` fails with `errSecParam`;
|
|
310
|
+
- iCloud Keychain sync is unavailable;
|
|
311
|
+
- Secure Enclave keys cannot be stored;
|
|
312
|
+
- Touch ID protection does not apply.
|
|
429
313
|
|
|
430
314
|
```swift
|
|
431
|
-
|
|
432
|
-
let query: [String: Any] = [
|
|
315
|
+
let macEntry: [String: Any] = [
|
|
433
316
|
kSecClass as String: kSecClassGenericPassword,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
kSecAttrAccessible as String:
|
|
438
|
-
|
|
317
|
+
kSecAttrService as String: "com.example.mac-notes",
|
|
318
|
+
kSecAttrAccount as String: "sync",
|
|
319
|
+
kSecValueData as String: Data("v".utf8),
|
|
320
|
+
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
321
|
+
kSecUseDataProtectionKeychain as String: true
|
|
439
322
|
]
|
|
440
323
|
```
|
|
441
324
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
##
|
|
447
|
-
|
|
448
|
-
The keychain
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
## Error Codes
|
|
463
|
-
|
|
464
|
-
| Code
|
|
465
|
-
|
|
|
466
|
-
|
|
|
467
|
-
|
|
|
468
|
-
|
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
|
|
|
472
|
-
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
##
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
7. **Implement re-authentication flows** for `ThisDeviceOnly` items that will be absent after device migration or backup restore
|
|
540
|
-
8. **Check `isProtectedDataAvailable`** before keychain access in app launch paths - iOS 15+ pre-warming can start your process before first unlock
|
|
541
|
-
9. **Delete and re-add** (not update) when changing `SecAccessControl` on an existing item - `SecItemUpdate` cannot modify access control attributes
|
|
542
|
-
10. **Test on physical devices** across lock/unlock, reboot, passcode removal, and biometric enrollment change scenarios - the Simulator does not enforce data protection
|
|
543
|
-
11. **Block deprecated `kSecAttrAccessibleAlways` constants** in CI/CD linting and migrate existing items to `AfterFirstUnlock` on next foreground authentication
|
|
325
|
+
iOS, tvOS and watchOS ignore the flag. The data protection keychain needs a
|
|
326
|
+
logged-in user context, so a `launchd` daemon running outside any user session
|
|
327
|
+
has to use the legacy keychain.
|
|
328
|
+
|
|
329
|
+
## File Protection Sidebar
|
|
330
|
+
|
|
331
|
+
The keychain is for small, discrete secrets. Documents, databases and images
|
|
332
|
+
use `NSFileProtection` instead.
|
|
333
|
+
|
|
334
|
+
| File class | Keychain counterpart | Behaviour |
|
|
335
|
+
| --- | --- | --- |
|
|
336
|
+
| A: `NSFileProtectionComplete` | `WhenUnlocked` | key discarded about ten seconds after lock |
|
|
337
|
+
| B: `NSFileProtectionCompleteUnlessOpen` | none | uses Curve25519 key agreement so a file opened before lock can still be written while locked (for example an attachment downloading in the background) |
|
|
338
|
+
| C: `NSFileProtectionCompleteUntilFirstUserAuthentication` | `AfterFirstUnlock` | the default for third-party app files |
|
|
339
|
+
| D: `NSFileProtectionNone` | deprecated `Always` | UID protection only |
|
|
340
|
+
|
|
341
|
+
For layered protection, keep an encryption key in the keychain as
|
|
342
|
+
`WhenUnlockedThisDeviceOnly`, encrypt the file with it, and mark the file
|
|
343
|
+
`NSFileProtectionComplete` as well.
|
|
344
|
+
|
|
345
|
+
## Error Codes
|
|
346
|
+
|
|
347
|
+
| Code | Name | Typical cause |
|
|
348
|
+
| --- | --- | --- |
|
|
349
|
+
| -25308 | `errSecInteractionNotAllowed` | a `WhenUnlocked` item read while locked; an `AfterFirstUnlock` item read before first unlock; a biometric flag used in the background |
|
|
350
|
+
| -50 | `errSecParam` | both accessibility attributes present; flags without a combinator; access control on macOS without the data protection flag |
|
|
351
|
+
| -25293 | `errSecAuthFailed` | biometric match failed; enrollment changed under `.biometryCurrentSet`; nothing enrolled |
|
|
352
|
+
| -25300 | `errSecItemNotFound` | never stored; a `ThisDeviceOnly` item lost in migration; a `WhenPasscodeSet` item deleted with the passcode |
|
|
353
|
+
| -25299 | `errSecDuplicateItem` | save with add-or-update |
|
|
354
|
+
| -128 | `errSecUserCanceled` | the user tapped Cancel on the prompt |
|
|
355
|
+
| -34018 | `errSecMissingEntitlement` | the access group is not in the entitlements; frequent in the Simulator |
|
|
356
|
+
|
|
357
|
+
-25308 is the hardest of these to catch: it happens in production and rarely on
|
|
358
|
+
a developer's unlocked phone. Postpone the work and retry once
|
|
359
|
+
`isProtectedDataAvailable` is `true`.
|
|
360
|
+
|
|
361
|
+
## Version Timeline
|
|
362
|
+
|
|
363
|
+
| Release | Change |
|
|
364
|
+
| --- | --- |
|
|
365
|
+
| iOS 8 | `WhenPasscodeSetThisDeviceOnly`, `SecAccessControlCreateWithFlags`, `.userPresence` |
|
|
366
|
+
| iOS 9 | `.devicePasscode`, `.applicationPassword`, `.privateKeyUsage`; plan to retire `Always` announced (WWDC 2015 Session 706) |
|
|
367
|
+
| iOS 11.3 | Touch ID flag names replaced by biometry names |
|
|
368
|
+
| iOS 12 | both `Always` constants deprecated; they still compile and run |
|
|
369
|
+
| iOS 15 | keychain items installed by MDM default to after-first-unlock and non-migratory instead of always; pre-warming can run the app before first unlock |
|
|
370
|
+
| iOS 16 | passkeys (FIDO2 / WebAuthn, synced end to end encrypted through iCloud Keychain); no access control API change |
|
|
371
|
+
| iOS 17 | passkeys for managed Apple Accounts; no change to accessibility or `SecAccessControl` |
|
|
372
|
+
| iOS 18 | the standalone Passwords app; no data protection API change |
|
|
373
|
+
| iOS 26 | Stolen Device Protection on by default (biometrics without passcode fallback for saved passwords away from familiar places; confirm against current release notes); passkey import and export using the FIDO Alliance format; no new accessibility constants |
|
|
374
|
+
|
|
375
|
+
## Testing
|
|
376
|
+
|
|
377
|
+
The Simulator enforces neither `kSecAttrAccessible` nor `NSFileProtection`.
|
|
378
|
+
Data protection behaviour can only be tested on a physical device with a
|
|
379
|
+
passcode set.
|
|
380
|
+
|
|
381
|
+
| Scenario | Expected |
|
|
382
|
+
| --- | --- |
|
|
383
|
+
| Reboot, then read an `AfterFirstUnlock` item before unlocking | -25308 or -25300; after one unlock and a relock, a background read succeeds |
|
|
384
|
+
| Lock, then immediately read a `WhenUnlocked` item | -25308 |
|
|
385
|
+
| Remove the passcode | `WhenPasscodeSet` item gone (-25300) |
|
|
386
|
+
| Enroll a new finger or face | `.biometryCurrentSet` item fails (-25293) |
|
|
387
|
+
| Restore a backup onto another device | `ThisDeviceOnly` items absent (-25300) |
|
|
388
|
+
| Notification extension or widget while locked | `AfterFirstUnlock` readable, `WhenUnlocked` not |
|
|
389
|
+
|
|
390
|
+
More on test structure: [testing-security-code.md](testing-security-code.md).
|
|
391
|
+
|
|
392
|
+
## Checklist
|
|
393
|
+
|
|
394
|
+
- Every add states its accessibility, chosen for foreground or background use.
|
|
395
|
+
- Never both attributes; with access control, the accessibility goes inside it.
|
|
396
|
+
- Extensions, widgets, VPN code and push handlers read
|
|
397
|
+
`AfterFirstUnlockThisDeviceOnly` items.
|
|
398
|
+
- Top secrets use `WhenPasscodeSetThisDeviceOnly` with `.biometryCurrentSet`,
|
|
399
|
+
and the app handles deletion when the passcode is removed.
|
|
400
|
+
- Multiple authentication flags are joined by `.or` or `.and`.
|
|
401
|
+
- macOS queries all set `kSecUseDataProtectionKeychain: true`.
|
|
402
|
+
- `ThisDeviceOnly` items have a re-authentication path after migration or
|
|
403
|
+
restore.
|
|
404
|
+
- Launch paths check `isProtectedDataAvailable` (iOS 15 pre-warming).
|
|
405
|
+
- Saves are add, then update; only an access-control change deletes and
|
|
406
|
+
re-adds.
|
|
407
|
+
- Biometric flags come with `NSFaceIDUsageDescription` in `Info.plist`.
|
|
408
|
+
- Lock, reboot, passcode removal and enrollment change are tested on hardware.
|
|
409
|
+
- CI rejects the deprecated `Always` constants, and existing items move to
|
|
410
|
+
`AfterFirstUnlock` the next time the user authenticates in the foreground.
|
|
411
|
+
|
|
412
|
+
## Related Files
|
|
413
|
+
|
|
414
|
+
[keychain-fundamentals.md](keychain-fundamentals.md),
|
|
415
|
+
[biometric-authentication.md](biometric-authentication.md),
|
|
416
|
+
[secure-enclave.md](secure-enclave.md),
|
|
417
|
+
[keychain-item-classes.md](keychain-item-classes.md),
|
|
418
|
+
[common-anti-patterns.md](common-anti-patterns.md) (items on missing
|
|
419
|
+
accessibility and on `LAContext`-only gates), and
|
|
420
|
+
[compliance-owasp-mapping.md](compliance-owasp-mapping.md) (M9, insecure data
|
|
421
|
+
storage).
|