fix(@desktop/general): invalid pin saved to keychain when enabling biometrics on account using keycard

Fixes: #7729
This commit is contained in:
Sale Djenic 2023-02-02 14:54:35 +01:00 committed by Anthony Laibe
parent ca3f82848b
commit 224fd3f42d
9 changed files with 144 additions and 233 deletions

View File

@ -95,7 +95,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface,
result.profileModule = profile_module.newModule(result, profileService, settingsService) result.profileModule = profile_module.newModule(result, profileService, settingsService)
result.contactsModule = contacts_module.newModule(result, events, contactsService, chatService) result.contactsModule = contacts_module.newModule(result, events, contactsService, chatService)
result.languageModule = language_module.newModule(result, events, languageService) result.languageModule = language_module.newModule(result, events, languageService)
result.privacyModule = privacy_module.newModule(result, events, settingsService, privacyService, generalService) result.privacyModule = privacy_module.newModule(result, events, settingsService, keychainService, privacyService, generalService)
result.aboutModule = about_module.newModule(result, events, aboutService) result.aboutModule = about_module.newModule(result, events, aboutService)
result.advancedModule = advanced_module.newModule(result, events, settingsService, stickersService, nodeConfigurationService) result.advancedModule = advanced_module.newModule(result, events, settingsService, stickersService, nodeConfigurationService)
result.devicesModule = devices_module.newModule(result, events, settingsService, devicesService) result.devicesModule = devices_module.newModule(result, events, settingsService, devicesService)

View File

@ -1,35 +1,68 @@
import io_interface import io_interface
import uuids
import ../../../../../constants as main_constants
import ../../../../global/global_singleton
import ../../../../core/eventemitter import ../../../../core/eventemitter
import ../../../../../app_service/service/settings/service as settings_service import ../../../../../app_service/service/settings/service as settings_service
import ../../../../../app_service/service/keychain/service as keychain_service
import ../../../../../app_service/service/privacy/service as privacy_service import ../../../../../app_service/service/privacy/service as privacy_service
import ../../../../../app_service/service/general/service as general_service import ../../../../../app_service/service/general/service as general_service
import ../../../shared_modules/keycard_popup/io_interface as keycard_shared_module
const UNIQUE_PRIVACY_SECTION_MODULE_AUTH_IDENTIFIER* = "PrivacySectionModule-Authentication"
type type
Controller* = ref object of RootObj Controller* = ref object of RootObj
delegate: io_interface.AccessInterface delegate: io_interface.AccessInterface
events: EventEmitter events: EventEmitter
settingsService: settings_service.Service settingsService: settings_service.Service
keychainService: keychain_service.Service
privacyService: privacy_service.Service privacyService: privacy_service.Service
generalService: general_service.Service generalService: general_service.Service
keychainConnectionIds: seq[UUID]
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter,
settingsService: settings_service.Service, settingsService: settings_service.Service,
keychainService: keychain_service.Service,
privacyService: privacy_service.Service, privacyService: privacy_service.Service,
generalService: general_service.Service): Controller = generalService: general_service.Service): Controller =
result = Controller() result = Controller()
result.delegate = delegate result.delegate = delegate
result.events = events result.events = events
result.settingsService = settingsService result.settingsService = settingsService
result.keychainService = keychainService
result.privacyService = privacyService result.privacyService = privacyService
result.generalService = generalService result.generalService = generalService
proc delete*(self: Controller) = proc delete*(self: Controller) =
discard discard
proc disconnectKeychain*(self: Controller) =
for id in self.keychainConnectionIds:
self.events.disconnect(id)
self.keychainConnectionIds = @[]
proc connectKeychain*(self: Controller) =
var handlerId = self.events.onWithUUID(SIGNAL_KEYCHAIN_SERVICE_SUCCESS) do(e:Args):
let args = KeyChainServiceArg(e)
self.disconnectKeychain()
self.delegate.onStoreToKeychainSuccess(args.data)
self.keychainConnectionIds.add(handlerId)
handlerId = self.events.onWithUUID(SIGNAL_KEYCHAIN_SERVICE_ERROR) do(e:Args):
let args = KeyChainServiceArg(e)
self.disconnectKeychain()
self.delegate.onStoreToKeychainError(args.errDescription, args.errType)
self.keychainConnectionIds.add(handlerId)
proc init*(self: Controller) = proc init*(self: Controller) =
self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_PRIVACY_SECTION_MODULE_AUTH_IDENTIFIER:
return
self.delegate.onUserAuthenticated(args.pin, args.password, args.keyUid)
self.events.on(SIGNAL_MNEMONIC_REMOVAL) do(e: Args): self.events.on(SIGNAL_MNEMONIC_REMOVAL) do(e: Args):
self.delegate.onMnemonicUpdated() self.delegate.onMnemonicUpdated()
@ -67,3 +100,30 @@ proc validatePassword*(self: Controller, password: string): bool =
method getPasswordStrengthScore*(self: Controller, password, userName: string): int = method getPasswordStrengthScore*(self: Controller, password, userName: string): int =
return self.generalService.getPasswordStrengthScore(password, userName) return self.generalService.getPasswordStrengthScore(password, userName)
proc storeToKeychain*(self: Controller, data: string) =
let myName = singletonInstance.userProfile.getName()
let value = singletonInstance.localAccountSettings.getStoreToKeychainValue()
if not main_constants.IS_MACOS or # Dealing with Keychain is the MacOS only feature
data.len == 0 or
value == LS_VALUE_STORE or
myName.len == 0:
self.delegate.onStoreToKeychainError("", "")
return
self.connectKeychain()
self.keychainService.storeData(myName, data)
proc removeFromKeychain*(self: Controller, key: string) =
let value = singletonInstance.localAccountSettings.getStoreToKeychainValue()
if not main_constants.IS_MACOS or # Dealing with Keychain is the MacOS only feature
key.len == 0 or
value != LS_VALUE_STORE:
self.delegate.onStoreToKeychainError("", "")
return
self.connectKeychain()
self.keychainService.tryToDeleteData(key)
proc authenticateLoggedInUser*(self: Controller) =
var data = SharedKeycarModuleAuthenticationArgs(uniqueIdentifier: UNIQUE_PRIVACY_SECTION_MODULE_AUTH_IDENTIFIER)
if singletonInstance.userProfile.getIsKeycardUser():
data.keyUid = singletonInstance.userProfile.getKeyUid()
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)

