Skip to content

MASTG-DEMO-0089: Uses of BiometricPrompt with Device Credential Fallback with semgrep

Download MASTG-DEMO-0089 APK Open MASTG-DEMO-0089 Folder Build MASTG-DEMO-0089 APK

Sample

The following sample demonstrates the use of the built-in android.hardware.biometrics.BiometricPrompt framework API (available from API level 28+) 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.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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()
    }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package org.owasp.mastestapp;

import android.content.Context;
import android.content.DialogInterface;
import android.hardware.biometrics.BiometricPrompt;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.CountDownLatch;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import kotlin.jvm.internal.Ref;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003Ā¢\u0006\u0004\b\u0004\u0010\u0005J\b\u0010\f\u001a\u00020\rH\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004Ā¢\u0006\u0002\n\u0000R\u0014\u0010\u0006\u001a\u00020\u0007X\u0086DĀ¢\u0006\b\n\u0000\u001a\u0004\b\b\u0010\tR\u000e\u0010\n\u001a\u00020\u000bX\u0082\u0004Ā¢\u0006\u0002\n\u0000ĀØ\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "shouldRunInMainThread", "", "getShouldRunInMainThread", "()Z", "mainHandler", "Landroid/os/Handler;", "mastgTest", "", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MastgTest {
    public static final int $stable = 8;
    private final Context context;
    private final Handler mainHandler;
    private final boolean shouldRunInMainThread;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
        this.mainHandler = new Handler(Looper.getMainLooper());
    }

    public final boolean getShouldRunInMainThread() {
        return this.shouldRunInMainThread;
    }

    public final String mastgTest() {
        final DemoResults results = new DemoResults("0083");
        final CountDownLatch latch1 = new CountDownLatch(1);
        final Ref.ObjectRef authResult1 = new Ref.ObjectRef();
        final BiometricPrompt prompt1 = new BiometricPrompt.Builder(this.context).setTitle("Test 1: Device Credential").setSubtitle("Using DEVICE_CREDENTIAL (Security: FAIL)").setDescription("This allows also PIN/pattern/password authentication").setAllowedAuthenticators(32783).setDeviceCredentialAllowed(true).build();
        Intrinsics.checkNotNullExpressionValue(prompt1, "build(...)");
        this.mainHandler.post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0
            @Override // java.lang.Runnable
            public final void run() {
                MastgTest.mastgTest$lambda$0(prompt1, this, authResult1, results, latch1);
            }
        });
        latch1.await();
        final CountDownLatch latch2 = new CountDownLatch(1);
        final Ref.ObjectRef authResult2 = new Ref.ObjectRef();
        final BiometricPrompt prompt2 = new BiometricPrompt.Builder(this.context).setTitle("Test 2: Biometric Strong").setSubtitle("Using BIOMETRIC_STRONG (Security: PASS)").setDescription("This only allows Class 3 biometrics").setAllowedAuthenticators(15).setNegativeButton("Cancel", this.context.getMainExecutor(), new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1
            @Override // android.content.DialogInterface.OnClickListener
            public final void onClick(DialogInterface dialogInterface, int i) {
                MastgTest.mastgTest$lambda$1(Ref.ObjectRef.this, latch2, dialogInterface, i);
            }
        }).build();
        Intrinsics.checkNotNullExpressionValue(prompt2, "build(...)");
        this.mainHandler.post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2
            @Override // java.lang.Runnable
            public final void run() {
                MastgTest.mastgTest$lambda$2(prompt2, this, authResult2, results, latch2);
            }
        });
        latch2.await();
        return results.toJson();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$0(BiometricPrompt prompt1, MastgTest this$0, final Ref.ObjectRef authResult1, final DemoResults results, final CountDownLatch latch1) {
        Intrinsics.checkNotNullParameter(prompt1, "$prompt1");
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        Intrinsics.checkNotNullParameter(authResult1, "$authResult1");
        Intrinsics.checkNotNullParameter(results, "$results");
        Intrinsics.checkNotNullParameter(latch1, "$latch1");
        prompt1.authenticate(new CancellationSignal(), this$0.context.getMainExecutor(), new BiometricPrompt.AuthenticationCallback() { // from class: org.owasp.mastestapp.MastgTest$mastgTest$1$1
            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                Intrinsics.checkNotNullParameter(result, "result");
                Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication succeeded");
                authResult1.element = "User can authenticate with DEVICE_CREDENTIAL (PIN/pattern/password)";
                results.add(Status.FAIL, ((Object) authResult1.element) + ". \nšŸ”“ AUTH - Success!\nāš ļø Allows also PIN/Pattern/Password\nāš ļø Uses DEVICE_CREDENTIAL fallback\n");
                latch1.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationFailed() {
                Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication failed");
                authResult1.element = "Authentication attempt failed";
                results.add(Status.FAIL, ((Object) authResult1.element) + ". \nāš ļø AUTH - Failed\nāš ļø Allows PIN/Pattern/Password\nāš ļø Uses DEVICE_CREDENTIAL fallback\n");
                latch1.countDown();
            }

            /* JADX WARN: Type inference failed for: r1v8, types: [T, java.lang.String] */
            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                Intrinsics.checkNotNullParameter(errString, "errString");
                Log.d("MASTG-TEST", "DEVICE_CREDENTIAL authentication error: " + ((Object) errString));
                authResult1.element = "Authentication error: " + ((Object) errString) + " (code: " + errorCode + ")";
                results.add(Status.ERROR, ((Object) authResult1.element) + ". \nāš ļø AUTH - Error\nāš ļø Allows PIN/Pattern/Password\nāš ļø Uses DEVICE_CREDENTIAL fallback\n");
                latch1.countDown();
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$1(Ref.ObjectRef authResult2, CountDownLatch latch2, DialogInterface dialogInterface, int i) {
        Intrinsics.checkNotNullParameter(authResult2, "$authResult2");
        Intrinsics.checkNotNullParameter(latch2, "$latch2");
        Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication cancelled");
        authResult2.element = "User cancelled authentication";
        latch2.countDown();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$2(BiometricPrompt prompt2, MastgTest this$0, final Ref.ObjectRef authResult2, final DemoResults results, final CountDownLatch latch2) {
        Intrinsics.checkNotNullParameter(prompt2, "$prompt2");
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        Intrinsics.checkNotNullParameter(authResult2, "$authResult2");
        Intrinsics.checkNotNullParameter(results, "$results");
        Intrinsics.checkNotNullParameter(latch2, "$latch2");
        prompt2.authenticate(new CancellationSignal(), this$0.context.getMainExecutor(), new BiometricPrompt.AuthenticationCallback() { // from class: org.owasp.mastestapp.MastgTest$mastgTest$2$1
            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                Intrinsics.checkNotNullParameter(result, "result");
                Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication succeeded");
                authResult2.element = "User authenticated with BIOMETRIC_STRONG";
                results.add(Status.PASS, ((Object) authResult2.element) + ". \nšŸ”“ AUTH - Success!\nāœ… Allows only Strong Biometric\n\nThis configuration is secure because it only allows Class 3 biometrics.");
                latch2.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationFailed() {
                Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication failed");
                authResult2.element = "Authentication attempt failed (biometric not recognized)";
                results.add(Status.FAIL, ((Object) authResult2.element) + ". \nāš ļø AUTH - Failed\n");
                latch2.countDown();
            }

            /* JADX WARN: Type inference failed for: r1v8, types: [T, java.lang.String] */
            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                Intrinsics.checkNotNullParameter(errString, "errString");
                Log.d("MASTG-TEST", "BIOMETRIC_STRONG authentication error: " + ((Object) errString));
                authResult2.element = "Authentication error: " + ((Object) errString) + " (code: " + errorCode + ")";
                results.add(Status.ERROR, ((Object) authResult2.element) + ". \nāš ļø AUTH - Error\n");
                latch2.countDown();
            }
        });
    }
}

Steps

Let's run semgrep rules against the sample code.

../../../../rules/mastg-android-biometric-device-credential-fallback.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
rules:
  - id: mastg-android-biometric-device-credential-fallback
    languages:
      - java
    severity: WARNING
    metadata:
      summary: This rule detects BiometricPrompt configurations that allow fallback to device credentials for the android.hardware.biometrics and androidx.biometric class.
    message: "[MASVS-AUTH-3] BiometricPrompt allows fallback to device credentials. For sensitive operations, consider using BIOMETRIC_STRONG only."
    pattern-either:
    # decompiled code contains integer values instead of the constants for biometric authentication:
    #  BIOMETRIC_STRONG = 15 (0x000F)
    #  BIOMETRIC_WEAK = 255 (0x00FF)
    #  DEVICE_CREDENTIAL = 32768 (0x8000)

      # this pattern matches for the usage of setAllowedAuthenticators() in the android.hardware.biometrics class - see https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder#setAllowedAuthenticators(int)
      - patterns:
          - pattern: $BUILDER.setAllowedAuthenticators($VALUE)
          - metavariable-pattern:
              metavariable: $VALUE
              patterns:
                - pattern-not: BiometricManager.Authenticators.BIOMETRIC_STRONG
                - pattern-not: 15

      # this pattern matches for the usage of canAuthenticate() in the androidx Biometric class - see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
      - patterns: 
          - pattern: $BM.canAuthenticate($VALUE)
          - metavariable-pattern:
              metavariable: $VALUE
              patterns:
                - pattern-not: BiometricManager.Authenticators.BIOMETRIC_STRONG
                - pattern-not: 15
      - pattern: |
          $BUILDER.setDeviceCredentialAllowed(true)
run.sh
1
2
3
4
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-biometric-device-credential-fallback.yml ./MastgTest_reversed.java --text | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g" > output.txt

# Using sed to remove the escaped ANSI code for text formatting (specifically for bold text). This happens because semgrep is outputting colored/formatted text even though it's redirected to a file. `NO_COLOR=true` should prevent this, but in case it doesn't, the sed command will clean the output.

Observation

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

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ 2 Code Findings │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

    MastgTest_reversed.java
    āÆā± rules.mastg-android-biometric-device-credential-fallback
          [MASVS-AUTH-3] BiometricPrompt allows fallback to device credentials. For sensitive operations,
          consider using BIOMETRIC_STRONG only.                                                          

           38┆ final BiometricPrompt prompt1 = new BiometricPrompt.Builder(this.context).setTitle("Test 1:
               Device Credential").setSubtitle("Using DEVICE_CREDENTIAL (Security:                        
               FAIL)").setDescription("This allows also PIN/pattern/password                              
               authentication").setAllowedAuthenticators(32783).setDeviceCredentialAllowed(true).build(); 
            ⋮┆----------------------------------------
           38┆ final BiometricPrompt prompt1 = new BiometricPrompt.Builder(this.context).setTitle("Test 1:
               Device Credential").setSubtitle("Using DEVICE_CREDENTIAL (Security:                        
               FAIL)").setDescription("This allows also PIN/pattern/password                              
               authentication").setAllowedAuthenticators(32783).setDeviceCredentialAllowed(true).build(); 

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 instead of the name:
    • BIOMETRIC_STRONG = 15 (0x000F)
    • BIOMETRIC_WEAK = 255 (0x00FF)
    • DEVICE_CREDENTIAL = 32768 (0x8000)
  • Also in line 38: setDeviceCredentialAllowed(true) 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 to enforce biometric-only authentication.