Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
87024d9
Initial plan
Copilot Dec 2, 2025
194cce1
Add Android biometric authentication tests and demo for MASTG-TEST-00…
Copilot Dec 2, 2025
a31e605
Remove accidentally added package files
Copilot Dec 2, 2025
0b4a607
Remove unused imports from demo code files
Copilot Dec 2, 2025
4693ffb
Finalize biometric authentication test port
Copilot Dec 2, 2025
092bb50
Remove accidentally committed gradle files
Copilot Dec 2, 2025
3e336e2
Add MASTG-TEST-0316 for authentication without explicit user action
Copilot Dec 29, 2025
27deb7a
Fix long line in MASTG-DEMO-0077 markdown
Copilot Dec 29, 2025
3af543c
updated test cases 313, 314 and 316 and added MastgTest.kt and revers…
Jan 3, 2026
0267cfb
new test cases added
Jan 5, 2026
dfa904d
changed IDs as DEMO-IDs were duplicates
Jan 5, 2026
3664490
updated IDs for tests and fixed markdown issues
Jan 5, 2026
ed2dd2e
to build the app androidx.biometric dependency is needed and Fragmen…
Jan 6, 2026
60595da
update test cases and added build.gradle.kts.libs
Jan 18, 2026
fa38f7a
fix Unordered list indentation
Jan 18, 2026
3db5f03
updated test cases, semgrep rules and 2 biometric apps for the demos
Feb 12, 2026
696ca77
deprecate test case
Feb 12, 2026
ed4ca89
update IDs
Feb 12, 2026
3230c5c
Address PR review feedback: fix deprecation field names, update test …
Copilot Feb 12, 2026
070bc00
updated semgrep rule event-bound
Feb 12, 2026
3b81a00
remove space
Feb 12, 2026
50d401a
updated note in tests
Feb 12, 2026
cea9ffb
remove empty lines
Feb 12, 2026
2b9fa8b
added best practice
Feb 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions best-practices/MASTG-BEST-0031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Enforce Strong Biometrics for Sensitive Operations
alias: enforce-strong-biometrics-for-sensitive-operations
id: MASTG-BEST-0031
platform: android
knowledge: [MASTG-KNOW-0001]
---

