@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,708 +1,438 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Insecure Patterns in Generated iOS Code
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [Quick
|
|
23
|
-
- [
|
|
24
|
-
- [iOS 26
|
|
25
|
-
- [
|
|
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
|
|
25
|
+
## Why generated code drifts toward insecure defaults
|
|
28
26
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
35
|
+
## 1. Secrets in UserDefaults
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
Severity: Critical. OWASP: M9. Fix effort: Medium.
|
|
40
38
|
|
|
41
|
-
UserDefaults writes
|
|
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
|
-
|
|
41
|
+
Insecure:
|
|
44
42
|
|
|
45
43
|
```swift
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
50
|
+
Secure:
|
|
57
51
|
|
|
58
52
|
```swift
|
|
59
|
-
func
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
69
|
+
Standards: MASTG-TEST-0300, MASTG-TEST-0302, MASWE-0006 (legacy MSTG-STORAGE-1).
|
|
89
70
|
|
|
90
|
-
|
|
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
|
-
```
|
|
93
|
-
grep -
|
|
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
|
-
|
|
79
|
+
Severity: Critical. OWASP: M1. Fix effort: High.
|
|
102
80
|
|
|
103
|
-
|
|
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
|
-
|
|
83
|
+
Insecure:
|
|
106
84
|
|
|
107
85
|
```swift
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
Secure, in order of preference:
|
|
126
95
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
Standards: MASTG-TEST-0213, MASTG-TEST-0214, MASWE-0005 (legacy MSTG-STORAGE-12); CWE-798 per the CISA/FBI guidance.
|
|
157
113
|
|
|
158
|
-
|
|
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
|
-
```
|
|
161
|
-
grep -
|
|
162
|
-
grep -
|
|
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
|
-
|
|
123
|
+
Severity: Critical. OWASP: M3. Fix effort: Medium.
|
|
169
124
|
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
**❌ Insecure - AI-generated pattern:**
|
|
127
|
+
Insecure:
|
|
175
128
|
|
|
176
129
|
```swift
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
217
|
-
func readWithBiometric(account: String) throws -> Data {
|
|
157
|
+
func openBalanceKey() throws -> Data {
|
|
218
158
|
let context = LAContext()
|
|
219
|
-
context.localizedReason = "
|
|
220
|
-
let
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
```sh
|
|
181
|
+
grep -rl 'evaluatePolicy' --include='*.swift' . | xargs grep -L 'SecAccessControlCreateWithFlags'
|
|
182
|
+
```
|
|
254
183
|
|
|
255
|
-
|
|
184
|
+
## 4. Ignoring SecItem status codes
|
|
256
185
|
|
|
257
|
-
|
|
186
|
+
Severity: High. OWASP: M8 (Security Misconfiguration). Fix effort: Low.
|
|
258
187
|
|
|
259
|
-
|
|
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
|
-
|
|
190
|
+
Insecure:
|
|
262
191
|
|
|
263
192
|
```swift
|
|
264
|
-
|
|
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
|
-
|
|
196
|
+
Secure:
|
|
276
197
|
|
|
277
198
|
```swift
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
Standards: MASTG-TEST-0300, MASTG-TEST-0301 (legacy MASVS-STORAGE-2).
|
|
308
217
|
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
+
Pick deliberately:
|
|
323
227
|
|
|
324
|
-
|
|
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
|
-
|
|
234
|
+
WWDC 2014 session 711's advice still holds: choose the most restrictive option that works for the feature.
|
|
327
235
|
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
Severity: Critical. OWASP: M10. Fix effort: Medium.
|
|
374
243
|
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
**❌ Insecure - AI-generated patterns:**
|
|
246
|
+
Insecure:
|
|
380
247
|
|
|
381
248
|
```swift
|
|
382
|
-
|
|
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
|
-
|
|
253
|
+
A counter nonce that restarts from zero after each launch fails the same way.
|
|
254
|
+
|
|
255
|
+
Secure:
|
|
396
256
|
|
|
397
257
|
```swift
|
|
398
|
-
|
|
258
|
+
let key = SymmetricKey(size: .bits256)
|
|
399
259
|
|
|
400
|
-
func encrypt(_
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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(_
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
278
|
+
## 7. MD5 or SHA-1 where security matters
|
|
430
279
|
|
|
431
|
-
|
|
280
|
+
Severity: High. OWASP: M10. Fix effort: Low.
|
|
432
281
|
|
|
433
|
-
MD5
|
|
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
|
-
|
|
284
|
+
Insecure:
|
|
436
285
|
|
|
437
286
|
```swift
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
+
Standards: MASTG-TEST-0211, MASTG-DEMO-0015, MASTG-DEMO-0016 (legacy MSTG-CRYPTO-1).
|
|
473
304
|
|
|
474
|
-
|
|
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
|
-
|
|
309
|
+
Severity: High. OWASP: M9. Fix effort: Low.
|
|
483
310
|
|
|
484
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
315
|
+
Insecure:
|
|
489
316
|
|
|
490
317
|
```swift
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
322
|
+
Secure:
|
|
500
323
|
|
|
501
324
|
```swift
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
+
Standards: MASTG-TEST-0296, MASTG-TEST-0297, MASWE-0001 (legacy MSTG-STORAGE-3).
|
|
525
333
|
|
|
526
|
-
|
|
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
|
-
##
|
|
336
|
+
## 9. No keychain cleanup after reinstall
|
|
538
337
|
|
|
539
|
-
|
|
338
|
+
Severity: Medium. OWASP: M9. Fix effort: Low.
|
|
540
339
|
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
355
|
+
UserDefaults.standard.set(true, forKey: "vault.installMarker")
|
|
552
356
|
}
|
|
553
357
|
```
|
|
554
358
|
|
|
555
|
-
|
|
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
|
-
|
|
558
|
-
@main
|
|
559
|
-
struct MyApp: App {
|
|
560
|
-
init() { clearKeychainIfFirstLaunch() }
|
|
361
|
+
Standards: MASTG-TEST-0300, MASTG-TEST-0301 (legacy MSTG-STORAGE-11).
|
|
561
362
|
|
|
562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
367
|
+
Severity: High. OWASP: M10. Fix effort: Low.
|
|
584
368
|
|
|
585
|
-
|
|
369
|
+
The generator and the amount of entropy both matter.
|
|
586
370
|
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
382
|
+
Secure:
|
|
619
383
|
|
|
620
384
|
```swift
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
635
|
-
let encryptionKey = SymmetricKey(size: .bits256)
|
|
392
|
+
let wrappingKey = SymmetricKey(size: .bits256)
|
|
636
393
|
```
|
|
637
394
|
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
414
|
+
## Automating detection in CI
|
|
683
415
|
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
+
## Checklist
|
|
697
427
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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.
|