Skip to content

MASTG-DEMO-0090: Uses of BiometricPrompt with Event-Bound Authentication with semgrep

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

Sample

The following sample demonstrates the use of the built-in android.hardware.biometrics.BiometricPrompt framework API (available from API level 28+) across multiple scenarios that highlight both insecure and partially secure patterns.

Test 1: No CryptoObject

This scenario calls BiometricPrompt.authenticate() without a CryptoObject and returns a secret token directly after successful authentication.

  • The sensitive operation is not cryptographically bound to the authentication event.
  • The secret token is released purely based on the callback result.
  • BIOMETRIC_STRONG and DEVICE_CREDENTIAL are allowed, meaning PIN, pattern, or password can satisfy the prompt.

This reflects an insecure pattern where authentication is treated as a UI gate rather than being enforced at the cryptographic or Keystore level.

Test 2a: Encrypt with CryptoObject

This scenario generates an AES key in the Android Keystore and uses BIOMETRIC_STRONG together with a CryptoObject to encrypt the secret token.

  • A Cipher is initialized in ENCRYPT_MODE and wrapped in a BiometricPrompt.CryptoObject.
  • BiometricPrompt.authenticate() is invoked with that CryptoObject.
  • Only after successful biometric authentication is the returned, authenticated cipher used to encrypt the token.
  • The resulting ciphertext and IV are stored for later decryption.

However, the Keystore key is explicitly configured not to require user authentication:

Because authentication is not required at the Keystore level, the key can be initialized and used outside of a biometric flow. The cryptographic operation is tied to the prompt instance, but the key itself is not protected by mandatory user presence.

Test 2b: Decrypt with CryptoObject

This scenario performs the complementary operation, decrypting the previously encrypted token.

  • A new Cipher is initialized in DECRYPT_MODE using the stored IV.
  • The cipher is wrapped in a CryptoObject and passed to BiometricPrompt.authenticate().
  • After successful BIOMETRIC_STRONG authentication, the authenticated cipher returned in the callback is used to decrypt the token.

Like Test 2a, the decryption operation is bound to the authentication event through CryptoObject. However, the same Keystore key is used, and it was generated without requiring user authentication. As a result, neither encryption nor decryption is backed by Keystore enforced user authenticated key usage.

  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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
package org.owasp.mastestapp

import android.content.Context
import android.hardware.biometrics.BiometricManager
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.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.annotation.RequiresApi
import java.security.KeyStore
import java.util.concurrent.CountDownLatch
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

