@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.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,543 +1,421 @@
1
1
  # Keychain Access Control
2
2
 
3
- > Scope: Selecting `kSecAttrAccessible` classes and `SecAccessControl` flags to enforce the correct lock-state and user-presence guarantees for keychain items.
3
+ Two independent settings decide whether a read succeeds:
4
4
 
5
- Data protection classes (`kSecAttrAccessible`) and runtime authentication gates (`SecAccessControl`) form the two-layer security model protecting every keychain item. The first controls **when** an item's class key is available in memory based on device state; the second controls **how** the user must authenticate at access time. Both must be satisfied for a read to succeed. Getting this wrong is the single most common cause of production keychain failures - background operations that silently return `nil`, items that vanish after device migration, or credentials left decryptable at rest.
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
- Sources: Apple Platform Security Guide (2024-2026 editions), Apple Keychain Services documentation, TN3137, WWDC 2014 Session 711 ("Keychain and Authentication with Touch ID"), WWDC 2015 Session 706, SecAccessControl documentation, OWASP MASTG.
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
- - [The "When" Layer: Seven Accessibility Constants](#the-when-layer-seven-accessibility-constants)
14
- - [The Protection Spectrum](#the-protection-spectrum)
15
- - [Quick Reference Table](#quick-reference-table)
16
- - [Lock-State Spectrum Explained](#lock-state-spectrum-explained)
17
- - [The "How" Layer: SecAccessControl Flags](#the-how-layer-secaccesscontrol-flags)
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
- - [Biometric protection with highest security](#biometric-protection-with-highest-security)
36
- - [Background-accessible token (push notifications, VPN, widgets)](#background-accessible-token-push-notifications-vpn-widgets)
37
- - [Accessing a `WhenUnlocked` item from a background extension](#accessing-a-whenunlocked-item-from-a-background-extension)
38
- - [macOS: `kSecUseDataProtectionKeychain`](#macos-ksecusedataprotectionkeychain)
39
- - [NSFileProtection Sidebar](#nsfileprotection-sidebar)
40
- - [Error Codes Reference](#error-codes-reference)
41
- - [iOS Version Timeline](#ios-version-timeline)
42
- - [Testing Requirements](#testing-requirements)
43
- - [Cross-References](#cross-references)
44
- - [Summary Checklist](#summary-checklist)
45
-
46
- ## The "When" Layer: Seven Accessibility Constants
47
-
48
- Every keychain item is encrypted with a class key derived from the device's hardware UID and (for most classes) the user's passcode. The `kSecAttrAccessible` attribute selects which class key protects the item, determining when the system can decrypt it. **If you omit `kSecAttrAccessible`, the default is `kSecAttrAccessibleWhenUnlocked`** - confirmed by Apple documentation. This default breaks all background operations.
49
-
50
- ### The Protection Spectrum
51
-
52
- Listed from most restrictive to least:
53
-
54
- **`kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly`** (iOS 8+) - the highest-security class. Items are accessible only while unlocked, and only if a device passcode is currently set. Two unique behaviors: (1) `SecItemAdd` fails on devices without a passcode, (2) **removing the passcode permanently deletes all items in this class** - class keys are discarded, data is unrecoverable. No non-`ThisDeviceOnly` variant exists. Items don't sync to iCloud Keychain, aren't backed up, and aren't in escrow keybags.
55
-
56
- **`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`** - Items decryptable only while unlocked. Device-bound: excluded from backups and device migration.
57
-
58
- **`kSecAttrAccessibleWhenUnlocked`** ⭐ (system default) - Same lock-state behavior as above, but items migrate with encrypted backups. Maps to `NSFileProtectionComplete`. Class key is discarded from memory shortly after the device locks (~10 seconds with Require Password set to Immediately).
59
-
60
- **`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`** - **The correct choice for background operations.** After the user unlocks the device once following a restart, the class key remains in memory until the next restart - even while locked. Device-bound.
61
-
62
- **`kSecAttrAccessibleAfterFirstUnlock`** - Same background accessibility, but items migrate with encrypted backups. Apple uses this for system Wi-Fi passwords, mail accounts, and iCloud tokens. Maps to `NSFileProtectionCompleteUntilFirstUserAuthentication`.
63
-
64
- **`kSecAttrAccessibleAlwaysThisDeviceOnly`** ⚠️ DEPRECATED - Deprecated in iOS 12 / macOS 10.14. Apple announced intent at WWDC 2015 Session 706.
65
-
66
- **`kSecAttrAccessibleAlways`** ⚠️ DEPRECATED - Same deprecation. Items encrypted with only the device UID (no passcode involvement), equivalent to `NSFileProtectionNone`.
67
-
68
- Deprecated "Always" constants have OS-version-specific behavior and should not be used for new or migrated code. **Migrate to `kSecAttrAccessibleAfterFirstUnlock` or a stricter class**, and block these constants in CI linting rather than relying on runtime behavior across OS versions.
69
-
70
- ### Quick Reference Table
71
-
72
- | Constant | Accessible When | Survives Lock | Migrates in Backup | Special |
73
- | -------------------------------- | ----------------------- | ------------- | ------------------ | ------------------------------- |
74
- | `WhenPasscodeSetThisDeviceOnly` | Unlocked + passcode set | No | No | **Deleted on passcode removal** |
75
- | `WhenUnlockedThisDeviceOnly` | Unlocked | No | No | - |
76
- | `WhenUnlocked` ⭐ default | Unlocked | No | Yes | - |
77
- | `AfterFirstUnlockThisDeviceOnly` | After first unlock | Yes | No | Background-safe |
78
- | `AfterFirstUnlock` | After first unlock | Yes | Yes | Background-safe + migratable |
79
- | `AlwaysThisDeviceOnly` ⚠️ | Always¹ | Yes | No | Deprecated iOS 12 |
80
- | `Always` ⚠️ | Always¹ | Yes | Yes | Deprecated iOS 12 |
81
-
82
- ¹ Behavior may be remapped to `AfterFirstUnlock` on modern iOS versions.
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?, // Pass nil
101
- _ protection: CFTypeRef, // A kSecAttrAccessible constant
81
+ _ allocator: CFAllocator?,
82
+ _ protection: CFTypeRef,
102
83
  _ flags: SecAccessControlCreateFlags,
103
84
  _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
104
85
  ) -> SecAccessControl?
105
86
  ```
106
87
 
107
- ### Available Flags
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
- - **`.privateKeyUsage`** (iOS 9+) - Required for Secure Enclave private key operations (signing, key agreement).
124
- - **`.applicationPassword`** (iOS 9+) - Adds an app-provided password to key derivation. Not a constraint - an additional encryption layer.
125
-
126
- ### Flag Compatibility Matrix
127
-
128
- | Flag | Works in Background? | Typical Pairing | Failure if Misused |
129
- | ---------------------- | ------------------------ | ----------------------------------------- | -------------------------------------------- |
130
- | `.userPresence` | No | Foreground + `WhenUnlocked` | `-25308` in background |
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
- > **Critical rule: `.or` / `.and` is required between authentication flags.** Combining `.biometryCurrentSet` and `.devicePasscode` without a logical operator causes `SecAccessControlCreateWithFlags` to return `nil` with `errSecParam` (-50). Both sources confirm this behavior.
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
- ## The Cardinal Rule: Never Set Both Attributes
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
- `kSecAttrAccessible` and `kSecAttrAccessControl` are **mutually exclusive** in the query dictionary. When you use `SecAccessControlCreateWithFlags`, the accessibility level is embedded inside the `SecAccessControl` object via the `protection` parameter. Setting both in the same `SecItemAdd` query causes **`errSecParam` (-50)**.
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
- ```swift
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
- // ✅ CORRECT - accessibility set only inside SecAccessControl
182
- var error: Unmanaged<CFError>?
183
- guard let access = SecAccessControlCreateWithFlags(
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
- **Use cases:** Enterprise VPN credentials, email account credentials, Wi-Fi configuration passwords.
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
- ### Dual-Item Strategy for Mixed Contexts
132
+ ## Accessibility and Access Control Are Exclusive
227
133
 
228
- If a credential needs both background access (no UI) and foreground biometric protection (with UI), **store two separate items**: a background-capable token with `AfterFirstUnlockThisDeviceOnly` (no `SecAccessControl` user-presence flags) and a stronger foreground-only item with `WhenUnlockedThisDeviceOnly` + biometric `SecAccessControl`. This avoids the logical contradiction of biometric flags on background-accessible items.
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
- // ✅ CORRECT - explicit accessibility for background use
251
- let query: [String: Any] = [
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: "authToken",
254
- kSecAttrService as String: "com.example.app",
255
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
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
- // ✅ CORRECT - explicit .or between constraints
299
- let access = SecAccessControlCreateWithFlags(
300
- nil,
301
- kSecAttrAccessibleWhenUnlocked,
302
- [.biometryCurrentSet, .or, .devicePasscode],
303
- &error
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
- ✅ The first two examples are correct patterns for foreground and background access. The third example is intentionally incorrect.
202
+ ### Top-sensitivity secret
312
203
 
313
- ### Biometric protection with highest security
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
- func saveBiometricProtectedItem(data: Data, account: String, service: String) throws {
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
- let query: [String: Any] = [
328
- kSecClass as String: kSecClassGenericPassword,
329
- kSecAttrAccount as String: account,
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
- kSecAttrAccount as String: account,
343
- kSecAttrService as String: service
216
+ kSecAttrService as String: "com.example.vault",
217
+ kSecAttrAccount as String: "master"
344
218
  ]
345
- let deleteStatus = SecItemDelete(searchQuery as CFDictionary)
346
- guard deleteStatus == errSecSuccess else {
347
- throw KeychainError.fromStatus(deleteStatus)
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
- let readdStatus = SecItemAdd(query as CFDictionary, nil)
350
- guard readdStatus == errSecSuccess else {
351
- throw KeychainError.fromStatus(readdStatus)
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
- default:
354
- throw KeychainError.fromStatus(status)
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
- > **Important:** `SecItemUpdate` **cannot** change a `SecAccessControl` attribute on an existing item. To change access control, you must delete and re-add. Both sources confirm this.
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
- ### Background-accessible token (push notifications, VPN, widgets)
266
+ ### Token read by background code
362
267
 
363
268
  ```swift
364
- func saveBackgroundToken(_ token: Data, account: String, service: String) throws {
365
- let query: [String: Any] = [
269
+ func saveSyncToken(_ token: Data) throws {
270
+ let match: [String: Any] = [
366
271
  kSecClass as String: kSecClassGenericPassword,
367
- kSecAttrAccount as String: account,
368
- kSecAttrService as String: service,
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
- let status = SecItemAdd(query as CFDictionary, nil)
374
- switch status {
375
- case errSecSuccess: return
376
- case errSecDuplicateItem:
377
- let updateAttrs: [String: Any] = [kSecValueData as String: token]
378
- let searchQuery: [String: Any] = [
379
- kSecClass as String: kSecClassGenericPassword,
380
- kSecAttrAccount as String: account,
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
- ### Accessing a `WhenUnlocked` item from a background extension
287
+ ### What goes wrong in an extension
394
288
 
395
289
  ```swift
396
- // Runs in WidgetKit TimelineProvider or NotificationServiceExtension while locked - WILL fail
397
- func fetchTokenInBackground() -> String? {
398
- let query: [String: Any] = [
399
- kSecClass as String: kSecClassGenericPassword,
400
- kSecAttrAccount as String: "authToken",
401
- kSecAttrService as String: "com.example.app",
402
- kSecReturnData as String: true,
403
- kSecMatchLimit as String: kSecMatchLimitOne
404
- // Item stored with default WhenUnlocked - inaccessible while locked
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
- macOS has **two keychain implementations** (per TN3137): the legacy file-based keychain (`~/Library/Keychains/login.keychain-db`) and the modern Data Protection keychain. The `SecItem` API defaults to the **legacy** keychain on macOS.
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
- Set `kSecUseDataProtectionKeychain: true` in every macOS keychain query to target the modern keychain. Without it:
424
-
425
- - `SecAccessControl` flags fail with `errSecParam` (-50)
426
- - iCloud Keychain sync doesn't work
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
- // ✅ macOS: always include kSecUseDataProtectionKeychain
432
- let query: [String: Any] = [
315
+ let macEntry: [String: Any] = [
433
316
  kSecClass as String: kSecClassGenericPassword,
434
- kSecAttrAccount as String: account,
435
- kSecAttrService as String: service,
436
- kSecUseDataProtectionKeychain as String: true, // ← Critical on macOS
437
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
438
- kSecValueData as String: data
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
- On iOS, tvOS, and watchOS, this flag is ignored (those platforms always use Data Protection). The Data Protection keychain requires a user login context - `launchd` daemons running outside a user session must use the legacy keychain. Mac Catalyst and iOS-on-Mac apps automatically use Data Protection.
443
-
444
- ---
445
-
446
- ## NSFileProtection Sidebar
447
-
448
- The keychain and file system share the same Data Protection architecture but expose it through different APIs. Use the keychain for small discrete secrets (passwords, tokens, keys). Use `NSFileProtection` for larger data (documents, databases, images).
449
-
450
- **`NSFileProtectionComplete`** (Class A) = `kSecAttrAccessibleWhenUnlocked`. File inaccessible while locked. Class key discarded ~10 seconds after lock.
451
-
452
- **`NSFileProtectionCompleteUnlessOpen`** (Class B) = **No keychain equivalent.** Uses asymmetric ECDH (Curve25519) to allow already-opened files to continue being written while locked. Designed for background downloads (e.g., mail attachment download continues writing to an already-open file).
453
-
454
- **`NSFileProtectionCompleteUntilFirstUserAuthentication`** (Class C) = `kSecAttrAccessibleAfterFirstUnlock`. The default for third-party app files when no explicit protection is set. Available after first unlock.
455
-
456
- **`NSFileProtectionNone`** (Class D) = deprecated `kSecAttrAccessibleAlways`. Protected only by device UID.
457
-
458
- **Recommended layered approach:** Store encryption keys in the keychain with `WhenUnlockedThisDeviceOnly`, then use those keys to encrypt larger files on disk with `NSFileProtectionComplete` as an additional layer.
459
-
460
- ---
461
-
462
- ## Error Codes Reference
463
-
464
- | Code | Constant | Meaning | Common Root Cause |
465
- | ---------- | ----------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
466
- | **-25308** | `errSecInteractionNotAllowed` | Item not accessible in current state | Device locked + `WhenUnlocked` item; BFU state + `AfterFirstUnlock` item; biometric flag in background |
467
- | **-50** | `errSecParam` | Invalid parameters | Both `kSecAttrAccessible` and `kSecAttrAccessControl` set; conflicting flags without `.or`/`.and`; missing `kSecUseDataProtectionKeychain` on macOS |
468
- | **-25293** | `errSecAuthFailed` | Authentication failed | Biometric auth failed; enrollment changed with `.biometryCurrentSet`; no biometrics enrolled |
469
- | **-25300** | `errSecItemNotFound` | Item not in keychain | Item never stored; `ThisDeviceOnly` lost after migration; `WhenPasscodeSet` deleted on passcode removal |
470
- | **-25299** | `errSecDuplicateItem` | Item already exists | `SecItemAdd` when matching primary keys exist - use add-or-update pattern |
471
- | **-128** | `errSecUserCanceled` | User canceled prompt | User tapped Cancel on biometric/passcode dialog |
472
- | **-34018** | `errSecMissingEntitlement` | Missing entitlement | Keychain access group not in entitlements; common on iOS Simulator |
473
-
474
- The most insidious is `-25308` - it surfaces in production but rarely during development because developers test with unlocked devices. Always handle it by deferring the operation and retrying when `UIApplication.shared.isProtectedDataAvailable` is `true`.
475
-
476
- ---
477
-
478
- ## iOS Version Timeline
479
-
480
- **iOS 8 (2014):** `WhenPasscodeSetThisDeviceOnly` introduced. `SecAccessControlCreateWithFlags` added. `.userPresence` flag.
481
-
482
- **iOS 9 (2015):** `.devicePasscode`, `.applicationPassword`, `.privateKeyUsage` flags added. Apple announced intent to deprecate `Always` at WWDC 2015 Session 706.
483
-
484
- **iOS 11.3 (2018):** `.touchIDAny` → `.biometryAny`; `.touchIDCurrentSet` → `.biometryCurrentSet` (unified naming for Face ID).
485
-
486
- **iOS 12 (2018):** `kSecAttrAccessibleAlways` and `AlwaysThisDeviceOnly` formally deprecated. Both still compile and run for backward compatibility.
487
-
488
- **iOS 15 (2021):** MDM-installed keychain items changed default from "always" to "after first unlock, nonmigratory." App pre-warming can launch processes before first unlock, making `AfterFirstUnlock` items temporarily unavailable.
489
-
490
- **iOS 16 (2022):** Passkeys launched (FIDO2/WebAuthn key pairs synced via E2E encrypted iCloud Keychain). No changes to access control APIs.
491
-
492
- **iOS 17 (2023):** Enterprise passkey support. No `kSecAttrAccessible` or `SecAccessControl` changes.
493
-
494
- **iOS 18 (2024):** Standalone Passwords app. No keychain data protection API changes.
495
-
496
- **iOS 26 (2025):** Stolen Device Protection enabled by default - requires biometric auth (no passcode fallback) for stored passwords when away from familiar locations. Secure passkey import/export via FIDO Alliance standard. No changes to `kSecAttrAccessible` constants.
497
-
498
- ---
499
-
500
- ## Testing Requirements
501
-
502
- All data protection testing **must** use physical devices with passcodes enabled. The iOS Simulator does not enforce `kSecAttrAccessible` or `NSFileProtection`, creating a false sense of security.
503
-
504
- **Critical test scenarios:**
505
-
506
- 1. **Reboot / BFU state:** Reboot device, attempt keychain access before unlocking. `AfterFirstUnlock` items should return `-25308` or `-25300`. Unlock once, lock again, test background access - should succeed.
507
-
508
- 2. **Lock timing:** Store a `WhenUnlocked` item. Lock the device. Attempt read immediately - expect `-25308`.
509
-
510
- 3. **Passcode removal:** Store a `WhenPasscodeSetThisDeviceOnly` item. Remove passcode in Settings. Verify item is deleted (`-25300`).
511
-
512
- 4. **Biometric enrollment change:** Store an item with `.biometryCurrentSet`. Add a new fingerprint or Face ID appearance. Verify authentication fails (`-25293`).
513
-
514
- 5. **Backup/restore migration:** Back up device, restore to a different physical device. Verify all `ThisDeviceOnly` items are absent (`-25300`).
515
-
516
- 6. **Background extension access:** Trigger a notification service extension or widget timeline update while the device is locked. Verify `AfterFirstUnlock` items are readable and `WhenUnlocked` items are not.
517
-
518
- ---
519
-
520
- ## Cross-References
521
-
522
- - `keychain-fundamentals.md` - SecItem CRUD patterns, add-or-update, OSStatus handling
523
- - `biometric-authentication.md` - Biometric flag selection (`.biometryCurrentSet`, `.biometryAny`, `.userPresence`) and keychain-bound patterns
524
- - `secure-enclave.md` - Hardware-backed keys with `SecAccessControl` and `.privateKeyUsage`
525
- - `keychain-item-classes.md` - Class-specific accessibility considerations and primary key composition
526
- - `common-anti-patterns.md` - Anti-pattern #5 (missing `kSecAttrAccessible`), #3 (LAContext-only gate)
527
- - `compliance-owasp-mapping.md` - M9 (Insecure Data Storage) accessibility requirements
528
-
529
- ---
530
-
531
- ## Summary Checklist
532
-
533
- 1. **Always set `kSecAttrAccessible` explicitly** - never rely on the `WhenUnlocked` default; choose the level matching your access context (foreground vs background)
534
- 2. **Never set both `kSecAttrAccessible` and `kSecAttrAccessControl`** in the same query dictionary - accessibility belongs inside `SecAccessControlCreateWithFlags`
535
- 3. **Use `AfterFirstUnlockThisDeviceOnly`** for any item accessed by background extensions, widgets, VPN, or push notification handlers
536
- 4. **Pair `WhenPasscodeSetThisDeviceOnly` with `.biometryCurrentSet`** for highest-security items, and handle item deletion on passcode removal gracefully
537
- 5. **Include `.or` or `.and`** when combining multiple authentication flags - omitting the operator causes `errSecParam` (-50)
538
- 6. **Set `kSecUseDataProtectionKeychain: true`** on all macOS keychain queries to target the modern Data Protection keychain
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).