Skip to content

Commit

Permalink
Onboarding: querying for biometrics login via Keychain
Browse files Browse the repository at this point in the history
Closes: #17085
  • Loading branch information
micieslak committed Feb 18, 2025
1 parent d9dac33 commit 61b2c40
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 100 deletions.
3 changes: 2 additions & 1 deletion src/app/boot/app_controller.nim
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,8 @@ proc finishAppLoading*(self: AppController) =
self.startupModule = nil

if not self.onboardingModule.isNil:
self.onboardingModule.onAppLoaded()
let account = self.accountsService.getLoggedInAccount()
self.onboardingModule.onAppLoaded(account.keyUid)
self.onboardingModule = nil

self.mainModule.checkAndPerformProfileMigrationIfNeeded()
Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/onboarding/io_interface.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import app/modules/onboarding/post_onboarding/task
method delete*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")

method onAppLoaded*(self: AccessInterface) {.base.} =
method onAppLoaded*(self: AccessInterface, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")

method onNodeLogin*(self: AccessInterface, error: string, account: AccountDto, settings: SettingsDto) {.base.} =
Expand Down
4 changes: 2 additions & 2 deletions src/app/modules/onboarding/module.nim
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ method delete*[T](self: Module[T]) =
self.viewVariant.delete
self.controller.delete

method onAppLoaded*[T](self: Module[T]) =
self.view.appLoaded()
method onAppLoaded*[T](self: Module[T], keyUid: string) =
self.view.appLoaded(keyUid)
singletonInstance.engine.setRootContextProperty("onboardingModule", newQVariant())
self.view.delete
self.view = nil
Expand Down
4 changes: 2 additions & 2 deletions src/app/modules/onboarding/view.nim
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ QtObject:

### QtSignals ###

proc appLoaded*(self: View) {.signal.}
proc appLoaded*(self: View, keyUid: string) {.signal.}
proc accountLoginError*(self: View, error: string, wrongPassword: bool) {.signal.}

### QtProperties ###
Expand Down Expand Up @@ -164,4 +164,4 @@ QtObject:
self.delegate.loginRequested(keyUid, loginFlow, dataJson)

proc startKeycardFactoryReset(self: View) {.slot.} =
self.delegate.startKeycardFactoryReset()
self.delegate.startKeycardFactoryReset()
8 changes: 0 additions & 8 deletions storybook/pages/LoginScreenPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ SplitView {
keycardRemainingPinAttempts: driver.keycardRemainingPinAttempts
keycardRemainingPukAttempts: driver.keycardRemainingPukAttempts

biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: ctrlTouchIdUser.checked

onBiometricsRequested: biometricsPopup.open()
Expand Down Expand Up @@ -133,16 +132,9 @@ SplitView {
placeholderText: "Example password"
selectByMouse: true
}
Switch {
id: ctrlBiometrics
text: "Biometrics available"
checked: true
}
Switch {
id: ctrlTouchIdUser
text: "Touch ID login"
visible: ctrlBiometrics.checked
checked: ctrlBiometrics.checked
}
}

Expand Down
22 changes: 13 additions & 9 deletions storybook/pages/OnboardingLayoutPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,9 @@ SplitView {
signal accountLoginError(string error, bool wrongPassword)
}

biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: ctrlTouchIdUser.checked
keychain: keychain

onBiometricsRequested: (profileId) => biometricsPopup.open()
onDismissBiometricsRequested: biometricsPopup.close()
biometricsAvailable: ctrlBiometrics.checked

onFinished: (flow, data) => {
console.warn("!!! ONBOARDING FINISHED; flow:", flow, "; data:", JSON.stringify(data))
Expand Down Expand Up @@ -337,16 +335,22 @@ SplitView {
}
}

BiometricsPopup {
id: biometricsPopup
KeychainMock {
id: keychain

x: root.Window.width - width
parent: root

onObtainingPasswordSuccess: {
readonly property alias touchIdChecked: ctrlTouchIdUser.checked
onTouchIdCheckedChanged: onboarding.keychainChanged()

function hasCredential(account) {
const isKeycard = onboarding.currentPage instanceof LoginScreen
&& onboarding.currentPage.selectedProfileIsKeycard

onboarding.setBiometricResponse(isKeycard ? mockDriver.pin : mockDriver.password)
keychain.saveCredential(account, isKeycard ? mockDriver.pin : mockDriver.password)

return touchIdChecked ? Keychain.StatusSuccess
: Keychain.StatusNotFound
}
}

Expand Down
21 changes: 15 additions & 6 deletions storybook/qmlTests/tests/tst_OnboardingLayout.qml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ Item {
biometricsAvailable: mockDriver.biometricsAvailable
keycardPinInfoPageDelay: 0

isBiometricsLogin: biometricsAvailable
keychain: Keychain {
function hasCredential(account) {
return mockDriver.biometricsAvailable ? Keychain.StatusSuccess
: Keychain.StatusNotFound
}
}

onboardingStore: OnboardingStore {
readonly property int keycardState: mockDriver.keycardState // enum Onboarding.KeycardState
Expand Down Expand Up @@ -985,7 +990,8 @@ Item {
if (data.biometrics) { // biometrics + password
if (data.password === mockDriver.dummyNewPassword) { // expecting correct fingerprint
// simulate the external biometrics response
controlUnderTest.setBiometricResponse(data.password)
controlUnderTest.keychain.getCredentialRequestCompleted(
Keychain.StatusSuccess, data.password)

tryCompare(passwordBox, "biometricsSuccessful", true)
tryCompare(passwordBox, "biometricsFailed", false)
Expand All @@ -995,11 +1001,12 @@ Item {
tryCompare(passwordInput, "text", data.password)
} else { // expecting failed fetching credentials via biometrics
// simulate the external biometrics response
controlUnderTest.setBiometricResponse("", "ERROR", "", true)
controlUnderTest.keychain.getCredentialRequestCompleted(
Keychain.StatusGenericError, "")

tryCompare(passwordBox, "biometricsSuccessful", false)
tryCompare(passwordBox, "biometricsFailed", true)
tryCompare(passwordBox, "validationError", "ERROR")
tryCompare(passwordBox, "validationError", "Fetching credentials failed.")

// this fails and switches to the password method; so just verify we have an error and can enter the pass manually
tryCompare(passwordInput, "hasError", true)
Expand Down Expand Up @@ -1037,7 +1044,8 @@ Item {
if (data.biometrics) { // biometrics + PIN
if (data.pin === mockDriver.existingPin) { // expecting correct fingerprint
// simulate the external biometrics response
controlUnderTest.setBiometricResponse(data.pin)
controlUnderTest.keychain.getCredentialRequestCompleted(
Keychain.StatusSuccess, data.pin)

tryCompare(keycardBox, "biometricsSuccessful", true)
tryCompare(keycardBox, "biometricsFailed", false)
Expand All @@ -1046,7 +1054,8 @@ Item {
tryCompare(pinInput, "pinInput", data.pin)
} else { // expecting failed fetching credentials via biometrics
// simulate the external biometrics response
controlUnderTest.setBiometricResponse("", "ERROR", "", true)
controlUnderTest.keychain.getCredentialRequestCompleted(
Keychain.StatusGenericError, "")

tryCompare(keycardBox, "biometricsSuccessful", false)
tryCompare(keycardBox, "biometricsFailed", true)
Expand Down
2 changes: 0 additions & 2 deletions ui/StatusQ/src/keychain_other.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ Keychain::Status Keychain::hasCredential(const QString &account) const
{
Q_UNUSED(account);

qWarning() << "Keychain::hasCredential is intended to be called only on MacOS.";

return Keychain::StatusNotSupported;
}

Expand Down
8 changes: 3 additions & 5 deletions ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,18 @@ OnboardingStackView {
required property int restoreKeysExportState
required property int addKeyPairState
required property int syncState
required property var generateMnemonic
required property int remainingPinAttempts
required property int remainingPukAttempts

required property bool isBiometricsLogin // FIXME should come from the loginAccountsModel for each profile separately?

required property bool biometricsAvailable
required property bool displayKeycardPromoBanner
required property bool networkChecksEnabled

property int keycardPinInfoPageDelay: 2000

// functions
required property var generateMnemonic
required property var isBiometricsLogin
required property var passwordStrengthScoreFunction
required property var isSeedPhraseValid
required property var validateConnectionString
Expand Down Expand Up @@ -191,8 +190,7 @@ OnboardingStackView {
keycardRemainingPukAttempts: root.remainingPukAttempts

loginAccountsModel: root.loginAccountsModel
biometricsAvailable: root.biometricsAvailable
isBiometricsLogin: root.isBiometricsLogin
isBiometricsLogin: root.isBiometricsLogin(loginScreen.selectedProfileKeyId)

onBiometricsRequested: (profileId) => root.biometricsRequested(profileId)
onDismissBiometricsRequested: root.dismissBiometricsRequested()
Expand Down
54 changes: 36 additions & 18 deletions ui/app/AppLayouts/Onboarding2/OnboardingLayout.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import AppLayouts.Onboarding2.pages 1.0
import AppLayouts.Onboarding2.stores 1.0
import AppLayouts.Onboarding.enums 1.0

import StatusQ.Core.Utils 0.1 as SQUtils

import utils 1.0

Page {
id: root

required property OnboardingStore onboardingStore
required property Keychain keychain

property bool biometricsAvailable: Qt.platform.os === Constants.mac

property bool isBiometricsLogin // FIXME should come from the loginAccountsModel for each profile separately?

property bool networkChecksEnabled: true
property alias keycardPinInfoPageDelay: onboardingFlow.keycardPinInfoPageDelay

Expand All @@ -32,9 +33,6 @@ Page {
// flow: Onboarding.OnboardingFlow
signal finished(int flow, var data)

signal biometricsRequested(string profileId)
signal dismissBiometricsRequested

// -> "keyUid:string": User ID to login; "method:int": password or keycard (cf Onboarding.LoginMethod.*) enum;
// "data:var": contains "password" or "pin"
signal loginRequested(string keyUid, int method, var data)
Expand All @@ -49,13 +47,6 @@ Page {
d.resetState()
}

function setBiometricResponse(secret: string, error = "",
detailedError = "",
wrongFingerprint = false) {
onboardingFlow.setBiometricResponse(secret, error, detailedError,
wrongFingerprint)
}

QtObject {
id: d

Expand Down Expand Up @@ -127,30 +118,27 @@ Page {
anchors.fill: parent

loginAccountsModel: root.onboardingStore.loginAccountsModel

keycardState: root.onboardingStore.keycardState
pinSettingState: root.onboardingStore.pinSettingState
authorizationState: root.onboardingStore.authorizationState
restoreKeysExportState: root.onboardingStore.restoreKeysExportState
syncState: root.onboardingStore.syncState
addKeyPairState: root.onboardingStore.addKeyPairState

generateMnemonic: root.onboardingStore.generateMnemonic

displayKeycardPromoBanner: !d.settings.keycardPromoShown
isBiometricsLogin: root.isBiometricsLogin

biometricsAvailable: root.biometricsAvailable
networkChecksEnabled: root.networkChecksEnabled

generateMnemonic: root.onboardingStore.generateMnemonic
isBiometricsLogin: (account) => keychain.hasCredential(account) === Keychain.StatusSuccess
passwordStrengthScoreFunction: root.onboardingStore.getPasswordStrengthScore
isSeedPhraseValid: root.onboardingStore.validMnemonic
validateConnectionString: root.onboardingStore.validateLocalPairingConnectionString
tryToSetPukFunction: root.onboardingStore.setPuk
remainingPinAttempts: root.onboardingStore.keycardRemainingPinAttempts
remainingPukAttempts: root.onboardingStore.keycardRemainingPukAttempts

onBiometricsRequested: (profileId) => root.biometricsRequested(profileId)
onDismissBiometricsRequested: root.dismissBiometricsRequested()
onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data)

onSetPinRequested: (pin) => {
Expand All @@ -171,6 +159,21 @@ Page {
onLinkActivated: (link) => Qt.openUrlExternally(link)
onExportKeysRequested: root.onboardingStore.exportRecoverKeys()
onFinished: (flow) => d.finishFlow(flow)

onBiometricsRequested: (profileId) => {
const isKeycardProfile = SQUtils.ModelUtils.getByKey(
onboardingStore.loginAccountsModel, "keyUid",
profileId, "keycardCreatedAccount")

const reason = isKeycardProfile ? qsTr("fetch pin") : qsTr("fetch password")

root.keychain.requestGetCredential(reason, profileId)
}

onDismissBiometricsRequested: {
if (root.keychain.loading)
root.keychain.cancelActiveRequest()
}
}

// needs to be on top of the stack
Expand Down Expand Up @@ -229,5 +232,20 @@ Page {
}
}

Connections {
target: root.keychain

function onGetCredentialRequestCompleted(status, secret) {
if (status === Keychain.StatusSuccess)
onboardingFlow.setBiometricResponse(secret)
else if (status === Keychain.StatusNotFound)
onboardingFlow.setBiometricResponse(
"", qsTr("Credentials not found."))
else if (status !== Keychain.StatusCancelled)
onboardingFlow.setBiometricResponse(
"", qsTr("Fetching credentials failed."))
}
}

Component.onCompleted: restartFlow()
}
Loading

0 comments on commit 61b2c40

Please sign in to comment.