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