View File

@ -58,3 +58,18 @@ method validatePassword*(self: AccessInterface, password: string): bool {.base.}
method getPasswordStrengthScore*(self: AccessInterface, password: string): int {.base.} = method getPasswordStrengthScore*(self: AccessInterface, password: string): int {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method onStoreToKeychainError*(self: AccessInterface, errorDescription: string, errorType: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onStoreToKeychainSuccess*(self: AccessInterface, data: string) {.base.} =
raise newException(ValueError, "No implementation available")
method tryStoreToKeyChain*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method tryRemoveFromKeyChain*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onUserAuthenticated*(self: AccessInterface, pin: string, password: string, keyUid: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -7,11 +7,17 @@ import ../io_interface as delegate_interface
import ../../../../core/eventemitter import ../../../../core/eventemitter
import ../../../../../app_service/service/settings/service as settings_service import ../../../../../app_service/service/settings/service as settings_service
import ../../../../../app_service/service/keychain/service as keychain_service
import ../../../../../app_service/service/privacy/service as privacy_service import ../../../../../app_service/service/privacy/service as privacy_service
import ../../../../../app_service/service/general/service as general_service import ../../../../../app_service/service/general/service as general_service
export io_interface export io_interface
type
KeychainActivityReason {.pure.} = enum
StoreTo = 0
RemoveFrom
type type
Module* = ref object of io_interface.AccessInterface Module* = ref object of io_interface.AccessInterface
delegate: delegate_interface.AccessInterface delegate: delegate_interface.AccessInterface
@ -19,9 +25,11 @@ type
view: View view: View
viewVariant: QVariant viewVariant: QVariant
moduleLoaded: bool moduleLoaded: bool
keychainActivityReason: KeychainActivityReason
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter,
settingsService: settings_service.Service, settingsService: settings_service.Service,
keychainService: keychain_service.Service,
privacyService: privacy_service.Service, privacyService: privacy_service.Service,
generalService: general_service.Service): generalService: general_service.Service):
Module = Module =
@ -29,7 +37,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
result.delegate = delegate result.delegate = delegate
result.view = newView(result) result.view = newView(result)
result.viewVariant = newQVariant(result.view) result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, settingsService, privacyService, generalService) result.controller = controller.newController(result, events, settingsService, keychainService, privacyService, generalService)
result.moduleLoaded = false result.moduleLoaded = false
method delete*(self: Module) = method delete*(self: Module) =
@ -85,3 +93,28 @@ method validatePassword*(self: Module, password: string): bool =
method getPasswordStrengthScore*(self: Module, password: string): int = method getPasswordStrengthScore*(self: Module, password: string): int =
return self.controller.getPasswordStrengthScore(password, singletonInstance.userProfile.getUsername()) return self.controller.getPasswordStrengthScore(password, singletonInstance.userProfile.getUsername())
method onStoreToKeychainError*(self: Module, errorDescription: string, errorType: string) =
self.view.emitStoreToKeychainError(errorDescription)
method onStoreToKeychainSuccess*(self: Module, data: string) =
if self.keychainActivityReason == KeychainActivityReason.StoreTo:
singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_STORE)
elif self.keychainActivityReason == KeychainActivityReason.RemoveFrom:
singletonInstance.localAccountSettings.setStoreToKeychainValue(LS_VALUE_NEVER)
self.view.emitStoreToKeychainSuccess()
method tryStoreToKeyChain*(self: Module) =
self.controller.authenticateLoggedInUser()
method tryRemoveFromKeyChain*(self: Module) =
self.keychainActivityReason = KeychainActivityReason.RemoveFrom
let myName = singletonInstance.userProfile.getName()
self.controller.removeFromKeychain(myName)
method onUserAuthenticated*(self: Module, pin: string, password: string, keyUid: string) =
self.keychainActivityReason = KeychainActivityReason.StoreTo
if pin.len > 0:
self.controller.storeToKeychain(pin)
else:
self.controller.storeToKeychain(password)

