@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. 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.