Skip to content

Commit ece52b0

Browse files
authored
Merge pull request #10340 from f321x/fingerprint
android: implement biometric authentication (fingerprint)
2 parents 2124881 + 47efb8b commit ece52b0

14 files changed

Lines changed: 645 additions & 225 deletions

File tree

contrib/android/buildozer_qml.spec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fullscreen = False
101101
#
102102

103103
# (list) Permissions
104-
android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS
104+
android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS, USE_BIOMETRIC
105105

106106
# (int) Android API to use (compileSdkVersion)
107107
# note: when changing, Dockerfile also needs to be changed to install corresponding build tools
@@ -171,7 +171,7 @@ android.gradle_dependencies =
171171
com.android.support:support-compat:28.0.0,
172172
org.jetbrains.kotlin:kotlin-stdlib:1.8.22
173173

174-
android.add_activities = org.electrum.qr.SimpleScannerActivity
174+
android.add_activities = org.electrum.qr.SimpleScannerActivity, org.electrum.biometry.BiometricActivity
175175

176176
# (list) Put these files or directories in the apk res directory.
177177
# The option may be used in three ways, the value may contain one or zero ':'

electrum/gui/qml/auth.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@
55
from electrum.logging import get_logger
66

77

8-
def auth_protect(func=None, reject=None, method='pin', message=''):
8+
def auth_protect(func=None, reject=None, method='payment_auth', message=''):
9+
"""
10+
Supported methods:
11+
* payment_auth: If the user has enabled the 'Payment authentication' config
12+
they need to authenticate to continue. If biometrics are enabled they
13+
can authenticate using the Android system dialog, else they will see the
14+
wallet password dialog.
15+
If the option is disabled they will have to confirm a dialog.
16+
* wallet: Same as payment_auth, but not dependent on user configuration,
17+
always requires authentication.
18+
* wallet_password_only: No biometric/system authentication, user has to enter wallet password.
19+
"""
920
if func is None:
1021
return partial(auth_protect, reject=reject, method=method, message=message)
1122

electrum/gui/qml/components/Pin.qml

Lines changed: 0 additions & 114 deletions
This file was deleted.

electrum/gui/qml/components/Preferences.qml

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -158,59 +158,83 @@ Pane {
158158
}
159159

160160
RowLayout {
161+
Layout.columnSpan: 2
161162
Layout.fillWidth: true
162163
spacing: 0
164+
// isAvailable checks phone support and if a fingerprint is enrolled on the system
165+
enabled: Biometrics.isAvailable && Daemon.currentWallet
166+
167+
Connections {
168+
target: Biometrics
169+
function onEnablingFailed(error) {
170+
if (error === 'CANCELLED') {
171+
return // don't show error popup
172+
}
173+
var err = app.messageDialog.createObject(app, {
174+
text: qsTr('Failed to enable biometric authentication: ') + error
175+
})
176+
err.open()
177+
}
178+
}
179+
163180
Switch {
164-
id: usePin
165-
checked: Config.pinCode
181+
id: useBiometrics
182+
checked: Biometrics.isEnabled
166183
onCheckedChanged: {
167184
if (activeFocus) {
168-
console.log('PIN active ' + checked)
185+
useBiometrics.focus = false
169186
if (checked) {
170-
var dialog = pinSetup.createObject(preferences, {mode: 'enter'})
171-
dialog.accepted.connect(function() {
172-
Config.pinCode = dialog.pincode
173-
dialog.close()
174-
})
175-
dialog.rejected.connect(function() {
176-
checked = false
177-
})
178-
dialog.open()
187+
if (Daemon.singlePasswordEnabled) {
188+
Biometrics.enable(Daemon.singlePassword)
189+
} else {
190+
useBiometrics.checked = false
191+
var err = app.messageDialog.createObject(app, {
192+
title: qsTr('Unavailable'),
193+
text: [
194+
qsTr("Cannot activate biometric authentication because you have wallets with different passwords."),
195+
qsTr("To use biometric authentication you first need to change all wallet passwords to the same password.")
196+
].join("\n")
197+
})
198+
err.open()
199+
}
179200
} else {
180-
focus = false
181-
Config.pinCode = ''
182-
// re-add binding, pincode still set if auth failed
183-
checked = Qt.binding(function () { return Config.pinCode })
201+
Biometrics.disableProtected()
184202
}
185203
}
186-
187204
}
188205
}
189206
Label {
190207
Layout.fillWidth: true
191-
text: qsTr('PIN protect payments')
208+
text: qsTr('Biometric authentication')
192209
wrapMode: Text.Wrap
193210
}
194211
}
195212

