Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contrib/android/buildozer_qml.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ':'
Expand Down
53 changes: 53 additions & 0 deletions electrum/gui/qml/components/Preferences.qml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,59 @@ 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) {
useBiometrics.checked = false
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: useBiometrics
checked: Biometrics.isEnabled
onToggled: {
if (activeFocus) {
if (checked) {
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 {
Biometrics.disable()
}
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Biometric authentication')
wrapMode: Text.Wrap
}
}

RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
Expand Down
14 changes: 14 additions & 0 deletions electrum/gui/qml/components/WalletDetails.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
SomberNight marked this conversation as resolved.
} 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
Expand Down Expand Up @@ -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
Expand Down
137 changes: 104 additions & 33 deletions electrum/gui/qml/components/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -633,18 +633,63 @@ ApplicationWindow
}
}

property var _opendialog: undefined
property var _pendingBiometricAuth: null
property var _loadingWalletContext: null

Connections {
target: Biometrics
function onUnlockSuccess(password) {
if (_pendingBiometricAuth) {
if (_pendingBiometricAuth.action === 'load_wallet') {
_loadingWalletContext = _pendingBiometricAuth
Daemon.loadWallet(_pendingBiometricAuth.path, password)
_pendingBiometricAuth = null
return
}

var qtobject = _pendingBiometricAuth.qtobject
var method = _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)
}
_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') {
// set loadingWalletContext to disable biometric auth until the OpenWalletDialog is closed
_loadingWalletContext = _pendingBiometricAuth
showOpenWalletDialog(_pendingBiometricAuth.name, _pendingBiometricAuth.path)
} else {
handleManualAuth(_pendingBiometricAuth.qtobject, _pendingBiometricAuth.method, _pendingBiometricAuth.authMessage)
}
_pendingBiometricAuth = null
}
}
}

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
_loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
_opendialog_startup = false
})
_opendialog.open()
Expand All @@ -655,7 +700,16 @@ ApplicationWindow
target: Daemon
function onWalletRequiresPassword(name, path) {
console.log('wallet requires password')
showOpenWalletDialog(name, path)
if (Biometrics.isAvailable && Biometrics.isEnabled && !_loadingWalletContext) {
_pendingBiometricAuth = {
action: 'load_wallet',
name: name,
path: path
}
Biometrics.unlock()
} else {
showOpenWalletDialog(name, path)
}
}
function onWalletOpenError(error) {
console.log('wallet open error')
Expand All @@ -676,6 +730,9 @@ ApplicationWindow
var dialog = loadingWalletDialog.createObject(app, { allowClose: false } )
dialog.open()
}
function onWalletLoaded() {
_loadingWalletContext = null // either biometric auth or manual auth was successful
}
}

Connections {
Expand Down Expand Up @@ -771,43 +828,57 @@ ApplicationWindow
}
}

if (method == 'wallet') {
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()
return
}
} else if (method == 'pin') {
if (Config.pinCode == '') {
} 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,
authMessage: authMessage
})
dialog.accepted.connect(function() {
return
}
}

if (Biometrics.isAvailable && Biometrics.isEnabled) {
_pendingBiometricAuth = { qtobject: qtobject, method: method, authMessage: authMessage }
Biometrics.unlock()
return
}

handleManualAuth(qtobject, method, authMessage)
}

function handleManualAuth(qtobject, method, authMessage) {
if (method == 'wallet') {
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
dialog.accepted.connect(function() {
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
qtobject.authProceed()
dialog.close()
})
dialog.rejected.connect(function() {
} else {
qtobject.authCancel()
})
dialog.open()
}
}
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
} else if (method == 'pin') {
var dialog = app.pinDialog.createObject(app, {
Comment thread
ecdsa marked this conversation as resolved.
Outdated
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()
Expand Down
Loading