@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,708 +1,438 @@
1
- # Common Anti-Patterns
1
+ # Insecure Patterns in Generated iOS Code
2
2
 
3
- > **Scope:** The 10 most dangerous security anti-patterns that AI coding assistants generate for iOS apps. Each entry includes the vulnerability explanation, realistic ❌ insecure code, ✅ correct replacement, detection heuristic, and OWASP risk mapping. This is the skill's backbone - the single most important file for correcting AI-generated security code.
4
- >
5
- > **Cross-references:** `biometric-authentication.md` (anti-pattern #3 deep dive), `keychain-fundamentals.md` (anti-pattern #4 CRUD patterns), `keychain-access-control.md` (anti-pattern #5 protection classes), `cryptokit-symmetric.md` (anti-patterns #6-7), `credential-storage-patterns.md` (anti-patterns #1-2 token lifecycle), `migration-legacy-stores.md` (anti-pattern #9 first-launch cleanup), `compliance-owasp-mapping.md` (full OWASP/MASVS mapping).
3
+ This is the review backbone of the skill. It lists the ten security mistakes that code generators produce most often on Apple platforms. Every entry gives the reason it is dangerous, an insecure sample, a corrected sample, a way to detect it, and its OWASP mapping.
6
4
 
7
- ---
5
+ Deeper material per entry: [credential-storage-patterns.md](credential-storage-patterns.md) (1 and 2), [biometric-authentication.md](biometric-authentication.md) (3), [keychain-fundamentals.md](keychain-fundamentals.md) (4), [keychain-access-control.md](keychain-access-control.md) (5), [cryptokit-symmetric.md](cryptokit-symmetric.md) (6 and 7), [migration-legacy-stores.md](migration-legacy-stores.md) (9), and [compliance-owasp-mapping.md](compliance-owasp-mapping.md) for the complete standards mapping.
8
6
 
9
7
  ## Contents
10
8
 
11
- - [Why AI Generates Insecure iOS Code](#why-ai-generates-insecure-ios-code)
12
- - [Anti-Pattern #1 - Storing Secrets in UserDefaults](#anti-pattern-1-storing-secrets-in-userdefaults)
13
- - [Anti-Pattern #2 - Hardcoded API Keys](#anti-pattern-2-hardcoded-api-keys)
14
- - [Anti-Pattern #3 - LAContext-Only Biometric Authentication](#anti-pattern-3-lacontext-only-biometric-authentication)
15
- - [Anti-Pattern #4 - Ignoring SecItem Error Codes](#anti-pattern-4-ignoring-secitem-error-codes)
16
- - [Anti-Pattern #5 - Wrong or Missing Data Protection Class](#anti-pattern-5-wrong-or-missing-data-protection-class)
17
- - [Anti-Pattern #6 - Nonce Reuse in AES-GCM](#anti-pattern-6-nonce-reuse-in-aes-gcm)
18
- - [Anti-Pattern #7 - MD5/SHA-1 for Security Purposes](#anti-pattern-7-md5sha-1-for-security-purposes)
19
- - [Anti-Pattern #8 - Logging Sensitive Data](#anti-pattern-8-logging-sensitive-data)
20
- - [Anti-Pattern #9 - Not Clearing Keychain on First Launch](#anti-pattern-9-not-clearing-keychain-on-first-launch)
21
- - [Anti-Pattern #10 - Non-Cryptographic RNG for Security Operations](#anti-pattern-10-non-cryptographic-rng-for-security-operations)
22
- - [Quick Reference Matrix](#quick-reference-matrix)
23
- - [CI/CD Detection Strategy](#cicd-detection-strategy)
24
- - [iOS 26 / WWDC 2025 Implications](#ios-26-wwdc-2025-implications)
25
- - [Summary Checklist](#summary-checklist)
9
+ - [Why generated code drifts toward insecure defaults](#why-generated-code-drifts-toward-insecure-defaults)
10
+ - [1. Secrets in UserDefaults](#1-secrets-in-userdefaults)
11
+ - [2. Hardcoded API keys](#2-hardcoded-api-keys)
12
+ - [3. Biometric check that only returns a Boolean](#3-biometric-check-that-only-returns-a-boolean)
13
+ - [4. Ignoring SecItem status codes](#4-ignoring-secitem-status-codes)
14
+ - [5. Missing or weak data protection class](#5-missing-or-weak-data-protection-class)
15
+ - [6. Reusing an AES-GCM nonce](#6-reusing-an-aes-gcm-nonce)
16
+ - [7. MD5 or SHA-1 where security matters](#7-md5-or-sha-1-where-security-matters)
17
+ - [8. Secrets in logs](#8-secrets-in-logs)
18
+ - [9. No keychain cleanup after reinstall](#9-no-keychain-cleanup-after-reinstall)
19
+ - [10. Weak randomness for security values](#10-weak-randomness-for-security-values)
20
+ - [Quick reference](#quick-reference)
21
+ - [Automating detection in CI](#automating-detection-in-ci)
22
+ - [What iOS 26 changes](#what-ios-26-changes)
23
+ - [Checklist](#checklist)
26
24
 
27
- ## Why AI Generates Insecure iOS Code
25
+ ## Why generated code drifts toward insecure defaults
28
26
 
29
- AI assistants optimize for functional correctness, not security - reproducing the most common patterns from training data, which are overwhelmingly insecure-by-default. Veracode's 2025 analysis: 45% of AI-generated code fails security tests. Cybernews: 815,000+ hardcoded secrets across 156,000 iOS apps (71% leaking ≥1 credential). Stanford: developers using AI write less secure code yet feel more confident.
27
+ Generators are rewarded for code that compiles and works, and their training data is full of tutorials that store tokens in `UserDefaults` or gate screens on a biometric Boolean. The insecure version is usually the shortest one, so it wins.
30
28
 
31
- Apple's security primitives (Keychain, CryptoKit, Secure Enclave) are excellent but AI consistently bypasses them. CISA/FBI classified hardcoded credentials as elevating "risk to national security" in their January 2025 Bad Practices v2.0 (CWE-798).
29
+ Published numbers point the same way. A 2025 industry study found that 45% of AI-produced code failed security tests. A 2025 scan of about 156,000 iOS apps found more than 815,000 hardcoded secrets, with 71% of apps leaking at least one. An academic user study found that developers with an AI assistant wrote less secure code while feeling more confident in it. The CISA and FBI "Product Security Bad Practices" guidance (version 2.0, January 2025) names hardcoded credentials (CWE-798) as a practice that raises risk to national security.
32
30
 
33
- **OWASP standard:** Mobile Top 10 (2024) with MASTG v2 test IDs. Legacy MSTG-\* identifiers noted where commonly referenced.
31
+ Mappings below use the OWASP Mobile Top 10 (2024) and MASTG v2 test identifiers, with legacy MSTG identifiers in parentheses where auditors still quote them.
34
32
 
35
- ---
33
+ The samples share two small error types: `KeychainFailure(status)` wraps an `OSStatus`, and `VaultError` carries app-level cases such as `deviceLocked` and `authenticationFailed`.
36
34
 
37
- ## Anti-Pattern #1 - Storing Secrets in UserDefaults
35
+ ## 1. Secrets in UserDefaults
38
36
 
39
- **Severity:** CRITICAL | **OWASP:** M9 (Insecure Data Storage) | **Fix effort:** Medium
37
+ Severity: Critical. OWASP: M9. Fix effort: Medium.
40
38
 
41
- UserDefaults writes to an unencrypted XML plist at `~/Library/Preferences/{BUNDLE_ID}.plist`. Apple's documentation: "Don't store personal or sensitive information as settings." Readable from unencrypted backups, jailbroken devices (Objection `ios nsuserdefaults get`), and third-party SDKs. **SwiftUI's `@AppStorage` is a wrapper over `UserDefaults`** - it has identical security properties and must never be used for tokens, keys, or credentials.
39
+ `UserDefaults` writes an unencrypted property list to `Library/Preferences/<bundle id>.plist` inside the container, and Apple's documentation says not to keep sensitive data there. The file shows up in unencrypted backups, on jailbroken devices (`objection` reads it with `ios nsuserdefaults get`), and to any third-party SDK in the process. SwiftUI's `@AppStorage` is the same store with a property wrapper around it, so it is equally unsuitable for tokens, keys or credentials.
42
40
 
43
- **❌ Insecure - AI-generated pattern:**
41
+ Insecure:
44
42
 
45
43
  ```swift
46
- // Plaintext on disk, readable from backups
47
- func saveAuthToken(_ token: String) {
48
- UserDefaults.standard.set(token, forKey: "userAuthToken")
49
- UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
50
- UserDefaults.standard.synchronize()
51
- }
52
-
53
- let token = UserDefaults.standard.string(forKey: "userAuthToken")
44
+ let prefs = UserDefaults.standard
45
+ prefs.set(session.accessToken, forKey: "access_token")
46
+ prefs.set(session.refreshToken, forKey: "refresh_token")
47
+ prefs.synchronize()
54
48
  ```
55
49
 
56
- **✅ Secure - Keychain with add-or-update:**
50
+ Secure:
57
51
 
58
52
  ```swift
59
- func saveTokenToKeychain(_ token: Data, account: String) throws {
60
- let query: [String: Any] = [
61
- kSecClass as String: kSecClassGenericPassword,
62
- kSecAttrService as String: "com.myapp.auth",
63
- kSecAttrAccount as String: account,
64
- kSecValueData as String: token,
65
- kSecAttrAccessible as String:
66
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly
67
- ]
68
- let status = SecItemAdd(query as CFDictionary, nil)
53
+ func storeAccessToken(_ token: String) throws {
54
+ let identity: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
55
+ kSecAttrService: "com.example.courier",
56
+ kSecAttrAccount: "access_token"]
57
+ var insert = identity
58
+ insert[kSecValueData] = Data(token.utf8)
59
+ insert[kSecAttrAccessible] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
60
+ var status = SecItemAdd(insert as CFDictionary, nil)
69
61
  if status == errSecDuplicateItem {
70
- // Full add-or-update pattern → see anti-pattern #4
71
- let search: [String: Any] = [
72
- kSecClass as String: kSecClassGenericPassword,
73
- kSecAttrService as String: "com.myapp.auth",
74
- kSecAttrAccount as String: account
75
- ]
76
- let updateStatus = SecItemUpdate(
77
- search as CFDictionary,
78
- [kSecValueData as String: token] as CFDictionary)
79
- guard updateStatus == errSecSuccess else {
80
- throw KeychainError.unexpectedStatus(updateStatus)
81
- }
82
- } else if status != errSecSuccess {
83
- throw KeychainError.unexpectedStatus(status)
62
+ let changes: [CFString: Any] = [kSecValueData: Data(token.utf8)]
63
+ status = SecItemUpdate(identity as CFDictionary, changes as CFDictionary)
84
64
  }
65
+ guard status == errSecSuccess else { throw KeychainFailure(status) }
85
66
  }
86
67
  ```
87
68
 
88
- **MASTG tests:** MASTG-TEST-0300, MASTG-TEST-0302. **MASWE:** MASWE-0006. **Legacy:** MSTG-STORAGE-1.
69
+ Standards: MASTG-TEST-0300, MASTG-TEST-0302, MASWE-0006 (legacy MSTG-STORAGE-1).
89
70
 
90
- **Detection heuristic:**
71
+ Detection: case-insensitive search of `.swift` files for lines containing `UserDefaults` together with token, password, secret, credential, auth, session, api key, jwt or bearer.
91
72
 
92
- ```bash
93
- grep -rn "UserDefaults" --include="*.swift" | \
94
- grep -iE "token|password|secret|credential|auth|session|api.?key|jwt|bearer"
73
+ ```sh
74
+ grep -rniE 'UserDefaults.*(token|password|secret|credential|auth|session|api_?key|jwt|bearer)' --include='*.swift' .
95
75
  ```
96
76
 
97
- ---
98
-
99
- ## Anti-Pattern #2 - Hardcoded API Keys
77
+ ## 2. Hardcoded API keys
100
78
 
101
- **Severity:** CRITICAL | **OWASP:** M1 (Improper Credential Usage) | **Fix effort:** High
79
+ Severity: Critical. OWASP: M1. Fix effort: High.
102
80
 
103
- API keys compiled into Swift appear in the binary's `__TEXT.__cstring` segment - `strings MyApp.app/MyApp` extracts them instantly. Even `.xcconfig` or `Info.plist` values ship inside the IPA. Cybernews found 78,800 Google API keys across 156,000 iOS apps.
81
+ A Swift string literal ends up in the binary's `__TEXT.__cstring` section, where `strings` prints it in seconds. Values in `.xcconfig` files or `Info.plist` ship inside the IPA just the same. The 2025 scan mentioned above found about 78,800 keys for a single cloud provider's APIs alone.
104
82
 
105
- **❌ Insecure - AI-generated pattern:**
83
+ Insecure:
106
84
 
107
85
  ```swift
108
- class PaymentService {
109
- private let stripeKey = "sk_live_51H7bK2E..." // In binary
110
- private let firebaseKey = "AIzaSyB..." // In binary
111
-
112
- func charge(amount: Int) async throws {
113
- var request = URLRequest(
114
- url: URL(string: "https://api.stripe.com/v1/charges")!)
115
- request.setValue("Bearer \(stripeKey)",
116
- forHTTPHeaderField: "Authorization")
117
- let (data, _) = try await URLSession.shared.data(for: request)
118
- }
86
+ enum Secrets {
87
+ static let paymentsKey = "sk_live_EXAMPLEONLY000000000000"
88
+ static let backendKey = "AIzaEXAMPLEONLY00000000000000000"
119
89
  }
120
-
121
- // Also dangerous: key in Info.plist or .xcconfig bundled in app
122
- let key = Bundle.main.infoDictionary?["API_KEY"] as? String
90
+ request.setValue("Bearer \(Secrets.paymentsKey)", forHTTPHeaderField: "Authorization")
91
+ let plistKey = Bundle.main.infoDictionary?["API_KEY"] as? String
123
92
  ```
124
93
 
125
- **✅ Secure - server proxy + Keychain cache:**
94
+ Secure, in order of preference:
126
95
 
127
- ```swift
128
- class SecureAPIKeyManager {
129
- static let shared = SecureAPIKeyManager()
130
-
131
- /// Best: proxy through your server (key never on device)
132
- func secureRequest(endpoint: String, params: [String: Any]) async throws -> Data {
133
- var request = URLRequest(
134
- url: URL(string: "https://api.myserver.com/proxy/\(endpoint)")!)
135
- request.httpMethod = "POST"
136
- request.httpBody = try JSONSerialization.data(withJSONObject: params)
137
- let (data, _) = try await URLSession.shared.data(for: request)
138
- return data
139
- }
96
+ 1. A server-side proxy holds the key and the app calls the proxy, so the key never reaches the device.
97
+ 2. If the device genuinely needs a key, fetch it at runtime from your server after the user or device is authenticated and cache it in the keychain.
140
98
 
141
- /// If client must hold key: fetch at runtime, cache in Keychain
142
- func getAPIKey() async throws -> String {
143
- if let cached = try? readFromKeychain(service: "api-keys", account: "primary") {
144
- return String(data: cached, encoding: .utf8)!
145
- }
146
- let (data, _) = try await URLSession.shared.data(
147
- from: URL(string: "https://api.myserver.com/config/key")!)
148
- try saveToKeychain(data, service: "api-keys", account: "primary")
149
- return String(data: data, encoding: .utf8)!
150
- }
99
+ ```swift
100
+ func quoteShipment(_ order: ShipmentRequest) async throws -> Quote {
101
+ var request = URLRequest(url: quotesEndpoint)
102
+ request.httpMethod = "POST"
103
+ request.httpBody = try JSONEncoder().encode(order)
104
+ request.setValue("Bearer \(try await session.userAccessToken())", forHTTPHeaderField: "Authorization")
105
+ let (body, _) = try await URLSession.shared.data(for: request)
106
+ return try JSONDecoder().decode(Quote.self, from: body)
151
107
  }
152
108
  ```
153
109
 
154
- Apple's DeviceCheck and App Attest frameworks provide server-side device verification without embedding secrets. WWDC 2019-709 advises storing credentials in Keychain, not in code.
110
+ DeviceCheck and App Attest let the server verify a genuine app instance without shipping any secret (details in the `device-integrity` skill). WWDC 2019 session 709 states the rule plainly: credentials belong in the keychain.
155
111
 
156
- **MASTG tests:** MASTG-TEST-0213, MASTG-TEST-0214. **MASWE:** MASWE-0005. **Legacy:** MSTG-STORAGE-12. **CISA/FBI:** CWE-798 - Product Security Bad Practices v2.0 (January 2025).
112
+ Standards: MASTG-TEST-0213, MASTG-TEST-0214, MASWE-0005 (legacy MSTG-STORAGE-12); CWE-798 per the CISA/FBI guidance.
157
113
 
158
- **Detection heuristic:**
114
+ Detection: in `.swift`, `.plist` and `.xcconfig` files, look for `let` constants whose name contains "key" assigned a literal of 20 or more alphanumeric characters, and for the prefixes `"sk_live_`, `"pk_live_`, `"AIza` and `"AKIA`.
159
115
 
160
- ```bash
161
- grep -rn 'let.*[Kk]ey.*=.*"[A-Za-z0-9_\-]\{20,\}"' --include="*.swift"
162
- grep -rn '"sk_live_\|"pk_live_\|"AIza[A-Za-z0-9]\|"AKIA[A-Z0-9]' \
163
- --include="*.swift" --include="*.plist" --include="*.xcconfig"
116
+ ```sh
117
+ grep -rnE 'let [A-Za-z]*[Kk]ey[A-Za-z]* *= *"[A-Za-z0-9_-]{20,}"' --include='*.swift' .
118
+ grep -rnE '"(sk_live_|pk_live_|AIza|AKIA)' --include='*.swift' --include='*.plist' --include='*.xcconfig' .
164
119
  ```
165
120
 
166
- ---
121
+ ## 3. Biometric check that only returns a Boolean
167
122
 
168
- ## Anti-Pattern #3 - LAContext-Only Biometric Authentication
123
+ Severity: Critical. OWASP: M3. Fix effort: Medium.
169
124
 
170
- **Severity:** CRITICAL | **OWASP:** M3 (Insecure Authentication) | **Fix effort:** Medium
125
+ This is the insecure pattern generators reproduce most. `evaluatePolicy` hands back a `Bool` in the app's own process, and one Frida command or `objection`'s `ios ui biometrics_bypass` flips it. MASTG's position is that biometric authentication must unlock something in the keychain, not just a screen.
171
126
 
172
- Using `LAContext.evaluatePolicy()` alone is the single most reproduced insecure pattern across iOS tutorials. The method returns a simple boolean callback in user-space - no cryptographic binding. Frida forces `success = true` in one command; Objection packages this as `ios ui biometrics_bypass`. OWASP MASTG: "Biometric authentication must be based on unlocking the keychain." Full deep dive: see `biometric-authentication.md`.
173
-
174
- **❌ Insecure - AI-generated pattern:**
127
+ Insecure:
175
128
 
176
129
  ```swift
177
- func authenticateUser(completion: @escaping (Bool) -> Void) {
178
- let context = LAContext()
179
- context.evaluatePolicy(
180
- .deviceOwnerAuthenticationWithBiometrics,
181
- localizedReason: "Authenticate to access your account"
182
- ) { success, authError in
183
- DispatchQueue.main.async {
184
- if success {
185
- self.showSensitiveData() // Gated on a hookable boolean
186
- }
187
- completion(success)
188
- }
189
- }
130
+ LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
131
+ localizedReason: "Show your balance") { granted, _ in
132
+ if granted { DispatchQueue.main.async { self.revealBalance() } }
190
133
  }
191
134
  ```
192
135
 
193
- **✅ Secure - Keychain + SecAccessControl hardware binding:**
136
+ Secure: bind the secret to a keychain item whose access control demands biometrics, and read it with an `LAContext` attached.
194
137
 
195
138
  ```swift
196
- // STORE: biometric-protected via Secure Enclave
197
- func storeWithBiometric(secret: Data, account: String) throws {
198
- let access = SecAccessControlCreateWithFlags(
199
- nil,
200
- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
201
- .biometryCurrentSet, nil)!
202
-
203
- let query: [String: Any] = [
204
- kSecClass as String: kSecClassGenericPassword,
205
- kSecAttrService as String: "com.myapp.biometric",
206
- kSecAttrAccount as String: account,
207
- kSecAttrAccessControl as String: access,
208
- kSecValueData as String: secret
209
- ]
210
- let status = SecItemAdd(query as CFDictionary, nil)
211
- guard status == errSecSuccess || status == errSecDuplicateItem else {
212
- throw KeychainError.unexpectedStatus(status)
139
+ func sealBalanceKey(_ secret: Data) throws {
140
+ guard let gate = SecAccessControlCreateWithFlags(nil,
141
+ kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .biometryCurrentSet, nil) else {
142
+ throw KeychainFailure(errSecParam)
143
+ }
144
+ let identity: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
145
+ kSecAttrService: "com.example.courier",
146
+ kSecAttrAccount: "balance-key"]
147
+ var insert = identity
148
+ insert[kSecValueData] = secret
149
+ insert[kSecAttrAccessControl] = gate
150
+ var status = SecItemAdd(insert as CFDictionary, nil)
151
+ if status == errSecDuplicateItem {
152
+ status = SecItemUpdate(identity as CFDictionary, [kSecValueData: secret] as CFDictionary)
213
153
  }
154
+ guard status == errSecSuccess else { throw KeychainFailure(status) }
214
155
  }
215
156
 
216
- // READ: Secure Enclave enforces biometric before releasing data
217
- func readWithBiometric(account: String) throws -> Data {
157
+ func openBalanceKey() throws -> Data {
218
158
  let context = LAContext()
219
- context.localizedReason = "Access your secure data"
220
- let query: [String: Any] = [
221
- kSecClass as String: kSecClassGenericPassword,
222
- kSecAttrService as String: "com.myapp.biometric",
223
- kSecAttrAccount as String: account,
224
- kSecReturnData as String: true,
225
- kSecMatchLimit as String: kSecMatchLimitOne,
226
- kSecUseAuthenticationContext as String: context
227
- ]
228
- var result: AnyObject?
229
- let status = SecItemCopyMatching(query as CFDictionary, &result)
230
- guard status == errSecSuccess, let data = result as? Data else {
231
- throw KeychainError.unexpectedStatus(status)
232
- }
233
- return data // Only returned after hardware biometric validation
159
+ context.localizedReason = "Show your balance"
160
+ let lookup: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
161
+ kSecAttrService: "com.example.courier",
162
+ kSecAttrAccount: "balance-key",
163
+ kSecReturnData: true,
164
+ kSecUseAuthenticationContext: context]
165
+ var out: CFTypeRef?
166
+ let status = SecItemCopyMatching(lookup as CFDictionary, &out)
167
+ guard status == errSecSuccess, let secret = out as? Data else { throw KeychainFailure(status) }
168
+ return secret
234
169
  }
235
170
  ```
236
171
 
237
- The `.biometryCurrentSet` flag invalidates the item if biometrics change, preventing an attacker with physical access from enrolling their own biometric. Objection's documentation confirms this bypass "will NOT work" with keychain-bound biometric items.
238
-
239
- **MASTG tests:** MASTG-TEST-0266, MASTG-TEST-0267. **MASWE:** MASWE-0044. **Legacy:** MSTG-AUTH-8. **WWDC:** 2014-711 introduced `SecAccessControlCreateWithFlags`.
172
+ A duplicate is updated, never silently accepted. If the item needs a different access control than the one it was created with, delete it and add it again, because that is the one case where add-or-update does not apply.
240
173
 
241
- **Detection heuristic:**
174
+ `.biometryCurrentSet` invalidates the item when the enrolled biometrics change, so an attacker who adds their own face or finger to a stolen, unlocked device gets nothing. `objection`'s own documentation notes that its bypass does not work against keychain-bound items.
242
175
 
243
- ```bash
244
- # evaluatePolicy without SecAccessControl → insecure
245
- grep -rn "evaluatePolicy" --include="*.swift" -l | \
246
- xargs grep -L "SecAccessControlCreateWithFlags"
247
- # Verify secure pattern exists
248
- grep -rn "\.biometryCurrentSet\|\.biometryAny" --include="*.swift"
249
- ```
176
+ Standards: MASTG-TEST-0266, MASTG-TEST-0267, MASWE-0044 (legacy MSTG-AUTH-8). `SecAccessControlCreateWithFlags` arrived with WWDC 2014 session 711.
250
177
 
251
- ---
178
+ Detection: files that call `evaluatePolicy` but never `SecAccessControlCreateWithFlags`; also confirm whether `.biometryCurrentSet` or `.biometryAny` appears at all.
252
179
 
253
- ## Anti-Pattern #4 - Ignoring SecItem Error Codes
180
+ ```sh
181
+ grep -rl 'evaluatePolicy' --include='*.swift' . | xargs grep -L 'SecAccessControlCreateWithFlags'
182
+ ```
254
183
 
255
- **Severity:** HIGH | **OWASP:** M8 (Security Misconfiguration) | **Fix effort:** Low
184
+ ## 4. Ignoring SecItem status codes
256
185
 
257
- `errSecDuplicateItem` (OSStatus -25299) is the most common Keychain failure. When `SecItemAdd` hits a duplicate, it silently discards the new value. Password updates never persist, refreshed tokens are lost, and auth breaks in hard-to-debug ways. Other critical codes: `errSecItemNotFound` (-25300), `errSecAuthFailed` (-25293), `errSecInteractionNotAllowed` (-25308).
186
+ Severity: High. OWASP: M8 (Security Misconfiguration). Fix effort: Low.
258
187
 
259
- Full CRUD patterns: see `keychain-fundamentals.md`.
188
+ The usual casualty is `errSecDuplicateItem` (-25299): the add fails, nobody looks, and the new value is dropped while the old one stays. Other statuses that must be handled: `errSecItemNotFound` (-25300), `errSecAuthFailed` (-25293), `errSecInteractionNotAllowed` (-25308). A cancelled authentication prompt surfaces as `errSecUserCanceled` (-128).
260
189
 
261
- **❌ Insecure - AI-generated pattern:**
190
+ Insecure:
262
191
 
263
192
  ```swift
264
- func saveToken(_ token: Data) {
265
- let query: [String: Any] = [
266
- kSecClass as String: kSecClassGenericPassword,
267
- kSecAttrService as String: "com.app.auth",
268
- kSecAttrAccount as String: "accessToken",
269
- kSecValueData as String: token
270
- ]
271
- SecItemAdd(query as CFDictionary, nil) // Return value ignored!
272
- }
193
+ SecItemAdd(insert as CFDictionary, nil)
273
194
  ```
274
195
 
275
- **✅ Secure - OSStatus switch with add-or-update:**
196
+ Secure:
276
197
 
277
198
  ```swift
278
- func saveToKeychain(value: Data, service: String, account: String) throws {
279
- let query: [String: Any] = [
280
- kSecClass as String: kSecClassGenericPassword,
281
- kSecAttrService as String: service,
282
- kSecAttrAccount as String: account,
283
- kSecValueData as String: value,
284
- kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
285
- ]
286
- let status = SecItemAdd(query as CFDictionary, nil)
287
- switch status {
288
- case errSecSuccess: return
289
- case errSecDuplicateItem:
290
- let search: [String: Any] = [
291
- kSecClass as String: kSecClassGenericPassword,
292
- kSecAttrService as String: service,
293
- kSecAttrAccount as String: account
294
- ]
295
- let updateStatus = SecItemUpdate(
296
- search as CFDictionary, [kSecValueData as String: value] as CFDictionary)
297
- guard updateStatus == errSecSuccess else { throw KeychainError.updateFailed(updateStatus) }
298
- case errSecInteractionNotAllowed: throw KeychainError.deviceLocked
299
- case errSecAuthFailed: throw KeychainError.authenticationFailed
300
- default: throw KeychainError.unexpectedStatus(status)
301
- }
199
+ switch SecItemAdd(insert as CFDictionary, nil) {
200
+ case errSecSuccess:
201
+ break
202
+ case errSecDuplicateItem:
203
+ let status = SecItemUpdate(identity as CFDictionary, [kSecValueData: payload] as CFDictionary)
204
+ guard status == errSecSuccess else { throw KeychainFailure(status) }
205
+ case errSecInteractionNotAllowed:
206
+ throw VaultError.deviceLocked
207
+ case errSecAuthFailed, errSecUserCanceled:
208
+ throw VaultError.authenticationFailed
209
+ case let other:
210
+ throw KeychainFailure(other)
302
211
  }
303
212
  ```
304
213
 
305
- Critical detail: `SecItemUpdate` takes two dictionaries - search query (without `kSecValueData`) and attributes to update. Passing the full query as the search parameter is a common mistake.
214
+ `SecItemUpdate` takes two dictionaries: a search query with no `kSecValueData`, and the attributes to change. Passing the full add dictionary as the search query is a frequent mistake that makes the update match nothing.
306
215
 
307
- **MASTG tests:** MASTG-TEST-0300, MASTG-TEST-0301. **Legacy:** MASVS-STORAGE-2.
216
+ Standards: MASTG-TEST-0300, MASTG-TEST-0301 (legacy MASVS-STORAGE-2).
308
217
 
309
- **Detection heuristic:**
218
+ Detection: files containing `SecItemAdd` with no mention of `errSecDuplicateItem` or -25299, and `SecItemAdd(` calls whose result is neither assigned nor compared.
310
219
 
311
- ```bash
312
- grep -rn "SecItemAdd" --include="*.swift" -l | \
313
- xargs grep -L "errSecDuplicateItem\|DuplicateItem\|-25299"
314
- grep -rn "SecItemAdd(" --include="*.swift" | \
315
- grep -v "let\|var\|status\|=\|switch\|if\|guard"
316
- ```
220
+ ## 5. Missing or weak data protection class
317
221
 
318
- ---
222
+ Severity: High. OWASP: M9. Fix effort: Low.
319
223
 
320
- ## Anti-Pattern #5 - Wrong or Missing Data Protection Class
224
+ Leaving `kSecAttrAccessible` out means inheriting a default that may not fit. `kSecAttrAccessibleAlways` (deprecated in iOS 12) keeps the item decryptable while the device is locked. A class without the `ThisDeviceOnly` suffix is included in backups.
321
225
 
322
- **Severity:** HIGH | **OWASP:** M9 (Insecure Data Storage) | **Fix effort:** Low
226
+ Pick deliberately:
323
227
 
324
- Omitting `kSecAttrAccessible` inherits a default that may be insufficient. Using deprecated `kSecAttrAccessibleAlways` (deprecated iOS 12) leaves data decryptable on a locked device. Missing `ThisDeviceOnly` suffix means items are included in backups. Full protection class guide: see `keychain-access-control.md`.
228
+ | Item | Class |
229
+ | --- | --- |
230
+ | Passwords, access tokens used in the foreground | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` |
231
+ | Highest-sensitivity secrets | `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` |
232
+ | Values background code must read (push tokens, refresh tokens) | `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` |
325
233
 
326
- **❌ Insecure - AI-generated patterns:**
234
+ WWDC 2014 session 711's advice still holds: choose the most restrictive option that works for the feature.
327
235
 
328
- ```swift
329
- // Missing kSecAttrAccessible entirely
330
- let query: [String: Any] = [
331
- kSecClass as String: kSecClassGenericPassword,
332
- kSecAttrAccount as String: "user_password",
333
- kSecValueData as String: passwordData
334
- ]
335
- SecItemAdd(query as CFDictionary, nil)
336
-
337
- // Deprecated - accessible when device is locked
338
- kSecAttrAccessible as String: kSecAttrAccessibleAlways
339
- ```
340
-
341
- **✅ Secure - selection by use case:**
342
-
343
- ```swift
344
- // Passwords, auth tokens (foreground-only)
345
- kSecAttrAccessible as String:
346
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly
347
-
348
- // Highest sensitivity - requires passcode to exist
349
- kSecAttrAccessible as String:
350
- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
236
+ Standards: MASTG-TEST-0299 (legacy MASTG-STORAGE-3).
351
237
 
352
- // Background-access items (push tokens, refresh tokens)
353
- kSecAttrAccessible as String:
354
- kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
355
- ```
356
-
357
- WWDC 2014-711: "Always use the most restrictive option that makes sense for your app."
358
-
359
- **MASTG test:** MASTG-TEST-0299. **Legacy:** MASTG-STORAGE-3.
360
-
361
- **Detection heuristic:**
362
-
363
- ```bash
364
- grep -rn "kSecAttrAccessibleAlways\b" --include="*.swift"
365
- grep -rn "SecItemAdd" --include="*.swift" -l | \
366
- xargs grep -L "kSecAttrAccessible\|kSecAttrAccessControl"
367
- grep -rn "kSecAttrAccessibleWhenUnlocked\b" --include="*.swift" | \
368
- grep -v "ThisDeviceOnly"
369
- ```
238
+ Detection: `kSecAttrAccessibleAlways\b`; files with `SecItemAdd` but neither `kSecAttrAccessible` nor `kSecAttrAccessControl`; `kSecAttrAccessibleWhenUnlocked\b` without `ThisDeviceOnly`.
370
239
 
371
- ---
240
+ ## 6. Reusing an AES-GCM nonce
372
241
 
373
- ## Anti-Pattern #6 - Nonce Reuse in AES-GCM
242
+ Severity: Critical. OWASP: M10. Fix effort: Medium.
374
243
 
375
- **Severity:** CRITICAL | **OWASP:** M10 (Insufficient Cryptography) | **Fix effort:** Medium
244
+ Two messages sealed with the same key and nonce share a keystream, so XOR of the ciphertexts equals XOR of the plaintexts, and the GHASH authentication key can be recovered, which lets an attacker forge messages (Joux, 2006). CryptoKit picks a fresh random nonce for you; the break only happens when code builds nonces by hand.
376
245
 
377
- Reusing a nonce with the same key in AES-GCM is a complete cryptographic break. Identical nonces produce identical keystreams, enabling plaintext recovery via `C1 ⊕ C2 = P1 ⊕ P2` and authentication key recovery via polynomial factorization ("forbidden attack," Joux 2006). CryptoKit's `AES.GCM.seal` has a safe default: omitting the `nonce` parameter auto-generates a random 12-byte nonce. Danger occurs when AI explicitly constructs nonces. Full patterns: see `cryptokit-symmetric.md`.
378
-
379
- **❌ Insecure - AI-generated patterns:**
246
+ Insecure:
380
247
 
381
248
  ```swift
382
- import CryptoKit
383
-
384
- // Hardcoded nonce - identical keystream every encryption
385
- let fixedNonce = try! AES.GCM.Nonce(data: Data(repeating: 0x00, count: 12))
386
-
387
- func encrypt(_ plaintext: Data, using key: SymmetricKey) throws -> Data {
388
- let sealedBox = try AES.GCM.seal(
389
- plaintext, using: key, nonce: fixedNonce) // CATASTROPHIC
390
- return sealedBox.combined!
391
- }
392
- // Also dangerous: counter-based nonce that resets on app restart → collision
249
+ let fixedNonce = try AES.GCM.Nonce(data: Data(repeating: 0, count: 12))
250
+ let box = try AES.GCM.seal(record, using: key, nonce: fixedNonce)
393
251
  ```
394
252
 
395
- **✅ Secure - let CryptoKit handle nonces:**
253
+ A counter nonce that restarts from zero after each launch fails the same way.
254
+
255
+ Secure:
396
256
 
397
257
  ```swift
398
- import CryptoKit
258
+ let key = SymmetricKey(size: .bits256)
399
259
 
400
- func encrypt(_ plaintext: Data, using key: SymmetricKey) throws -> Data {
401
- // Nonce omitted → CryptoKit generates random 12-byte nonce
402
- let sealedBox = try AES.GCM.seal(plaintext, using: key)
403
- return sealedBox.combined! // Contains: nonce ‖ ciphertext ‖ tag
260
+ func encrypt(_ record: Data) throws -> Data {
261
+ guard let wire = try AES.GCM.seal(record, using: key).combined else {
262
+ throw CryptoKitError.incorrectParameterSize
263
+ }
264
+ return wire
404
265
  }
405
266
 
406
- func decrypt(_ combined: Data, using key: SymmetricKey) throws -> Data {
407
- let sealedBox = try AES.GCM.SealedBox(combined: combined)
408
- return try AES.GCM.open(sealedBox, using: key)
267
+ func decrypt(_ wire: Data) throws -> Data {
268
+ try AES.GCM.open(try AES.GCM.SealedBox(combined: wire), using: key)
409
269
  }
410
-
411
- let key = SymmetricKey(size: .bits256) // AES-256 per WWDC 2025 guidance
412
270
  ```
413
271
 
414
- WWDC 2019-709 introduced CryptoKit with the design philosophy: "easy to use, hard to misuse."
415
-
416
- **MASTG test:** MASTG-TEST-0317. **MASWE:** MASWE-0022. **Legacy:** MASTG-CRYPTO-4.
272
+ `combined` packs nonce, ciphertext and tag together, so nothing needs managing separately. This is the design goal WWDC 2019 session 709 set for CryptoKit: easy to use correctly, hard to use wrongly.
417
273
 
418
- **Detection heuristic:**
419
-
420
- ```bash
421
- grep -rn "AES\.GCM\.Nonce(data:" --include="*.swift"
422
- grep -rn "let.*nonce.*=.*AES\.GCM\.Nonce" --include="*.swift"
423
- grep -rn "Data(repeating:.*count:\s*12)" --include="*.swift"
424
- grep -rn "\.seal(.*nonce:" --include="*.swift"
425
- ```
274
+ Standards: MASTG-TEST-0317, MASWE-0022 (legacy MASTG-CRYPTO-4).
426
275
 
427
- ---
276
+ Detection: `AES.GCM.Nonce(data:`, variables named like `nonce`, `Data(repeating:` with `count: 12`, and `.seal(` calls that pass `nonce:`.
428
277
 
429
- ## Anti-Pattern #7 - MD5/SHA-1 for Security Purposes
278
+ ## 7. MD5 or SHA-1 where security matters
430
279
 
431
- **Severity:** HIGH | **OWASP:** M10 (Insufficient Cryptography) | **Fix effort:** Low
280
+ Severity: High. OWASP: M10. Fix effort: Low.
432
281
 
433
- MD5 broken since Wang & Yu (2005); SHA-1 broken by SHAttered (2017). CISA January 2025 lists both as insecure. Apple signals this via CryptoKit's `Insecure.MD5` and `Insecure.SHA1` namespacing.
282
+ MD5 collisions have been practical since 2005 (Wang and Yu); SHA-1 fell publicly with SHAttered in 2017. The January 2025 CISA guidance lists both as insecure, and CryptoKit parks them in the `Insecure` namespace for a reason.
434
283
 
435
- **❌ Insecure - AI-generated pattern:**
284
+ Insecure:
436
285
 
437
286
  ```swift
438
- import CryptoKit
439
- func hashPassword(_ password: String) -> String {
440
- let hash = Insecure.MD5.hash(data: password.data(using: .utf8)!)
441
- return hash.map { String(format: "%02x", $0) }.joined()
442
- }
443
- // Also: CC_MD5, CC_SHA1 from CommonCrypto
287
+ let stored = Insecure.MD5.hash(data: Data(password.utf8))
444
288
  ```
445
289
 
446
- **✅ Secure - SHA-256 minimum, KDF for passwords:**
447
-
448
- ```swift
449
- import CryptoKit
450
-
451
- // Integrity verification
452
- func hashData(_ data: Data) -> String {
453
- let hash = SHA256.hash(data: data)
454
- return hash.map { String(format: "%02x", $0) }.joined()
455
- }
290
+ Also `CC_MD5` and `CC_SHA1` from CommonCrypto.
456
291
 
457
- // HMAC for message authentication
458
- func authenticate(_ data: Data, key: SymmetricKey) -> Data {
459
- Data(HMAC<SHA256>.authenticationCode(for: data, using: key))
460
- }
292
+ Secure: use the hash that fits the job.
461
293
 
462
- // Password storage - NEVER raw hashes. Use a KDF:
463
- // Server-side: Argon2id, bcrypt, or scrypt
464
- // On-device: PBKDF2 with ≥600,000 iterations (OWASP 2023 minimum for HMAC-SHA256)
465
- // See cryptokit-symmetric.md for full PBKDF2 implementation
294
+ ```swift
295
+ let fingerprint = SHA256.hash(data: download)
296
+ let tag = HMAC<SHA256>.authenticationCode(for: message, using: macKey)
466
297
  ```
467
298
 
468
- iOS 26 adds SHA-3 family (`SHA3_256`, `SHA3_384`, `SHA3_512`) in CryptoKit. WWDC 2025-314 covers post-quantum additions (ML-KEM, ML-DSA), not SHA-3.
299
+ Passwords are never hashed with a plain digest. Hash them on the server with Argon2id, bcrypt or scrypt. When a key must be derived on the device, use PBKDF2 with at least 600,000 iterations.
469
300
 
470
- **MASTG test:** MASTG-TEST-0211. **MASTG demos:** MASTG-DEMO-0015, MASTG-DEMO-0016. **Legacy:** MSTG-CRYPTO-1.
301
+ iOS 26 adds `SHA3_256`, `SHA3_384` and `SHA3_512`. WWDC 2025 session 314 is about post-quantum cryptography and does not cover SHA-3.
471
302
 
472
- **Detection heuristic:**
303
+ Standards: MASTG-TEST-0211, MASTG-DEMO-0015, MASTG-DEMO-0016 (legacy MSTG-CRYPTO-1).
473
304
 
474
- ```bash
475
- grep -rn "Insecure\.\(MD5\|SHA1\)" --include="*.swift"
476
- grep -rn "CC_MD5\|CC_SHA1\|CC_MD5_DIGEST_LENGTH\|CC_SHA1_DIGEST_LENGTH" \
477
- --include="*.swift" --include="*.m"
478
- ```
305
+ Detection: `Insecure.MD5` or `Insecure.SHA1`, plus `CC_MD5`, `CC_SHA1` and the matching digest-length constants in `.swift` and `.m` files.
479
306
 
480
- ---
307
+ ## 8. Secrets in logs
481
308
 
482
- ## Anti-Pattern #8 - Logging Sensitive Data
309
+ Severity: High. OWASP: M9. Fix effort: Low.
483
310
 
484
- **Severity:** HIGH | **OWASP:** M9 (Insecure Data Storage) | **Fix effort:** Low
311
+ Whatever `print()`, `NSLog()` or `os_log()` emits can persist in the device log, where Xcode's console, `idevicesyslog` or `log collect --device` retrieve it; on a jailbroken device any process can read it.
485
312
 
486
- `print()`, `NSLog()`, and `os_log()` with sensitive values persist in device logs - accessible via Xcode Console, `idevicesyslog`, and `log collect --device`. On jailbroken devices, any process reads log storage. Apple's `OSLogPrivacy` (iOS 14+): `.private` redacts in production; `.sensitive` (iOS 15+) always redacted.
313
+ `OSLogPrivacy` (iOS 14 and later) controls redaction: `.private` values are hidden in production logs, and `.sensitive` (iOS 15 and later) is always hidden.
487
314
 
488
- **❌ Insecure - AI-generated pattern:**
315
+ Insecure:
489
316
 
490
317
  ```swift
491
- func login(username: String, password: String) async throws {
492
- print("Logging in with password: \(password)") // In device logs!
493
- let token = try await authService.authenticate(username, password)
494
- print("Got auth token: \(token)") // In device logs!
495
- os_log("API key loaded: %{public}@", apiKey) // Explicitly public!
496
- }
318
+ print("login ok, token=\(token)")
319
+ os_log("using key %{public}@", apiKey)
497
320
  ```
498
321
 
499
- **✅ Secure - OSLogPrivacy with redaction:**
322
+ Secure:
500
323
 
501
324
  ```swift
502
- import os
503
-
504
- let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "auth")
505
-
506
- func login(username: String, password: String) async throws {
507
- // Log events, not values - .private(mask: .hash) enables correlation
508
- logger.info("Login attempt: \(username, privacy: .private(mask: .hash))")
509
- let token = try await authService.authenticate(username, password)
510
- logger.info("Authentication succeeded") // No token value
511
- }
512
-
513
- // Legacy os_log
514
- os_log("Account: %{private}@", log: .default, type: .info, accountNumber)
515
-
516
- // Strip debug logging in release builds
517
- #if DEBUG
518
- print("Debug: \(sensitiveValue)")
519
- #endif
325
+ let authLog = Logger(subsystem: "com.example.courier", category: "auth")
326
+ authLog.info("sign-in succeeded")
327
+ authLog.debug("session \(sessionID, privacy: .private(mask: .hash))")
520
328
  ```
521
329
 
522
- **MASTG tests:** MASTG-TEST-0296, MASTG-TEST-0297. **MASWE:** MASWE-0001. **Legacy:** MSTG-STORAGE-3.
330
+ Log the event, not the value. `.private(mask: .hash)` still lets you correlate entries for the same session. With the older API, use `%{private}@`. Keep any diagnostic `print` inside `#if DEBUG`.
523
331
 
524
- **Detection heuristic:**
332
+ Standards: MASTG-TEST-0296, MASTG-TEST-0297, MASWE-0001 (legacy MSTG-STORAGE-3).
525
333
 
526
- ```bash
527
- grep -rn "print(.*\\\(" --include="*.swift" | \
528
- grep -iE "password|token|secret|key|credential|ssn|credit"
529
- grep -rn "NSLog(.*%@" --include="*.swift" --include="*.m" | \
530
- grep -iE "password|token|secret|key"
531
- grep -rn 'os_log.*%{public}' --include="*.swift" | \
532
- grep -iE "password|token|secret|key"
533
- ```
534
-
535
- ---
334
+ Detection: `print(` with string interpolation, `NSLog(` with `%@`, and `os_log` with `%{public}` near words like password, token, secret or key.
536
335
 
537
- ## Anti-Pattern #9 - Not Clearing Keychain on First Launch
336
+ ## 9. No keychain cleanup after reinstall
538
337
 
539
- **Severity:** MEDIUM | **OWASP:** M9 (Insecure Data Storage) | **Fix effort:** Low
338
+ Severity: Medium. OWASP: M9. Fix effort: Low.
540
339
 
541
- Keychain items persist in a system-wide encrypted database managed by `securityd`, outside the app sandbox. App deletion removes the sandbox but keychain items survive. Apple DTS engineer Quinn "The Eskimo!" confirmed this as "currently expected behaviour despite being an obvious privacy concern." Consequences: stale credentials on reinstall, cross-user data leakage on device resale, and Firebase SDK authentication errors on reinstall. Full migration patterns: see `migration-legacy-stores.md`.
340
+ The keychain database lives in `securityd`, outside the app container, so items survive deleting the app. Apple DTS has described this as expected behaviour even though it has privacy consequences. The effects: stale credentials after reinstall, one user's session leaking to the next owner of a resold device, and backend SDKs failing authentication with leftover state.
542
341
 
543
- **❌ The missing pattern - AI never generates this:**
342
+ Secure: before any SDK initialises, wait until protected data is readable, then check a marker in `UserDefaults`; if it is unset, delete every item class with `kSecAttrSynchronizable: kSecAttrSynchronizableAny` and set the marker.
544
343
 
545
344
  ```swift
546
- @main
547
- struct MyApp: App {
548
- var body: some Scene {
549
- WindowGroup { ContentView() }
345
+ @MainActor
346
+ func purgeKeychainOnFreshInstall() async {
347
+ await ReinstallSweeper.waitUntilProtectedDataIsReadable()
348
+ guard !UserDefaults.standard.bool(forKey: "vault.installMarker") else { return }
349
+ for itemClass in [kSecClassGenericPassword, kSecClassInternetPassword,
350
+ kSecClassCertificate, kSecClassKey, kSecClassIdentity] {
351
+ let scope: [CFString: Any] = [kSecClass: itemClass,
352
+ kSecAttrSynchronizable: kSecAttrSynchronizableAny]
353
+ SecItemDelete(scope as CFDictionary)
550
354
  }
551
- // Stale keychain items from previous install persist silently
355
+ UserDefaults.standard.set(true, forKey: "vault.installMarker")
552
356
  }
553
357
  ```
554
358
 
555
- **✅ Secure - first-launch keychain cleanup:**
359
+ The protected-data wait is not optional. A pre-warmed launch before first unlock reads the marker as unset and would erase a signed-in user. The full version, including retry on failed deletes, is in [migration-legacy-stores.md](migration-legacy-stores.md).
556
360
 
557
- ```swift
558
- @main
559
- struct MyApp: App {
560
- init() { clearKeychainIfFirstLaunch() }
361
+ Standards: MASTG-TEST-0300, MASTG-TEST-0301 (legacy MSTG-STORAGE-11).
561
362
 
562
- var body: some Scene {
563
- WindowGroup { ContentView() }
564
- }
363
+ Detection: files using `SecItemAdd` or `SecItemCopyMatching` with no first-launch marker anywhere in the project; files with `SecItemDelete` that reference such a marker are the cleanup to review.
565
364
 
566
- private func clearKeychainIfFirstLaunch() {
567
- let defaults = UserDefaults.standard
568
- guard !defaults.bool(forKey: "hasLaunchedBefore") else { return }
569
-
570
- // UserDefaults was cleared on uninstall → this is first launch
571
- for secClass in [kSecClassGenericPassword, kSecClassInternetPassword,
572
- kSecClassCertificate, kSecClassKey, kSecClassIdentity] {
573
- SecItemDelete([
574
- kSecClass: secClass,
575
- kSecAttrSynchronizable: kSecAttrSynchronizableAny
576
- ] as NSDictionary)
577
- }
578
- defaults.set(true, forKey: "hasLaunchedBefore")
579
- }
580
- }
581
- ```
365
+ ## 10. Weak randomness for security values
582
366
 
583
- Place this before initializing any SDKs (Firebase, analytics) that read from Keychain. Including `kSecAttrSynchronizableAny` ensures iCloud Keychain items are also cleared.
367
+ Severity: High. OWASP: M10. Fix effort: Low.
584
368
 
585
- **MASTG tests:** MASTG-TEST-0300, MASTG-TEST-0301. **Legacy:** MSTG-STORAGE-11.
369
+ The generator and the amount of entropy both matter.
586
370
 
587
- **Detection heuristic:**
371
+ - `rand()`, `random()`, `drand48()` and `srand(time(nil))` are predictable and never acceptable. GameplayKit sources such as `GKMersenneTwisterRandomSource` and `GKARC4RandomSource` are built for games and are not for security either.
372
+ - `arc4random`, `arc4random_buf` and `arc4random_uniform` are backed by the system cryptographic generator on Apple platforms, as their manual page states, and so is Swift's `SystemRandomNumberGenerator`, which `randomElement()` and `Int.random(in:)` use by default. The generator is fine; the usual defect is how little of it the code uses. `arc4random()` returns a single 32-bit `UInt32`, `arc4random() % n` adds modulo bias, and `arc4random_uniform(999_999)` produces a token with about 20 bits of entropy. A few `randomElement()` picks from a character set make an equally guessable session identifier. These functions also cannot report failure.
373
+ - For keys, tokens, salts and nonces, prefer `SecRandomCopyBytes`, which draws from the same system generator (corecrypto, seeded from hardware entropy), returns a status you can check, and makes the security intent obvious in review. For key material, use `SymmetricKey(size: .bits256)` directly.
588
374
 
589
- ```bash
590
- grep -rn "SecItemAdd\|SecItemCopyMatching" --include="*.swift" -l | \
591
- xargs grep -L "hasLaunchedBefore\|isFirstLaunch\|firstRun"
592
- grep -rn "SecItemDelete" --include="*.swift" -l | \
593
- xargs grep "hasLaunchedBefore\|isFirstLaunch"
594
- ```
595
-
596
- ---
597
-
598
- ## Anti-Pattern #10 - Non-Cryptographic RNG for Security Operations
599
-
600
- **Severity:** HIGH | **OWASP:** M10 (Insufficient Cryptography) | **Fix effort:** Low
601
-
602
- `arc4random()` returns only 32-bit `UInt32` - insufficient for cryptographic purposes requiring 128-256 bits. Character-by-character token construction introduces bias. Truly non-cryptographic alternatives (`rand()`, `drand48()`, GameplayKit RNG) must never be used for security operations.
603
-
604
- **❌ Insecure - AI-generated patterns:**
375
+ Insecure:
605
376
 
606
377
  ```swift
607
- func generateToken() -> String {
608
- return String(arc4random_uniform(999_999)) // ~20 bits of entropy
609
- }
610
-
611
- func generateSessionId(length: Int = 16) -> String {
612
- let chars = "abcdefghijklmnopqrstuvwxyz0123456789"
613
- return String((0..<length).map { _ in chars.randomElement()! }) // Bias
614
- }
615
- // Also dangerous: srand48/drand48, rand(), GameplayKit RNG
378
+ let resetCode = String(arc4random_uniform(999_999))
379
+ let sessionID = String((0..<6).compactMap { _ in "abcdef0123456789".randomElement() })
616
380
  ```
617
381
 
618
- **✅ Secure - SecRandomCopyBytes / CryptoKit:**
382
+ Secure:
619
383
 
620
384
  ```swift
621
- import Security
622
- import CryptoKit
623
-
624
- // SecRandomCopyBytes - canonical iOS crypto RNG
625
- func generateSecureToken(byteCount: Int = 32) throws -> String {
626
- var bytes = [UInt8](repeating: 0, count: byteCount)
385
+ func makeSessionToken() throws -> String {
386
+ var bytes = [UInt8](repeating: 0, count: 32)
627
387
  let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
628
- guard status == errSecSuccess else {
629
- throw CryptoError.randomGenerationFailed(status)
630
- }
388
+ guard status == errSecSuccess else { throw KeychainFailure(status) }
631
389
  return bytes.map { String(format: "%02x", $0) }.joined()
632
390
  }
633
391
 
634
- // CryptoKit key generation (secure RNG internally)
635
- let encryptionKey = SymmetricKey(size: .bits256)
392
+ let wrappingKey = SymmetricKey(size: .bits256)
636
393
  ```
637
394
 
638
- `SecRandomCopyBytes` sources entropy from the Secure Enclave's hardware TRNG via corecrypto's `ccrng_generate`. It reports errors via return status - unlike `arc4random`, which silently cannot fail.
639
-
640
- **MASTG test:** MASTG-TEST-0311. **MASTG demos:** MASTG-DEMO-0073, MASTG-DEMO-0074. **Legacy:** MSTG-CRYPTO-6.
641
-
642
- **Detection heuristic:**
643
-
644
- ```bash
645
- grep -rn "arc4random\|arc4random_uniform\|arc4random_buf" --include="*.swift" | \
646
- grep -iE "token|nonce|salt|key|secret|session|iv"
647
- grep -rn "\bsrand\b\|\brand()\|\brandom()\|\bdrand48\b" --include="*.swift"
648
- grep -rn "GKARC4RandomSource\|GKMersenneTwisterRandomSource" --include="*.swift"
649
- ```
650
-
651
- ---
652
-
653
- ## Quick Reference Matrix
654
-
655
- | # | Anti-Pattern | OWASP 2024 | MASTG Test | Dangerous API | Secure API | Fix Effort |
656
- | --- | ------------------------ | ---------- | --------------- | -------------------------- | ----------------------------------- | ---------- |
657
- | 1 | UserDefaults secrets | M9 | MASTG-TEST-0302 | `UserDefaults.set` | `SecItemAdd` + Keychain | Medium |
658
- | 2 | Hardcoded API keys | M1 | MASTG-TEST-0213 | String literals | Server proxy + Keychain cache | High |
659
- | 3 | LAContext-only biometric | M3 | MASTG-TEST-0266 | `evaluatePolicy` | `SecAccessControlCreateWithFlags` | Medium |
660
- | 4 | Ignored SecItem errors | M8 | MASTG-TEST-0300 | Unchecked `SecItemAdd` | OSStatus switch + `SecItemUpdate` | Low |
661
- | 5 | Wrong data protection | M9 | MASTG-TEST-0299 | `kSecAttrAccessibleAlways` | `WhenUnlockedThisDeviceOnly` | Low |
662
- | 6 | Nonce reuse AES-GCM | M10 | MASTG-TEST-0317 | `AES.GCM.Nonce(data:)` | Omit nonce (auto-random) | Medium |
663
- | 7 | MD5/SHA-1 for security | M10 | MASTG-TEST-0211 | `Insecure.MD5/.SHA1` | `SHA256`+ / KDF for passwords | Low |
664
- | 8 | Logging sensitive data | M9 | MASTG-TEST-0297 | `print(token)` | `Logger` + `.private` | Low |
665
- | 9 | No keychain cleanup | M9 | MASTG-TEST-0300 | Missing cleanup | UserDefaults flag + `SecItemDelete` | Low |
666
- | 10 | Non-crypto RNG | M10 | MASTG-TEST-0311 | `arc4random()` | `SecRandomCopyBytes` | Low |
667
-
668
- ---
669
-
670
- ## CI/CD Detection Strategy
671
-
672
- **Semgrep** (pre-commit/PR gate): Fast structural pattern matching for `UserDefaults` misuse, missing `errSecDuplicateItem`, `LAContext` booleans. Limited data-flow analysis.
673
-
674
- **CodeQL** (nightly/PR gate): Deep semantic taint tracking - catches tokens assigned to variables then logged. Slower execution.
395
+ Standards: MASTG-TEST-0311, MASTG-DEMO-0073, MASTG-DEMO-0074 (legacy MSTG-CRYPTO-6).
675
396
 
676
- **Binary scanning** (post-build): `strings`/`class-dump` on compiled binary catches hardcoded keys surviving source-level obfuscation.
397
+ Detection: `srand`, `rand()`, `random()`, `drand48`, `GKARC4RandomSource`, `GKMersenneTwisterRandomSource` are findings. `arc4random` calls near token, nonce, salt, key, secret, session or iv are review prompts: check how many bits the value carries.
677
398
 
678
- Recommended: Semgrep on every PR + post-build binary scanning. CodeQL nightly for deep analysis.
399
+ ## Quick reference
679
400
 
680
- ---
401
+ | # | OWASP | Main MASTG test | Look for | Replace with | Effort |
402
+ | --- | --- | --- | --- | --- | --- |
403
+ | 1 | M9 | 0302 | `UserDefaults.set` of a secret | `SecItemAdd` with update on duplicate | Medium |
404
+ | 2 | M1 | 0213 | Key literals | Server proxy, runtime fetch cached in keychain | High |
405
+ | 3 | M3 | 0266 | `evaluatePolicy` as the gate | `SecAccessControlCreateWithFlags` | Medium |
406
+ | 4 | M8 | 0300 | Unchecked `SecItemAdd` | Status `switch` with update on duplicate | Low |
407
+ | 5 | M9 | 0299 | `Always`, missing class | `WhenUnlockedThisDeviceOnly` or stricter | Low |
408
+ | 6 | M10 | 0317 | `AES.GCM.Nonce(data:)` | Let CryptoKit choose the nonce | Medium |
409
+ | 7 | M10 | 0211 | `Insecure.*` | SHA-256 or stronger, a password KDF | Low |
410
+ | 8 | M9 | 0297 | `print(token)` | `Logger` with `.private` | Low |
411
+ | 9 | M9 | 0300 | No reinstall cleanup | Marker plus `SecItemDelete` after protected data | Low |
412
+ | 10 | M10 | 0311 | Low-entropy or predictable RNG | `SecRandomCopyBytes`, `SymmetricKey` | Low |
681
413
 
682
- ## iOS 26 / WWDC 2025 Implications
414
+ ## Automating detection in CI
683
415
 
684
- WWDC 2025-314 introduced the most significant CryptoKit expansion since 2019:
416
+ - **Semgrep**: fast structural rules, ideal as a pre-commit hook or pull-request gate for `UserDefaults` secrets, add calls without duplicate handling and Boolean biometric gates. Its data-flow tracking is limited.
417
+ - **CodeQL**: semantic taint analysis, for example a token flowing into a logging call. Slower; run nightly or on pull requests.
418
+ - **Binary scan after the build**: `strings` and `class-dump` over the built product catch keys that survived obfuscation or came in through configuration files.
685
419
 
686
- - **Symmetric keys:** `.bits256` recommended over `.bits128` for quantum resistance (anti-patterns #6, #10)
687
- - **Hashing:** SHA-3 family (`SHA3_256/384/512`) in CryptoKit on iOS 26+ (anti-pattern #7)
688
- - **Post-quantum:** ML-KEM 768/1024, ML-DSA 65/87, X-Wing - all with Secure Enclave support
689
- - **TLS:** `X25519MLKEM768` enabled by default for `URLSession` in iOS 26
690
- - **Secure Enclave:** Hardware post-quantum key creation strengthens anti-patterns #3 and #5 fixes
420
+ A practical setup runs Semgrep on every pull request, a binary scan after each build, and CodeQL nightly.
691
421
 
692
- ---
422
+ ## What iOS 26 changes
693
423
 
694
- ## Summary Checklist
424
+ Prefer `.bits256` symmetric keys over `.bits128` for headroom against quantum attacks. The SHA-3 family is available from iOS 26. CryptoKit adds ML-KEM-768 and ML-KEM-1024, ML-DSA-65 and ML-DSA-87, and the X-Wing hybrid KEM, with Secure Enclave support, and `URLSession` negotiates the hybrid `X25519MLKEM768` TLS key exchange by default. Creating post-quantum keys in the Secure Enclave strengthens the fixes for entries 3 and 5.
695
425
 
696
- When reviewing iOS code for security anti-patterns, verify each item:
426
+ ## Checklist
697
427
 
698
- 1. **No secrets in UserDefaults** - tokens, passwords, API keys, JWTs use Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` or stricter
699
- 1. **No hardcoded keys in source** - API keys fetched at runtime via server proxy or authenticated endpoint; no high-entropy string literals, no secrets in `.xcconfig` or `Info.plist`
700
- 1. **Biometrics bound to Keychain** - `evaluatePolicy` is never used alone to gate sensitive actions; `SecAccessControlCreateWithFlags` with `.biometryCurrentSet` protects keychain items
701
- 1. **All SecItem calls checked** - `SecItemAdd` handles `errSecDuplicateItem` with `SecItemUpdate` fallback; `SecItemCopyMatching` handles `errSecItemNotFound`; no discarded `OSStatus` return values
702
- 1. **Explicit data protection class** - every `SecItemAdd` includes `kSecAttrAccessible` or `kSecAttrAccessControl`; no `kSecAttrAccessibleAlways`; `ThisDeviceOnly` variants used for non-syncing items
703
- 1. **No nonce reuse** - `AES.GCM.seal` called without explicit `nonce:` parameter (auto-random); no stored/global/counter-based nonce variables
704
- 1. **No broken hashes** - no `Insecure.MD5`, `Insecure.SHA1`, `CC_MD5`, `CC_SHA1` for security purposes; passwords use a password KDF such as Argon2id, bcrypt, or PBKDF2-HMAC-SHA256 with at least 600,000 iterations
705
- 1. **No sensitive data in logs** - `print()` and `NSLog()` never contain tokens, keys, or credentials; `os_log` uses `%{private}@`; `Logger` uses `.private` or `.private(mask: .hash)`
706
- 1. **First-launch keychain cleanup** - `UserDefaults` flag + `SecItemDelete` for all classes runs before SDK initialization at app startup
707
- 1. **Cryptographic RNG only** - `SecRandomCopyBytes` or CryptoKit APIs for tokens, nonces, salts, keys; no `arc4random` / `rand()` / `drand48()` / GameplayKit RNG in security contexts
708
- 1. **iOS 26 readiness** - symmetric keys use `.bits256`; no deprecated algorithms; aware of post-quantum CryptoKit APIs for forward-looking implementations
428
+ - [ ] No secrets in `UserDefaults` or `@AppStorage`; keychain with `WhenUnlockedThisDeviceOnly` or stricter.
429
+ - [ ] No hardcoded keys or high-entropy literals; nothing secret in `.xcconfig` or `Info.plist`.
430
+ - [ ] Biometrics unlock a keychain item protected with `.biometryCurrentSet`.
431
+ - [ ] Every `SecItem` call checks its status; duplicates are updated, not-found is handled.
432
+ - [ ] Every add sets accessibility or access control; no `Always`; `ThisDeviceOnly` for items that should not sync.
433
+ - [ ] No explicit, global or counter nonces.
434
+ - [ ] No MD5 or SHA-1; passwords go through Argon2id, bcrypt, or PBKDF2-HMAC-SHA256 with at least 600,000 iterations.
435
+ - [ ] No secrets in logs; `%{private}@`, or `Logger` with `.private` or `.private(mask: .hash)`.
436
+ - [ ] Reinstall cleanup runs before SDK setup and after protected data is available.
437
+ - [ ] Security values come from a cryptographic generator with enough bits.
438
+ - [ ] Ready for iOS 26: `.bits256`, no deprecated algorithms, post-quantum APIs on the radar.