android: implement biometric authentication

Allows to unlock the android app with the android biometric api (e.g.
fingerprint). Can be enabled in the settings.
This commit is contained in:
user
2025-11-30 20:04:32 +01:00
committed by f321x
parent 2529911df9
commit 5dd3dda238
10 changed files with 558 additions and 36 deletions
@@ -0,0 +1,168 @@
package org.electrum.biometry;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.content.Intent;
import android.hardware.biometrics.BiometricPrompt;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import android.widget.Toast;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.util.concurrent.Executor;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import org.electrum.electrum.res.R;
public class BiometricActivity extends Activity {
private static final String TAG = "BiometricActivity";
private static final String KEY_NAME = "electrum_biometric_key";
private static final int RESULT_SETUP_FAILED = 101;
private static final int RESULT_POPUP_CANCELLED = 102;
private CancellationSignal cancellationSignal;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Log.e(TAG, "Biometrics not supported on this Android version (requires API 29+)");
setResult(RESULT_CANCELED);
finish();
return;
}
handleIntent();
}
private void handleIntent() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;
Intent intent = getIntent();
String action = intent.getStringExtra("action");
Executor executor = getMainExecutor();
BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this)
.setTitle("Electrum Wallet")
.setSubtitle("Confirm your identity")
.setNegativeButton("Cancel", executor, (dialog, which) -> {
Log.d(TAG, "Authentication cancelled");
setResult(RESULT_POPUP_CANCELLED);
finish();
})
.build();
cancellationSignal = new CancellationSignal();
BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
Log.e(TAG, "Authentication error: " + errString);
setResult(RESULT_CANCELED);
finish();
}
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
Log.d(TAG, "Authentication succeeded!");
handleAuthenticationSuccess(result);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Log.d(TAG, "Authentication failed");
}
};
try {
if ("ENCRYPT".equals(action)) {
Cipher cipher = getCipher();
SecretKey secretKey = genSecretKey();
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(cipher), cancellationSignal, executor, callback);
} else if ("DECRYPT".equals(action)) {
String ivStr = intent.getStringExtra("iv");
byte[] iv = Base64.decode(ivStr, Base64.NO_WRAP);
Cipher cipher = getCipher();
SecretKey secretKey = getSecretKey();
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
biometricPrompt.authenticate(new BiometricPrompt.CryptoObject(cipher), cancellationSignal, executor, callback);
} else {
finish();
}
} catch (Exception e) {
Log.e(TAG, "Setup error", e);
Toast.makeText(this, "Biometric setup failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
setResult(RESULT_SETUP_FAILED);
finish();
}
}
private void handleAuthenticationSuccess(BiometricPrompt.AuthenticationResult result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;
try {
BiometricPrompt.CryptoObject cryptoObject = result.getCryptoObject();
Cipher cipher = cryptoObject.getCipher();
Intent intent = getIntent();
String action = intent.getStringExtra("action");
Intent resultIntent = new Intent();
if ("ENCRYPT".equals(action)) {
String data = intent.getStringExtra("data"); // wrap_key string to encrypt
byte[] encrypted = cipher.doFinal(data.getBytes(Charset.forName("UTF-8")));
resultIntent.putExtra("data", Base64.encodeToString(encrypted, Base64.NO_WRAP));
resultIntent.putExtra("iv", Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP));
} else {
String dataStr = intent.getStringExtra("data"); // Encrypted blob
byte[] encrypted = Base64.decode(dataStr, Base64.NO_WRAP);
byte[] decrypted = cipher.doFinal(encrypted);
resultIntent.putExtra("data", new String(decrypted, Charset.forName("UTF-8")));
}
setResult(RESULT_OK, resultIntent);
} catch (Exception e) {
Log.e(TAG, "Crypto error", e);
setResult(RESULT_CANCELED);
}
finish();
}
private SecretKey getSecretKey() throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
return (SecretKey) keyStore.getKey(KEY_NAME, null);
}
private SecretKey genSecretKey() throws Exception {
// https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder?hl=en
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true);
keyGenerator.init(builder.build());
keyGenerator.generateKey();
return getSecretKey();
}
private Cipher getCipher() throws Exception {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
}
}
@@ -0,0 +1,20 @@
package org.electrum.biometry;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class BiometricHelper {
public static boolean isAvailable(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // API 30+
BiometricManager biometricManager = context.getSystemService(BiometricManager.class);
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS;
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { // API 29
BiometricManager biometricManager = context.getSystemService(BiometricManager.class);
return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS;
}
return false;
}
}