Skip to content

MASTG-DEMO-0092: Uses of BiometricPrompt without Explicit User Confirmation with semgrep

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

Sample

This sample demonstrates the use of setConfirmationRequired(). It shows both, insecure configurations that allow authentication without explicit user action and secure configurations that require explicit confirmation.

When setConfirmationRequired(false) is used, passive biometrics (like face recognition) can authenticate the user as soon as the device detects their biometric data, without requiring them to tap a confirmation button.

  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-no-confirmation-required.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
rules:
  - id: mastg-android-biometric-no-confirmation-required
    languages:
      - java
    severity: WARNING
    metadata:
      summary: This rule detects BiometricPrompt configurations that disable explicit user confirmation.
    message: "[MASVS-AUTH-3] BiometricPrompt allows authentication without explicit user confirmation. For sensitive operations, consider using setConfirmationRequired(true)."
    pattern: |
      $BUILDER.setConfirmationRequired(false)
run.sh
1
2
3
4
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-biometric-no-confirmation-required.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 uses of setConfirmationRequired().

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ 1 Code Finding │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

    ../MASTG-DEMO-0090/MastgTest_reversed.java
    āÆā± rules.mastg-android-biometric-no-confirmation-required
          [MASVS-AUTH-3] BiometricPrompt allows authentication without explicit user confirmation. For
          sensitive operations, consider using setConfirmationRequired(true).                         

           82┆ 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();       

Evaluation

The test fails because the output shows one reference to a biometric authentication that disables explicitly user confirmation:

  • Line 82: setConfirmationRequired(false) is called, which allows the authentication to succeed implicitly without the user actively confirming the action.