qml: remove pin code authentication
Completely removes the pin code authentication from qml. The config option in the wallet preferences has been renamed to "Payment authentication" and now either asks for the Android system authentication (Biometric or system pin/password) if enabled or will ask for the wallet password as fallback.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,62 +157,6 @@ Pane {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
Switch {
|
||||
id: usePin
|
||||
checked: Config.pinCode
|
||||
onCheckedChanged: {
|
||||
if (activeFocus) {
|
||||
console.log('PIN active ' + checked)
|
||||
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()
|
||||
} else {
|
||||
focus = false
|
||||
Config.pinCode = ''
|
||||
// re-add binding, pincode still set if auth failed
|
||||
checked = Qt.binding(function () { return Config.pinCode })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr('PIN protect payments')
|
||||
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
|
||||
@@ -223,7 +167,6 @@ Pane {
|
||||
Connections {
|
||||
target: Biometrics
|
||||
function onEnablingFailed(error) {
|
||||
useBiometrics.checked = false
|
||||
if (error === 'CANCELLED') {
|
||||
return // don't show error popup
|
||||
}
|
||||
@@ -237,8 +180,9 @@ Pane {
|
||||
Switch {
|
||||
id: useBiometrics
|
||||
checked: Biometrics.isEnabled
|
||||
onToggled: {
|
||||
onCheckedChanged: {
|
||||
if (activeFocus) {
|
||||
useBiometrics.focus = false
|
||||
if (checked) {
|
||||
if (Daemon.singlePasswordEnabled) {
|
||||
Biometrics.enable(Daemon.singlePassword)
|
||||
@@ -254,7 +198,7 @@ Pane {
|
||||
err.open()
|
||||
}
|
||||
} else {
|
||||
Biometrics.disable()
|
||||
Biometrics.disableProtected()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +210,33 @@ Pane {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
@@ -515,11 +486,6 @@ Pane {
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pinSetup
|
||||
Pin {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
language.currentIndex = language.indexOfValue(Config.language)
|
||||
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)
|
||||
|
||||
@@ -577,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
|
||||
@@ -639,42 +631,49 @@ ApplicationWindow
|
||||
Connections {
|
||||
target: Biometrics
|
||||
function onUnlockSuccess(password) {
|
||||
if (_pendingBiometricAuth) {
|
||||
if (_pendingBiometricAuth.action === 'load_wallet') {
|
||||
_loadingWalletContext = _pendingBiometricAuth
|
||||
Daemon.loadWallet(_pendingBiometricAuth.path, password)
|
||||
_pendingBiometricAuth = null
|
||||
if (app._pendingBiometricAuth) {
|
||||
if (app._pendingBiometricAuth.action === 'load_wallet') {
|
||||
app._loadingWalletContext = _pendingBiometricAuth
|
||||
Daemon.loadWallet(app._pendingBiometricAuth.path, password)
|
||||
app._pendingBiometricAuth = null
|
||||
return
|
||||
}
|
||||
|
||||
var qtobject = _pendingBiometricAuth.qtobject
|
||||
var method = _pendingBiometricAuth.method
|
||||
let qtobject = app._pendingBiometricAuth.qtobject
|
||||
let method = app._pendingBiometricAuth.method
|
||||
|
||||
if (Daemon.currentWallet.verifyPassword(password)) {
|
||||
qtobject.authProceed()
|
||||
} else {
|
||||
console.log("Biometric password invalid falling back to manual input")
|
||||
handleManualAuth(qtobject, method, _pendingBiometricAuth.authMessage)
|
||||
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)
|
||||
}
|
||||
_pendingBiometricAuth = null
|
||||
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 (_pendingBiometricAuth) {
|
||||
// try manual auth
|
||||
if (_pendingBiometricAuth.action === 'load_wallet') {
|
||||
if (app._pendingBiometricAuth) {
|
||||
if (app._pendingBiometricAuth.action === 'load_wallet') {
|
||||
// set loadingWalletContext to disable biometric auth until the OpenWalletDialog is closed
|
||||
_loadingWalletContext = _pendingBiometricAuth
|
||||
showOpenWalletDialog(_pendingBiometricAuth.name, _pendingBiometricAuth.path)
|
||||
app._loadingWalletContext = app._pendingBiometricAuth
|
||||
showOpenWalletDialog(app._pendingBiometricAuth.name, app._pendingBiometricAuth.path)
|
||||
} else {
|
||||
handleManualAuth(_pendingBiometricAuth.qtobject, _pendingBiometricAuth.method, _pendingBiometricAuth.authMessage)
|
||||
console.log('biometric auth failed, not falling back to passwordDialog')
|
||||
app._pendingBiometricAuth.qtobject.authCancel() // no fallback to password dialog
|
||||
}
|
||||
_pendingBiometricAuth = null
|
||||
app._pendingBiometricAuth = null
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthRequired(method, authMessage) {
|
||||
handleAuthRequired(Biometrics, method, authMessage)
|
||||
}
|
||||
}
|
||||
|
||||
property var _opendialog: null
|
||||
@@ -689,7 +688,7 @@ ApplicationWindow
|
||||
})
|
||||
_opendialog.closed.connect(function() {
|
||||
_opendialog = null
|
||||
_loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
|
||||
app._loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
|
||||
_opendialog_startup = false
|
||||
})
|
||||
_opendialog.open()
|
||||
@@ -700,8 +699,8 @@ ApplicationWindow
|
||||
target: Daemon
|
||||
function onWalletRequiresPassword(name, path) {
|
||||
console.log('wallet requires password')
|
||||
if (Biometrics.isAvailable && Biometrics.isEnabled && !_loadingWalletContext) {
|
||||
_pendingBiometricAuth = {
|
||||
if (Biometrics.isAvailable && Biometrics.isEnabled && !app._loadingWalletContext) {
|
||||
app._pendingBiometricAuth = {
|
||||
action: 'load_wallet',
|
||||
name: name,
|
||||
path: path
|
||||
@@ -731,7 +730,7 @@ ApplicationWindow
|
||||
dialog.open()
|
||||
}
|
||||
function onWalletLoaded() {
|
||||
_loadingWalletContext = null // either biometric auth or manual auth was successful
|
||||
app._loadingWalletContext = null // either biometric auth or manual auth was successful
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,42 +817,41 @@ 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'
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'wallet') {
|
||||
if (Daemon.currentWallet.verifyPassword('')) {
|
||||
// wallet has no password
|
||||
qtobject.authProceed()
|
||||
return
|
||||
}
|
||||
} else if (method === 'pin') {
|
||||
if (Config.pinCode === '') {
|
||||
// no PIN configured
|
||||
handleAuthConfirmationOnly(qtobject, authMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (Biometrics.isAvailable && Biometrics.isEnabled) {
|
||||
_pendingBiometricAuth = { qtobject: qtobject, method: method, authMessage: authMessage }
|
||||
Biometrics.unlock()
|
||||
return
|
||||
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
|
||||
}
|
||||
Biometrics.unlock(authMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleManualAuth(qtobject, method, authMessage)
|
||||
}
|
||||
|
||||
function handleManualAuth(qtobject, method, authMessage) {
|
||||
if (method == 'wallet') {
|
||||
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
|
||||
// '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()
|
||||
@@ -865,20 +863,6 @@ ApplicationWindow
|
||||
qtobject.authCancel()
|
||||
})
|
||||
dialog.open()
|
||||
} else if (method == 'pin') {
|
||||
var dialog = app.pinDialog.createObject(app, {
|
||||
mode: 'check',
|
||||
pincode: Config.pinCode,
|
||||
authMessage: authMessage
|
||||
})
|
||||
dialog.accepted.connect(function() {
|
||||
qtobject.authProceed()
|
||||
dialog.close()
|
||||
})
|
||||
dialog.rejected.connect(function() {
|
||||
qtobject.authCancel()
|
||||
})
|
||||
dialog.open()
|
||||
} else {
|
||||
console.log('unknown auth method ' + method)
|
||||
qtobject.authCancel()
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -34,8 +35,8 @@ public class BiometricActivity extends Activity {
|
||||
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+)");
|
||||
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;
|
||||
@@ -45,20 +46,17 @@ public class BiometricActivity extends Activity {
|
||||
}
|
||||
|
||||
private void handleIntent() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;
|
||||
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")
|
||||
.setSubtitle("Confirm your identity")
|
||||
.setNegativeButton("Cancel", executor, (dialog, which) -> {
|
||||
Log.d(TAG, "Authentication cancelled");
|
||||
setResult(RESULT_POPUP_CANCELLED);
|
||||
finish();
|
||||
})
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
|
||||
.setSubtitle(authMessage)
|
||||
.build();
|
||||
|
||||
cancellationSignal = new CancellationSignal();
|
||||
@@ -67,8 +65,17 @@ public class BiometricActivity extends Activity {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
Log.e(TAG, "Authentication error: " + errString);
|
||||
setResult(RESULT_CANCELED);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -152,7 +159,7 @@ public class BiometricActivity extends Activity {
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setInvalidatedByBiometricEnrollment(true);
|
||||
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL);
|
||||
|
||||
keyGenerator.init(builder.build());
|
||||
keyGenerator.generateKey();
|
||||
|
||||
@@ -10,10 +10,7 @@ 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 biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ 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 PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
from .auth import auth_protect, AuthMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.simple_config import SimpleConfig
|
||||
@@ -37,7 +40,7 @@ class BiometricAction(str, Enum):
|
||||
DECRYPT = "DECRYPT"
|
||||
|
||||
|
||||
class QEBiometrics(QObject):
|
||||
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
|
||||
@@ -94,22 +97,40 @@ class QEBiometrics(QObject):
|
||||
_logger.info("Android biometric authentication disabled")
|
||||
|
||||
@pyqtSlot()
|
||||
def unlock(self):
|
||||
@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)
|
||||
self._start_activity(BiometricAction.DECRYPT, data=encrypted_wrap_key, auth_message=auth_message)
|
||||
|
||||
def _start_activity(self, action: BiometricAction, data: str):
|
||||
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:
|
||||
@@ -178,4 +199,3 @@ class QEBiometrics(QObject):
|
||||
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)
|
||||
|
||||
@@ -388,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()
|
||||
|
||||
@@ -853,6 +853,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(
|
||||
@@ -924,7 +925,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