Merge pull request #10340 from f321x/fingerprint

android: implement biometric authentication (fingerprint)
This commit is contained in:
ghost43
2026-01-19 15:22:17 +00:00
committed by GitHub
14 changed files with 645 additions and 225 deletions
@@ -0,0 +1,175 @@
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.BiometricManager;
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.R) {
Log.e(TAG, "Biometrics not supported on this Android version (requires API 30+)");
setResult(RESULT_CANCELED);
finish();
return;
}
handleIntent();
}
private void handleIntent() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;
Intent intent = getIntent();
String action = intent.getStringExtra("action");
String authMessage = intent.getStringExtra("auth_message");
Executor executor = getMainExecutor();
BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this)
.setTitle("Electrum Wallet")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setSubtitle(authMessage)
.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: " + errorCode + " " + errString);
if (
errorCode == BiometricPrompt.BIOMETRIC_ERROR_CANCELED ||
errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT
) {
setResult(RESULT_POPUP_CANCELLED);
} else {
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)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL);
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,17 @@
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.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
}
return false;
}
}