Apps should use the [`BIOMETRIC_STRONG`](https://developer.android.com/reference/android/hardware/biometrics/BiometricManager.Authenticators) authenticator for sensitive operations protected by biometrics. Using `DEVICE_CREDENTIAL` (PINs, patterns or passwords) are more susceptible to shoulder surfing and social engineering.

Check failure on line 9 in best-practices/MASTG-BEST-0031.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Trailing spaces

best-practices/MASTG-BEST-0031.md:9:327 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md

For high-security operations (e.g. payments or access to health data), enforcing biometrics only provides strong protection and verifies user presence.
31 changes: 31 additions & 0 deletions demos/android/MASVS-AUTH/MASTG-DEMO-0089/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MASTestApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.MASTestApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
39 changes: 39 additions & 0 deletions demos/android/MASVS-AUTH/MASTG-DEMO-0089/MASTG-DEMO-0089.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
platform: android
title: Uses of BiometricPrompt with Device Credential Fallback with semgrep
id: MASTG-DEMO-0089
code: [kotlin]
test: MASTG-TEST-0326
---

### Sample

The following sample demonstrates the use of the `BiometricPrompt` API with different authenticator configurations used in `BiometricPrompt.Builder()`. It shows both weaker configurations that allow fallback to device credentials (PIN, pattern, password), which are more susceptible to compromise (e.g., through shoulder surfing) and secure configurations that requires a strong biometric authentication only.

{{ MastgTest.kt # MastgTest_reversed.java }}

### Steps

Let's run @MASTG-TOOL-0110 rules against the sample code.

{{ ../../../../rules/mastg-android-biometric-device-credential-fallback.yml }}

{{ run.sh }}

### Observation

The output shows all usages of APIs that configure biometric authentication.

{{ output.txt }}

### Evaluation

The test fails because the output shows references to biometric authentication configurations that allow fallback to device credentials:

- Line 38: `setAllowedAuthenticators(32783)` is called with `BIOMETRIC_STRONG | DEVICE_CREDENTIAL`, which allows the user to authenticate with either biometrics or their device PIN/pattern/password. The value `32783` is the sum of `32768` and `15`. Decompiled code contains integer values of the [`Authenticator` constants](https://developer.android.com/reference/android/hardware/biometrics/BiometricManager.Authenticators) instead of the name:
- `BIOMETRIC_STRONG` = 15 (0x000F)
- `BIOMETRIC_WEAK` = 255 (0x00FF)
- `DEVICE_CREDENTIAL` = 32768 (0x8000)
- Also in line 38: [`setDeviceCredentialAllowed(true)`](https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder#setDeviceCredentialAllowed(boolean)) is called and can give the user the option to authenticate with their device PIN, pattern, or password instead of a biometric.

For sensitive operations, the app should use [`BIOMETRIC_STRONG`](https://developer.android.com/identity/sign-in/biometric-auth#declare-supported-authentication-types) to enforce biometric-only authentication.
163 changes: 163 additions & 0 deletions demos/android/MASVS-AUTH/MASTG-DEMO-0089/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.owasp.mastestapp

import android.content.Context
import android.hardware.biometrics.BiometricManager.Authenticators.*
import android.hardware.biometrics.BiometricPrompt
import android.os.Build
import android.os.CancellationSignal
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.RequiresApi
import java.util.concurrent.CountDownLatch

// SUMMARY: This sample demonstrates insecure biometric authentication that allows fallback to device credentials (PIN, pattern, password).

class MastgTest(private val context: Context) {

// Run in background thread - we'll post UI operations to main thread
val shouldRunInMainThread = false

private val mainHandler = Handler(Looper.getMainLooper())

@RequiresApi(Build.VERSION_CODES.R)
fun mastgTest(): String {
val results = DemoResults("0083")

// Test 1: DEVICE_CREDENTIAL - This FAILS the security test
// Allows PIN/pattern/password which is less secure than biometrics
val latch1 = CountDownLatch(1)
var authResult1: String? = null

// FAIL: [MASTG-TEST-0326] Using DEVICE_CREDENTIAL allows fallback to PIN/pattern/password
val prompt1 = BiometricPrompt.Builder(context)
.setTitle("Test 1: Device Credential")
.setSubtitle("Using DEVICE_CREDENTIAL (Security: FAIL)")
.setDescription("This allows also PIN/pattern/password authentication")
.setAllowedAuthenticators(
BIOMETRIC_STRONG or
DEVICE_CREDENTIAL
)
.setDeviceCredentialAllowed(true)
.build()

// Post authenticate to main thread
mainHandler.post {
prompt1.authenticate(
CancellationSignal(),
context.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication succeeded")
authResult1 = "User can authenticate with DEVICE_CREDENTIAL (PIN/pattern/password)"
results.add(
Status.FAIL,
"$authResult1. " +
"\n🔓 AUTH - Success!\n" +
"⚠️ Allows also PIN/Pattern/Password\n" +
"⚠️ Uses DEVICE_CREDENTIAL fallback\n"
)
latch1.countDown()
}

override fun onAuthenticationFailed() {
Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication failed")
authResult1 = "Authentication attempt failed"
results.add(
Status.FAIL,
"$authResult1. " +
"\n⚠️ AUTH - Failed\n" +
"⚠️ Allows PIN/Pattern/Password\n" +
"⚠️ Uses DEVICE_CREDENTIAL fallback\n"
)
latch1.countDown()
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication error: $errString")
authResult1 = "Authentication error: $errString (code: $errorCode)"
results.add(
Status.ERROR,
"$authResult1. " +
"\n⚠️ AUTH - Error\n" +
"⚠️ Allows PIN/Pattern/Password\n" +
"⚠️ Uses DEVICE_CREDENTIAL fallback\n"
)
latch1.countDown()
}
}
)
}

// Wait for first authentication to complete (background thread waits, main thread is free)
latch1.await()


// Test 2: BIOMETRIC_STRONG - This PASSES the security test
// Only allows Class 3 biometrics (fingerprint, face with depth)
val latch2 = CountDownLatch(1)
var authResult2: String? = null

// PASS: [MASTG-TEST-0326] Using BIOMETRIC_STRONG only requires biometric authentication
val prompt2 = BiometricPrompt.Builder(context)
.setTitle("Test 2: Biometric Strong")
.setSubtitle("Using BIOMETRIC_STRONG (Security: PASS)")
.setDescription("This only allows Class 3 biometrics")
.setAllowedAuthenticators(BIOMETRIC_STRONG)
.setNegativeButton("Cancel", context.mainExecutor) { _, _ ->
Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication cancelled")
authResult2 = "User cancelled authentication"
latch2.countDown()
}
.build()

// Post authenticate to main thread
mainHandler.post {
prompt2.authenticate(
CancellationSignal(),
context.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication succeeded")
authResult2 = "User authenticated with BIOMETRIC_STRONG"
results.add(
Status.PASS,
"$authResult2. " +
"\n🔓 AUTH - Success!\n" +
"✅ Allows only Strong Biometric\n" +
"\nThis configuration is secure because it only allows Class 3 biometrics."
)
latch2.countDown()
}

override fun onAuthenticationFailed() {
Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication failed")
authResult2 = "Authentication attempt failed (biometric not recognized)"
results.add(
Status.FAIL,
"$authResult2. " +
"\n⚠️ AUTH - Failed\n"
)
latch2.countDown()
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication error: $errString")
authResult2 = "Authentication error: $errString (code: $errorCode)"
results.add(
Status.ERROR,
"$authResult2. " +
"\n⚠️ AUTH - Error\n"
)
latch2.countDown()
}
}
)
}

// Wait for second authentication to complete
latch2.await()

return results.toJson()
}
}
Loading
Loading