View File

@ -63,3 +63,18 @@ QtObject:
proc getPasswordStrengthScore*(self: View, password: string): int {.slot.} = proc getPasswordStrengthScore*(self: View, password: string): int {.slot.} =
return self.delegate.getPasswordStrengthScore(password) return self.delegate.getPasswordStrengthScore(password)
proc storeToKeychainError*(self:View, errorDescription: string) {.signal.}
proc emitStoreToKeychainError*(self: View, errorDescription: string) =
self.storeToKeychainError(errorDescription)
proc storeToKeychainSuccess*(self:View) {.signal.}
proc emitStoreToKeychainSuccess*(self: View) =
self.storeToKeychainSuccess()
proc tryStoreToKeyChain*(self: View) {.slot.} =
self.delegate.tryStoreToKeyChain()
proc tryRemoveFromKeyChain*(self: View) {.slot.} =
self.delegate.tryRemoveFromKeyChain()

View File

@ -1,207 +0,0 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Dialogs 1.3
import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import utils 1.0
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.controls 1.0
import "../stores"
// TODO: replace with StatusModal
ModalPopup {
property var privacyStore
property bool loading: false
property bool firstPasswordFieldValid: false
property bool repeatPasswordFieldValid: false
property string passwordValidationError: ""
property string repeatPasswordValidationError: ""
signal offerToStorePassword(string password, bool runStoreToKeychainPopup)
id: popup
title: qsTr("Store password")
height: 500
onOpened: {
if (userProfile.isKeycardUser) {
firstPinInputField.statesInitialization()
firstPinInputField.forceFocus()
}
else {
firstPasswordField.text = "";
firstPasswordField.forceActiveFocus(Qt.MouseFocusReason)
}
}
Column {
anchors.fill: parent
leftPadding: d.padding
rightPadding: d.padding
topPadding: Style.current.xlPadding
bottomPadding: Style.current.xlPadding
spacing: Style.current.xlPadding
QtObject {
id: d
readonly property int padding: 56
readonly property int fontSize: 15
}
Input {
id: firstPasswordField
width: parent.width - 2 * d.padding
visible: !userProfile.isKeycardUser
placeholderText: qsTr("Current password...")
textField.echoMode: TextInput.Password
onTextChanged: {
[firstPasswordFieldValid, passwordValidationError] =
Utils.validatePasswords("first", firstPasswordField, repeatPasswordField);
[repeatPasswordFieldValid, repeatPasswordValidationError] =
Utils.validatePasswords("repeat", firstPasswordField, repeatPasswordField);
}
}
Input {
id: repeatPasswordField
visible: !userProfile.isKeycardUser
width: parent.width - 2 * d.padding
enabled: firstPasswordFieldValid
placeholderText: qsTr("Confirm password…")
textField.echoMode: TextInput.Password
Keys.onReturnPressed: function(event) {
if (submitBtn.enabled) {
submitBtn.clicked(event)
}
}
onTextChanged: {
[repeatPasswordFieldValid, repeatPasswordValidationError] =
Utils.validatePasswords("repeat", firstPasswordField, repeatPasswordField);
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
visible: userProfile.isKeycardUser
text: qsTr("Enter new PIN")
font.pixelSize: d.fontSize
}
StatusPinInput {
id: firstPinInputField
anchors.horizontalCenter: parent.horizontalCenter
visible: userProfile.isKeycardUser
validator: StatusIntValidator{bottom: 0; top: 999999;}
pinLen: Constants.keycard.general.keycardPinLength
onPinInputChanged: {
if (pinInput.length == Constants.keycard.general.keycardPinLength) {
repeatPinInputField.statesInitialization()
repeatPinInputField.forceFocus()
}
[firstPasswordFieldValid, passwordValidationError] =
Utils.validatePINs("first", firstPinInputField, repeatPinInputField);
[repeatPasswordFieldValid, repeatPasswordValidationError] =
Utils.validatePINs("repeat", firstPinInputField, repeatPinInputField);
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
visible: userProfile.isKeycardUser
text: qsTr("Repeat PIN")
font.pixelSize: d.fontSize
}
StatusPinInput {
id: repeatPinInputField
anchors.horizontalCenter: parent.horizontalCenter
visible: userProfile.isKeycardUser
validator: StatusIntValidator{bottom: 0; top: 999999;}
pinLen: Constants.keycard.general.keycardPinLength
onPinInputChanged: {
[repeatPasswordFieldValid, repeatPasswordValidationError] =
Utils.validatePINs("repeat", firstPinInputField, repeatPinInputField);
}
}
StyledText {
id: validationError
text: {
if (passwordValidationError !== "") return passwordValidationError;
if (repeatPasswordValidationError !== "") return repeatPasswordValidationError;
return "";
}
anchors.horizontalCenter: parent.horizontalCenter
color: Style.current.danger
font.pixelSize: 11
}
}
footer: Item {
width: parent.width
height: submitBtn.height
StatusButton {
id: submitBtn
anchors.bottom: parent.bottom
anchors.topMargin: Style.current.padding
anchors.right: parent.right
state: loading ? "pending" : "default"
text: userProfile.isKeycardUser? qsTr("Store PIN") : qsTr("Store password")
enabled: firstPasswordFieldValid && repeatPasswordFieldValid && !loading
MessageDialog {
id: importError
title: qsTr("Error importing account")
text: qsTr("An error occurred while importing your account: ")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onVisibilityChanged: {
loading = false
}
}
MessageDialog {
id: importLoginError
title: qsTr("Login failed")
text: qsTr("Login failed. Please re-enter your password and try again.")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onVisibilityChanged: {
loading = false
}
}
onClicked: {
if (!userProfile.isKeycardUser) {
// validate the entered password
var validatePassword = privacyStore.validatePassword(repeatPasswordField.text)
if(!validatePassword) {
firstPasswordFieldValid = false
passwordValidationError = qsTr("Incorrect password")
}
else {
popup.offerToStorePassword(repeatPasswordField.text, true)
popup.close()
}
}
else {
popup.offerToStorePassword(repeatPinInputField.pinInput, true)
popup.close()
}
}
}
}
}

View File

@ -29,7 +29,11 @@ QtObject {
return root.privacyModule.validatePassword(password) return root.privacyModule.validatePassword(password)
} }
function storeToKeyChain(pass) { function tryStoreToKeyChain() {
mainModule.storePassword(pass); root.privacyModule.tryStoreToKeyChain()
}
function tryRemoveFromKeyChain() {
root.privacyModule.tryRemoveFromKeyChain()
} }
} }