// SUMMARY: This sample demonstrates event-bound biometric authentication where authenticate() is called without CryptoObject, making it vulnerable to bypass.

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())

    // Secret token (simulating an API key or sensitive data)
    private val secretToken = "7xK9mP2qR5tY8wZ3aB6cD0eF"

    // Keystore constants
    private val KEY_NAME = "biometric_secret_key"
    private val ANDROID_KEYSTORE = "AndroidKeyStore"

    // Encrypted token storage
    private var encryptedToken: ByteArray? = null
    private var encryptionIv: ByteArray? = null

    private fun generateSecretKey() {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE
        )
        keyGenerator.init(
            KeyGenParameterSpec.Builder(
                KEY_NAME,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setUserAuthenticationRequired(false)
                .setInvalidatedByBiometricEnrollment(false)
                .setUserAuthenticationParameters(86400, 0)
                .setUserAuthenticationValidityDurationSeconds(86400)
                .build()
        )
        keyGenerator.generateKey()
    }

    private fun getSecretKey(): SecretKey {
        val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
        keyStore.load(null)
        return keyStore.getKey(KEY_NAME, null) as SecretKey
    }

    private fun getCipherForEncryption(): Cipher {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
        return cipher
    }

    private fun getCipherForDecryption(): Cipher {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(128, encryptionIv))
        return cipher
    }

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

        // Test 1: INSECURE - No CryptoObject
        // This demonstrates the insecure pattern: authentication without cryptographic binding.
        // The secret token is returned directly after auth success, without using CryptoObject.
        // Also allows DEVICE_CREDENTIAL fallback (PIN/pattern/password).
        val latch1 = CountDownLatch(1)
        var authResult1: String? = null

        val prompt1 = BiometricPrompt.Builder(context)
            .setTitle("Test 1: No CryptoObject")
            .setSubtitle("Insecure: No cryptographic binding")
            .setDescription("Secret returned directly without CryptoObject protection")
            .setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_STRONG or
                BiometricManager.Authenticators.DEVICE_CREDENTIAL
            )
            .setConfirmationRequired(false)
            .build()

        // FAIL: [MASTG-TEST-0327] BiometricPrompt.authenticate() called without CryptoObject for sensitive operations
        mainHandler.post {
            prompt1.authenticate(
                CancellationSignal(),
                context.mainExecutor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                        Log.d("MASTG-TEST", "Test 1: Auth succeeded (no CryptoObject)")
                        authResult1 = "Authenticated without CryptoObject"
                        // Simply return the secret token without crypto protection
                        results.add(
                            Status.FAIL,
                            "Secret Token: $secretToken\n" +
                                    "\nšŸ”“ AUTH - Success!\n" +
                                    "āš ļø No CryptoObject used - secret token returned directly\n" +
                                    "āš ļø Allows DEVICE_CREDENTIAL fallback\n"
                        )
                        latch1.countDown()
                    }

                    override fun onAuthenticationFailed() {
                        Log.d("MASTG-TEST", "Test 1: Auth failed (biometric not recognized)")
                        authResult1 = "Authentication attempt failed"
                        results.add(
                            Status.FAIL,
                            "$authResult1\n" +
                                    "\nāš ļø AUTH - Failed\n"
                        )
                        latch1.countDown()
                    }

                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                        Log.d("MASTG-TEST", "Test 1: Auth error - $errString")
                        authResult1 = "Authentication error: $errString (code: $errorCode)"
                        results.add(
                            Status.ERROR,
                            "$authResult1\n" +
                                    "\nāš ļø AUTH - Error\n"
                        )
                        latch1.countDown()
                    }
                }
            )
        }

        // Wait for Test 1 to complete
        latch1.await()


        // Test 2: SECURE - BIOMETRIC_STRONG with CryptoObject
        // This demonstrates the secure pattern: cryptographic operations bound to biometric auth.
        // The secret token is encrypted/decrypted using a key that requires BIOMETRIC_STRONG.
        // Each crypto operation (encrypt and decrypt) requires separate biometric authentication.

        // Generate AES key in Android Keystore with setUserAuthenticationRequired(true)
        generateSecretKey()

        // Step 2a: Authenticate with CryptoObject to ENCRYPT the token
        val latchEncrypt = CountDownLatch(1)
        var encryptionSucceeded = false

        val encryptCipher = getCipherForEncryption()
        val encryptCryptoObject = BiometricPrompt.CryptoObject(encryptCipher)

        val promptEncrypt = BiometricPrompt.Builder(context)
            .setTitle("Test 2a: Encrypt with CryptoObject")
            .setSubtitle("Secure: BIOMETRIC_STRONG required")
            .setDescription("Biometric required to encrypt the secret token")
            .setAllowedAuthenticators(BIOMETRIC_STRONG)
            .setNegativeButton("Cancel", context.mainExecutor) { _, _ ->
                Log.d("MASTG-TEST", "Test 2a: Encryption cancelled by user")
                latchEncrypt.countDown()
            }
            .build()

        // PASS: [MASTG-TEST-0327] BiometricPrompt.authenticate() called with CryptoObject for crypto-bound authentication
        mainHandler.post {
            promptEncrypt.authenticate(
                encryptCryptoObject,
                CancellationSignal(),
                context.mainExecutor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                        Log.d("MASTG-TEST", "Test 2a: Biometric auth succeeded, encrypting token")
                        try {
                            val authenticatedCipher = result.cryptoObject?.cipher
                            encryptionIv = authenticatedCipher?.iv
                            encryptedToken = authenticatedCipher?.doFinal(secretToken.toByteArray(Charsets.UTF_8))
                            encryptionSucceeded = true
                            Log.d("MASTG-TEST", "Test 2a: Token encrypted successfully with CryptoObject")
                        } catch (e: Exception) {
                            Log.e("MASTG-TEST", "Test 2a: Encryption failed - ${e.message}", e)
                            results.add(
                                Status.ERROR,
                                "Test 2a: Encryption failed: ${e.message}\n"
                            )
                        }
                        latchEncrypt.countDown()
                    }

                    override fun onAuthenticationFailed() {
                        Log.d("MASTG-TEST", "Test 2a: Biometric not recognized")
                        latchEncrypt.countDown()
                    }

                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                        Log.d("MASTG-TEST", "Test 2a: Auth error - $errString")
                        results.add(
                            Status.ERROR,
                            "Test 2a: Auth error: $errString (code: $errorCode)\n"
                        )
                        latchEncrypt.countDown()
                    }
                }
            )
        }

        latchEncrypt.await()

        // Step 2b: Authenticate with CryptoObject to DECRYPT the token
        if (encryptionSucceeded && encryptedToken != null && encryptionIv != null) {
            val latchDecrypt = CountDownLatch(1)

            val decryptCipher = getCipherForDecryption()
            val decryptCryptoObject = BiometricPrompt.CryptoObject(decryptCipher)

            val promptDecrypt = BiometricPrompt.Builder(context)
                .setTitle("Test 2b: Decrypt with CryptoObject")
                .setSubtitle("Secure: BIOMETRIC_STRONG required")
                .setDescription("Biometric required to decrypt the secret token")
                .setAllowedAuthenticators(BIOMETRIC_STRONG)
                .setNegativeButton("Cancel", context.mainExecutor) { _, _ ->
                    Log.d("MASTG-TEST", "Test 2b: Decryption cancelled by user")
                    latchDecrypt.countDown()
                }
                .build()

            mainHandler.post {
                promptDecrypt.authenticate(
                    decryptCryptoObject,
                    CancellationSignal(),
                    context.mainExecutor,
                    object : BiometricPrompt.AuthenticationCallback() {
                        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                            Log.d("MASTG-TEST", "Test 2b: Biometric auth succeeded, decrypting token")
                            try {
                                val authenticatedCipher = result.cryptoObject?.cipher
                                val decryptedBytes = authenticatedCipher?.doFinal(encryptedToken)
                                val decryptedToken = decryptedBytes?.toString(Charsets.UTF_8)

                                Log.d("MASTG-TEST", "Test 2b: Token decrypted successfully with CryptoObject")
                                results.add(
                                    Status.FAIL,
                                    "Decrypted Secret Token: $decryptedToken\n" +
                                            "\nšŸ”“ AUTH - Success!\n" +
                                            "āœ… Uses CryptoObject for encryption and decryption operations\n" +
                                            "āœ… BIOMETRIC_STRONG required for all crypto operations\n" +
                                            "āš ļø Key created with setUserAuthenticationRequired(false)\n" +
                                            "āš ļø Key created with setInvalidatedByBiometricEnrollment(false)\n" +

                                            "\nCrypto operations are bound to biometric authentication, but it is not implemented according to security best practices."
                                )
                            } catch (e: Exception) {
                                Log.e("MASTG-TEST", "Test 2b: Decryption failed - ${e.message}", e)
                                results.add(
                                    Status.ERROR,
                                    "Test 2b: Decryption failed: ${e.message}\n" +
                                            "\nāš ļø AUTH succeeded but decryption failed\n"
                                )
                            }
                            latchDecrypt.countDown()
                        }

                        override fun onAuthenticationFailed() {
                            Log.d("MASTG-TEST", "Test 2b: Biometric not recognized")
                            results.add(
                                Status.FAIL,
                                "Test 2b: Biometric not recognized\n" +
                                        "\nāš ļø AUTH - Failed\n"
                            )
                            latchDecrypt.countDown()
                        }

                        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                            Log.d("MASTG-TEST", "Test 2b: Auth error - $errString")
                            results.add(
                                Status.ERROR,
                                "Test 2b: Auth error: $errString (code: $errorCode)\n" +
                                        "\nāš ļø AUTH - Error\n"
                            )
                            latchDecrypt.countDown()
                        }
                    }
                )
            }

            latchDecrypt.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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.security.keystore.KeyGenParameterSpec;
