Merge pull request #10340 from f321x/fingerprint
android: implement biometric authentication (fingerprint)
This commit is contained in:
@@ -101,7 +101,7 @@ fullscreen = False
|
||||
#
|
||||
|
||||
# (list) Permissions
|
||||
android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS
|
||||
android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS, USE_BIOMETRIC
|
||||
|
||||
# (int) Android API to use (compileSdkVersion)
|
||||
# note: when changing, Dockerfile also needs to be changed to install corresponding build tools
|
||||
@@ -171,7 +171,7 @@ android.gradle_dependencies =
|
||||
com.android.support:support-compat:28.0.0,
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.8.22
|
||||
|
||||
android.add_activities = org.electrum.qr.SimpleScannerActivity
|
||||
android.add_activities = org.electrum.qr.SimpleScannerActivity, org.electrum.biometry.BiometricActivity
|
||||
|
||||
# (list) Put these files or directories in the apk res directory.
|
||||
# The option may be used in three ways, the value may contain one or zero ':'
|
||||
|
||||
@@ -5,7 +5,18 @@ from PyQt6.QtCore import pyqtSignal, pyqtSlot
|
||||
from electrum.logging import get_logger
|
||||
|
||||
|
||||
def auth_protect(func=None, reject=None, method='pin', message=''):
|
||||
def auth_protect(func=None, reject=None, method='payment_auth', message=''):
|
||||
"""
|
||||
Supported methods:
|
||||
* payment_auth: If the user has enabled the 'Payment authentication' config
|
||||
they need to authenticate to continue. If biometrics are enabled they
|
||||
can authenticate using the Android system dialog, else they will see the
|
||||
wallet password dialog.
|
||||
If the option is disabled they will have to confirm a dialog.
|
||||
* wallet: Same as payment_auth, but not dependent on user configuration,
|
||||
always requires authentication.
|
||||
* wallet_password_only: No biometric/system authentication, user has to enter wallet password.
|
||||
"""
|
||||
if func is None:
|
||||
return partial(auth_protect, reject=reject, method=method, message=message)
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
|
||||
import org.electrum 1.0
|
||||
|
||||
import "controls"
|
||||
|
||||
ElDialog {
|
||||
id: root
|
||||
|
||||
property bool canCancel: true
|
||||
property string mode // [check, enter, change]
|
||||
property string pincode // old one passed in when change, new one passed out
|
||||
property bool checkError: false
|
||||
property string authMessage
|
||||
property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin
|
||||
property string _pin
|
||||
|
||||
title: authMessage ? authMessage : qsTr('PIN')
|
||||
iconSource: Qt.resolvedUrl('../../icons/lock.png')
|
||||
width: parent.width * 3/4
|
||||
z: 1000
|
||||
focus: true
|
||||
closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose
|
||||
allowClose: canCancel
|
||||
needsSystemBarPadding: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: canCancel ? "#aa000000" : "#ff000000"
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (_phase == 0) {
|
||||
if (pin.text == pincode) {
|
||||
pin.text = ''
|
||||
if (mode == 'check')
|
||||
accepted()
|
||||
else
|
||||
_phase = 1
|
||||
} else {
|
||||
pin.text = ''
|
||||
checkError = true
|
||||
}
|
||||
} else if (_phase == 1) {
|
||||
_pin = pin.text
|
||||
pin.text = ''
|
||||
_phase = 2
|
||||
} else if (_phase == 2) {
|
||||
if (_pin == pin.text) {
|
||||
pincode = pin.text
|
||||
accepted()
|
||||
} else {
|
||||
pin.text = ''
|
||||
checkError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: result = Dialog.Accepted
|
||||
onRejected: result = Dialog.Rejected
|
||||
onClosed: {
|
||||
if (!root.result) {
|
||||
root.reject() // make sure we reject the authed fn()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase]
|
||||
font.pixelSize: constants.fontSizeXLarge
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: pin
|
||||
Layout.preferredWidth: fontMetrics.advanceWidth(passwordCharacter) * 6 + pin.leftPadding + pin.rightPadding
|
||||
Layout.preferredHeight: fontMetrics.height + pin.topPadding + pin.bottomPadding
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: constants.fontSizeXXLarge
|
||||
maximumLength: 6
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
|
||||
echoMode: TextInput.Password
|
||||
focus: true
|
||||
onTextChanged: {
|
||||
checkError = false
|
||||
if (text.length == 6) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
opacity: checkError ? 1 : 0
|
||||
text: _phase == 0 ? qsTr('Wrong PIN') : qsTr('PIN doesn\'t match')
|
||||
color: constants.colorError
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: fontMetrics
|
||||
font: pin.font
|
||||
}
|
||||
|
||||
}
|
||||
@@ -158,59 +158,83 @@ Pane {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
// isAvailable checks phone support and if a fingerprint is enrolled on the system
|
||||
enabled: Biometrics.isAvailable && Daemon.currentWallet
|
||||
|
||||
Connections {
|
||||
target: Biometrics
|
||||
function onEnablingFailed(error) {
|
||||
if (error === 'CANCELLED') {
|
||||
return // don't show error popup
|
||||
}
|
||||
var err = app.messageDialog.createObject(app, {
|
||||
text: qsTr('Failed to enable biometric authentication: ') + error
|
||||
})
|
||||
err.open()
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: usePin
|
||||
checked: Config.pinCode
|
||||
id: useBiometrics
|
||||
checked: Biometrics.isEnabled
|
||||
onCheckedChanged: {
|
||||
if (activeFocus) {
|
||||
console.log('PIN active ' + checked)
|
||||
useBiometrics.focus = false
|
||||
if (checked) {
|
||||
var dialog = pinSetup.createObject(preferences, {mode: 'enter'})
|
||||
dialog.accepted.connect(function() {
|
||||
Config.pinCode = dialog.pincode
|
||||
dialog.close()
|
||||
})
|
||||
dialog.rejected.connect(function() {
|
||||
checked = false
|
||||
})
|
||||
dialog.open()
|
||||
if (Daemon.singlePasswordEnabled) {
|
||||
Biometrics.enable(Daemon.singlePassword)
|
||||
} else {
|
||||
useBiometrics.checked = false
|
||||
var err = app.messageDialog.createObject(app, {
|
||||
title: qsTr('Unavailable'),
|
||||
text: [
|
||||
qsTr("Cannot activate biometric authentication because you have wallets with different passwords."),
|
||||
qsTr("To use biometric authentication you first need to change all wallet passwords to the same password.")
|
||||
].join("\n")
|
||||
})
|
||||
err.open()
|
||||
}
|
||||
} else {
|
||||
focus = false
|
||||
Config.pinCode = ''
|
||||
// re-add binding, pincode still set if auth failed
|
||||
checked = Qt.binding(function () { return Config.pinCode })
|
||||
Biometrics.disableProtected()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('PIN protect payments')
|
||||
text: qsTr('Biometric authentication')
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
background: Rectangle { color: Material.dialogColor }
|
||||
padding: 0
|
||||
visible: Config.pinCode != ''
|
||||
FlatButton {
|
||||
text: qsTr('Modify')
|
||||
onClicked: {
|
||||
var dialog = pinSetup.createObject(preferences, {
|
||||
mode: 'change',
|
||||
pincode: Config.pinCode
|
||||
})
|
||||
dialog.accepted.connect(function() {
|
||||
Config.pinCode = dialog.pincode
|
||||
dialog.close()
|
||||
})
|
||||
dialog.open()
|
||||
RowLayout {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
property bool noWalletPassword: Daemon.currentWallet ? Daemon.currentWallet.verifyPassword('') : true
|
||||
enabled: Daemon.currentWallet && !noWalletPassword
|
||||
|
||||
Switch {
|
||||
id: paymentAuthentication
|
||||
// showing the toggle as checked even if the wallet has no password would be misleading
|
||||
checked: Config.paymentAuthentication && !(Daemon.currentWallet && parent.noWalletPassword)
|
||||
onCheckedChanged: {
|
||||
if (activeFocus) {
|
||||
// will request authentication when checked = false
|
||||
console.log('paymentAuthentication: ' + checked)
|
||||
Config.paymentAuthentication = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('Payment authentication')
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -462,11 +486,6 @@ Pane {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pinSetup
|
||||
Pin {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
language.currentIndex = language.indexOfValue(Config.language)
|
||||
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)
|
||||
|
||||
@@ -475,6 +475,15 @@ Pane {
|
||||
})
|
||||
dialog.accepted.connect(function() {
|
||||
var success = Daemon.setPassword(dialog.password)
|
||||
if (success && Biometrics.isEnabled) {
|
||||
if (Biometrics.isAvailable) {
|
||||
// also update the biometric authentication
|
||||
Biometrics.enable(dialog.password)
|
||||
} else {
|
||||
// disable biometric authentication as it is not available
|
||||
Biometrics.disable()
|
||||
}
|
||||
}
|
||||
var done_dialog = app.messageDialog.createObject(app, {
|
||||
title: success ? qsTr('Success') : qsTr('Error'),
|
||||
iconSource: success
|
||||
@@ -546,6 +555,11 @@ Pane {
|
||||
}
|
||||
var error_msg = qsTr('Password change failed')
|
||||
}
|
||||
if (success && Biometrics.isEnabled) {
|
||||
// unlikely to happen as this means the user somehow moved from
|
||||
// a unified password to differing passwords
|
||||
Biometrics.disable()
|
||||
}
|
||||
var done_dialog = app.messageDialog.createObject(app, {
|
||||
title: success ? qsTr('Success') : qsTr('Error'),
|
||||
iconSource: success
|
||||
@@ -563,6 +577,25 @@ Pane {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Biometrics
|
||||
function onEnablingFailed(error) {
|
||||
if (error === 'CANCELLED') {
|
||||
var biometrics_disabled_dialog = app.messageDialog.createObject(app, {
|
||||
title: qsTr('Biometric Authentication'),
|
||||
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
|
||||
text: qsTr('Biometric authentication disabled. You can enable it again in the settings.')
|
||||
})
|
||||
biometrics_disabled_dialog.open()
|
||||
return
|
||||
}
|
||||
var err = app.messageDialog.createObject(app, {
|
||||
text: qsTr('Failed to update biometric authentication to new password: ') + error
|
||||
})
|
||||
err.open()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: importAddressesKeysDialog
|
||||
ImportAddressesKeysDialog {
|
||||
|
||||
@@ -419,14 +419,6 @@ ApplicationWindow
|
||||
}
|
||||
}
|
||||
|
||||
property alias pinDialog: _pinDialog
|
||||
Component {
|
||||
id: _pinDialog
|
||||
Pin {
|
||||
onClosed: destroy()
|
||||
}
|
||||
}
|
||||
|
||||
property alias genericShareDialog: _genericShareDialog
|
||||
Component {
|
||||
id: _genericShareDialog
|
||||
@@ -633,18 +625,70 @@ ApplicationWindow
|
||||
}
|
||||
}
|
||||
|
||||
property var _opendialog: undefined
|
||||
property var _pendingBiometricAuth: null
|
||||
property var _loadingWalletContext: null
|
||||
|
||||
Connections {
|
||||
target: Biometrics
|
||||
function onUnlockSuccess(password) {
|
||||
if (app._pendingBiometricAuth) {
|
||||
if (app._pendingBiometricAuth.action === 'load_wallet') {
|
||||
app._loadingWalletContext = _pendingBiometricAuth
|
||||
Daemon.loadWallet(app._pendingBiometricAuth.path, password)
|
||||
app._pendingBiometricAuth = null
|
||||
return
|
||||
}
|
||||
|
||||
let qtobject = app._pendingBiometricAuth.qtobject
|
||||
let method = app._pendingBiometricAuth.method
|
||||
|
||||
if (Daemon.currentWallet.verifyPassword(password)) {
|
||||
qtobject.authProceed()
|
||||
} else {
|
||||
console.warn("Biometric password invalid falling back to manual input")
|
||||
// this shouldn't really happen so we better disable biometric auth
|
||||
Biometrics.disable()
|
||||
handleManualAuth(qtobject, method, app._pendingBiometricAuth.authMessage)
|
||||
}
|
||||
app._pendingBiometricAuth = null
|
||||
}
|
||||
}
|
||||
|
||||
function onUnlockError(error) {
|
||||
console.log("Biometric auth failed: " + error)
|
||||
// we end up here if QEBiometrics fails to give us the decrypted password. The user might
|
||||
// have cancelled the biometric auth popup or the key got invalidated because a new fingerprint got registered.
|
||||
if (app._pendingBiometricAuth) {
|
||||
if (app._pendingBiometricAuth.action === 'load_wallet') {
|
||||
// set loadingWalletContext to disable biometric auth until the OpenWalletDialog is closed
|
||||
app._loadingWalletContext = app._pendingBiometricAuth
|
||||
showOpenWalletDialog(app._pendingBiometricAuth.name, app._pendingBiometricAuth.path)
|
||||
} else {
|
||||
console.log('biometric auth failed, not falling back to passwordDialog')
|
||||
app._pendingBiometricAuth.qtobject.authCancel() // no fallback to password dialog
|
||||
}
|
||||
app._pendingBiometricAuth = null
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthRequired(method, authMessage) {
|
||||
handleAuthRequired(Biometrics, method, authMessage)
|
||||
}
|
||||
}
|
||||
|
||||
property var _opendialog: null
|
||||
property var _opendialog_startup: true
|
||||
|
||||
function showOpenWalletDialog(name, path) {
|
||||
if (_opendialog == undefined) {
|
||||
if (!_opendialog) {
|
||||
_opendialog = openWalletDialog.createObject(app, {
|
||||
name: name,
|
||||
path: path,
|
||||
isStartup: _opendialog_startup,
|
||||
})
|
||||
_opendialog.closed.connect(function() {
|
||||
_opendialog = undefined
|
||||
_opendialog = null
|
||||
app._loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
|
||||
_opendialog_startup = false
|
||||
})
|
||||
_opendialog.open()
|
||||
@@ -655,7 +699,16 @@ ApplicationWindow
|
||||
target: Daemon
|
||||
function onWalletRequiresPassword(name, path) {
|
||||
console.log('wallet requires password')
|
||||
showOpenWalletDialog(name, path)
|
||||
if (Biometrics.isAvailable && Biometrics.isEnabled && !app._loadingWalletContext) {
|
||||
app._pendingBiometricAuth = {
|
||||
action: 'load_wallet',
|
||||
name: name,
|
||||
path: path
|
||||
}
|
||||
Biometrics.unlock()
|
||||
} else {
|
||||
showOpenWalletDialog(name, path)
|
||||
}
|
||||
}
|
||||
function onWalletOpenError(error) {
|
||||
console.log('wallet open error')
|
||||
@@ -676,6 +729,9 @@ ApplicationWindow
|
||||
var dialog = loadingWalletDialog.createObject(app, { allowClose: false } )
|
||||
dialog.open()
|
||||
}
|
||||
function onWalletLoaded() {
|
||||
app._loadingWalletContext = null // either biometric auth or manual auth was successful
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -761,53 +817,52 @@ ApplicationWindow
|
||||
function handleAuthRequired(qtobject, method, authMessage) {
|
||||
console.log('auth using method ' + method)
|
||||
|
||||
if (method == 'wallet_else_pin') {
|
||||
// if there is a loaded wallet and all wallets use the same password, use that
|
||||
// else delegate to pin auth
|
||||
if (Daemon.currentWallet && Daemon.singlePasswordEnabled) {
|
||||
if (method === 'payment_auth') {
|
||||
if (Config.paymentAuthentication) {
|
||||
// treat like a wallet auth request
|
||||
method = 'wallet'
|
||||
} else {
|
||||
method = 'pin'
|
||||
handleAuthConfirmationOnly(qtobject, authMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (method == 'wallet') {
|
||||
if (Daemon.currentWallet.verifyPassword('')) {
|
||||
// wallet has no password
|
||||
qtobject.authProceed()
|
||||
} else {
|
||||
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
|
||||
dialog.accepted.connect(function() {
|
||||
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
|
||||
qtobject.authProceed()
|
||||
} else {
|
||||
qtobject.authCancel()
|
||||
}
|
||||
})
|
||||
dialog.rejected.connect(function() {
|
||||
qtobject.authCancel()
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
} else if (method == 'pin') {
|
||||
if (Config.pinCode == '') {
|
||||
// no PIN configured
|
||||
handleAuthConfirmationOnly(qtobject, authMessage)
|
||||
} else {
|
||||
var dialog = app.pinDialog.createObject(app, {
|
||||
mode: 'check',
|
||||
pincode: Config.pinCode,
|
||||
if (Daemon.currentWallet.verifyPassword('')) {
|
||||
// wallet has no password
|
||||
qtobject.authProceed()
|
||||
return
|
||||
}
|
||||
|
||||
if (method !== 'wallet_password_only') {
|
||||
if (Biometrics.isAvailable && Biometrics.isEnabled) {
|
||||
app._pendingBiometricAuth = {
|
||||
qtobject: qtobject,
|
||||
method: method,
|
||||
authMessage: authMessage
|
||||
})
|
||||
dialog.accepted.connect(function() {
|
||||
qtobject.authProceed()
|
||||
dialog.close()
|
||||
})
|
||||
dialog.rejected.connect(function() {
|
||||
qtobject.authCancel()
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
Biometrics.unlock(authMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleManualAuth(qtobject, method, authMessage)
|
||||
}
|
||||
|
||||
function handleManualAuth(qtobject, method, authMessage) {
|
||||
// 'payment_auth' should have been converted to 'wallet' at this point
|
||||
if (method === 'wallet' || method === 'wallet_password_only') {
|
||||
var dialog = app.passwordDialog.createObject(app, authMessage ? {'title': authMessage} : {})
|
||||
dialog.accepted.connect(function() {
|
||||
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
|
||||
qtobject.authProceed()
|
||||
} else {
|
||||
qtobject.authCancel()
|
||||
}
|
||||
})
|
||||
dialog.rejected.connect(function() {
|
||||
qtobject.authCancel()
|
||||
})
|
||||
dialog.open()
|
||||
} else {
|
||||
console.log('unknown auth method ' + method)
|
||||
qtobject.authCancel()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ from .qeswaphelper import QESwapHelper
|
||||
from .qewizard import QENewWalletWizard, QEServerConnectWizard, QETermsOfUseWizard
|
||||
from .qemodelfilter import QEFilterProxyModel
|
||||
from .qebip39recovery import QEBip39RecoveryListModel
|
||||
from .qebiometrics import QEBiometrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.simple_config import SimpleConfig
|
||||
@@ -537,6 +538,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
self.daemon = QEDaemon(daemon, self.plugins)
|
||||
self.appController = QEAppController(self, self.plugins)
|
||||
self.maxAmount = QEAmount(is_max=True)
|
||||
self.biometrics = QEBiometrics(config=config, parent=self)
|
||||
self.context.setContextProperty('AppController', self.appController)
|
||||
self.context.setContextProperty('Config', self.config)
|
||||
self.context.setContextProperty('Network', self.network)
|
||||
@@ -544,6 +546,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
self.context.setContextProperty('FixedFont', self.fixedFont)
|
||||
self.context.setContextProperty('MAX', self.maxAmount)
|
||||
self.context.setContextProperty('QRIP', self.qr_ip_h)
|
||||
self.context.setContextProperty('Biometrics', self.biometrics)
|
||||
self.context.setContextProperty('BUILD', {
|
||||
'electrum_version': version.ELECTRUM_VERSION,
|
||||
'protocol_version': f"[{version.PROTOCOL_VERSION_MIN}, {version.PROTOCOL_VERSION_MAX}]",
|
||||
|
||||
201
electrum/gui/qml/qebiometrics.py
Normal file
201
electrum/gui/qml/qebiometrics.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import os
|
||||
import secrets
|
||||
from enum import Enum
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.logging import get_logger
|
||||
from electrum.base_crash_reporter import send_exception_to_crash_reporter
|
||||
from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
|
||||
|
||||
from .auth import auth_protect, AuthMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.simple_config import SimpleConfig
|
||||
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
jBiometricHelper = None
|
||||
jBiometricActivity = None
|
||||
jPythonActivity = None
|
||||
jIntent = None
|
||||
jString = None
|
||||
|
||||
if 'ANDROID_DATA' in os.environ:
|
||||
from jnius import autoclass
|
||||
from android import activity
|
||||
jPythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity
|
||||
jBiometricHelper = autoclass('org.electrum.biometry.BiometricHelper')
|
||||
jBiometricActivity = autoclass('org.electrum.biometry.BiometricActivity')
|
||||
jIntent = autoclass('android.content.Intent')
|
||||
jString = autoclass('java.lang.String')
|
||||
|
||||
|
||||
class BiometricAction(str, Enum):
|
||||
ENCRYPT = "ENCRYPT"
|
||||
DECRYPT = "DECRYPT"
|
||||
|
||||
|
||||
class QEBiometrics(AuthMixin, QObject):
|
||||
REQUEST_CODE_BIOMETRIC_ACTIVITY = 24553 # random 16 bit int
|
||||
RESULT_CODE_SETUP_FAILED = 101 # codes duplicated from BiometricActivity.java
|
||||
RESULT_CODE_POPUP_CANCELLED = 102
|
||||
|
||||
enablingFailed = pyqtSignal(str, arguments=['error'])
|
||||
unlockSuccess = pyqtSignal(str, arguments=['password'])
|
||||
unlockError = pyqtSignal(str, arguments=['error'])
|
||||
|
||||
def __init__(self, *, config: 'SimpleConfig', parent=None):
|
||||
super().__init__(parent)
|
||||
self.config = config
|
||||
self._current_action: Optional[BiometricAction] = None
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def isAvailable(self) -> bool:
|
||||
if 'ANDROID_DATA' not in os.environ:
|
||||
return False
|
||||
try:
|
||||
return jBiometricHelper.isAvailable(jPythonActivity)
|
||||
except Exception as e:
|
||||
send_exception_to_crash_reporter(e)
|
||||
return False
|
||||
|
||||
isEnabledChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, notify=isEnabledChanged)
|
||||
def isEnabled(self) -> bool:
|
||||
return self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION
|
||||
|
||||
@pyqtSlot(str)
|
||||
def enable(self, unified_wallet_password: str):
|
||||
"""
|
||||
We encrypt (`wrap`) the wallet password with a random key 'wrap_key' and encrypt the random key
|
||||
with the AndroidKeyStore.
|
||||
Both the encrypted wrap_key and the encrypted wallet password are stored in the config.
|
||||
The encryption key for the wrap_key is stored in the AndroidKeyStore.
|
||||
This way the wallet password doesn't have to leave the process.
|
||||
"""
|
||||
wrap_key, iv = secrets.token_bytes(32), secrets.token_bytes(16)
|
||||
wrapped_wallet_password = aes_encrypt_with_iv(
|
||||
key=wrap_key,
|
||||
iv=iv,
|
||||
data=unified_wallet_password.encode('utf-8'),
|
||||
)
|
||||
encrypted_password_bundle = f"{iv.hex()}:{wrapped_wallet_password.hex()}"
|
||||
self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = encrypted_password_bundle
|
||||
self._start_activity(BiometricAction.ENCRYPT, data=wrap_key.hex())
|
||||
|
||||
@pyqtSlot()
|
||||
def disable(self):
|
||||
self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = False
|
||||
self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ''
|
||||
self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ''
|
||||
self.isEnabledChanged.emit()
|
||||
_logger.info("Android biometric authentication disabled")
|
||||
|
||||
@pyqtSlot()
|
||||
@auth_protect(method='wallet_password_only', reject='_disable_protected_failed')
|
||||
def disableProtected(self):
|
||||
"""
|
||||
Exists to ensure the user knows the wallet password when manually disabling
|
||||
biometric authentication. If they don't remember the password they can still do a seed
|
||||
backup or transactions if biometrics stay enabled. However, note it is still possible for
|
||||
biometrics to get disabled automatically on invalidation or error, so this cannot
|
||||
fully protect the user from forgetting their wallet password either.
|
||||
"""
|
||||
self.disable()
|
||||
|
||||
def _disable_protected_failed(self):
|
||||
self.isEnabledChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
@pyqtSlot(str)
|
||||
def unlock(self, auth_message: str = None):
|
||||
"""
|
||||
Called when the user needs to authenticate.
|
||||
Makes the AndroidKeyStore decrypt our encrypted wrap key, we then use the decrypted wrap key
|
||||
to decrypt the encrypted wallet password.
|
||||
auth_message is shown in the system auth popup and defaults to 'Confirm your identity'.
|
||||
"""
|
||||
encrypted_wrap_key = self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY
|
||||
assert encrypted_wrap_key, "shouldn't unlock if biometric auth is disabled"
|
||||
self._start_activity(BiometricAction.DECRYPT, data=encrypted_wrap_key, auth_message=auth_message)
|
||||
|
||||
def _start_activity(self, action: BiometricAction, data: str, auth_message: str = None):
|
||||
self._current_action = action
|
||||
|
||||
_logger.debug(f"_start_activity: {action.value}, {len(data)=}")
|
||||
intent = jIntent(jPythonActivity, jBiometricActivity)
|
||||
intent.putExtra(jString("action"), jString(action.value))
|
||||
intent.putExtra(jString("auth_message"), jString(auth_message or _("Confirm your identity")))
|
||||
if action == BiometricAction.ENCRYPT:
|
||||
intent.putExtra(jString("data"), jString(data)) # wrap_key
|
||||
elif action == BiometricAction.DECRYPT:
|
||||
assert ':' in data, f"malformed encrypted_bundle: {data=}"
|
||||
iv, encrypted_wrap_key = data.split(':')
|
||||
intent.putExtra(jString("iv"), jString(iv))
|
||||
intent.putExtra(jString("data"), jString(encrypted_wrap_key))
|
||||
else:
|
||||
raise ValueError(f"unsupported {action=}")
|
||||
|
||||
activity.bind(on_activity_result=self._on_activity_result)
|
||||
jPythonActivity.startActivityForResult(intent, self.REQUEST_CODE_BIOMETRIC_ACTIVITY)
|
||||
|
||||
def _on_activity_result(self, requestCode: int, resultCode: int, intent):
|
||||
if requestCode != self.REQUEST_CODE_BIOMETRIC_ACTIVITY:
|
||||
return
|
||||
|
||||
action = self._current_action
|
||||
self._current_action = None
|
||||
|
||||
try:
|
||||
activity.unbind(on_activity_result=self._on_activity_result)
|
||||
if resultCode == -1: # RESULT_OK
|
||||
data = intent.getStringExtra(jString("data"))
|
||||
if action == BiometricAction.ENCRYPT:
|
||||
iv = intent.getStringExtra(jString("iv"))
|
||||
encrypted_bundle = f"{iv}:{data}"
|
||||
self._on_wrap_key_encrypted(encrypted_bundle=encrypted_bundle)
|
||||
else:
|
||||
self._on_wrap_key_decrypted(wrap_key=data)
|
||||
return
|
||||
except Exception as e: # prevent exc from getting lost
|
||||
send_exception_to_crash_reporter(e)
|
||||
|
||||
# on qml side we act on specific errors, so these error strings shouldn't be changed
|
||||
if resultCode == self.RESULT_CODE_SETUP_FAILED and action == BiometricAction.DECRYPT:
|
||||
# setup failed, we need to delete the biometry data, it cannot be decrypted anymore
|
||||
_logger.debug(f"biometric decryption failed, probably invalidated key")
|
||||
error = 'INVALIDATED'
|
||||
self.disable() # reset
|
||||
elif resultCode == self.RESULT_CODE_POPUP_CANCELLED: # user clicked cancel on auth popup
|
||||
_logger.debug(f"biometric auth cancelled by user")
|
||||
error = 'CANCELLED'
|
||||
else: # some other error
|
||||
_logger.error(f"biometric auth failed: {action=}, {resultCode=}")
|
||||
error = f"{resultCode=}"
|
||||
|
||||
if action == BiometricAction.DECRYPT:
|
||||
self.unlockError.emit(error)
|
||||
else:
|
||||
self.disable() # reset
|
||||
self.enablingFailed.emit(error)
|
||||
|
||||
def _on_wrap_key_decrypted(self, *, wrap_key: str):
|
||||
encrypted_password_bundle = self.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD
|
||||
assert encrypted_password_bundle and ':' in encrypted_password_bundle
|
||||
iv, encrypted_password = encrypted_password_bundle.split(':')
|
||||
decrypted_password = aes_decrypt_with_iv(
|
||||
key=bytes.fromhex(wrap_key),
|
||||
iv=bytes.fromhex(iv),
|
||||
data=bytes.fromhex(encrypted_password),
|
||||
)
|
||||
self.unlockSuccess.emit(decrypted_password.decode('utf-8'))
|
||||
|
||||
def _on_wrap_key_encrypted(self, *, encrypted_bundle: str):
|
||||
self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = encrypted_bundle
|
||||
self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = True
|
||||
self.isEnabledChanged.emit()
|
||||
@@ -153,23 +153,26 @@ class QEConfig(AuthMixin, QObject):
|
||||
self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry
|
||||
self.requestExpiryChanged.emit()
|
||||
|
||||
pinCodeChanged = pyqtSignal()
|
||||
@pyqtProperty(str, notify=pinCodeChanged)
|
||||
def pinCode(self):
|
||||
return self.config.CONFIG_PIN_CODE or ""
|
||||
paymentAuthenticationChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, notify=paymentAuthenticationChanged)
|
||||
def paymentAuthentication(self):
|
||||
return self.config.GUI_QML_PAYMENT_AUTHENTICATION
|
||||
|
||||
@pinCode.setter
|
||||
def pinCode(self, pin_code):
|
||||
if pin_code == '':
|
||||
self.pinCodeRemoveAuth()
|
||||
@paymentAuthentication.setter
|
||||
def paymentAuthentication(self, enabled: bool):
|
||||
if enabled:
|
||||
self.config.GUI_QML_PAYMENT_AUTHENTICATION = True
|
||||
self.paymentAuthenticationChanged.emit()
|
||||
else:
|
||||
self.config.CONFIG_PIN_CODE = pin_code
|
||||
self.pinCodeChanged.emit()
|
||||
self._disable_payment_authentication()
|
||||
|
||||
@auth_protect(method='wallet_else_pin')
|
||||
def pinCodeRemoveAuth(self):
|
||||
self.config.CONFIG_PIN_CODE = ""
|
||||
self.pinCodeChanged.emit()
|
||||
@auth_protect(method='wallet', reject='_payment_auth_reject')
|
||||
def _disable_payment_authentication(self):
|
||||
self.config.GUI_QML_PAYMENT_AUTHENTICATION = False
|
||||
self.paymentAuthenticationChanged.emit()
|
||||
|
||||
def _payment_auth_reject(self):
|
||||
self.paymentAuthenticationChanged.emit()
|
||||
|
||||
useGossipChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, notify=useGossipChanged)
|
||||
|
||||
@@ -232,6 +232,13 @@ class QEDaemon(AuthMixin, QObject):
|
||||
|
||||
if self.daemon.config.WALLET_SHOULD_USE_SINGLE_PASSWORD:
|
||||
self._use_single_password = self.daemon.update_password_for_directory(old_password=local_password, new_password=local_password)
|
||||
if not self._use_single_password and self.daemon.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION:
|
||||
# we need to disable biometric auth if the user creates wallets with different passwords as
|
||||
# we only store one encrypted password which is not associated to a specific wallet
|
||||
self._logger.warning(f"disabling biometric authentication, not in single password mode")
|
||||
self.daemon.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = False
|
||||
self.daemon.config.WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ''
|
||||
self.daemon.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ''
|
||||
self._password = local_password
|
||||
self.singlePasswordChanged.emit()
|
||||
self._logger.info(f'use single password: {self._use_single_password}')
|
||||
@@ -381,7 +388,7 @@ class QEDaemon(AuthMixin, QObject):
|
||||
return f'wallet_{i}'
|
||||
|
||||
@pyqtSlot()
|
||||
@auth_protect(method='wallet')
|
||||
@auth_protect(method='wallet_password_only')
|
||||
def startChangePassword(self):
|
||||
if self._use_single_password:
|
||||
self.requestNewPassword.emit()
|
||||
|
||||
@@ -19,6 +19,8 @@ if 'ANDROID_DATA' in os.environ:
|
||||
|
||||
|
||||
class QEQRScanner(QObject):
|
||||
REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY = 30368 # random 16 bit int
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
foundText = pyqtSignal(str)
|
||||
@@ -54,7 +56,7 @@ class QEQRScanner(QObject):
|
||||
intent.putExtra(jIntent.EXTRA_TEXT, jString(self._hint))
|
||||
|
||||
activity.bind(on_activity_result=self.on_qr_activity_result)
|
||||
jpythonActivity.startActivityForResult(intent, 0)
|
||||
jpythonActivity.startActivityForResult(intent, self.REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY)
|
||||
|
||||
@pyqtSlot()
|
||||
def close(self):
|
||||
@@ -62,6 +64,9 @@ class QEQRScanner(QObject):
|
||||
pass
|
||||
|
||||
def on_qr_activity_result(self, requestCode, resultCode, intent):
|
||||
if requestCode != self.REQUEST_CODE_SIMPLE_SCANNER_ACTIVITY:
|
||||
self._logger.warning(f"got activity result with invalid {requestCode=}")
|
||||
return
|
||||
try:
|
||||
if resultCode == -1: # RESULT_OK:
|
||||
if (contents := intent.getStringExtra(jString("text"))) is not None:
|
||||
|
||||
@@ -679,6 +679,11 @@ class SimpleConfig(Logger):
|
||||
WALLET_SHOULD_USE_SINGLE_PASSWORD = ConfigVar('should_use_single_password', default=False, type_=bool)
|
||||
# TODO: consider removing WALLET_DID_USE_SINGLE_PASSWORD once encrypted wallet file headers are available
|
||||
WALLET_DID_USE_SINGLE_PASSWORD = ConfigVar('did_use_single_password', default=False, type_=bool)
|
||||
WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = ConfigVar('android_use_biometrics', default=False, type_=bool)
|
||||
# this is the wrap key encrypted with a secret stored in AndroidKeyStore
|
||||
WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = ConfigVar('android_biometrics_encrypted_wrap_key', default='', type_=str)
|
||||
# this is the "unified wallet password", encrypted with the wrap key
|
||||
WALLET_ANDROID_BIOMETRIC_AUTH_WRAPPED_WALLET_PASSWORD = ConfigVar('android_biometrics_wrapped_wallet_password', default='', type_=str)
|
||||
# note: 'use_change' and 'multiple_change' are per-wallet settings
|
||||
WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(
|
||||
'send_change_to_lightning', default=False, type_=bool,
|
||||
@@ -849,6 +854,7 @@ Warning: setting this to too low will result in lots of payment failures."""),
|
||||
GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
|
||||
GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
|
||||
GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
|
||||
GUI_QML_PAYMENT_AUTHENTICATION = ConfigVar('qml_payment_authentication', default=False, type_=bool)
|
||||
|
||||
BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
|
||||
BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
|
||||
@@ -920,7 +926,6 @@ Warning: setting this to too low will result in lots of payment failures."""),
|
||||
RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
|
||||
IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)
|
||||
WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)
|
||||
CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str)
|
||||
QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)
|
||||
WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)
|
||||
CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)
|
||||
|
||||
Reference in New Issue
Block a user