feat(@desktop/keycard): adding wallet account using `Authenticate` flow

Fixes: #7509
This commit is contained in:
Sale Djenic 2022-09-23 15:53:13 +02:00 committed by saledjenic
parent 4c3aca273a
commit 861c585d2b
16 changed files with 211 additions and 89 deletions

View File

@ -240,8 +240,11 @@ proc init*(self: Controller) =
self.authenticateUserFlowRequestedBy.len == 0:
return
self.delegate.onSharedKeycarModuleFlowTerminated(args.lastStepInTheCurrentFlow)
var password = args.password
if password.len == 0 and args.keyUid.len > 0:
password = args.keyUid
let data = SharedKeycarModuleArgs(uniqueIdentifier: self.authenticateUserFlowRequestedBy,
data: args.data,
password: password,
keyUid: args.keyUid,
txR: args.txR,
txS: args.txS,

View File

@ -2,20 +2,29 @@ import io_interface
import ../../../../../app_service/service/wallet_account/service as wallet_account_service
import ../../../../../app_service/service/accounts/service as accounts_service
import ../../../../global/global_singleton
import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
import ../../../../core/eventemitter
const UNIQUE_WALLET_SECTION_ACCOUNTS_MODULE_IDENTIFIER* = "WalletSection-AccountsModule"
type
Controller* = ref object of RootObj
delegate: io_interface.AccessInterface
events: EventEmitter
walletAccountService: wallet_account_service.Service
accountsService: accounts_service.Service
proc newController*(
delegate: io_interface.AccessInterface,
walletAccountService: wallet_account_service.Service
events: EventEmitter,
walletAccountService: wallet_account_service.Service,
accountsService: accounts_service.Service
): Controller =
result = Controller()
result.delegate = delegate
result.events = events
result.walletAccountService = walletAccountService
result.accountsService = accountsService
@ -23,7 +32,11 @@ proc delete*(self: Controller) =
discard
proc init*(self: Controller) =
discard
self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_WALLET_SECTION_ACCOUNTS_MODULE_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.password)
proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] =
return self.walletAccountService.getWalletAccounts()
@ -56,3 +69,18 @@ proc validSeedPhrase*(self: Controller, seedPhrase: string): bool =
let err = self.accountsService.validateMnemonic(seedPhrase)
return err.len == 0
proc loggedInUserUsesBiometricLogin*(self: Controller): bool =
if(not defined(macosx)):
return false
let value = singletonInstance.localAccountSettings.getStoreToKeychainValue()
if (value != LS_VALUE_STORE):
return false
return true
proc getLoggedInAccount*(self: Controller): AccountDto =
return self.accountsService.getLoggedInAccount()
proc authenticateUser*(self: Controller, keyUid = "") =
let data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_WALLET_SECTION_ACCOUNTS_MODULE_IDENTIFIER,
keyUid: keyUid)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)

View File