import android.util.Log;
import java.security.Key;
import java.security.KeyStore;
import java.util.concurrent.CountDownLatch;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import kotlin.jvm.internal.Ref;
import kotlin.text.Charsets;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u0000D\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\u0002\b\u0003\n\u0002\u0010\u0012\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\b\u0010\u0013\u001a\u00020\u0014H\u0002J\b\u0010\u0015\u001a\u00020\u0016H\u0002J\b\u0010\u0017\u001a\u00020\u0018H\u0002J\b\u0010\u0019\u001a\u00020\u0018H\u0002J\b\u0010\u001a\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\u0000R\u000e\u0010\f\u001a\u00020\rX\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\u000e\u001a\u00020\rX\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\u000f\u001a\u00020\rX\u0082D¢\u0006\u0002\n\u0000R\u0010\u0010\u0010\u001a\u0004\u0018\u00010\u0011X\u0082\u000e¢\u0006\u0002\n\u0000R\u0010\u0010\u0012\u001a\u0004\u0018\u00010\u0011X\u0082\u000e¢\u0006\u0002\n\u0000¨\u0006\u001b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "shouldRunInMainThread", "", "getShouldRunInMainThread", "()Z", "mainHandler", "Landroid/os/Handler;", "secretToken", "", "KEY_NAME", "ANDROID_KEYSTORE", "encryptedToken", "", "encryptionIv", "generateSecretKey", "", "getSecretKey", "Ljavax/crypto/SecretKey;", "getCipherForEncryption", "Ljavax/crypto/Cipher;", "getCipherForDecryption", "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 String ANDROID_KEYSTORE;
    private final String KEY_NAME;
    private final Context context;
    private byte[] encryptedToken;
    private byte[] encryptionIv;
    private final Handler mainHandler;
    private final String secretToken;
    private final boolean shouldRunInMainThread;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
        this.mainHandler = new Handler(Looper.getMainLooper());
        this.secretToken = "7xK9mP2qR5tY8wZ3aB6cD0eF";
        this.KEY_NAME = "biometric_secret_key";
        this.ANDROID_KEYSTORE = "AndroidKeyStore";
    }

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

    private final void generateSecretKey() {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", this.ANDROID_KEYSTORE);
        keyGenerator.init(new KeyGenParameterSpec.Builder(this.KEY_NAME, 3).setBlockModes("GCM").setEncryptionPaddings("NoPadding").setUserAuthenticationRequired(false).setInvalidatedByBiometricEnrollment(false).setUserAuthenticationParameters(86400, 0).setUserAuthenticationValidityDurationSeconds(86400).build());
        keyGenerator.generateKey();
    }

    private final SecretKey getSecretKey() {
        KeyStore keyStore = KeyStore.getInstance(this.ANDROID_KEYSTORE);
        keyStore.load(null);
        Key key = keyStore.getKey(this.KEY_NAME, null);
        Intrinsics.checkNotNull(key, "null cannot be cast to non-null type javax.crypto.SecretKey");
        return (SecretKey) key;
    }

    private final Cipher getCipherForEncryption() {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(1, getSecretKey());
        Intrinsics.checkNotNull(cipher);
        return cipher;
    }

    private final Cipher getCipherForDecryption() {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(2, getSecretKey(), new GCMParameterSpec(128, this.encryptionIv));
        Intrinsics.checkNotNull(cipher);
        return cipher;
    }

    public final String mastgTest() {
        final DemoResults results = new DemoResults("0084");
        final CountDownLatch latch1 = new CountDownLatch(1);
        final Ref.ObjectRef authResult1 = new Ref.ObjectRef();
        final BiometricPrompt prompt1 = new BiometricPrompt.Builder(this.context).setTitle("Test 1: No CryptoObject").setSubtitle("Insecure: No cryptographic binding").setDescription("Secret returned directly without CryptoObject protection").setAllowedAuthenticators(32783).setConfirmationRequired(false).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();
        generateSecretKey();
        final CountDownLatch latchEncrypt = new CountDownLatch(1);
        final Ref.BooleanRef encryptionSucceeded = new Ref.BooleanRef();
        Cipher encryptCipher = getCipherForEncryption();
        final BiometricPrompt.CryptoObject encryptCryptoObject = new BiometricPrompt.CryptoObject(encryptCipher);
        final BiometricPrompt promptEncrypt = new BiometricPrompt.Builder(this.context).setTitle("Test 2a: Encrypt with CryptoObject").setSubtitle("Secure: BIOMETRIC_STRONG required").setDescription("Biometric required to encrypt the secret token").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(latchEncrypt, dialogInterface, i);
            }
        }).build();
        Intrinsics.checkNotNullExpressionValue(promptEncrypt, "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(promptEncrypt, encryptCryptoObject, this, encryptionSucceeded, results, latchEncrypt);
            }
        });
        latchEncrypt.await();
        if (encryptionSucceeded.element && this.encryptedToken != null && this.encryptionIv != null) {
            final CountDownLatch latchDecrypt = new CountDownLatch(1);
            Cipher decryptCipher = getCipherForDecryption();
            final BiometricPrompt.CryptoObject decryptCryptoObject = new BiometricPrompt.CryptoObject(decryptCipher);
            final BiometricPrompt promptDecrypt = new BiometricPrompt.Builder(this.context).setTitle("Test 2b: Decrypt with CryptoObject").setSubtitle("Secure: BIOMETRIC_STRONG required").setDescription("Biometric required to decrypt the secret token").setAllowedAuthenticators(15).setNegativeButton("Cancel", this.context.getMainExecutor(), new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3
                @Override // android.content.DialogInterface.OnClickListener
                public final void onClick(DialogInterface dialogInterface, int i) {
                    MastgTest.mastgTest$lambda$3(latchDecrypt, dialogInterface, i);
                }
            }).build();
            Intrinsics.checkNotNullExpressionValue(promptDecrypt, "build(...)");
            this.mainHandler.post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda4
                @Override // java.lang.Runnable
                public final void run() {
                    MastgTest.mastgTest$lambda$4(promptDecrypt, decryptCryptoObject, this, results, latchDecrypt);
                }
            });
            latchDecrypt.await();
        }
        return results.toJson();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$0(BiometricPrompt prompt1, final 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) {
                String str;
                Intrinsics.checkNotNullParameter(result, "result");
                Log.d("MASTG-TEST", "Test 1: Auth succeeded (no CryptoObject)");
                authResult1.element = "Authenticated without CryptoObject";
                DemoResults demoResults = results;
                Status status = Status.FAIL;
                str = this$0.secretToken;
                demoResults.add(status, "Secret Token: " + str + "\n\nšŸ”“ AUTH - Success!\nāš ļø No CryptoObject used - secret token returned directly\nāš ļø Allows DEVICE_CREDENTIAL fallback\n");
                latch1.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationFailed() {
                Log.d("MASTG-TEST", "Test 1: Auth failed (biometric not recognized)");
                authResult1.element = "Authentication attempt failed";
                results.add(Status.FAIL, ((Object) authResult1.element) + "\n\nāš ļø AUTH - Failed\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", "Test 1: Auth error - " + ((Object) errString));
                authResult1.element = "Authentication error: " + ((Object) errString) + " (code: " + errorCode + ")";
                results.add(Status.ERROR, ((Object) authResult1.element) + "\n\nāš ļø AUTH - Error\n");
                latch1.countDown();
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$1(CountDownLatch latchEncrypt, DialogInterface dialogInterface, int i) {
        Intrinsics.checkNotNullParameter(latchEncrypt, "$latchEncrypt");
        Log.d("MASTG-TEST", "Test 2a: Encryption cancelled by user");
        latchEncrypt.countDown();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$2(BiometricPrompt promptEncrypt, BiometricPrompt.CryptoObject encryptCryptoObject, final MastgTest this$0, final Ref.BooleanRef encryptionSucceeded, final DemoResults results, final CountDownLatch latchEncrypt) {
        Intrinsics.checkNotNullParameter(promptEncrypt, "$promptEncrypt");
        Intrinsics.checkNotNullParameter(encryptCryptoObject, "$encryptCryptoObject");
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        Intrinsics.checkNotNullParameter(encryptionSucceeded, "$encryptionSucceeded");
        Intrinsics.checkNotNullParameter(results, "$results");
        Intrinsics.checkNotNullParameter(latchEncrypt, "$latchEncrypt");
        promptEncrypt.authenticate(encryptCryptoObject, 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) {
                String str;
                Intrinsics.checkNotNullParameter(result, "result");
                Log.d("MASTG-TEST", "Test 2a: Biometric auth succeeded, encrypting token");
                try {
                    BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
                    byte[] bArr = null;
                    Cipher authenticatedCipher = cryptoObject != null ? cryptoObject.getCipher() : null;
                    MastgTest.this.encryptionIv = authenticatedCipher != null ? authenticatedCipher.getIV() : null;
                    MastgTest mastgTest = MastgTest.this;
                    if (authenticatedCipher != null) {
                        str = MastgTest.this.secretToken;
                        byte[] bytes = str.getBytes(Charsets.UTF_8);
                        Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)");
                        bArr = authenticatedCipher.doFinal(bytes);
                    }
                    mastgTest.encryptedToken = bArr;
                    encryptionSucceeded.element = true;
                    Log.d("MASTG-TEST", "Test 2a: Token encrypted successfully with CryptoObject");
                } catch (Exception e) {
                    Log.e("MASTG-TEST", "Test 2a: Encryption failed - " + e.getMessage(), e);
                    results.add(Status.ERROR, "Test 2a: Encryption failed: " + e.getMessage() + "\n");
                }
                latchEncrypt.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationFailed() {
                Log.d("MASTG-TEST", "Test 2a: Biometric not recognized");
                latchEncrypt.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                Intrinsics.checkNotNullParameter(errString, "errString");
                Log.d("MASTG-TEST", "Test 2a: Auth error - " + ((Object) errString));
                results.add(Status.ERROR, "Test 2a: Auth error: " + ((Object) errString) + " (code: " + errorCode + ")\n");
                latchEncrypt.countDown();
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$3(CountDownLatch latchDecrypt, DialogInterface dialogInterface, int i) {
        Intrinsics.checkNotNullParameter(latchDecrypt, "$latchDecrypt");
        Log.d("MASTG-TEST", "Test 2b: Decryption cancelled by user");
        latchDecrypt.countDown();
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void mastgTest$lambda$4(BiometricPrompt promptDecrypt, BiometricPrompt.CryptoObject decryptCryptoObject, final MastgTest this$0, final DemoResults results, final CountDownLatch latchDecrypt) {
        Intrinsics.checkNotNullParameter(promptDecrypt, "$promptDecrypt");
        Intrinsics.checkNotNullParameter(decryptCryptoObject, "$decryptCryptoObject");
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        Intrinsics.checkNotNullParameter(results, "$results");
        Intrinsics.checkNotNullParameter(latchDecrypt, "$latchDecrypt");
        promptDecrypt.authenticate(decryptCryptoObject, new CancellationSignal(), this$0.context.getMainExecutor(), new BiometricPrompt.AuthenticationCallback() { // from class: org.owasp.mastestapp.MastgTest$mastgTest$3$1
            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                byte[] decryptedBytes;
                byte[] bArr;
                Intrinsics.checkNotNullParameter(result, "result");
                Log.d("MASTG-TEST", "Test 2b: Biometric auth succeeded, decrypting token");
                try {
                    BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
                    Cipher authenticatedCipher = cryptoObject != null ? cryptoObject.getCipher() : null;
                    if (authenticatedCipher != null) {
                        bArr = MastgTest.this.encryptedToken;
                        decryptedBytes = authenticatedCipher.doFinal(bArr);
                    } else {
                        decryptedBytes = null;
                    }
                    String decryptedToken = decryptedBytes != null ? new String(decryptedBytes, Charsets.UTF_8) : null;
                    Log.d("MASTG-TEST", "Test 2b: Token decrypted successfully with CryptoObject");
                    results.add(Status.FAIL, "Decrypted Secret Token: " + decryptedToken + "\n\nšŸ”“ AUTH - Success!\nāœ… Uses CryptoObject for encryption and decryption operations\nāœ… BIOMETRIC_STRONG required for all crypto operations\nāš ļø Key created with setUserAuthenticationRequired(false)\nāš ļø Key created with setInvalidatedByBiometricEnrollment(false)\n\nCrypto operations are bound to biometric authentication, but it is not implemented according to security best practices.");
                } catch (Exception e) {
                    Log.e("MASTG-TEST", "Test 2b: Decryption failed - " + e.getMessage(), e);
                    results.add(Status.ERROR, "Test 2b: Decryption failed: " + e.getMessage() + "\n\nāš ļø AUTH succeeded but decryption failed\n");
                }
                latchDecrypt.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationFailed() {
                Log.d("MASTG-TEST", "Test 2b: Biometric not recognized");
                results.add(Status.FAIL, "Test 2b: Biometric not recognized\n\nāš ļø AUTH - Failed\n");
                latchDecrypt.countDown();
            }

            @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                Intrinsics.checkNotNullParameter(errString, "errString");
                Log.d("MASTG-TEST", "Test 2b: Auth error - " + ((Object) errString));
                results.add(Status.ERROR, "Test 2b: Auth error: " + ((Object) errString) + " (code: " + errorCode + ")\n\nāš ļø AUTH - Error\n");
                latchDecrypt.countDown();
            }
        });
    }
}

Steps

Run semgrep rules against the sample code.

../../../../rules/mastg-android-biometric-event-bound.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
rules:
  - id: mastg-android-biometric-event-bound
    languages:
      - java
      - kotlin
    severity: WARNING
    metadata:
      summary: This rule detects uses of BiometricPrompt.authenticate without a CryptoObject (event-bound biometric authentication) and insecure key configurations.
    message: "[MASVS-AUTH-2] BiometricPrompt.authenticate called without a CryptoObject or key not requiring user authentication. Use authenticate(PromptInfo, CryptoObject) with a keystore-backed key configured with setUserAuthenticationRequired(true) for sensitive operations."

    pattern-either:
      # Pattern 1: BiometricPrompt.authenticate() without CryptoObject (event-bound authentication)
      # Used in androidx Biometric library
      - patterns:
          - pattern: $INSTANCE.authenticate($PROMPT_INFO)
          - pattern-not: $ANY.authenticate($PROMPT_INFO, $CRYPTO_OBJECT)
          - pattern-inside: |
              import androidx.biometric.BiometricPrompt;
              ...

      # Pattern 2: android.hardware.biometrics.BiometricPrompt.authenticate() without CryptoObject
      # This is the older Android framework API (deprecated in favor of androidx.biometric)
      # The insecure version has 3 parameters: authenticate(CancellationSignal, Executor, Callback)
      # The secure version has 4 parameters: authenticate(CryptoObject, CancellationSignal, Executor, Callback)
      - patterns:
          - pattern: $INSTANCE.authenticate($CANCEL, $EXECUTOR, $CALLBACK)
          - pattern-not: $ANY.authenticate($CRYPTO_OBJECT, $CANCEL, $EXECUTOR, $CALLBACK)
          - pattern-inside: |
              import android.hardware.biometrics.BiometricPrompt;
              ...

      # Pattern 3a: KeyGenParameterSpec.Builder without setUserAuthenticationRequired(true)
      # Scoped to files using android.hardware.biometrics.BiometricPrompt to avoid false positives on non-biometric keys
      - patterns:
          - pattern: new KeyGenParameterSpec.Builder(...)
          - pattern-not-inside: |
              new KeyGenParameterSpec.Builder(...). ... .setUserAuthenticationRequired(true)
          - pattern-inside: |
              import android.hardware.biometrics.BiometricPrompt;
              ...

      # Pattern 3b: Same check but for androidx.biometric library
      - patterns:
          - pattern: new KeyGenParameterSpec.Builder(...)
          - pattern-not-inside: |
              new KeyGenParameterSpec.Builder(...). ... .setUserAuthenticationRequired(true)
          - pattern-inside: |
              import androidx.biometric.BiometricPrompt;
              ...
run.sh
1
2
3
4
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-biometric-event-bound.yml ../MASTG-DEMO-0090/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 the usage of the BiometricPrompt.authenticate and setUserAuthenticationRequired APIs.

output.txt
 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
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ 2 Code Findings │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

    ../MASTG-DEMO-0090/MastgTest_reversed.java
    āÆā± rules.mastg-android-biometric-event-bound
          [MASVS-AUTH-2] BiometricPrompt.authenticate called without a CryptoObject or key not requiring user
          authentication. Use authenticate(PromptInfo, CryptoObject) with a keystore-backed key configured   
          with setUserAuthenticationRequired(true) for sensitive operations.                                 

           52┆ keyGenerator.init(new KeyGenParameterSpec.Builder(this.KEY_NAME, 3).setBlockModes("GCM").se
               tEncryptionPaddings("NoPadding").setUserAuthenticationRequired(false).setInvalidatedByBiome
               tricEnrollment(false).setUserAuthenticationParameters(86400,                               
               0).setUserAuthenticationValidityDurationSeconds(86400).build());                           
            ⋮┆----------------------------------------
          139┆ prompt1.authenticate(new CancellationSignal(), this$0.context.getMainExecutor(), new
               BiometricPrompt.AuthenticationCallback() { // from class:                           
               org.owasp.mastestapp.MastgTest$mastgTest$1$1                                        
          140┆     @Override // android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
          141┆     public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
          142┆         String str;
          143┆         Intrinsics.checkNotNullParameter(result, "result");
          144┆         Log.d("MASTG-TEST", "Test 1: Auth succeeded (no CryptoObject)");
          145┆         authResult1.element = "Authenticated without CryptoObject";
          146┆         DemoResults demoResults = results;
          147┆         Status status = Status.FAIL;
          148┆         str = this$0.secretToken;
             [hid 22 additional lines, adjust with --max-lines-per-finding] 

Evaluation

The test fails because the output shows both:

  • Line 139: BiometricPrompt.authenticate(PromptInfo) is used without a CryptoObject and
  • Line 52: setUserAuthenticationRequired(false) is set for key generation.

For sensitive operations, the app should use CryptoObject when doing biometric authentication and the key generated should have set setUserAuthenticationRequired(true).