@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,595 +1,440 @@
1
1
  # Biometric Authentication
2
2
 
3
- > **Domain scope:** SecAccessControl + LAContext integration, the LAContext-only bypass vulnerability, hardware-bound biometric gating, fallback behavior, UI customization, enrollment change detection, thread safety.
4
- >
5
- > **Risk level:** CRITICAL - #1 most dangerous AI-generated pattern. `LAContext.evaluatePolicy()` used alone is trivially bypassable at runtime.
3
+ Severity when violated: **CRITICAL.** Of all the insecure patterns that code
4
+ generators produce, a lone `LAContext.evaluatePolicy()` call guarding sensitive
5
+ content is the most dangerous, because it can be defeated at runtime without
6
+ any exploit.
6
7
 
7
- ---
8
+ Before any Face ID prompt can appear, the app's `Info.plist` needs
9
+ `NSFaceIDUsageDescription` with a sentence explaining why the app uses Face ID.
10
+ Apple's LocalAuthentication documentation notes that requests can fail without
11
+ it. This applies equally to `evaluatePolicy` and to keychain reads that trigger
12
+ Face ID through an access control. Touch ID needs no usage string.
8
13
 
9
14
  ## Contents
10
15
 
11
- - [The Boolean Gate Vulnerability](#the-boolean-gate-vulnerability)
12
- - [The Dangerous Pattern - Boolean Gate](#the-dangerous-pattern-boolean-gate)
13
- - [How Attackers Bypass It](#how-attackers-bypass-it)
14
- - [The Secure Pattern - Hardware-Bound Secrets](#the-secure-pattern-hardware-bound-secrets)
15
- - [Step 1 - Create the Access Control Object](#step-1-create-the-access-control-object)
16
- - [Step 2 - Store a Secret Bound to Biometric Auth](#step-2-store-a-secret-bound-to-biometric-auth)
17
- - [Step 3 - Retrieve the Secret (Biometric Prompt Appears Automatically)](#step-3-retrieve-the-secret-biometric-prompt-appears-automatically)
18
- - [`evaluatePolicy` vs `evaluateAccessControl`](#evaluatepolicy-vs-evaluateaccesscontrol)
19
- - [Biometric Flag Selection](#biometric-flag-selection)
20
- - [`.biometryCurrentSet` - Banking, Payments, Credential Storage](#biometrycurrentset-banking-payments-credential-storage)
21
- - [`.biometryAny` - Convenience Features, Moderate Sensitivity](#biometryany-convenience-features-moderate-sensitivity)
22
- - [`.userPresence` - Maximum Device Compatibility](#userpresence-maximum-device-compatibility)
23
- - [Combining Flags](#combining-flags)
24
- - [Biometric Availability Checks and Graceful Degradation](#biometric-availability-checks-and-graceful-degradation)
25
- - [Incomplete Availability Check](#incomplete-availability-check)
26
- - [Complete Availability Evaluation](#complete-availability-evaluation)
27
- - [Graceful Degradation Flow](#graceful-degradation-flow)
28
- - [Enrollment Change Detection](#enrollment-change-detection)
29
- - [Thread Safety and async/await](#thread-safety-and-asyncawait)
30
- - [Actor-Isolated Biometric Keychain (iOS 15+)](#actor-isolated-biometric-keychain-ios-15)
31
- - [SwiftUI ViewModel Integration](#swiftui-viewmodel-integration)
32
- - [Secure Enclave-Backed Keys with Biometric Protection](#secure-enclave-backed-keys-with-biometric-protection)
33
- - [SDLC Controls - Catching the Anti-Pattern in CI](#sdlc-controls-catching-the-anti-pattern-in-ci)
34
- - [Dynamic Verification - Proving Bypass Resistance](#dynamic-verification-proving-bypass-resistance)
35
- - [Key References](#key-references)
36
- - [Cross-References](#cross-references)
37
- - [Summary Checklist](#summary-checklist)
38
-
39
- ## The Boolean Gate Vulnerability
40
-
41
- The most dangerous pattern AI coding assistants generate for iOS biometric authentication is `LAContext.evaluatePolicy()` used as a standalone authentication gate. This pattern appears in virtually every tutorial, Stack Overflow answer, and AI training corpus - and it is **trivially bypassable**.
42
-
43
- The attack requires no exploit. An attacker uses Frida or objection to hook the Objective-C callback and force `success = true`, bypassing Face ID or Touch ID entirely. The formal weakness classification is CWE-288: Authentication Bypass Using an Alternate Path or Channel.
44
-
45
- OWASP MASTG explicitly fails any app relying solely on `evaluatePolicy` (test MASTG-TEST-0266, requirements MSTG-AUTH-8 and MSTG-AUTH-12). The standard states: biometric authentication must not be event-bound (returning `true`/`false`); it must be based on unlocking the keychain/keystore.
46
-
47
- ### The Dangerous Pattern - Boolean Gate
16
+ - [Why a Boolean Gate Fails](#why-a-boolean-gate-fails)
17
+ - [The Secure Pattern: Let the Keychain Ask](#the-secure-pattern-let-the-keychain-ask)
18
+ - [evaluatePolicy and evaluateAccessControl](#evaluatepolicy-and-evaluateaccesscontrol)
19
+ - [Which Flag](#which-flag)
20
+ - [Availability and Degradation](#availability-and-degradation)
21
+ - [Detecting Enrollment Changes](#detecting-enrollment-changes)
22
+ - [Threads and async/await](#threads-and-asyncawait)
23
+ - [Secure Enclave Keys Behind Biometrics](#secure-enclave-keys-behind-biometrics)
24
+ - [Controls in the Development Process](#controls-in-the-development-process)
25
+ - [Verifying at Runtime](#verifying-at-runtime)
26
+ - [Checklist](#checklist)
27
+ - [Sources and Related Files](#sources-and-related-files)
28
+
29
+ ## Why a Boolean Gate Fails
30
+
31
+ `evaluatePolicy` asks the system to authenticate the user and then calls your
32
+ closure with `true` or `false`. The Secure Enclave did the matching correctly,
33
+ but all your app receives is a Boolean in ordinary process memory, and nothing
34
+ cryptographic depends on it.
48
35
 
49
36
  ```swift
50
- // ❌ DANGEROUS: Trivially bypassable with Frida - do NOT use for security
51
- import LocalAuthentication
52
-
53
- func authenticateUser() {
54
- let context = LAContext()
55
- var error: NSError?
56
-
57
- if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
58
- context.evaluatePolicy(
59
- .deviceOwnerAuthenticationWithBiometrics,
60
- localizedReason: "Authenticate to access your account"
61
- ) { success, authError in
62
- DispatchQueue.main.async {
63
- if success {
64
- self.isAuthenticated = true // ← Just a boolean in hookable memory
65
- self.showProtectedContent() // ← No secret unlocked, no key released
66
- }
37
+ // Wrong: the only barrier is a Bool the attacker can flip.
38
+ @MainActor
39
+ final class StatementsGate {
40
+ var unlocked = false
41
+ func unlock(show: @escaping @MainActor () -> Void) {
42
+ let ctx = LAContext()
43
+ var probe: NSError?
44
+ guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &probe) else { return }
45
+ ctx.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
46
+ localizedReason: "View statements") { @Sendable ok, _ in
47
+ Task { @MainActor in
48
+ self.unlocked = ok
49
+ if ok { show() }
67
50
  }
68
51
  }
69
52
  }
70
53
  }
71
54
  ```
72
55
 
73
- **Why this fails:** `evaluatePolicy()` asks the OS "did the user authenticate?" and receives a boolean answer in user-space. No cryptographic material is involved. No secret is decrypted. The entire security model rests on a boolean that exists in hookable memory.
74
-
75
- ### How Attackers Bypass It
76
-
77
- The objection tool (built on Frida) provides a one-command bypass:
78
-
79
- ```bash
80
- objection -g "com.example.targetapp" explore
81
- ios ui biometrics_bypass
82
- ```
83
-
84
- The hook listens for invocations of `-[LAContext evaluatePolicy:localizedReason:reply:]`, intercepts the reply block, and replaces the `success` boolean with `true`. The equivalent raw Frida script:
85
-
86
- ```javascript
87
- // Frida script - forces evaluatePolicy success = true
88
- if (ObjC.available) {
89
- var hook = ObjC.classes.LAContext["- evaluatePolicy:localizedReason:reply:"];
90
- Interceptor.attach(hook.implementation, {
91
- onEnter: function (args) {
92
- var block = new ObjC.Block(args[4]);
93
- const callback = block.implementation;
94
- block.implementation = function (error, value) {
95
- const result = callback(1, null); // 1 = true, null = no error
96
- return result;
97
- };
98
- },
99
- });
100
- }
101
- ```
102
-
103
- The objection wiki confirms the attack boundary: this bypass **does not work** against keychain items protected with access control flags like `.biometryCurrentSet` or `.biometryAny`. That boundary is the entire basis of the secure pattern.
104
-
105
- ✅ Correct pattern in this threat model: use biometrics only to unlock keychain-protected secrets (`SecAccessControl` + `SecItemCopyMatching`), never as a standalone boolean gate.
56
+ On a jailbroken or instrumented device:
106
57
 
107
- ---
58
+ - objection, a Frida-based toolkit, does it in two commands: attach with
59
+ `objection -g "<bundle id>" explore`, then run `ios ui biometrics_bypass`.
60
+ - A hand-written Frida script hooks
61
+ `-[LAContext evaluatePolicy:localizedReason:reply:]`, swaps the reply block
62
+ (argument index 4 counting `self` and `_cmd`) for one that reports success
63
+ with a nil error, and the app proceeds.
108
64
 
109
- ## The Secure Pattern - Hardware-Bound Secrets
65
+ This weakness is CWE-288, Authentication Bypass Using an Alternate Path or
66
+ Channel. OWASP MASTG fails apps whose only protection is `evaluatePolicy`
67
+ (test MASTG-TEST-0266; requirements MSTG-AUTH-8 and MSTG-AUTH-12): biometrics
68
+ must unlock something in the keychain, not flip an event-driven true or false.
110
69
 
111
- The correct architecture stores a secret in the iOS keychain with biometric access control. The secret's encryption key is held by the Secure Enclave - a dedicated processor running its own microkernel (sepOS), with its own encrypted memory, completely isolated from the application processor.
70
+ The objection documentation states that its bypass does not work against
71
+ keychain items protected with `.biometryCurrentSet` or `.biometryAny`. That
72
+ limitation is exactly the defence.
112
73
 
113
- When the app requests the secret, the Secure Enclave independently verifies the biometric match and only then releases the decryption key. There is no boolean to hook. The data physically cannot be read without valid biometric authentication.
74
+ ## The Secure Pattern: Let the Keychain Ask
114
75
 
115
- WWDC 2014 Session 711 ("Keychain and Authentication with Touch ID") drew the critical distinction:
76
+ Store the secret as a keychain item with a biometric access control. The
77
+ item's key is held by the Secure Enclave, which runs its own microkernel
78
+ (sepOS) and has its own encrypted memory. The enclave checks the biometric
79
+ match itself and releases the decryption key only on success, so there is no
80
+ Boolean in the app's process to patch.
116
81
 
117
- - **`evaluatePolicy`**: "Trust the OS" - vulnerable if runtime is compromised
118
- - **Keychain + SecAccessControl**: "Trust the Secure Enclave" - ACLs evaluated inside hardware
82
+ WWDC 2014 Session 711 frames the difference as trusting the OS (the
83
+ `evaluatePolicy` result) versus trusting the Secure Enclave (a keychain ACL
84
+ evaluated in hardware).
119
85
 
120
- ### Step 1 - Create the Access Control Object
86
+ ### Step 1: an access control
121
87
 
122
88
  ```swift
123
89
  import LocalAuthentication
124
90
  import Security
125
91
 
126
- enum BiometricKeychainError: Error {
127
- case accessControlCreationFailed
128
- case keychainOperationFailed(status: OSStatus)
129
- case dataConversionFailed
130
- case biometryNotAvailable(reason: String)
92
+ enum BioVaultError: Error {
93
+ case policyCreation(Error?)
94
+ case keychain(OSStatus)
95
+ case badData
96
+ case biometryUnavailable(LAError.Code?)
131
97
  }
132
98
 
133
- func createBiometricAccessControl() throws -> SecAccessControl {
134
- var error: Unmanaged<CFError>?
135
- guard let accessControl = SecAccessControlCreateWithFlags(
99
+ func makeBiometricPolicy() throws -> SecAccessControl {
100
+ var cfError: Unmanaged<CFError>?
101
+ guard let acl = SecAccessControlCreateWithFlags(
136
102
  kCFAllocatorDefault,
137
- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, // Strongest: requires passcode, device-only
138
- .biometryCurrentSet, // Invalidates on enrollment change
139
- &error
140
- ) else {
141
- throw BiometricKeychainError.accessControlCreationFailed
103
+ kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
104
+ .biometryCurrentSet,
105
+ &cfError) else {
106
+ throw BioVaultError.policyCreation(cfError?.takeRetainedValue())
142
107
  }
143
- return accessControl
108
+ return acl
144
109
  }
145
110
  ```
146
111
 
147
- ### Step 2 - Store a Secret Bound to Biometric Auth
112
+ ### Step 2: store it
148
113
 
149
- ```swift
150
- // ✅ SECURE: Secret is encrypted by Secure Enclave, released only on biometric match
151
- func storeSecretWithBiometric(secret: Data, account: String, service: String) throws {
152
- let accessControl = try createBiometricAccessControl()
153
-
154
- // Delete any existing item first (add-or-update pattern)
155
- let deleteQuery: [String: Any] = [
156
- kSecClass as String: kSecClassGenericPassword,
157
- kSecAttrAccount as String: account,
158
- kSecAttrService as String: service
159
- ]
160
- SecItemDelete(deleteQuery as CFDictionary)
114
+ Save with add, then update on duplicate. The update of a protected item needs
115
+ the user to authenticate, so it runs with an `LAContext`. Changing the policy
116
+ of an existing item (for example from `.biometryAny` to
117
+ `.biometryCurrentSet`) is the one case for delete and re-add, because
118
+ `SecItemUpdate` cannot alter access control; see
119
+ [keychain-access-control.md](keychain-access-control.md).
161
120
 
162
- let addQuery: [String: Any] = [
121
+ ```swift
122
+ func storeAccountNumber(_ digits: Data, context: LAContext) throws {
123
+ let match: [String: Any] = [
163
124
  kSecClass as String: kSecClassGenericPassword,
164
- kSecAttrAccount as String: account,
165
- kSecAttrService as String: service,
166
- kSecValueData as String: secret,
167
- kSecAttrAccessControl as String: accessControl,
168
- kSecAttrSynchronizable as String: kCFBooleanFalse // Never sync biometric-gated secrets
169
- // NOTE: Do NOT set kSecAttrAccessible - it conflicts with kSecAttrAccessControl
125
+ kSecAttrService as String: "com.example.bank",
126
+ kSecAttrAccount as String: "account-number",
127
+ kSecAttrSynchronizable as String: kCFBooleanFalse as Any
170
128
  ]
171
-
172
- let status = SecItemAdd(addQuery as CFDictionary, nil)
173
- guard status == errSecSuccess else {
174
- throw BiometricKeychainError.keychainOperationFailed(status: status)
175
- }
129
+ var insert = match
130
+ insert[kSecAttrAccessControl as String] = try makeBiometricPolicy()
131
+ insert[kSecValueData as String] = digits
132
+
133
+ let added = SecItemAdd(insert as CFDictionary, nil)
134
+ if added == errSecSuccess { return }
135
+ guard added == errSecDuplicateItem else { throw BioVaultError.keychain(added) }
136
+
137
+ var existing = match
138
+ existing[kSecUseAuthenticationContext as String] = context
139
+ let changed = SecItemUpdate(existing as CFDictionary,
140
+ [kSecValueData as String: digits] as CFDictionary)
141
+ guard changed == errSecSuccess else { throw BioVaultError.keychain(changed) }
176
142
  }
177
143
  ```
178
144
 
179
- **Critical detail:** Do NOT set both `kSecAttrAccessible` and `kSecAttrAccessControl` in the same query. They conflict - `SecAccessControl` already encodes the accessibility level. Setting both causes `errSecParam`.
145
+ - The dictionary has `kSecAttrAccessControl` and no `kSecAttrAccessible`;
146
+ both together are `errSecParam`.
147
+ - Biometric-protected secrets use a `ThisDeviceOnly` class and are never
148
+ synchronizable. Syncing would put the secret on devices whose biometrics the
149
+ policy never saw.
180
150
 
181
- **Critical detail:** Always use `ThisDeviceOnly` accessibility for biometric-gated secrets. The `ThisDeviceOnly` suffix ensures the secret is hardware-bound and excluded from iCloud backups. Syncing biometric-gated secrets across devices expands the attack surface.
182
-
183
- ### Step 3 - Retrieve the Secret (Biometric Prompt Appears Automatically)
151
+ ### Step 3: read it
184
152
 
185
153
  ```swift
186
- // ✅ SECURE: System presents biometric prompt; Secure Enclave gates decryption
187
- func retrieveSecretWithBiometric(account: String, service: String) throws -> Data {
188
- let context = LAContext()
189
- context.localizedReason = "Authenticate to access your credentials"
190
-
154
+ func readAccountNumber() throws -> Data {
155
+ let ctx = LAContext()
156
+ ctx.localizedReason = "Show your account number"
191
157
  let query: [String: Any] = [
192
158
  kSecClass as String: kSecClassGenericPassword,
193
- kSecAttrAccount as String: account,
194
- kSecAttrService as String: service,
159
+ kSecAttrService as String: "com.example.bank",
160
+ kSecAttrAccount as String: "account-number",
195
161
  kSecReturnData as String: true,
196
162
  kSecMatchLimit as String: kSecMatchLimitOne,
197
- kSecUseAuthenticationContext as String: context
163
+ kSecUseAuthenticationContext as String: ctx
198
164
  ]
199
-
200
- var result: AnyObject?
201
- let status = SecItemCopyMatching(query as CFDictionary, &result)
202
-
165
+ var out: CFTypeRef?
166
+ let status = SecItemCopyMatching(query as CFDictionary, &out)
203
167
  switch status {
204
168
  case errSecSuccess:
205
- guard let data = result as? Data else {
206
- throw BiometricKeychainError.dataConversionFailed
207
- }
208
- return data // Secret returned ONLY after Secure Enclave validates biometric
169
+ guard let digits = out as? Data else { throw BioVaultError.badData }
170
+ return digits
209
171
  case errSecItemNotFound:
210
- throw BiometricKeychainError.keychainOperationFailed(status: status)
172
+ throw BioVaultError.keychain(status)
211
173
  case errSecUserCanceled:
212
- throw BiometricKeychainError.keychainOperationFailed(status: status)
174
+ throw BioVaultError.keychain(status)
213
175
  case errSecAuthFailed:
214
- throw BiometricKeychainError.keychainOperationFailed(status: status)
176
+ throw BioVaultError.keychain(status)
215
177
  default:
216
- throw BiometricKeychainError.keychainOperationFailed(status: status)
178
+ throw BioVaultError.keychain(status)
217
179
  }
218
180
  }
219
181
  ```
220
182
 
221
- **Key insight:** Authentication and data protection are the same operation, not sequential ones. When `SecItemCopyMatching` encounters an item with biometric access control, the system presents the biometric prompt automatically. The Secure Enclave verifies the match internally and only then unwraps the AES-256-GCM decryption key. There is no callback to intercept.
222
-
223
- ---
224
-
225
- ## `evaluatePolicy` vs `evaluateAccessControl`
226
-
227
- These two `LAContext` methods represent the two trust models from WWDC 2014 Session 711:
228
-
229
- **`evaluatePolicy(_:localizedReason:reply:)`** triggers biometric authentication and returns a boolean. The Secure Enclave validates the biometric correctly, but the result is communicated to user-space as `true`/`false`. No key is released. The app branches on a boolean in hookable memory. This is "trust the OS."
230
-
231
- **`evaluateAccessControl(_:operation:localizedReason:reply:)`** evaluates a `SecAccessControl` object for a specific cryptographic operation (`.useItem`, `.useKeySign`, `.useKeyDecrypt`). When used with keychain items, the authenticated `LAContext` is passed to `SecItemCopyMatching` via `kSecUseAuthenticationContext`, and the Secure Enclave recognizes the prior authentication. This is "trust the Secure Enclave."
232
-
233
- **In practice, you rarely call `evaluateAccessControl` directly.** The recommended flow: store data with `SecAccessControl` via `SecItemAdd`, then retrieve with `SecItemCopyMatching`. The system handles the biometric prompt automatically when the query encounters an ACL-protected item.
234
-
235
- The only legitimate use of `evaluatePolicy` is **non-security-critical UI gating** - deciding whether to show a "Sign in with Face ID" button. It must never protect sensitive data or gate access to secrets.
236
-
237
- ---
238
-
239
- ## Biometric Flag Selection
240
-
241
- `SecAccessControlCreateFlags` provides three biometric-related flags. Choosing the wrong one is a common mistake even in otherwise-correct implementations.
242
-
243
- ### `.biometryCurrentSet` - Banking, Payments, Credential Storage
244
-
245
- Ties the keychain item to the **exact biometric enrollment** at time of storage. If the user adds a fingerprint, re-enrolls Face ID, or removes a biometric entry, the item becomes **permanently inaccessible**.
246
-
247
- ```swift
248
- // ✅ Strongest biometric binding - invalidates on enrollment change
249
- let access = SecAccessControlCreateWithFlags(
250
- nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
251
- .biometryCurrentSet, nil
252
- )
253
- ```
254
-
255
- **Tradeoff:** Users who change biometrics must re-authenticate via your app's password flow. Detect enrollment changes via `LAContext.evaluatedPolicyDomainState` (see Enrollment Change Detection below) and present graceful re-enrollment.
183
+ `errSecUserCanceled` is -128 (the user backed out). `errSecAuthFailed` is -25293
184
+ (the match failed, or the enrollment behind a `.biometryCurrentSet` item
185
+ changed). Authentication and decryption are a single operation: the query
186
+ itself raises the system prompt, the enclave unwraps the item's AES-256-GCM
187
+ key, and there is no callback to intercept.
256
188
 
257
- ### `.biometryAny` - Convenience Features, Moderate Sensitivity
189
+ ## evaluatePolicy and evaluateAccessControl
258
190
 
259
- Survives biometric enrollment changes. An attacker who enrolls their own biometrics on a compromised device can access the data.
191
+ - `evaluatePolicy(_:localizedReason:reply:)`: the enclave checks the user, but
192
+ only a Boolean reaches your process. No key is released.
193
+ - `evaluateAccessControl(_:operation:localizedReason:reply:)`: checks a
194
+ `SecAccessControl` for a given operation (`.useItem`, `.useKeySign`,
195
+ `.useKeyDecrypt`). You then pass the same context to `SecItemCopyMatching`
196
+ through `kSecUseAuthenticationContext`, and the enclave accepts the earlier
197
+ authentication.
260
198
 
261
- ```swift
262
- // Survives re-enrollment - better UX, weaker security
263
- let access = SecAccessControlCreateWithFlags(
264
- nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
265
- .biometryAny, nil
266
- )
267
- ```
199
+ You rarely need `evaluateAccessControl` directly. Store with an ACL through
200
+ `SecItemAdd`, read with `SecItemCopyMatching`, and let the system prompt.
268
201
 
269
- **Use case:** "Remember me" features, non-critical app locks, preferences that benefit from biometric convenience without protecting financial data.
202
+ `evaluatePolicy` still has one legitimate use: UI decisions that carry no
203
+ security weight, such as whether to show an "Unlock with Face ID" button.
270
204
 
271
- ### `.userPresence` - Maximum Device Compatibility
205
+ ## Which Flag
272
206
 
273
- Allows passcode fallback when biometrics are unavailable. Weaker because passcodes are susceptible to shoulder-surfing.
274
-
275
- ```swift
276
- // Broadest compatibility - biometric or passcode
277
- let access = SecAccessControlCreateWithFlags(
278
- nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
279
- .userPresence, nil
280
- )
281
- ```
207
+ | Flag | Behaviour | Fits | Pair with |
208
+ | --- | --- | --- | --- |
209
+ | `.biometryCurrentSet` | tied to the exact enrollment; adding a finger, re-enrolling Face ID or removing an entry makes the item unreadable forever | banking, payments, stored credentials | `WhenPasscodeSetThisDeviceOnly` |
210
+ | `.biometryAny` | survives enrollment changes, so someone who enrols their own biometrics on a compromised device gets in | "remember me", non-critical app locks, preferences | `WhenUnlockedThisDeviceOnly` |
211
+ | `.userPresence` | biometrics or passcode; the passcode can be shoulder-surfed | accessibility-first apps, devices without biometrics, degradation path | `WhenPasscodeSetThisDeviceOnly` |
282
212
 
283
- **Use case:** Accessibility-first apps, devices without biometric hardware, or as a `.biometryCurrentSet` degradation path.
213
+ With `.biometryCurrentSet`, users who change their biometrics must re-verify
214
+ through the app's own sign-in. Detect the change with
215
+ `LAContext.evaluatedPolicyDomainState` (below).
284
216
 
285
- ### Combining Flags
217
+ For most production apps, `[.biometryCurrentSet, .or, .devicePasscode]` with
218
+ `WhenPasscodeSetThisDeviceOnly` gives tight binding and still leaves a way in
219
+ when Face ID fails.
286
220
 
287
- Flags can be combined with `.or` and `.and` conjunctions:
221
+ ## Availability and Degradation
288
222
 
289
223
  ```swift
290
- // ✅ Strong biometric binding WITH passcode escape hatch
291
- let access = SecAccessControlCreateWithFlags(
292
- nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
293
- [.biometryCurrentSet, .or, .devicePasscode], nil
294
- )
295
- ```
296
-
297
- This combination is practical for most production apps - strong biometric security with a recovery path when biometrics become unavailable.
298
-
299
- ---
300
-
301
- ## Biometric Availability Checks and Graceful Degradation
302
-
303
- ### Incomplete Availability Check
304
-
305
- ```swift
306
- // ❌ WRONG: Ignores WHY biometrics failed - user gets no guidance
307
- func checkBiometrics() -> Bool {
308
- let context = LAContext()
309
- var error: NSError?
310
- return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
224
+ // Wrong: a bare Bool hides why biometrics are unavailable.
225
+ func biometricsReady() -> Bool {
226
+ LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
311
227
  }
312
228
  ```
313
229
 
314
- ### Complete Availability Evaluation
315
-
316
230
  ```swift
317
- // ✅ CORRECT: Evaluates every failure reason with actionable guidance
318
- enum BiometricAvailability {
319
- case available(type: LABiometryType)
320
- case notEnrolled // Hardware exists, no biometrics registered
321
- case lockedOut // Too many failed attempts - passcode required
322
- case notAvailable // No hardware or restricted by MDM
323
- case passcodeNotSet // No device passcode - biometrics require one
231
+ enum BiometricState {
232
+ case ready(LABiometryType)
233
+ case notEnrolled
234
+ case lockedOut
235
+ case unsupported
236
+ case noPasscode
324
237
  }
325
238
 
326
- func evaluateBiometricAvailability() -> BiometricAvailability {
327
- let context = LAContext()
328
- var error: NSError?
329
-
330
- if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
331
- return .available(type: context.biometryType)
239
+ func biometricState() -> BiometricState {
240
+ let ctx = LAContext()
241
+ var nsError: NSError?
242
+ if ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &nsError) {
243
+ return .ready(ctx.biometryType)
332
244
  }
333
-
334
- guard let laError = error as? LAError else { return .notAvailable }
335
-
336
- switch laError.code {
337
- case .biometryNotEnrolled:
338
- return .notEnrolled // → "Enable Face ID in Settings"
339
- case .biometryLockout:
340
- return .lockedOut // → Prompt passcode to reset sensor
341
- case .biometryNotAvailable:
342
- return .notAvailable // → Hide biometric UI entirely
343
- case .passcodeNotSet:
344
- return .passcodeNotSet // → "Set a passcode to use Face ID"
345
- default:
346
- return .notAvailable
245
+ switch LAError.Code(rawValue: nsError?.code ?? 0) {
246
+ case .biometryNotEnrolled: return .notEnrolled
247
+ case .biometryLockout: return .lockedOut
248
+ case .passcodeNotSet: return .noPasscode
249
+ default: return .unsupported
347
250
  }
348
251
  }
349
252
  ```
350
253
 
351
- ### Graceful Degradation Flow
254
+ | State | What to do |
255
+ | --- | --- |
256
+ | `.biometryNotEnrolled` | explain how to turn on Face ID or Touch ID in Settings |
257
+ | `.biometryLockout` | too many failures; entering the device passcode resets the sensor |
258
+ | `.biometryNotAvailable` | no hardware, or restricted by MDM; hide biometric UI |
259
+ | `.passcodeNotSet` | ask the user to set a passcode |
260
+ | ready | `context.biometryType` (`LABiometryType`) says Face ID, Touch ID or Optic ID |
352
261
 
353
- ```swift
354
- // ✅ Degrades from biometric → passcode → password login
355
- func authenticateWithGracefulDegradation() async throws -> Data {
356
- let availability = evaluateBiometricAvailability()
357
-
358
- switch availability {
359
- case .available:
360
- return try retrieveSecretWithBiometric(account: "user", service: "com.app.auth")
361
-
362
- case .lockedOut:
363
- // Biometrics locked - use .userPresence item for passcode fallback
364
- return try retrieveSecretWithPasscodeFallback(account: "user", service: "com.app.auth")
365
-
366
- case .notEnrolled:
367
- throw BiometricKeychainError.biometryNotAvailable(
368
- reason: "Please enable Face ID in Settings > Face ID & Passcode"
369
- )
370
-
371
- case .notAvailable, .passcodeNotSet:
372
- throw BiometricKeychainError.biometryNotAvailable(
373
- reason: "Biometric authentication is not available on this device"
374
- )
375
- }
376
- }
377
- ```
378
-
379
- **Critical:** Failing to handle `.biometryLockout` strands users. The app cannot bypass this lockout - the user must successfully enter their device passcode to re-enable the biometric sensor. If your app has no fallback, users are permanently locked out until they leave your app and unlock with passcode.
262
+ A degradation flow reads the biometric item when biometrics are ready, falls
263
+ back to an item protected with `.userPresence` when the sensor is locked out
264
+ (so the passcode works), and otherwise throws an error carrying guidance for the
265
+ user. An unhandled `.biometryLockout` strands users: the app cannot clear a
266
+ lockout, and only a successful passcode entry on the device does.
380
267
 
381
- **Important:** `canEvaluatePolicy()` is strictly for pre-flight UI decisions (showing or hiding a "Sign in with Face ID" button). It must never be used as a security control.
268
+ `canEvaluatePolicy()` is for deciding what UI to show. It is never a security
269
+ control.
382
270
 
383
- ---
384
-
385
- ## Enrollment Change Detection
386
-
387
- When using `.biometryCurrentSet`, detect enrollment changes proactively so your app can guide the user through re-enrollment rather than presenting a cryptic keychain error.
271
+ ## Detecting Enrollment Changes
388
272
 
389
273
  ```swift
390
- // ✅ Detect biometric enrollment changes via domainState
391
- class BiometricEnrollmentMonitor {
392
- private let domainStateKey = "com.app.biometric.domainState"
393
-
394
- /// Call after successful biometric setup to snapshot current enrollment
395
- func saveCurrentEnrollment() {
396
- let context = LAContext()
397
- guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return }
398
-
399
- // domainState changes whenever biometric enrollment changes
400
- if let domainState = context.evaluatedPolicyDomainState {
401
- UserDefaults.standard.set(domainState, forKey: domainStateKey)
274
+ struct EnrollmentWatcher {
275
+ private let storeKey = "bio.domainState"
276
+
277
+ private func currentState() -> Data? {
278
+ let ctx = LAContext()
279
+ guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return nil }
280
+ if #available(iOS 18.0, macOS 15.0, *) {
281
+ return ctx.domainState.biometry.stateHash
402
282
  }
283
+ return ctx.evaluatedPolicyDomainState
403
284
  }
404
285
 
405
- /// Call on app launch or before biometric retrieval
406
- func hasEnrollmentChanged() -> Bool {
407
- let context = LAContext()
408
- guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else {
409
- return true // Can't evaluate - treat as changed
410
- }
411
-
412
- guard let currentState = context.evaluatedPolicyDomainState,
413
- let savedState = UserDefaults.standard.data(forKey: domainStateKey) else {
414
- return true // No saved state - first run or data cleared
415
- }
286
+ func remember() {
287
+ guard let state = currentState() else { return }
288
+ UserDefaults.standard.set(state, forKey: storeKey)
289
+ }
416
290
 
417
- return currentState != savedState
291
+ func hasChanged() -> Bool {
292
+ guard let now = currentState(),
293
+ let before = UserDefaults.standard.data(forKey: storeKey) else { return true }
294
+ return now != before
418
295
  }
419
296
  }
420
297
  ```
421
298
 
422
- **Note:** `evaluatedPolicyDomainState` is an opaque `Data` blob. It changes whenever biometric enrollment changes but reveals no information about the biometrics themselves. Store it in `UserDefaults` (not keychain) since it is not sensitive - it's only used for change detection.
423
-
424
- ---
299
+ The state hash is opaque `Data` that changes whenever the enrolled set changes
300
+ and reveals nothing about the biometrics themselves, so `UserDefaults` is an
301
+ acceptable place for it. When it cannot be read, or no earlier value was saved,
302
+ treat that as a change. From iOS 18 it is `domainState.biometry.stateHash`;
303
+ `evaluatedPolicyDomainState` is deprecated there and stays only as the fallback
304
+ for older systems. Nothing promises the two values are equal, so the first check
305
+ after an update past iOS 18 may report a change, which fails safe.
425
306
 
426
- ## Thread Safety and async/await
307
+ ## Threads and async/await
427
308
 
428
- `SecItemCopyMatching` with biometric access control **blocks the calling thread** until the user completes authentication. Never run it on `@MainActor` or the main thread.
429
-
430
- `LAContext.evaluatePolicy`'s legacy completion handler executes on a private queue in an unspecified threading context. Direct UI updates from this callback cause crashes, especially on iOS 18 where threading strictness increased.
431
-
432
- ### Actor-Isolated Biometric Keychain (iOS 15+)
309
+ - A `SecItemCopyMatching` on a biometric item blocks until the user finishes
310
+ with the prompt. Never call it on `@MainActor` or the main thread.
311
+ - The completion of the callback-based `evaluatePolicy` runs on a private queue.
312
+ Touching UI from there crashes, and iOS 18 is stricter about it.
433
313
 
434
314
  ```swift
435
- @available(iOS 15.0, *)
436
- actor BiometricKeychain {
437
-
438
- func retrieveSecret(account: String, service: String) async throws -> Data {
439
- return try await withCheckedThrowingContinuation { continuation in
315
+ actor BiometricVault {
316
+ func read(account: String, reason: String) async throws -> Data {
317
+ try await withCheckedThrowingContinuation { continuation in
440
318
  DispatchQueue.global(qos: .userInitiated).async {
441
- let context = LAContext()
442
- context.localizedReason = "Authenticate to access your account"
443
-
319
+ let ctx = LAContext()
320
+ ctx.localizedReason = reason
444
321
  let query: [String: Any] = [
445
322
  kSecClass as String: kSecClassGenericPassword,
446
323
  kSecAttrAccount as String: account,
447
- kSecAttrService as String: service,
448
324
  kSecReturnData as String: true,
449
325
  kSecMatchLimit as String: kSecMatchLimitOne,
450
- kSecUseAuthenticationContext as String: context
326
+ kSecUseAuthenticationContext as String: ctx
451
327
  ]
452
-
453
- var result: AnyObject?
454
- let status = SecItemCopyMatching(query as CFDictionary, &result)
455
-
456
- switch status {
457
- case errSecSuccess:
458
- if let data = result as? Data {
459
- continuation.resume(returning: data)
460
- } else {
461
- continuation.resume(throwing: BiometricKeychainError.dataConversionFailed)
462
- }
463
- case errSecUserCanceled, errSecAuthFailed:
464
- continuation.resume(throwing: BiometricKeychainError.keychainOperationFailed(status: status))
465
- default:
466
- continuation.resume(throwing: BiometricKeychainError.keychainOperationFailed(status: status))
328
+ var out: CFTypeRef?
329
+ let status = SecItemCopyMatching(query as CFDictionary, &out)
330
+ if status == errSecSuccess, let data = out as? Data {
331
+ continuation.resume(returning: data)
332
+ } else {
333
+ continuation.resume(throwing: BioVaultError.keychain(status))
467
334
  }
468
335
  }
469
336
  }
470
337
  }
471
338
  }
472
- ```
473
339
 
474
- ### SwiftUI ViewModel Integration
475
-
476
- ```swift
477
340
  @MainActor
478
- class AuthViewModel: ObservableObject {
479
- @Published var isAuthenticated = false
480
- @Published var errorMessage: String?
481
-
482
- private let keychain = BiometricKeychain()
341
+ @Observable
342
+ final class RevealModel {
343
+ var revealed: String?
344
+ var problem: String?
345
+ private let vault = BiometricVault()
483
346
 
484
- func authenticate() {
347
+ func reveal() {
485
348
  Task {
486
349
  do {
487
- let secret = try await keychain.retrieveSecret(
488
- account: "user_token",
489
- service: "com.myapp.auth"
490
- )
491
- self.isAuthenticated = true
492
- self.processToken(secret)
350
+ let data = try await vault.read(account: "account-number", reason: "Reveal")
351
+ revealed = String(decoding: data, as: UTF8.self)
493
352
  } catch {
494
- self.errorMessage = error.localizedDescription
353
+ problem = "Could not unlock the account number."
495
354
  }
496
355
  }
497
356
  }
498
357
  }
499
358
  ```
500
359
 
501
- **Note on native async:** `LAContext` gained `evaluatePolicy(_:localizedReason:) async throws -> Bool` in iOS 15. However, this is only relevant for the non-security-critical UI gating use case. For the secure keychain pattern, you wrap `SecItemCopyMatching` as shown above - there is no native async overload for SecItem\* APIs.
360
+ The continuation keeps the blocking call off the actor's executor. iOS 15 added
361
+ `evaluatePolicy(_:localizedReason:) async throws -> Bool`, but it only helps
362
+ with UI gating; SecItem has no async variant, hence the wrapper.
502
363
 
503
- ---
504
-
505
- ## Secure Enclave-Backed Keys with Biometric Protection
506
-
507
- For asymmetric key operations (signing, key agreement), combine Secure Enclave key generation with biometric access control via CryptoKit. The private key **never leaves the Secure Enclave** - all operations happen in hardware.
364
+ ## Secure Enclave Keys Behind Biometrics
508
365
 
509
366
  ```swift
510
- // ✅ Secure Enclave P-256 key with biometric protection (WWDC 2019-709)
511
- let accessControl = SecAccessControlCreateWithFlags(
512
- nil,
513
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
514
- [.privateKeyUsage, .biometryCurrentSet],
515
- nil
516
- )!
517
-
518
- let privateKey = try SecureEnclave.P256.Signing.PrivateKey(
519
- accessControl: accessControl
520
- )
521
-
522
- // Signing triggers biometric prompt automatically
523
- let signature = try privateKey.signature(for: dataToSign)
367
+ import CryptoKit
368
+
369
+ func makeApprovalKey() throws -> SecureEnclave.P256.Signing.PrivateKey {
370
+ var cfError: Unmanaged<CFError>?
371
+ guard let acl = SecAccessControlCreateWithFlags(
372
+ nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
373
+ [.privateKeyUsage, .biometryCurrentSet], &cfError) else {
374
+ throw BioVaultError.policyCreation(cfError?.takeRetainedValue())
375
+ }
376
+ return try SecureEnclave.P256.Signing.PrivateKey(accessControl: acl)
377
+ }
524
378
  ```
525
379
 
526
- Frida operates in user-space on the application processor and has zero access to the Secure Enclave's internal state. The Secure Enclave's memory is encrypted with its own AES engine. Even a kernel-level compromise cannot extract the keys.
527
-
528
- ---
529
-
530
- ## SDLC Controls - Catching the Anti-Pattern in CI
531
-
532
- Because AI coding assistants frequently generate the vulnerable `evaluatePolicy` pattern, teams should implement automated detection:
533
-
534
- **Conceptual SAST rule (`INSECURE_BIOMETRIC_GATE`):** Identify all calls to `LAContext.evaluatePolicy`. If the `success` boolean directly gates access to a resource AND there is no corresponding `SecItemCopyMatching` using a `SecAccessControl` object in the same flow, flag the code.
535
-
536
- **Security review sign-off criteria:**
537
-
538
- 1. Zero instances of standalone `LAContext.evaluatePolicy` gating sensitive operations
539
- 2. Evidence of `SecItemAdd` with `kSecAttrAccessControl` using `ThisDeviceOnly` accessibility
540
- 3. Documented proof that objection bypass (`ios ui biometrics_bypass`) fails to unlock protected data
541
- 4. All `LAError` cases handled with graceful degradation
542
- 5. If `.biometryCurrentSet` is used, a tested re-enrollment recovery flow exists
543
-
544
- ---
545
-
546
- ## Dynamic Verification - Proving Bypass Resistance
547
-
548
- Static code review alone is insufficient. Verification requires dynamic testing:
549
-
550
- **Test procedure:** On a jailbroken or instrumented device, inject a Frida script to hook `-[LAContext evaluatePolicy:localizedReason:reply:]` and force `success = true`.
551
-
552
- **Pass criteria:** The app prevents access to protected data despite the manipulated callback. The secret remains locked because `SecAccessControl` + Secure Enclave enforcement is independent of the boolean.
553
-
554
- **Fail criteria:** The app grants access after the hook forces success. This proves reliance on the vulnerable boolean gate.
555
-
556
- ---
557
-
558
- ## Key References
559
-
560
- - **WWDC 2014 Session 711** - "Keychain and Authentication with Touch ID": Introduced the two trust models (evaluatePolicy vs keychain+ACL)
561
- - **WWDC 2019 Session 709** - "Cryptography and Your Apps": CryptoKit + Secure Enclave key generation with access control
562
- - **Apple Platform Security Guide** - Secure Enclave architecture, keychain encryption chain (metadata key + secret key), ACL evaluation in hardware
563
- - **OWASP MASTG MSTG-AUTH-8** - Biometric authentication must not be event-bound
564
- - **OWASP MASTG MSTG-AUTH-12** - Integrity of biometric mechanism must be verified
565
- - **OWASP MASTG MASTG-TEST-0266** - Test for local authentication bypass
566
- - **objection wiki** - "Understanding the iOS Biometrics Bypass": Confirms attack boundary at SecAccessControl
567
- - **TN3137** - "On Mac Keychain APIs and implementations" (macOS keychain unification)
568
-
569
- ---
570
-
571
- ## Cross-References
572
-
573
- - `keychain-fundamentals.md` - SecItem CRUD patterns used by the keychain-bound biometric flow
574
- - `keychain-access-control.md` - `SecAccessControlCreateWithFlags`, accessibility constants, and flag composition rules
575
- - `secure-enclave.md` - Hardware-backed keys with biometric gating via `SecAccessControl`
576
- - `common-anti-patterns.md` - Anti-pattern #3 (LAContext-only biometric gate)
577
- - `credential-storage-patterns.md` - Biometric protection for high-value credentials (OAuth tokens, API keys)
578
- - `testing-security-code.md` - Protocol-based mocking for biometric flows, LAContext test strategies
579
- - `compliance-owasp-mapping.md` - M3 (Insecure Authentication/Authorization) biometric requirements
580
-
581
- ---
582
-
583
- ## Summary Checklist
584
-
585
- 1. **No standalone boolean gates** - `LAContext.evaluatePolicy()` is NEVER the sole authentication mechanism for sensitive data; secrets are always bound to keychain + `SecAccessControl`
586
- 2. **Hardware-gated secrets** - All sensitive data protected by biometrics uses `SecAccessControlCreateWithFlags` with the Secure Enclave enforcing the ACL
587
- 3. **Correct flag selection** - `.biometryCurrentSet` for high-security (banking, payments); `.biometryAny` for convenience; `.userPresence` for broad compatibility or fallback
588
- 4. **No kSecAttrAccessible conflict** - `kSecAttrAccessible` and `kSecAttrAccessControl` are never set on the same keychain item
589
- 5. **ThisDeviceOnly accessibility** - Biometric-gated secrets use `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` or `WhenUnlockedThisDeviceOnly`; never syncable
590
- 6. **Complete error handling** - All `LAError` codes handled: `.biometryNotEnrolled`, `.biometryLockout`, `.biometryNotAvailable`, `.passcodeNotSet`, `.userCancel`, `.userFallback`
591
- 7. **Graceful degradation** - App provides fallback path (passcode or password) when biometrics are unavailable or locked out
592
- 8. **Enrollment change detection** - `evaluatedPolicyDomainState` monitored when using `.biometryCurrentSet`; re-enrollment flow implemented
593
- 9. **Thread safety** - `SecItemCopyMatching` with biometric ACL never runs on `@MainActor`; actor-isolated or dispatched to background queue
594
- 10. **Dynamic verification** - objection/Frida bypass test confirms protected data remains inaccessible when `evaluatePolicy` callback is hooked
595
- 11. **SAST/linting** - CI pipeline includes rule to flag standalone `evaluatePolicy` without corresponding `SecAccessControl` keychain operations
380
+ Each `signature(for:)` on that key raises the biometric prompt (WWDC 2019
381
+ Session 709, "Cryptography and Your Apps"). The private key never leaves the
382
+ enclave. Frida runs in user space on the Application Processor and has no path
383
+ into the enclave; the enclave's memory is encrypted by its own AES engine, and
384
+ even a kernel compromise cannot pull the key out. More in
385
+ [secure-enclave.md](secure-enclave.md).
386
+
387
+ ## Controls in the Development Process
388
+
389
+ - A static-analysis rule, call it `INSECURE_BIOMETRIC_GATE`, flags any
390
+ `evaluatePolicy` whose success value unlocks a resource when the same flow
391
+ has no `SecItemCopyMatching` against an item with `SecAccessControl`.
392
+ - Sign-off requires: no standalone gates on sensitive operations; `SecItemAdd`
393
+ calls using `kSecAttrAccessControl` with a `ThisDeviceOnly` class; a recorded
394
+ run showing the objection bypass fails; every `LAError` case handled; a tested
395
+ re-enrollment path wherever `.biometryCurrentSet` is used;
396
+ `NSFaceIDUsageDescription` present.
397
+
398
+ ## Verifying at Runtime
399
+
400
+ On a jailbroken or instrumented test device, hook
401
+ `-[LAContext evaluatePolicy:localizedReason:reply:]` so it always reports
402
+ success, then try to reach the protected data.
403
+
404
+ - Pass: the data stays locked.
405
+ - Fail: the hook grants access.
406
+
407
+ ## Checklist
408
+
409
+ - No standalone Boolean gates; secrets sit in the keychain behind an ACL.
410
+ - Every biometric-protected item is created with
411
+ `SecAccessControlCreateWithFlags`, so the enclave enforces the ACL.
412
+ - Flag choice: `.biometryCurrentSet` for high security, `.biometryAny` for
413
+ convenience, `.userPresence` for compatibility and fallback.
414
+ - Never both accessibility attributes on one item.
415
+ - Biometric items use `WhenPasscodeSetThisDeviceOnly` or
416
+ `WhenUnlockedThisDeviceOnly` and are never synchronizable.
417
+ - `Info.plist` contains `NSFaceIDUsageDescription`.
418
+ - `LAError` cases handled: `.biometryNotEnrolled`, `.biometryLockout`,
419
+ `.biometryNotAvailable`, `.passcodeNotSet`, `.userCancel`, `.userFallback`.
420
+ - A passcode or password fallback exists for when biometrics are unavailable or
421
+ locked out.
422
+ - With `.biometryCurrentSet`, `evaluatedPolicyDomainState` is monitored and a
423
+ re-enrollment flow exists.
424
+ - Biometric ACL reads never run on `@MainActor`.
425
+ - The objection or Frida bypass test is part of release testing.
426
+ - CI runs the static rule against standalone `evaluatePolicy`.
427
+
428
+ ## Sources and Related Files
429
+
430
+ Sources: WWDC 2014 Session 711; WWDC 2019 Session 709; Apple Platform Security
431
+ Guide; MSTG-AUTH-8, MSTG-AUTH-12 and MASTG-TEST-0266; the objection wiki page
432
+ "Understanding the iOS Biometrics Bypass"; TN3137.
433
+
434
+ Related: [keychain-fundamentals.md](keychain-fundamentals.md),
435
+ [keychain-access-control.md](keychain-access-control.md),
436
+ [secure-enclave.md](secure-enclave.md),
437
+ [common-anti-patterns.md](common-anti-patterns.md) (the `LAContext`-only gate),
438
+ [credential-storage-patterns.md](credential-storage-patterns.md),
439
+ [testing-security-code.md](testing-security-code.md),
440
+ [compliance-owasp-mapping.md](compliance-owasp-mapping.md) (M3).