Skip to content

Commit

Permalink
feat: LoginScreen biometrics integration using Keychain
Browse files Browse the repository at this point in the history
Required for #17085
  • Loading branch information
micieslak committed Feb 5, 2025
1 parent 21872d5 commit 6811518
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 145 deletions.
20 changes: 8 additions & 12 deletions storybook/pages/LoginScreenPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ SplitView {

// password signals
signal accountLoginError(string error, bool wrongPassword)

// biometrics signals
signal obtainingPasswordSuccess(string password)
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
}

LoginScreen {
Expand All @@ -72,7 +68,9 @@ SplitView {

biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store

onBiometricsRequested: biometricsPopup.open()

onLoginRequested: (keyUid, method, data) => {
logs.logEvent("onLoginRequested", ["keyUid", "method", "data"], arguments)

Expand All @@ -92,19 +90,17 @@ SplitView {
id: localAccountSettings
readonly property string storeToKeychainValue: ctrlTouchIdUser.checked ? Constants.keychain.storedValue.store : ""
}
onSelectedProfileKeyIdChanged: biometricsPopup.visible = Qt.binding(() => ctrlBiometrics.checked && ctrlTouchIdUser.checked)
}

BiometricsPopup {
id: biometricsPopup
visible: ctrlBiometrics.checked && ctrlTouchIdUser.checked

x: root.Window.width - width
password: ctrlPassword.text
pin: ctrlPin.text
selectedProfileIsKeycard: loginScreen.selectedProfileIsKeycard
onAccountLoginError: (error, wrongPassword) => driver.accountLoginError(error, wrongPassword)
onObtainingPasswordSuccess: (password) => driver.obtainingPasswordSuccess(password)
onObtainingPasswordError: (errorDescription, errorType, wrongFingerprint) => driver.obtainingPasswordError(errorDescription, errorType, wrongFingerprint)

onObtainingPasswordSuccess: {
loginScreen.setBiometricResponse(loginScreen.selectedProfileIsKeycard
? ctrlPin.text : ctrlPassword.text)
}
}

LogsAndControlsPanel {
Expand Down
54 changes: 30 additions & 24 deletions storybook/pages/OnboardingLayoutPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Models 1.0

SplitView {
id: root

orientation: Qt.Vertical

Logs { id: logs }
Expand Down Expand Up @@ -56,6 +57,13 @@ SplitView {
SplitView.fillWidth: true
SplitView.fillHeight: true

readonly property Item currentPage: {
if (stack.currentItem instanceof Loader)
return stack.currentItem.item

return stack.currentItem
}

onboardingStore: OnboardingStore {
id: store

Expand Down Expand Up @@ -144,15 +152,12 @@ SplitView {

// password signals
signal accountLoginError(string error, bool wrongPassword)

// biometrics signals
signal obtainingPasswordSuccess(string password)
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
}

biometricsAvailable: ctrlBiometrics.checked
isBiometricsLogin: localAccountSettings.storeToKeychainValue === Constants.keychain.storedValue.store
onBiometricsRequested: biometricsPopup.open()

onBiometricsRequested: (profileId) => biometricsPopup.open()

onFinished: (flow, data) => {
console.warn("!!! ONBOARDING FINISHED; flow:", flow, "; data:", JSON.stringify(data))
Expand Down Expand Up @@ -197,8 +202,8 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof CreatePasswordPage ||
(onboarding.stack.currentItem instanceof LoginScreen && !onboarding.stack.currentItem.selectedProfileIsKeycard)
visible: onboarding.currentPage instanceof CreatePasswordPage ||
(onboarding.currentPage instanceof LoginScreen && !onboarding.currentPage.selectedProfileIsKeycard)

onClicked: {
const currentItem = onboarding.stack.currentItem
Expand Down Expand Up @@ -234,12 +239,12 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof SeedphrasePage
visible: onboarding.currentPage instanceof SeedphrasePage

onClicked: {
for (let i = 1;; i++) {
const input = StorybookUtils.findChild(
onboarding.stack.currentItem,
onboarding.currentPage,
`enterSeedPhraseInputField${i}`)

if (input === null)
Expand All @@ -255,9 +260,9 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof KeycardEnterPinPage ||
onboarding.stack.currentItem instanceof KeycardCreatePinPage ||
(onboarding.stack.currentItem instanceof LoginScreen && onboarding.stack.currentItem.selectedProfileIsKeycard && store.keycardState === Onboarding.KeycardState.NotEmpty)
visible: onboarding.currentPage instanceof KeycardEnterPinPage ||
onboarding.currentPage instanceof KeycardCreatePinPage ||
(onboarding.currentPage instanceof LoginScreen && onboarding.currentPage.selectedProfileIsKeycard && store.keycardState === Onboarding.KeycardState.NotEmpty)

text: "Copy valid PIN (\"%1\")".arg(mockDriver.pin)
focusPolicy: Qt.NoFocus
Expand All @@ -269,7 +274,7 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof KeycardEnterPukPage
visible: onboarding.currentPage instanceof KeycardEnterPukPage

text: "Copy valid PUK (\"%1\")".arg(mockDriver.puk)
focusPolicy: Qt.NoFocus
Expand All @@ -281,14 +286,14 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof BackupSeedphraseVerify
visible: onboarding.currentPage instanceof BackupSeedphraseVerify

text: "Paste seed phrase verification"
focusPolicy: Qt.NoFocus
onClicked: {
for (let i = 0;; i++) {
const input = StorybookUtils.findChild(
onboarding.stack.currentItem,
onboarding.currentPage,
`seedInput_${i}`)

if (input === null)
Expand All @@ -305,14 +310,14 @@ SplitView {
anchors.right: parent.right
anchors.margins: 10

visible: onboarding.stack.currentItem instanceof BackupSeedphraseAcks
visible: onboarding.currentPage instanceof BackupSeedphraseAcks

text: "Paste seed phrase verification"
focusPolicy: Qt.NoFocus
onClicked: {
for (let i = 1;; i++) {
const checkBox = StorybookUtils.findChild(
onboarding.stack.currentItem,
onboarding.currentPage,
`ack${i}`)

if (checkBox === null)
Expand All @@ -326,14 +331,15 @@ SplitView {

BiometricsPopup {
id: biometricsPopup
visible: onboarding.stack.currentItem instanceof LoginScreen && ctrlBiometrics.checked && ctrlTouchIdUser.checked

x: root.Window.width - width
password: mockDriver.password
pin: mockDriver.pin
selectedProfileIsKeycard: onboarding.stack.currentItem instanceof LoginScreen && onboarding.stack.currentItem.selectedProfileIsKeycard
onAccountLoginError: (error, wrongPassword) => store.accountLoginError(error, wrongPassword)
onObtainingPasswordSuccess: (password) => store.obtainingPasswordSuccess(password)
onObtainingPasswordError: (errorDescription, errorType, wrongFingerprint) => store.obtainingPasswordError(errorDescription, errorType, wrongFingerprint)

onObtainingPasswordSuccess: {
const isKeycard = onboarding.currentPage instanceof LoginScreen
&& onboarding.currentPage.selectedProfileIsKeycard

onboarding.setBiometricResponse(isKeycard ? mockDriver.pin : mockDriver.password)
}
}

Component {
Expand Down
32 changes: 14 additions & 18 deletions storybook/qmlTests/tests/tst_OnboardingLayout.qml
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,8 @@ Item {

// password signals
signal accountLoginError(string error, bool wrongPassword)

// biometrics signals
signal obtainingPasswordSuccess(string password)
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
}

onLoginRequested: (keyUid, method, data) => {
// SIMULATION: emit an error in case of wrong password
if (method === Onboarding.LoginMethod.Password && data.password !== mockDriver.dummyNewPassword) {
Expand Down Expand Up @@ -979,28 +976,28 @@ Item {
tryCompare(passwordInput, "activeFocus", true)
if (data.biometrics) { // biometrics + password
if (data.password === mockDriver.dummyNewPassword) { // expecting correct fingerprint
// simulate the external biometrics signal
controlUnderTest.onboardingStore.obtainingPasswordSuccess(data.password)
// simulate the external biometrics response
controlUnderTest.setBiometricResponse(data.password)

tryCompare(passwordBox, "biometricsSuccessful", true)
tryCompare(passwordBox, "biometricsFailed", false)
tryCompare(passwordBox, "validationError", "")

// this fills the password and submits it, emits the loginRequested() signal below
tryCompare(passwordInput, "text", data.password)
} else { // expecting wrong fingerprint
// simulate the external biometrics signal
controlUnderTest.onboardingStore.obtainingPasswordError("ERROR", Constants.keychain.errorType.keychain, true)
} else { // expecting failed fetching credentials via biometrics
// simulate the external biometrics response
controlUnderTest.setBiometricResponse("", "ERROR", "", true)

tryCompare(passwordBox, "biometricsSuccessful", false)
tryCompare(passwordBox, "biometricsFailed", true)
tryCompare(passwordBox, "validationError", "Fingerprint not recognised. Try entering password instead.")
tryCompare(passwordBox, "validationError", "ERROR")

// 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)
tryCompare(passwordInput, "activeFocus", true)
tryCompare(passwordInput, "text", "")
expectFail(data.tag, "Wrong fingerprint, expected to fail to login")
expectFail(data.tag, "Biometrics failed, expected to fail to login")
}
} else { // manual password
keyClickSequence(data.password)
Expand Down Expand Up @@ -1032,25 +1029,24 @@ Item {

if (data.biometrics) { // biometrics + PIN
if (data.pin === mockDriver.existingPin) { // expecting correct fingerprint
// simulate the external biometrics signal
controlUnderTest.onboardingStore.obtainingPasswordSuccess(data.pin)
// simulate the external biometrics response
controlUnderTest.setBiometricResponse(data.pin)

tryCompare(keycardBox, "biometricsSuccessful", true)
tryCompare(keycardBox, "biometricsFailed", false)

// this fills the password and submits it, emits the loginRequested() signal below
tryCompare(pinInput, "pinInput", data.pin)
} else { // expecting wrong fingerprint
// simulate the external biometrics signal
controlUnderTest.onboardingStore.obtainingPasswordError("Fingerprint not recognized",
Constants.keychain.errorType.keychain, true)
} else { // expecting failed fetching credentials via biometrics
// simulate the external biometrics response
controlUnderTest.setBiometricResponse("", "ERROR", "", true)

tryCompare(keycardBox, "biometricsSuccessful", false)
tryCompare(keycardBox, "biometricsFailed", true)

// this fails and lets the user enter the PIN manually; so just verify we have an error and empty PIN
tryCompare(pinInput, "pinInput", "")
expectFail(data.tag, "Wrong fingerprint, expected to fail to login")
expectFail(data.tag, "Biometrics failed, expected to fail to login")
}
} else { // manual PIN
keyClickSequence(data.pin)
Expand Down
14 changes: 3 additions & 11 deletions storybook/src/Storybook/BiometricsPopup.qml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,9 @@ import utils 1.0
Dialog {
id: root

required property string password
required property string pin
required property string selectedProfileIsKeycard

// password signals
signal accountLoginError(string error, bool wrongPassword)

// biometrics signals
signal obtainingPasswordSuccess(string password)
signal obtainingPasswordError(string errorDescription, string errorType /* Constants.keychain.errorType.* */, bool wrongFingerprint)
signal cancelled()
signal obtainingPasswordSuccess
signal cancelled

function cancel() {
close()
Expand Down Expand Up @@ -72,7 +64,7 @@ Dialog {
text: "Simulate correct fingerprint"
onClicked: {
root.close()
root.obtainingPasswordSuccess(root.selectedProfileIsKeycard ? root.pin : root.password)
root.obtainingPasswordSuccess()
}
}
}
Expand Down
10 changes: 3 additions & 7 deletions ui/StatusQ/include/StatusQ/keychain.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class Keychain : public QObject {
Q_OBJECT

Q_PROPERTY(QString service READ service WRITE setService NOTIFY serviceChanged)
Q_PROPERTY(QString reason READ reason WRITE setReason NOTIFY reasonChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)

public:
Expand All @@ -34,14 +33,11 @@ class Keychain : public QObject {
QString service() const;
void setService(const QString &service);

QString reason() const;
void setReason(const QString& reason);

bool loading() const;

Q_INVOKABLE void requestSaveCredential(const QString &account, const QString &password);
Q_INVOKABLE void requestDeleteCredential(const QString &account);
Q_INVOKABLE void requestGetCredential(const QString &account);
Q_INVOKABLE void requestSaveCredential(const QString &reason, const QString &account, const QString &password);
Q_INVOKABLE void requestDeleteCredential(const QString &reason, const QString &account);
Q_INVOKABLE void requestGetCredential(const QString &reason, const QString &account);
Q_INVOKABLE void cancelActiveRequest();

signals:
Expand Down
16 changes: 13 additions & 3 deletions ui/app/AppLayouts/Onboarding2/OnboardingFlow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ SQUtils.QObject {
required property var tryToSetPinFunction
required property var tryToSetPukFunction

signal biometricsRequested
signal biometricsRequested(string profileId)
signal loginRequested(string keyUid, int method, var data)
signal keycardPinCreated(string pin)
signal enableBiometricsRequested(bool enable)
Expand All @@ -64,6 +64,16 @@ SQUtils.QObject {
root.stackView.push(entryPage)
}

function setBiometricResponse(secret: string, error = "",
detailedError = "",
wrongFingerprint = false) {
if (!loginScreen)
return

loginScreen.setBiometricResponse(secret, error, detailedError,
wrongFingerprint)
}

readonly property LoginScreen loginScreen: d.loginScreen

QtObject {
Expand Down Expand Up @@ -141,9 +151,9 @@ SQUtils.QObject {
loginAccountsModel: root.loginAccountsModel
biometricsAvailable: root.biometricsAvailable
isBiometricsLogin: root.isBiometricsLogin
onBiometricsRequested: root.biometricsRequested()
onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data)

onBiometricsRequested: (profileId) => root.biometricsRequested(profileId)
onLoginRequested: (keyUid, method, data) => root.loginRequested(keyUid, method, data)
onOnboardingCreateProfileFlowRequested: root.stackView.push(createProfilePage)
onOnboardingLoginFlowRequested: root.stackView.push(loginPage)
onLostKeycard: root.stackView.push(keycardLostPage)
Expand Down
Loading

0 comments on commit 6811518

Please sign in to comment.