196-
Pane {
197-
background: Rectangle { color: Material.dialogColor }
198-
padding: 0
199-
visible: Config.pinCode != ''
200-
FlatButton {
201-
text: qsTr('Modify')
202-
onClicked: {
203-
var dialog = pinSetup.createObject(preferences, {
204-
mode: 'change',
205-
pincode: Config.pinCode
206-
})
207-
dialog.accepted.connect(function() {
208-
Config.pinCode = dialog.pincode
209-
dialog.close()
210-
})
211-
dialog.open()
213+
RowLayout {
214+
Layout.columnSpan: 2
215+
Layout.fillWidth: true
216+
spacing: 0
217+
218+
property bool noWalletPassword: Daemon.currentWallet ? Daemon.currentWallet.verifyPassword('') : true
219+
enabled: Daemon.currentWallet && !noWalletPassword
220+
221+
Switch {
222+
id: paymentAuthentication
223+
// showing the toggle as checked even if the wallet has no password would be misleading
224+
checked: Config.paymentAuthentication && !(Daemon.currentWallet && parent.noWalletPassword)
225+
onCheckedChanged: {
226+
if (activeFocus) {
227+
// will request authentication when checked = false
228+
console.log('paymentAuthentication: ' + checked)
229+
Config.paymentAuthentication = checked;
230+
}
212231
}
213232
}
233+
Label {
234+
Layout.fillWidth: true
235+
text: qsTr('Payment authentication')
236+
wrapMode: Text.Wrap
237+
}
214238
}
215239

216240
RowLayout {
@@ -462,11 +486,6 @@ Pane {
462486
}
463487
}
464488

465-
Component {
466-
id: pinSetup
467-
Pin {}
468-
}
469-
470489
Component.onCompleted: {
471490
language.currentIndex = language.indexOfValue(Config.language)
472491
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)

electrum/gui/qml/components/WalletDetails.qml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,15 @@ Pane {
475475
})
476476
dialog.accepted.connect(function() {
477477
var success = Daemon.setPassword(dialog.password)
478+
if (success && Biometrics.isEnabled) {
479+
if (Biometrics.isAvailable) {
480+
// also update the biometric authentication
481+
Biometrics.enable(dialog.password)
482+
} else {
483+
// disable biometric authentication as it is not available
484+
Biometrics.disable()
485+
}
486+
}
478487
var done_dialog = app.messageDialog.createObject(app, {
479488
title: success ? qsTr('Success') : qsTr('Error'),
480489
iconSource: success
@@ -546,6 +555,11 @@ Pane {
546555
}
547556
var error_msg = qsTr('Password change failed')
548557
}
558+
if (success && Biometrics.isEnabled) {
559+
// unlikely to happen as this means the user somehow moved from
560+
// a unified password to differing passwords
561+
Biometrics.disable()
562+
}
549563
var done_dialog = app.messageDialog.createObject(app, {
550564
title: success ? qsTr('Success') : qsTr('Error'),
551565
iconSource: success
@@ -563,6 +577,25 @@ Pane {
563577
}
564578
}
565579

580+
Connections {
581+
target: Biometrics
582+
function onEnablingFailed(error) {
583+
if (error === 'CANCELLED') {
584+
var biometrics_disabled_dialog = app.messageDialog.createObject(app, {
585+
title: qsTr('Biometric Authentication'),
586+
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
587+
text: qsTr('Biometric authentication disabled. You can enable it again in the settings.')
588+
})
589+
biometrics_disabled_dialog.open()
590+
return
591+
}
592+
var err = app.messageDialog.createObject(app, {
593+
text: qsTr('Failed to update biometric authentication to new password: ') + error
594+
})
595+
err.open()
596+
}
597+
}
598+
566599
Component {
567600
id: importAddressesKeysDialog
568601
ImportAddressesKeysDialog {

0 commit comments

Comments
 (0)