View File

@ -12,7 +12,6 @@ import "../../popups"
import "../../stores" import "../../stores"
import "../../controls" import "../../controls"
import "../../panels" import "../../panels"
import "../../../Onboarding/shared" as OnboardingComponents
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
@ -69,20 +68,23 @@ ColumnLayout {
} }
if (biometricsSwitch.checked && !biometricsSwitch.currentStoredValue) if (biometricsSwitch.checked && !biometricsSwitch.currentStoredValue)
Global.openPopup(storePasswordModal) root.privacyStore.tryStoreToKeyChain()
else if (!biometricsSwitch.checked) else if (!biometricsSwitch.checked)
localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.never; root.privacyStore.tryRemoveFromKeyChain()
reset() reset()
} }
function offerToStorePassword(password, runStoreToKeyChainPopup) Connections {
{ target: Qt.platform.os === Constants.mac? root.privacyStore.privacyModule : null
if (Qt.platform.os !== "osx")
return;
localAccountSettings.storeToKeychainValue = Constants.keychain.storedValue.store; function onStoreToKeychainError(errorDescription: string) {
root.privacyStore.storeToKeyChain(password); root.reset()
}
function onStoreToKeychainSuccess() {
root.reset()
}
} }
ProfileHeader { ProfileHeader {
@ -247,15 +249,4 @@ ColumnLayout {
} }
} }
} }
Component {
id: storePasswordModal
OnboardingComponents.CreatePasswordModal {
privacyStore: root.privacyStore
onOfferToStorePassword: {
root.offerToStorePassword(password, runStoreToKeychainPopup)
}
}
}
} }

View File

@ -659,7 +659,7 @@ QtObject {
readonly property string windows: "windows" readonly property string windows: "windows"
readonly property string linux: "linux" readonly property string linux: "linux"
readonly property string mac: "mac" readonly property string mac: "osx"
// Transaction states // Transaction states
readonly property int addressRequested: 1 readonly property int addressRequested: 1