@ -47,4 +47,16 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method validSeedPhrase*(self: AccessInterface, value: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method authenticateUser*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onUserAuthenticated*(self: AccessInterface, password: string) {.base.} =
raise newException(ValueError, "No implementation available")
method loggedInUserUsesBiometricLogin*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
method isProfileKeyPairMigrated*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -172,3 +172,22 @@ method getDerivedAddressForPrivateKey*(self: Module, privateKey: string) =
method validSeedPhrase*(self: Module, value: string): bool =
return self.controller.validSeedPhrase(value)
method loggedInUserUsesBiometricLogin*(self: Module): bool =
return self.controller.loggedInUserUsesBiometricLogin()
method isProfileKeyPairMigrated*(self: Module): bool =
return self.controller.getLoggedInAccount().keycardPairing.len > 0
method authenticateUser*(self: Module) =
if self.isProfileKeyPairMigrated():
let keyUid = singletonInstance.userProfile.getKeyUid()
self.controller.authenticateUser(keyUid)
else:
self.controller.authenticateUser()
method onUserAuthenticated*(self: Module, password: string) =
if password.len > 0:
self.view.userAuthenticaionSuccess(password)
else:
self.view.userAuthentiactionFail()

View File

@ -277,3 +277,15 @@ QtObject:
proc validSeedPhrase*(self: View, value: string): bool {.slot.} =
return self.delegate.validSeedPhrase(value)
proc userAuthenticaionSuccess*(self: View, password: string) {.signal.}
proc userAuthentiactionFail*(self: View) {.signal.}
proc authenticateUser*(self: View) {.slot.} =
self.delegate.authenticateUser()
proc loggedInUserUsesBiometricLogin*(self: View): bool {.slot.} =
return self.delegate.loggedInUserUsesBiometricLogin()
proc isProfileKeyPairMigrated*(self: View): bool {.slot.} =
return self.delegate.isProfileKeyPairMigrated()

View File

@ -44,6 +44,7 @@ type
tmpKeyUidWhichIsBeingAuthenticating: string
tmpKeyUidWhichIsBeingUnlocking: string
tmpUsePinFromBiometrics: bool
tmpOfferToStoreUpdatedPinToKeychain: bool
tmpKeycardUid: string
proc newController*(delegate: io_interface.AccessInterface,
@ -125,7 +126,7 @@ proc init*(self: Controller) =
if args.uniqueIdentifier != self.uniqueIdentifier:
return
self.connectKeycardReponseSignal()
self.delegate.onUserAuthenticated(args.data)
self.delegate.onUserAuthenticated(args.password)
self.connectionIds.add(handlerId)
proc getKeycardData*(self: Controller): string =
@ -170,6 +171,12 @@ proc setPinMatch*(self: Controller, value: bool) =
proc getPinMatch*(self: Controller): bool =
return self.tmpPinMatch
proc setOfferToStoreUpdatedPinToKeychain*(self: Controller, value: bool) =
self.tmpOfferToStoreUpdatedPinToKeychain = value
proc offerToStoreUpdatedPinToKeychain*(self: Controller): bool =
return self.tmpOfferToStoreUpdatedPinToKeychain
proc setPassword*(self: Controller, value: string) =
self.tmpPassword = value
@ -307,7 +314,7 @@ proc terminateCurrentFlow*(self: Controller, lastStepInTheCurrentFlow: bool) =
var data = SharedKeycarModuleFlowTerminatedArgs(uniqueIdentifier: self.uniqueIdentifier,
lastStepInTheCurrentFlow: lastStepInTheCurrentFlow)
if lastStepInTheCurrentFlow:
data.data = self.tmpPassword
data.password = self.tmpPassword
data.keyUid = flowEvent.keyUid
data.txR = flowEvent.txSignature.r
data.txS = flowEvent.txSignature.s

View File

@ -15,6 +15,7 @@ method executePrimaryCommand*(self: BiometricsPinInvalidState, controller: Contr
method executeSecondaryCommand*(self: BiometricsPinInvalidState, controller: Controller) =
if self.flowType == FlowType.Authentication:
controller.setUsePinFromBiometrics(true)
controller.setOfferToStoreUpdatedPinToKeychain(true)
method getNextSecondaryState*(self: BiometricsPinInvalidState, controller: Controller): State =
if self.flowType == FlowType.Authentication:

View File

@ -98,6 +98,8 @@ method resolveKeycardNextState*(self: EnterPinState, keycardFlowType: string, ke
return createState(StateType.MaxPinRetriesReached, self.flowType, nil)
if keycardFlowType == ResponseTypeValueKeycardFlowResult:
if keycardEvent.error.len == 0:
if controller.offerToStoreUpdatedPinToKeychain():
controller.tryToStoreDataToKeychain(controller.getPin())
controller.terminateCurrentFlow(lastStepInTheCurrentFlow = true)
return nil
if self.flowType == FlowType.DisplayKeycardContent:

View File

@ -8,13 +8,33 @@ const SIGNAL_SHARED_KEYCARD_MODULE_FLOW_TERMINATED* = "sharedKeycarModuleFlowTer
const SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER* = "sharedKeycarModuleAuthenticateUser"
const SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED* = "sharedKeycarModuleUserAuthenticated"
## Authentication in the app is a global thing and may be used from any part of the app. How to achieve that... it's enough just to send
## `SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER` signal with properly set `SharedKeycarModuleAuthenticationArgs` and props there:
## -- `uniqueIdentifier` - some unique string, for the readability usually name of the module which needs authentication,
## -- in case of non keycard user (regular) user that's enough,
## -- in case of keycard user we want to authenticate it with a card that his profile is migrated to, that means apart of `uniqueIdentifier`
## we need to set `keyUid` as well,
## -- in case we want to sign a transaction for a certain wallet's account, then apart of `uniqueIdentifier` and `keyUid`of a key pair that
## account belongs to, we need to set and `bip44Path` and `txHash`
##
## `SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER` will be handled in the `mainModule` (shared keycard popup module will be run) and as a
## result, when authentication gets done `SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED` signal with properly set `SharedKeycarModuleArgs`
## and props there will be emitted:
## -- `uniqueIdentifier` - will be the same as one used for running authentication process
## -- in case of success of a regular user authentication `password` will be sent, otherwise it will be empty
## -- in case of success of a keycard user authentication `keyUid` will be sent, otherwise it will be empty
## -- in case of success of a signing a transaction `keyUid`, `txR` and `txS` will be sent, otherwise it will be empty
##
## TLDR: when you need to authenticate user, from the module where it's needed you have to send `SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER`
## signal to run authentication process and connect to `SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED` signal to get the results of it.
type
SharedKeycarModuleBaseArgs* = ref object of Args
uniqueIdentifier*: string
type
SharedKeycarModuleArgs* = ref object of SharedKeycarModuleBaseArgs
data*: string
password*: string
keyUid*: string
txR*: string
txS*: string

View File

@ -590,8 +590,6 @@ proc verifyAccountPassword*(self: Service, account: string, password: string): b
proc convertToKeycardAccount*(self: Service, keyUid: string, password: string): bool =
try:
self.setKeyStoreDir(keyUid)
var accountDataJson = %* {
"name": self.getLoggedInAccount().name,
"key-uid": keyUid

View File

@ -244,11 +244,11 @@ Item {
This function resets the text input validation and text.
*/
function reset() {
statusBaseInput.text = ""
root.errorMessage = ""
statusBaseInput.valid = false
statusBaseInput.dirty = false
statusBaseInput.pristine = true
statusBaseInput.text = ""
root.errorMessage = ""
}
property string _previousText: text

View File

@ -24,8 +24,6 @@ StatusModal {
property int minPswLen: 10
readonly property int marginBetweenInputs: 38
readonly property alias passwordValidationError: d.passwordValidationError
property var emojiPopup: null
header.title: qsTr("Generate an account")
@ -33,14 +31,6 @@ StatusModal {
signal afterAddAccount()
Timer {
id: waitTimer
interval: 1000
running: false
onTriggered: d.getDerivedAddressList()
}
Connections {
target: emojiPopup
enabled: root.opened
@ -51,16 +41,15 @@ StatusModal {
}
Connections {
target: RootStore
enabled: root.opened
function onDerivedAddressesListChanged() {
d.isPasswordCorrect = RootStore.derivedAddressesError.length === 0
target: walletSectionAccounts
onUserAuthenticaionSuccess: {
validationError.text = ""
d.password = password
d.getDerivedAddressList()
}
function onDerivedAddressesErrorChanged() {
if(Utils.isInvalidPasswordMessage(RootStore.derivedAddressesError))
d.passwordValidationError = qsTr("Password must be at least %n character(s) long", "", root.minPswLen);
onUserAuthentiactionFail: {
d.password = ""
validationError.text = qsTr("An authentication failed")
}
}
@ -70,33 +59,27 @@ StatusModal {
readonly property int numOfItems: 100
readonly property int pageNumber: 1
property string passwordValidationError: ""
property bool isPasswordCorrect: false
property string password: ""
property int selectedAccountType: SelectGeneratedAccount.AddAccountType.GenerateNew
readonly property bool authenticationNeeded: d.selectedAccountType !== SelectGeneratedAccount.AddAccountType.WatchOnly &&
d.password === ""
function getDerivedAddressList() {
if(advancedSelection.expandableItem.addAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase
if(d.selectedAccountType === SelectGeneratedAccount.AddAccountType.ImportSeedPhrase
&& !!advancedSelection.expandableItem.path
&& !!advancedSelection.expandableItem.mnemonicText) {
RootStore.getDerivedAddressListForMnemonic(advancedSelection.expandableItem.mnemonicText,
advancedSelection.expandableItem.path, numOfItems, pageNumber)
} else if(!!advancedSelection.expandableItem.path && !!advancedSelection.expandableItem.derivedFromAddress
&& (passwordInput.text.length > 0)) {
RootStore.getDerivedAddressList(passwordInput.text, advancedSelection.expandableItem.derivedFromAddress,
&& (d.password.length > 0)) {
RootStore.getDerivedAddressList(d.password, advancedSelection.expandableItem.derivedFromAddress,
advancedSelection.expandableItem.path, numOfItems, pageNumber)
}
}
function showPasswordError(errMessage) {
if (errMessage) {
if (Utils.isInvalidPasswordMessage(errMessage)) {
d.passwordValidationError = qsTr("Wrong password")
scroll.contentY = -scroll.padding
} else {
console.warn(`Unhandled error case. Status-go message: ${errMessage}`)
}
}
}
function generateNewAccount() {
// TODO the loading doesn't work because the function freezes the view. Might need to use threads
nextButton.loading = true
@ -107,19 +90,19 @@ StatusModal {
let errMessage = ""
switch(advancedSelection.expandableItem.addAccountType) {
switch(d.selectedAccountType) {
case SelectGeneratedAccount.AddAccountType.GenerateNew:
errMessage = RootStore.generateNewAccount(passwordInput.text, accountNameInput.text, colorSelectionGrid.selectedColor,
errMessage = RootStore.generateNewAccount(d.password, accountNameInput.text, colorSelectionGrid.selectedColor,
accountNameInput.input.asset.emoji, advancedSelection.expandableItem.completePath,
advancedSelection.expandableItem.derivedFromAddress)
break
case SelectGeneratedAccount.AddAccountType.ImportSeedPhrase:
errMessage = RootStore.addAccountsFromSeed(advancedSelection.expandableItem.mnemonicText, passwordInput.text,
errMessage = RootStore.addAccountsFromSeed(advancedSelection.expandableItem.mnemonicText, d.password,
accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.asset.emoji,
advancedSelection.expandableItem.completePath)
break
case SelectGeneratedAccount.AddAccountType.ImportPrivateKey:
errMessage = RootStore.addAccountsFromPrivateKey(advancedSelection.expandableItem.privateKey, passwordInput.text,
errMessage = RootStore.addAccountsFromPrivateKey(advancedSelection.expandableItem.privateKey, d.password,
accountNameInput.text, colorSelectionGrid.selectedColor, accountNameInput.input.asset.emoji)
break
case SelectGeneratedAccount.AddAccountType.WatchOnly:
@ -131,23 +114,33 @@ StatusModal {
nextButton.loading = false
if (errMessage) {
d.showPasswordError(errMessage)
console.warn(`Unhandled error case. Status-go message: ${errMessage}`)
} else {
root.afterAddAccount()
root.close()
}
}
function nextButtonClicked() {
if (d.authenticationNeeded) {
d.password = ""
RootStore.authenticateUser()
}
else {
d.generateNewAccount()
}
}
}
onOpened: {
accountNameInput.input.asset.emoji = StatusQUtils.Emoji.getRandomEmoji(StatusQUtils.Emoji.size.verySmall)
colorSelectionGrid.selectedColorIndex = Math.floor(Math.random() * colorSelectionGrid.model.length)
passwordInput.forceActiveFocus(Qt.MouseFocusReason)
accountNameInput.input.edit.forceActiveFocus()
}
onClosed: {
d.passwordValidationError = ""
passwordInput.text = ""
d.password = ""
validationError.text = ""
accountNameInput.reset()
advancedSelection.expanded = false
advancedSelection.reset()
@ -169,34 +162,15 @@ StatusModal {
spacing: Style.current.halfPadding
topPadding: 20
// To-Do Password hidden option not supported in StatusQ StatusInput
Item {
StatusBaseText {
id: validationError
visible: text !== ""
width: parent.width
height: passwordInput.height
visible: advancedSelection.expandableItem.addAccountType !== SelectGeneratedAccount.AddAccountType.WatchOnly
Input {
id: passwordInput
anchors.fill: parent
placeholderText: qsTr("Enter your password...")
label: qsTr("Password")
textField.echoMode: TextInput.Password
validationError: d.passwordValidationError
textField.objectName: "accountModalPassword"
inputLabel.font.pixelSize: 15
inputLabel.font.weight: Font.Normal
onTextChanged: {
d.isPasswordCorrect = false
d.passwordValidationError = ""
waitTimer.restart()
}
onKeyPressed: {
if(event.key === Qt.Key_Tab) {
accountNameInput.input.edit.forceActiveFocus(Qt.MouseFocusReason)
event.accepted = true
}
}
}
height: 16
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 12
color: Style.current.danger
wrapMode: TextEdit.Wrap
}
StatusInput {
@ -206,6 +180,7 @@ StatusModal {
input.isIconSelectable: true
input.asset.color: colorSelectionGrid.selectedColor ? colorSelectionGrid.selectedColor : Theme.palette.directColor1
input.leftPadding: Style.current.padding
enabled: !d.authenticationNeeded
onIconClicked: {
root.emojiPopup.open()
root.emojiPopup.emojiSize = StatusQUtils.Emoji.size.verySmall
@ -231,6 +206,7 @@ StatusModal {
StatusColorSelectorGrid {
id: colorSelectionGrid
anchors.horizontalCenter: parent.horizontalCenter
enabled: !d.authenticationNeeded
titleText: qsTr("color").toUpperCase()
}
@ -262,7 +238,15 @@ StatusModal {
return
}
}
Component.onCompleted: advancedSelection.isValid = Qt.binding(() => isValid)
onAddAccountTypeChanged: {
d.selectedAccountType = addAccountType
}
Component.onCompleted: {
d.selectedAccountType = addAccountType
advancedSelection.isValid = Qt.binding(() => isValid)
}
}
}
}
@ -272,21 +256,42 @@ StatusModal {
StatusButton {
id: nextButton
text: loading ? qsTr("Loading...") : qsTr("Add account")
text: {
if (d.authenticationNeeded) {
return qsTr("Authenticate")
}
if (loading) {
return qsTr("Loading...")
}
return qsTr("Add account")
}
enabled: {
if (d.authenticationNeeded) {
return true
}
if (loading) {
return false
}
return accountNameInput.text !== "" && advancedSelection.isValid
}
return (advancedSelection.expandableItem.addAccountType === SelectGeneratedAccount.AddAccountType.WatchOnly || d.isPasswordCorrect)
&& accountNameInput.text !== "" && advancedSelection.isValid
icon.name: {
if (d.authenticationNeeded) {
if (RootStore.loggedInUserUsesBiometricLogin())
return "touch-id"
if (RootStore.isProfileKeyPairMigrated())
return "keycard"
return "password"
}
return ""
}
highlighted: focus
Keys.onReturnPressed: d.generateNewAccount()
onClicked : d.generateNewAccount()
Keys.onReturnPressed: d.nextButtonClicked()
onClicked : d.nextButtonClicked()
}
]
}

View File

@ -222,4 +222,16 @@ QtObject {
function getNextSelectableDerivedAddressIndex() {
return walletSectionAccounts.getNextSelectableDerivedAddressIndex()
}
function authenticateUser() {
walletSectionAccounts.authenticateUser()
}
function loggedInUserUsesBiometricLogin() {
return walletSectionAccounts.loggedInUserUsesBiometricLogin()
}
function isProfileKeyPairMigrated() {
return walletSectionAccounts.isProfileKeyPairMigrated()
}
}

View File

@ -439,10 +439,11 @@ StatusModal {
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.biometricsReadyToSign ||
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.notKeycard ||
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.biometricsPinFailed ||
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.biometricsPinInvalid ||
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.wrongKeycard ||
root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.keycardEmpty)
return qsTr("Use PIN")
if (root.sharedKeycardModule.currentState.stateType === Constants.keycardSharedState.biometricsPinInvalid)
return qsTr("Update PIN")
}
}
if (root.sharedKeycardModule.currentState.flowType === Constants.keycardSharedFlow.unlockKeycard) {

View File

@ -826,7 +826,9 @@ Item {
}
PropertyChanges {
target: message
text: ""
text: qsTr("The PIN length doesn't match Keycard's PIN length")
font.pixelSize: Constants.keycard.general.fontSize2
color: Theme.palette.baseColor1
}
}
]

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 698c32f3e3684dd5918b8f38aa55fc568e1e7639
Subproject commit d89c0c8d9e333dc9b0c4ea36fd9c49c09a9b7d19