From 420c143798adadca2cb08f2d8fc6dcec4ddad5a1 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Tue, 12 Mar 2024 19:44:46 +0200 Subject: [PATCH] Passkeys: Register to an existing entry --- share/translations/keepassxc_en.ts | 108 ++++++++++-------- src/browser/BrowserMessageBuilder.cpp | 2 +- .../BrowserPasskeysConfirmationDialog.cpp | 17 +-- src/browser/BrowserService.cpp | 38 ++++-- src/browser/BrowserSettingsWidget.ui | 4 +- src/gui/passkeys/PasskeyExportDialog.ui | 2 +- src/gui/passkeys/PasskeyImportDialog.cpp | 18 ++- src/gui/passkeys/PasskeyImportDialog.h | 6 +- src/gui/passkeys/PasskeyImportDialog.ui | 2 +- src/gui/passkeys/PasskeyImporter.cpp | 32 +++--- src/gui/passkeys/PasskeyImporter.h | 15 ++- src/gui/reports/ReportsWidgetPasskeys.cpp | 4 +- 12 files changed, 150 insertions(+), 98 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 13ed25a208..8d2c57a9eb 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -875,32 +875,36 @@ Please select the correct database for saving credentials. - Do you want to register Passkey for: + Relying Party: %1 - Existing Passkey found. -Do you want to register a new Passkey for: + Username: %1 - Select the existing Passkey and press Update to replace it. + KeePassXC - Passkey credentials - Authenticate Passkey credentials for: + Add to existing entry - Relying Party: %1 + Do you want to register passkey for: - Username: %1 + Existing passkey found. +Do you want to register a new passkey for: - KeePassXC - Passkey credentials + Select the existing passkey and press Update to replace it. + + + + Authenticate passkey credentials for: @@ -943,11 +947,6 @@ Do you want to delete the entry? %1 (Passkey) - - Entry already has a Passkey. -Do you want to overwrite the Passkey in %1 - %2? - - KeePassXC - Create a new group @@ -960,10 +959,6 @@ Do you want to overwrite the Passkey in %1 - %2? KeePassXC - Overwrite existing key? - - KeePassXC - Update Passkey - - KeePassXC - Update Entry @@ -980,6 +975,23 @@ Do you want to overwrite the Passkey in %1 - %2? Passkey + + KeePassXC - Passkey credentials + + + + Register a new passkey to this entry: + + + + KeePassXC - Update Passkey + + + + Entry already has a Passkey. +Do you want to overwrite the Passkey in %1 - %2? + + BrowserSettingsWidget @@ -1227,11 +1239,11 @@ Do you want to overwrite the Passkey in %1 - %2? - Allows using insecure http://localhost with Passkeys for testing purposes. + Allows using insecure http://localhost with passkeys for testing purposes. - Allow using localhost with Passkeys + Allow using localhost with passkeys @@ -6210,10 +6222,6 @@ We recommend you use the AppImage available on our downloads page. KeePassXC - Passkey Export - - Export the following Passkey entries. - - Filenames will be generated with title and .passkey file extension. @@ -6234,6 +6242,10 @@ We recommend you use the AppImage available on our downloads page. Export to folder + + Export the following passkey entries. + + PasskeyExporter @@ -6291,27 +6303,27 @@ Do you want to overwrite it? - Import the following Passkey: + Entry - Entry + Create new entry - Import the following Passkey to this entry: + Relying Party: %1 - Create new entry + Import the following passkey: - Default Passkeys group (Imported Passkeys) + Import the following passkey to this entry: - Relying Party: %1 + Default passkeys group (Imported Passkeys) @@ -6325,10 +6337,6 @@ Do you want to overwrite it? All files - - Open Passkey file - - Cannot open file @@ -6338,23 +6346,27 @@ Do you want to overwrite it? - Cannot import Passkey + Open passkey file - Cannot import Passkey file "%1". Data is missing. + Cannot import passkey - Cannot import Passkey file "%1". Private key is missing or malformed. + Cannot import passkey file "%1". Data is missing. - Cannot import Passkey file "%1". + Cannot import passkey file "%1". The following data is missing: %2 + + Cannot import passkey file "%1". Private key is missing or malformed. + + PasswordEditWidget @@ -8663,15 +8675,15 @@ Kernel: %3 %4 - Unknown Passkeys error + Challenge is shorter than required minimum length - Challenge is shorter than required minimum length + user.id does not match the required length - user.id does not match the required length + Unknown passkeys error @@ -9000,14 +9012,6 @@ Kernel: %3 %4 List of entry URLs - - Please wait, list of entries with Passkeys is being updated… - - - - No entries with Passkeys. - - Title @@ -9055,6 +9059,14 @@ Kernel: %3 %4 The passkey file will be vulnerable to theft and unauthorized use, if left unsecured. Are you sure you want to continue? + + Please wait, list of entries with passkeys is being updated… + + + + No entries with passkeys. + + ReportsWidgetStatistics diff --git a/src/browser/BrowserMessageBuilder.cpp b/src/browser/BrowserMessageBuilder.cpp index 317c161bd2..e7b398c6a0 100644 --- a/src/browser/BrowserMessageBuilder.cpp +++ b/src/browser/BrowserMessageBuilder.cpp @@ -151,7 +151,7 @@ QString BrowserMessageBuilder::getErrorMessage(const int errorCode) const case ERROR_PASSKEYS_WAIT_FOR_LIFETIMER: return QObject::tr("Wait for timer to expire"); case ERROR_PASSKEYS_UNKNOWN_ERROR: - return QObject::tr("Unknown Passkeys error"); + return QObject::tr("Unknown passkeys error"); case ERROR_PASSKEYS_INVALID_CHALLENGE: return QObject::tr("Challenge is shorter than required minimum length"); case ERROR_PASSKEYS_INVALID_USER_ID: diff --git a/src/browser/BrowserPasskeysConfirmationDialog.cpp b/src/browser/BrowserPasskeysConfirmationDialog.cpp index 644bec599d..6885cc39ce 100644 --- a/src/browser/BrowserPasskeysConfirmationDialog.cpp +++ b/src/browser/BrowserPasskeysConfirmationDialog.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2024 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,7 +32,6 @@ BrowserPasskeysConfirmationDialog::BrowserPasskeysConfirmationDialog(QWidget* pa setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); m_ui->setupUi(this); - m_ui->updateButton->setVisible(false); m_ui->verticalLayout->setAlignment(Qt::AlignTop); connect(m_ui->credentialsTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(accept())); @@ -53,21 +52,22 @@ void BrowserPasskeysConfirmationDialog::registerCredential(const QString& userna const QList& existingEntries, int timeout) { - m_ui->firstLabel->setText(tr("Do you want to register Passkey for:")); + m_ui->firstLabel->setText(tr("Do you want to register passkey for:")); m_ui->relyingPartyLabel->setText(tr("Relying Party: %1").arg(relyingParty)); m_ui->usernameLabel->setText(tr("Username: %1").arg(username)); + m_ui->updateButton->setVisible(true); m_ui->secondLabel->setText(""); if (!existingEntries.isEmpty()) { - m_ui->firstLabel->setText(tr("Existing Passkey found.\nDo you want to register a new Passkey for:")); - m_ui->secondLabel->setText(tr("Select the existing Passkey and press Update to replace it.")); - - m_ui->updateButton->setVisible(true); + m_ui->firstLabel->setText(tr("Existing passkey found.\nDo you want to register a new passkey for:")); + m_ui->secondLabel->setText(tr("Select the existing passkey and press Update to replace it.")); + m_ui->updateButton->setText(tr("Update")); m_ui->confirmButton->setText(tr("Register new")); updateEntriesToTable(existingEntries); } else { m_ui->verticalLayout->setSizeConstraint(QLayout::SetFixedSize); m_ui->confirmButton->setText(tr("Register")); + m_ui->updateButton->setText(tr("Add to existing entry")); m_ui->credentialsTable->setVisible(false); } @@ -78,9 +78,10 @@ void BrowserPasskeysConfirmationDialog::authenticateCredential(const QListfirstLabel->setText(tr("Authenticate Passkey credentials for:")); + m_ui->firstLabel->setText(tr("Authenticate passkey credentials for:")); m_ui->relyingPartyLabel->setText(tr("Relying Party: %1").arg(relyingParty)); m_ui->usernameLabel->setVisible(false); + m_ui->updateButton->setVisible(false); m_ui->secondLabel->setText(""); updateEntriesToTable(entries); startCounter(timeout); diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index c7f734c347..8375d660d4 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -34,6 +34,7 @@ #include "BrowserPasskeysClient.h" #include "BrowserPasskeysConfirmationDialog.h" #include "PasskeyUtils.h" +#include "gui/passkeys/PasskeyImporter.h" #endif #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" @@ -658,13 +659,32 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public const auto rpName = publicKeyOptions["rp"]["name"].toString(); if (confirmDialog.isPasskeyUpdated()) { - addPasskeyToEntry(confirmDialog.getSelectedEntry(), - rpId, - rpName, - username, - publicKeyCredentials.credentialId, - userId, - publicKeyCredentials.key); + // If no entry is selected, show the import dialog for manual entry selection + auto selectedEntry = confirmDialog.getSelectedEntry(); + if (!selectedEntry) { + PasskeyImporter passkeyImporter; + const auto result = passkeyImporter.showImportDialog(db, + nullptr, + origin, + rpId, + username, + publicKeyCredentials.credentialId, + userId, + publicKeyCredentials.key, + tr("KeePassXC - Passkey credentials"), + tr("Register a new passkey to this entry:")); + if (!result) { + return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED); + } + } else { + addPasskeyToEntry(selectedEntry, + rpId, + rpName, + username, + publicKeyCredentials.credentialId, + userId, + publicKeyCredentials.key); + } } else { addPasskeyToGroup(nullptr, origin, @@ -789,8 +809,8 @@ void BrowserService::addPasskeyToEntry(Entry* entry, // Ask confirmation if entry already contains a Passkey if (entry->hasPasskey()) { if (MessageBox::question(m_currentDatabaseWidget, - tr("KeePassXC - Update Passkey"), - tr("Entry already has a Passkey.\nDo you want to overwrite the Passkey in %1 - %2?") + tr("KeePassXC - Update passkey"), + tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?") .arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)), MessageBox::Overwrite | MessageBox::Cancel, MessageBox::Cancel) diff --git a/src/browser/BrowserSettingsWidget.ui b/src/browser/BrowserSettingsWidget.ui index 8f69c6d2c3..facabfa4a9 100644 --- a/src/browser/BrowserSettingsWidget.ui +++ b/src/browser/BrowserSettingsWidget.ui @@ -313,10 +313,10 @@ - Allows using insecure http://localhost with Passkeys for testing purposes. + Allows using insecure http://localhost with passkeys for testing purposes. - Allow using localhost with Passkeys + Allow using localhost with passkeys diff --git a/src/gui/passkeys/PasskeyExportDialog.ui b/src/gui/passkeys/PasskeyExportDialog.ui index c974ebaac4..bf9f4c42d8 100755 --- a/src/gui/passkeys/PasskeyExportDialog.ui +++ b/src/gui/passkeys/PasskeyExportDialog.ui @@ -23,7 +23,7 @@ - Export the following Passkey entries. + Export the following passkey entries. Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter diff --git a/src/gui/passkeys/PasskeyImportDialog.cpp b/src/gui/passkeys/PasskeyImportDialog.cpp index 2762df510d..7d1f89874b 100644 --- a/src/gui/passkeys/PasskeyImportDialog.cpp +++ b/src/gui/passkeys/PasskeyImportDialog.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2024 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -48,14 +48,16 @@ PasskeyImportDialog::~PasskeyImportDialog() void PasskeyImportDialog::setInfo(const QString& relyingParty, const QString& username, const QSharedPointer& database, - bool isEntry) + bool isEntry, + const QString& titleText, + const QString& infoText) { m_ui->relyingPartyLabel->setText(tr("Relying Party: %1").arg(relyingParty)); m_ui->usernameLabel->setText(tr("Username: %1").arg(username)); if (isEntry) { m_ui->verticalLayout->setSizeConstraint(QLayout::SetFixedSize); - m_ui->infoLabel->setText(tr("Import the following Passkey to this entry:")); + m_ui->infoLabel->setText(tr("Import the following passkey to this entry:")); m_ui->groupBox->setVisible(false); } @@ -70,6 +72,14 @@ void PasskeyImportDialog::setInfo(const QString& relyingParty, } } m_ui->selectDatabaseCombobBox->setEnabled(openDatabaseCount > 1); + + if (!titleText.isEmpty()) { + setWindowTitle(titleText); + } + + if (!infoText.isEmpty()) { + m_ui->infoLabel->setText(infoText); + } } QSharedPointer PasskeyImportDialog::getSelectedDatabase() const @@ -155,7 +165,7 @@ void PasskeyImportDialog::addGroups() } m_ui->selectGroupComboBox->clear(); - m_ui->selectGroupComboBox->addItem(tr("Default Passkeys group (Imported Passkeys)"), {}); + m_ui->selectGroupComboBox->addItem(tr("Default passkeys group (Imported Passkeys)"), {}); for (const auto& group : m_selectedDatabase->rootGroup()->groupsRecursive(true)) { if (!group || group->isRecycled() || group == m_selectedDatabase->metadata()->recycleBin()) { diff --git a/src/gui/passkeys/PasskeyImportDialog.h b/src/gui/passkeys/PasskeyImportDialog.h index 9a7c4437ca..8e7eb2e9ec 100644 --- a/src/gui/passkeys/PasskeyImportDialog.h +++ b/src/gui/passkeys/PasskeyImportDialog.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2024 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,7 +39,9 @@ class PasskeyImportDialog : public QDialog void setInfo(const QString& relyingParty, const QString& username, const QSharedPointer& database, - bool isEntry); + bool isEntry, + const QString& titleText = {}, + const QString& infoText = {}); QSharedPointer getSelectedDatabase() const; QUuid getSelectedEntryUuid() const; QUuid getSelectedGroupUuid() const; diff --git a/src/gui/passkeys/PasskeyImportDialog.ui b/src/gui/passkeys/PasskeyImportDialog.ui index ecca97b14a..3b00809970 100755 --- a/src/gui/passkeys/PasskeyImportDialog.ui +++ b/src/gui/passkeys/PasskeyImportDialog.ui @@ -36,7 +36,7 @@ - Import the following Passkey: + Import the following passkey: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter diff --git a/src/gui/passkeys/PasskeyImporter.cpp b/src/gui/passkeys/PasskeyImporter.cpp index 77e37c689b..5c26e3d273 100644 --- a/src/gui/passkeys/PasskeyImporter.cpp +++ b/src/gui/passkeys/PasskeyImporter.cpp @@ -34,7 +34,7 @@ void PasskeyImporter::importPasskey(QSharedPointer& database, Entry* e { auto filter = QString("%1 (*.passkey);;%2 (*)").arg(tr("Passkey file"), tr("All files")); auto fileName = - fileDialog()->getOpenFileName(nullptr, tr("Open Passkey file"), FileDialog::getLastDir("passkey"), filter); + fileDialog()->getOpenFileName(nullptr, tr("Open passkey file"), FileDialog::getLastDir("passkey"), filter); if (fileName.isEmpty()) { return; } @@ -57,8 +57,8 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer& const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData); if (passkeyObject.isEmpty()) { MessageBox::information(nullptr, - tr("Cannot import Passkey"), - tr("Cannot import Passkey file \"%1\". Data is missing.").arg(file.fileName())); + tr("Cannot import passkey"), + tr("Cannot import passkey file \"%1\". Data is missing.").arg(file.fileName())); return; } @@ -73,40 +73,42 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer& if (!missingKeys.isEmpty()) { MessageBox::information(nullptr, - tr("Cannot import Passkey"), - tr("Cannot import Passkey file \"%1\".\nThe following data is missing:\n%2") + tr("Cannot import passkey"), + tr("Cannot import passkey file \"%1\".\nThe following data is missing:\n%2") .arg(file.fileName(), missingKeys.join(", "))); } else if (!privateKey.startsWith("-----BEGIN PRIVATE KEY-----") || !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) { MessageBox::information( nullptr, - tr("Cannot import Passkey"), - tr("Cannot import Passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName())); + tr("Cannot import passkey"), + tr("Cannot import passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName())); } else { const auto relyingParty = passkeyObject["relyingParty"].toString(); const auto url = passkeyObject["url"].toString(); const auto username = passkeyObject["username"].toString(); const auto credentialId = passkeyObject["credentialId"].toString(); const auto userHandle = passkeyObject["userHandle"].toString(); - showImportDialog(database, url, relyingParty, username, credentialId, userHandle, privateKey, entry); + showImportDialog(database, entry, url, relyingParty, username, credentialId, userHandle, privateKey); } } -void PasskeyImporter::showImportDialog(QSharedPointer& database, +bool PasskeyImporter::showImportDialog(QSharedPointer& database, + Entry* entry, const QString& url, const QString& relyingParty, const QString& username, const QString& credentialId, const QString& userHandle, const QString& privateKey, - Entry* entry) + const QString& titleText, + const QString& infoText) { PasskeyImportDialog passkeyImportDialog; - passkeyImportDialog.setInfo(relyingParty, username, database, entry != nullptr); + passkeyImportDialog.setInfo(relyingParty, username, database, entry != nullptr, titleText, infoText); auto ret = passkeyImportDialog.exec(); if (ret != QDialog::Accepted) { - return; + return false; } auto db = passkeyImportDialog.getSelectedDatabase(); @@ -118,7 +120,7 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, if (entry) { browserService()->addPasskeyToEntry( entry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey); - return; + return true; } // Import to entry selected instead of creating a new one @@ -134,7 +136,7 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, } } - return; + return true; } // Group settings. Use default group "Imported Passkeys" if user did not select a specific one. @@ -153,6 +155,8 @@ void PasskeyImporter::showImportDialog(QSharedPointer& database, browserService()->addPasskeyToGroup( group, url, relyingParty, relyingParty, username, credentialId, userHandle, privateKey); + + return true; } Group* PasskeyImporter::getDefaultGroup(QSharedPointer& database) const diff --git a/src/gui/passkeys/PasskeyImporter.h b/src/gui/passkeys/PasskeyImporter.h index 9cc7fab48f..6a446485a7 100644 --- a/src/gui/passkeys/PasskeyImporter.h +++ b/src/gui/passkeys/PasskeyImporter.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2024 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,17 +33,20 @@ class PasskeyImporter : public QObject explicit PasskeyImporter() = default; void importPasskey(QSharedPointer& database, Entry* entry = nullptr); - -private: - void importSelectedFile(QFile& file, QSharedPointer& database, Entry* entry); - void showImportDialog(QSharedPointer& database, + bool showImportDialog(QSharedPointer& database, + Entry* entry, const QString& url, const QString& relyingParty, const QString& username, const QString& credentialId, const QString& userHandle, const QString& privateKey, - Entry* entry); + const QString& titleText = {}, + const QString& infoText = {}); + +private: + void importSelectedFile(QFile& file, QSharedPointer& database, Entry* entry); + Group* getDefaultGroup(QSharedPointer& database) const; }; diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp index f14b092a94..e671509060 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.cpp +++ b/src/gui/reports/ReportsWidgetPasskeys.cpp @@ -152,7 +152,7 @@ void ReportsWidgetPasskeys::loadSettings(QSharedPointer db) m_rowToEntry.clear(); auto row = QList(); - row << new QStandardItem(tr("Please wait, list of entries with Passkeys is being updated…")); + row << new QStandardItem(tr("Please wait, list of entries with passkeys is being updated…")); m_referencesModel->appendRow(row); } @@ -188,7 +188,7 @@ void ReportsWidgetPasskeys::updateEntries() // Set the table header if (m_referencesModel->rowCount() == 0) { - m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("No entries with Passkeys.")); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("No entries with passkeys.")); } else { m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Username") << tr("Relying Party") << tr("URLs"));