Passkeys: Add support for importing Passkey to entry (#9987)

---------
Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
Sami Vänttinen 2023-11-23 06:11:25 +02:00 committed by GitHub
parent 013db199cb
commit 13c88e1013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 459 additions and 176 deletions

View File

@ -959,6 +959,15 @@ Do you want to delete the entry?
<source>%1 (Passkey)</source> <source>%1 (Passkey)</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>KeePassXC: Update Passkey</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Entry already has a Passkey.
Do you want to overwrite the Passkey in %1 - %2?</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>BrowserSettingsWidget</name> <name>BrowserSettingsWidget</name>
@ -5992,10 +6001,6 @@ Do you want to overwrite it?
<source>KeePassXC - Passkey Import</source> <source>KeePassXC - Passkey Import</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Do you want to import the Passkey?</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>URL: %1</source> <source>URL: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -6004,10 +6009,6 @@ Do you want to overwrite it?
<source>Username: %1</source> <source>Username: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Use default group (Imported Passkeys)</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Group</source> <source>Group</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -6016,10 +6017,6 @@ Do you want to overwrite it?
<source>Database</source> <source>Database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Select Database</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Import Passkey</source> <source>Import Passkey</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -6033,11 +6030,23 @@ Do you want to overwrite it?
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Database: %1</source> <source>Import the following Passkey:</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Group:</source> <source>Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import the following Passkey to this entry:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create new entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Default Passkeys group (Imported Passkeys)</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -6075,6 +6084,12 @@ Do you want to overwrite it?
<source>Cannot import Passkey file &quot;%1&quot;. Private key is missing or malformed.</source> <source>Cannot import Passkey file &quot;%1&quot;. Private key is missing or malformed.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Cannot import Passkey file &quot;%1&quot;.
The following data is missing:
%2</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>PasswordEditWidget</name> <name>PasswordEditWidget</name>

View File

@ -64,8 +64,6 @@ const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEnt
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth"); const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth"); const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww"); const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
// Multiple URL's
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
Q_GLOBAL_STATIC(BrowserService, s_browserService); Q_GLOBAL_STATIC(BrowserService, s_browserService);
@ -775,6 +773,20 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
return; return;
} }
// 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?")
.arg(entry->title(), entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME)),
MessageBox::Overwrite | MessageBox::Cancel,
MessageBox::Cancel)
!= MessageBox::Overwrite) {
return;
}
}
entry->beginUpdate(); entry->beginUpdate();
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username); entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
@ -1084,7 +1096,13 @@ void BrowserService::denyEntry(Entry* entry, const QString& siteHost, const QStr
QJsonObject BrowserService::prepareEntry(const Entry* entry) QJsonObject BrowserService::prepareEntry(const Entry* entry)
{ {
QJsonObject res; QJsonObject res;
#ifdef WITH_XC_BROWSER_PASSKEYS
// Use Passkey's username instead if found
res["login"] = entry->hasPasskey() ? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME)
: entry->resolveMultiplePlaceholders(entry->username());
#else
res["login"] = entry->resolveMultiplePlaceholders(entry->username()); res["login"] = entry->resolveMultiplePlaceholders(entry->username());
#endif
res["password"] = entry->resolveMultiplePlaceholders(entry->password()); res["password"] = entry->resolveMultiplePlaceholders(entry->password());
res["name"] = entry->resolveMultiplePlaceholders(entry->title()); res["name"] = entry->resolveMultiplePlaceholders(entry->title());
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex()); res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
@ -1291,8 +1309,7 @@ QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const Strin
{ {
QList<Entry*> entries; QList<Entry*> entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) { for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
if (entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM) if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
entries << entry; entries << entry;
} }
} }
@ -1419,14 +1436,34 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false; return false;
} }
QSharedPointer<Database> BrowserService::getDatabase() QSharedPointer<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid)
{ {
if (!rootGroupUuid.isNull()) {
const auto openDatabases = getOpenDatabases();
for (const auto& db : openDatabases) {
if (db->rootGroup()->uuid() == rootGroupUuid) {
return db;
}
}
}
if (m_currentDatabaseWidget) { if (m_currentDatabaseWidget) {
return m_currentDatabaseWidget->database(); return m_currentDatabaseWidget->database();
} }
return {}; return {};
} }
QList<QSharedPointer<Database>> BrowserService::getOpenDatabases()
{
QList<QSharedPointer<Database>> databaseList;
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
if (!dbWidget->isLocked()) {
databaseList << dbWidget->database();
}
}
return databaseList;
}
QSharedPointer<Database> BrowserService::selectedDatabase() QSharedPointer<Database> BrowserService::selectedDatabase()
{ {
QList<DatabaseWidget*> databaseWidgets; QList<DatabaseWidget*> databaseWidgets;

View File

@ -84,7 +84,9 @@ public:
QString getCurrentTotp(const QString& uuid); QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage); void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const; bool isPasswordGeneratorRequested() const;
QSharedPointer<Database> getDatabase(const QUuid& rootGroupUuid = {});
QSharedPointer<Database> selectedDatabase(); QSharedPointer<Database> selectedDatabase();
QList<QSharedPointer<Database>> getOpenDatabases();
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject QJsonObject
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList); showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
@ -124,7 +126,6 @@ public:
static const QString OPTION_ONLY_HTTP_AUTH; static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH; static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW; static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;
signals: signals:
void requestUnlock(); void requestUnlock();
@ -191,7 +192,6 @@ private:
const QString& siteUrl, const QString& siteUrl,
const QString& formUrl, const QString& formUrl,
const bool omitWwwSubdomain = false); const bool omitWwwSubdomain = false);
QSharedPointer<Database> getDatabase();
QString getDatabaseRootUuid(); QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid(); QString getDatabaseRecycleBinUuid();
bool checkLegacySettings(QSharedPointer<Database> db); bool checkLegacySettings(QSharedPointer<Database> db);

View File

@ -385,7 +385,8 @@ QStringList Entry::getAllUrls() const
} }
for (const auto& key : m_attributes->keys()) { for (const auto& key : m_attributes->keys()) {
if (key.startsWith("KP2A_URL")) { if (key.startsWith(EntryAttributes::AdditionalUrlAttribute)
|| key == QString("%1_RELYING_PARTY").arg(EntryAttributes::PasskeyAttribute)) {
auto additionalUrl = m_attributes->value(key); auto additionalUrl = m_attributes->value(key);
if (!additionalUrl.isEmpty()) { if (!additionalUrl.isEmpty()) {
urlList << resolveMultiplePlaceholders(additionalUrl); urlList << resolveMultiplePlaceholders(additionalUrl);
@ -545,6 +546,11 @@ bool Entry::hasTotp() const
return !m_data.totpSettings.isNull(); return !m_data.totpSettings.isNull();
} }
bool Entry::hasPasskey() const
{
return m_attributes->hasPasskey();
}
QString Entry::totp() const QString Entry::totp() const
{ {
if (hasTotp()) { if (hasTotp()) {

View File

@ -121,6 +121,7 @@ public:
void setExcludeFromReports(bool state); void setExcludeFromReports(bool state);
bool hasTotp() const; bool hasTotp() const;
bool hasPasskey() const;
bool isExpired() const; bool isExpired() const;
bool willExpireInDays(int days) const; bool willExpireInDays(int days) const;
bool isRecycled() const; bool isRecycled() const;

View File

@ -34,6 +34,7 @@ const QString EntryAttributes::SearchInGroupName = "SearchIn";
const QString EntryAttributes::SearchTextGroupName = "SearchText"; const QString EntryAttributes::SearchTextGroupName = "SearchText";
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD"; const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY"; const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";
EntryAttributes::EntryAttributes(QObject* parent) EntryAttributes::EntryAttributes(QObject* parent)
@ -52,6 +53,18 @@ bool EntryAttributes::hasKey(const QString& key) const
return m_attributes.contains(key); return m_attributes.contains(key);
} }
bool EntryAttributes::hasPasskey() const
{
const auto keyList = keys();
for (const auto& key : keyList) {
if (isPasskeyAttribute(key)) {
return true;
}
}
return false;
}
QList<QString> EntryAttributes::customKeys() const QList<QString> EntryAttributes::customKeys() const
{ {
QList<QString> customKeys; QList<QString> customKeys;

View File

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de> * Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -33,6 +33,7 @@ public:
explicit EntryAttributes(QObject* parent = nullptr); explicit EntryAttributes(QObject* parent = nullptr);
QList<QString> keys() const; QList<QString> keys() const;
bool hasKey(const QString& key) const; bool hasKey(const QString& key) const;
bool hasPasskey() const;
QList<QString> customKeys() const; QList<QString> customKeys() const;
QString value(const QString& key) const; QString value(const QString& key) const;
QList<QString> values(const QList<QString>& keys) const; QList<QString> values(const QList<QString>& keys) const;
@ -61,6 +62,7 @@ public:
static const QString NotesKey; static const QString NotesKey;
static const QStringList DefaultAttributes; static const QStringList DefaultAttributes;
static const QString RememberCmdExecAttr; static const QString RememberCmdExecAttr;
static const QString AdditionalUrlAttribute;
static const QString PasskeyAttribute; static const QString PasskeyAttribute;
static bool isDefaultAttribute(const QString& key); static bool isDefaultAttribute(const QString& key);
static bool isPasskeyAttribute(const QString& key); static bool isPasskeyAttribute(const QString& key);

View File

@ -22,6 +22,7 @@
#include "core/Global.h" #include "core/Global.h"
#include <QDateTime> #include <QDateTime>
#include <QList>
#include <QProcessEnvironment> #include <QProcessEnvironment>
class QIODevice; class QIODevice;
@ -100,6 +101,19 @@ namespace Tools
return version; return version;
} }
// Checks if all values are found inside the list. Returns a list of values not found.
template <typename T> QList<T> getMissingValuesFromList(const QList<T>& list, const QList<T>& required)
{
QList<T> missingValues;
for (const auto& r : required) {
if (!list.contains(r)) {
missingValues << r;
}
}
return missingValues;
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"}); QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"});
QString substituteBackupFilePath(QString pattern, const QString& databasePath); QString substituteBackupFilePath(QString pattern, const QString& databasePath);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -243,7 +243,8 @@ bool OpVaultReader::fillAttributes(Entry* entry, const QJsonObject& bandEntry)
auto newUrl = urlObj["u"].toString(); auto newUrl = urlObj["u"].toString();
if (newUrl != url) { if (newUrl != url) {
// Add this url if it isn't the base one // Add this url if it isn't the base one
entry->attributes()->set(QString("KP2A_URL_%1").arg(i), newUrl); entry->attributes()->set(
QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), newUrl);
++i; ++i;
} }
} }

View File

@ -564,7 +564,12 @@ void DatabaseTabWidget::showPasskeys()
void DatabaseTabWidget::importPasskey() void DatabaseTabWidget::importPasskey()
{ {
currentDatabaseWidget()->switchToImportPasskey(); currentDatabaseWidget()->showImportPasskeyDialog();
}
void DatabaseTabWidget::importPasskeyToEntry()
{
currentDatabaseWidget()->showImportPasskeyDialog(true);
} }
#endif #endif

View File

@ -88,6 +88,7 @@ public slots:
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
void showPasskeys(); void showPasskeys();
void importPasskey(); void importPasskey();
void importPasskeyToEntry();
#endif #endif
void performGlobalAutoType(const QString& search); void performGlobalAutoType(const QString& search);
void performBrowserUnlock(); void performBrowserUnlock();

View File

@ -1407,10 +1407,20 @@ void DatabaseWidget::switchToPasskeys()
m_reportsDialog->activatePasskeysPage(); m_reportsDialog->activatePasskeysPage();
} }
void DatabaseWidget::switchToImportPasskey() void DatabaseWidget::showImportPasskeyDialog(bool isEntry)
{ {
PasskeyImporter passkeyImporter; PasskeyImporter passkeyImporter;
passkeyImporter.importPasskey(m_db);
if (isEntry) {
auto currentEntry = currentSelectedEntry();
if (!currentEntry) {
return;
}
passkeyImporter.importPasskey(m_db, currentEntry);
} else {
passkeyImporter.importPasskey(m_db);
}
} }
#endif #endif

View File

@ -214,7 +214,7 @@ public slots:
void switchToDatabaseSettings(); void switchToDatabaseSettings();
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
void switchToPasskeys(); void switchToPasskeys();
void switchToImportPasskey(); void showImportPasskeyDialog(bool isEntry = false);
#endif #endif
void switchToOpenDatabase(); void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath); void switchToOpenDatabase(const QString& filePath);

View File

@ -135,6 +135,10 @@ MainWindow::MainWindow()
m_entryContextMenu->addSeparator(); m_entryContextMenu->addSeparator();
m_entryContextMenu->addAction(m_ui->actionEntryAutoType); m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
m_entryContextMenu->addSeparator(); m_entryContextMenu->addSeparator();
#ifdef WITH_XC_BROWSER_PASSKEYS
m_entryContextMenu->addAction(m_ui->actionEntryImportPasskey);
m_entryContextMenu->addSeparator();
#endif
m_entryContextMenu->addAction(m_ui->actionEntryEdit); m_entryContextMenu->addAction(m_ui->actionEntryEdit);
m_entryContextMenu->addAction(m_ui->actionEntryClone); m_entryContextMenu->addAction(m_ui->actionEntryClone);
m_entryContextMenu->addAction(m_ui->actionEntryDelete); m_entryContextMenu->addAction(m_ui->actionEntryDelete);
@ -441,6 +445,7 @@ MainWindow::MainWindow()
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setIcon(icons()->icon("passkey")); m_ui->actionPasskeys->setIcon(icons()->icon("passkey"));
m_ui->actionImportPasskey->setIcon(icons()->icon("document-import")); m_ui->actionImportPasskey->setIcon(icons()->icon("document-import"));
m_ui->actionEntryImportPasskey->setIcon(icons()->icon("document-import"));
#endif #endif
m_actionMultiplexer.connect( m_actionMultiplexer.connect(
@ -491,6 +496,7 @@ MainWindow::MainWindow()
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
connect(m_ui->actionPasskeys, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showPasskeys())); connect(m_ui->actionPasskeys, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showPasskeys()));
connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey())); connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey()));
connect(m_ui->actionEntryImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskeyToEntry()));
#endif #endif
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv()));
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database()));
@ -989,6 +995,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setEnabled(true); m_ui->actionPasskeys->setEnabled(true);
m_ui->actionImportPasskey->setEnabled(true); m_ui->actionImportPasskey->setEnabled(true);
m_ui->actionEntryImportPasskey->setEnabled(true);
#endif #endif
#ifdef WITH_XC_SSHAGENT #ifdef WITH_XC_SSHAGENT
bool singleEntryHasSshKey = bool singleEntryHasSshKey =
@ -1060,9 +1067,11 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
#ifdef WITH_XC_BROWSER_PASSKEYS #ifdef WITH_XC_BROWSER_PASSKEYS
m_ui->actionPasskeys->setEnabled(false); m_ui->actionPasskeys->setEnabled(false);
m_ui->actionImportPasskey->setEnabled(false); m_ui->actionImportPasskey->setEnabled(false);
m_ui->actionEntryImportPasskey->setEnabled(false);
#else #else
m_ui->actionPasskeys->setVisible(false); m_ui->actionPasskeys->setVisible(false);
m_ui->actionImportPasskey->setVisible(false); m_ui->actionImportPasskey->setVisible(false);
m_ui->actionEntryImportPasskey->setVisible(false);
#endif #endif
m_searchWidgetAction->setEnabled(false); m_searchWidgetAction->setEnabled(false);

View File

@ -342,6 +342,8 @@
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionEntryAutoType"/> <addaction name="actionEntryAutoType"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionEntryImportPasskey"/>
<addaction name="separator"/>
<addaction name="actionEntryOpenUrl"/> <addaction name="actionEntryOpenUrl"/>
<addaction name="actionEntryDownloadIcon"/> <addaction name="actionEntryDownloadIcon"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -730,6 +732,14 @@
<string>Perform &amp;Auto-Type</string> <string>Perform &amp;Auto-Type</string>
</property> </property>
</action> </action>
<action name="actionEntryImportPasskey">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Import Passkey</string>
</property>
</action>
<action name="actionEntryAutoTypeUsername"> <action name="actionEntryAutoTypeUsername">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>

View File

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de> * Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -329,11 +329,11 @@ void EditEntryWidget::insertURL()
{ {
Q_ASSERT(!m_history); Q_ASSERT(!m_history);
QString name(BrowserService::ADDITIONAL_URL); QString name(EntryAttributes::AdditionalUrlAttribute);
int i = 1; int i = 1;
while (m_entryAttributes->keys().contains(name)) { while (m_entryAttributes->keys().contains(name)) {
name = QString("%1_%2").arg(BrowserService::ADDITIONAL_URL, QString::number(i)); name = QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i));
i++; i++;
} }

View File

@ -70,7 +70,7 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const
const auto urlValid = urlTools()->isUrlValid(value); const auto urlValid = urlTools()->isUrlValid(value);
// Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison. // Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison.
auto customAttributeKeys = m_entryAttributes->customKeys().filter(BrowserService::ADDITIONAL_URL); auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute);
customAttributeKeys.removeOne(key); customAttributeKeys.removeOne(key);
const auto duplicateUrl = const auto duplicateUrl =
@ -148,7 +148,7 @@ void EntryURLModel::updateAttributes()
const auto attributesKeyList = m_entryAttributes->keys(); const auto attributesKeyList = m_entryAttributes->keys();
for (const auto& key : attributesKeyList) { for (const auto& key : attributesKeyList) {
if (!EntryAttributes::isDefaultAttribute(key) && key.contains(BrowserService::ADDITIONAL_URL)) { if (!EntryAttributes::isDefaultAttribute(key) && key.contains(EntryAttributes::AdditionalUrlAttribute)) {
const auto value = m_entryAttributes->value(key); const auto value = m_entryAttributes->value(key);
m_urls.append(qMakePair(key, value)); m_urls.append(qMakePair(key, value));

View File

@ -27,33 +27,41 @@
PasskeyImportDialog::PasskeyImportDialog(QWidget* parent) PasskeyImportDialog::PasskeyImportDialog(QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_ui(new Ui::PasskeyImportDialog()) , m_ui(new Ui::PasskeyImportDialog())
, m_useDefaultGroup(true)
{ {
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
m_ui->setupUi(this); m_ui->setupUi(this);
m_ui->useDefaultGroupCheckbox->setChecked(true);
m_ui->selectGroupComboBox->setEnabled(false);
connect(this, SIGNAL(updateGroups()), this, SLOT(addGroups()));
connect(this, SIGNAL(updateEntries()), this, SLOT(addEntries()));
connect(m_ui->importButton, SIGNAL(clicked()), SLOT(accept())); connect(m_ui->importButton, SIGNAL(clicked()), SLOT(accept()));
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject())); connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
connect(m_ui->selectDatabaseButton, SIGNAL(clicked()), SLOT(selectDatabase())); connect(m_ui->selectDatabaseCombobBox, SIGNAL(currentIndexChanged(int)), SLOT(changeDatabase(int)));
connect(m_ui->selectEntryComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeEntry(int)));
connect(m_ui->selectGroupComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeGroup(int))); connect(m_ui->selectGroupComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeGroup(int)));
connect(m_ui->useDefaultGroupCheckbox, SIGNAL(stateChanged(int)), SLOT(useDefaultGroupChanged()));
} }
PasskeyImportDialog::~PasskeyImportDialog() PasskeyImportDialog::~PasskeyImportDialog()
{ {
} }
void PasskeyImportDialog::setInfo(const QString& url, const QString& username, const QSharedPointer<Database>& database) void PasskeyImportDialog::setInfo(const QString& url,
const QString& username,
const QSharedPointer<Database>& database,
bool isEntry)
{ {
m_ui->urlLabel->setText(tr("URL: %1").arg(url)); m_ui->urlLabel->setText(tr("URL: %1").arg(url));
m_ui->usernameLabel->setText(tr("Username: %1").arg(username)); m_ui->usernameLabel->setText(tr("Username: %1").arg(username));
m_ui->selectDatabaseLabel->setText(tr("Database: %1").arg(getDatabaseName(database)));
m_ui->selectGroupLabel->setText(tr("Group:"));
addGroups(database); if (isEntry) {
m_ui->verticalLayout->setSizeConstraint(QLayout::SetFixedSize);
m_ui->infoLabel->setText(tr("Import the following Passkey to this entry:"));
m_ui->groupBox->setVisible(false);
}
m_selectedDatabase = database;
addDatabases();
addGroups();
auto openDatabaseCount = 0; auto openDatabaseCount = 0;
for (auto dbWidget : getMainWindow()->getOpenDatabases()) { for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
@ -61,34 +69,96 @@ void PasskeyImportDialog::setInfo(const QString& url, const QString& username, c
openDatabaseCount++; openDatabaseCount++;
} }
} }
m_ui->selectDatabaseButton->setEnabled(openDatabaseCount > 1); m_ui->selectDatabaseCombobBox->setEnabled(openDatabaseCount > 1);
} }
QSharedPointer<Database> PasskeyImportDialog::getSelectedDatabase() QSharedPointer<Database> PasskeyImportDialog::getSelectedDatabase() const
{ {
return m_selectedDatabase; return m_selectedDatabase;
} }
QUuid PasskeyImportDialog::getSelectedGroupUuid() QUuid PasskeyImportDialog::getSelectedEntryUuid() const
{
return m_selectedEntryUuid;
}
QUuid PasskeyImportDialog::getSelectedGroupUuid() const
{ {
return m_selectedGroupUuid; return m_selectedGroupUuid;
} }
bool PasskeyImportDialog::useDefaultGroup() bool PasskeyImportDialog::useDefaultGroup() const
{ {
return m_useDefaultGroup; return m_selectedGroupUuid.isNull();
} }
QString PasskeyImportDialog::getDatabaseName(const QSharedPointer<Database>& database) const bool PasskeyImportDialog::createNewEntry() const
{ {
return QFileInfo(database->filePath()).fileName(); return m_selectedEntryUuid.isNull();
} }
void PasskeyImportDialog::addGroups(const QSharedPointer<Database>& database) void PasskeyImportDialog::addDatabases()
{ {
auto currentDatabaseIndex = 0;
const auto openDatabases = browserService()->getOpenDatabases();
const auto currentDatabase = browserService()->getDatabase();
m_ui->selectDatabaseCombobBox->clear();
for (const auto& db : openDatabases) {
m_ui->selectDatabaseCombobBox->addItem(db->metadata()->name(), db->rootGroup()->uuid());
if (db->rootGroup()->uuid() == currentDatabase->rootGroup()->uuid()) {
currentDatabaseIndex = m_ui->selectDatabaseCombobBox->count() - 1;
}
}
m_ui->selectDatabaseCombobBox->setCurrentIndex(currentDatabaseIndex);
}
void PasskeyImportDialog::addEntries()
{
if (!m_selectedDatabase || !m_selectedDatabase->rootGroup()) {
return;
}
m_ui->selectEntryComboBox->clear();
m_ui->selectEntryComboBox->addItem(tr("Create new entry"), {});
const auto group = m_selectedDatabase->rootGroup()->findGroupByUuid(m_selectedGroupUuid);
if (!group) {
return;
}
// Collect all entries in the group and resolve the title
QList<QPair<QString, QUuid>> entries;
for (const auto entry : group->entries()) {
if (!entry || entry->isRecycled()) {
continue;
}
entries.append({entry->resolveMultiplePlaceholders(entry->title()), entry->uuid()});
}
// Sort entries by title
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
return a.first.compare(b.first, Qt::CaseInsensitive) < 0;
});
// Add sorted entries to the combobox
for (const auto& pair : entries) {
m_ui->selectEntryComboBox->addItem(pair.first, pair.second);
}
}
void PasskeyImportDialog::addGroups()
{
if (!m_selectedDatabase) {
return;
}
m_ui->selectGroupComboBox->clear(); m_ui->selectGroupComboBox->clear();
for (const auto& group : database->rootGroup()->groupsRecursive(true)) { m_ui->selectGroupComboBox->addItem(tr("Default Passkeys group (Imported Passkeys)"), {});
if (!group || group->isRecycled() || group == database->metadata()->recycleBin()) {
for (const auto& group : m_selectedDatabase->rootGroup()->groupsRecursive(true)) {
if (!group || group->isRecycled() || group == m_selectedDatabase->metadata()->recycleBin()) {
continue; continue;
} }
@ -96,26 +166,20 @@ void PasskeyImportDialog::addGroups(const QSharedPointer<Database>& database)
} }
} }
void PasskeyImportDialog::selectDatabase() void PasskeyImportDialog::changeDatabase(int index)
{ {
auto selectedDatabase = browserService()->selectedDatabase(); m_selectedDatabaseUuid = m_ui->selectDatabaseCombobBox->itemData(index).value<QUuid>();
if (!selectedDatabase) { m_selectedDatabase = browserService()->getDatabase(m_selectedDatabaseUuid);
return; emit updateGroups();
} }
m_selectedDatabase = selectedDatabase; void PasskeyImportDialog::changeEntry(int index)
m_ui->selectDatabaseLabel->setText(QString("Database: %1").arg(getDatabaseName(m_selectedDatabase))); {
m_selectedEntryUuid = m_ui->selectEntryComboBox->itemData(index).value<QUuid>();
addGroups(m_selectedDatabase);
} }
void PasskeyImportDialog::changeGroup(int index) void PasskeyImportDialog::changeGroup(int index)
{ {
m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value<QUuid>(); m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value<QUuid>();
} emit updateEntries();
void PasskeyImportDialog::useDefaultGroupChanged()
{
m_ui->selectGroupComboBox->setEnabled(!m_ui->useDefaultGroupCheckbox->isChecked());
m_useDefaultGroup = m_ui->useDefaultGroupCheckbox->isChecked();
} }

View File

@ -36,25 +36,33 @@ public:
explicit PasskeyImportDialog(QWidget* parent = nullptr); explicit PasskeyImportDialog(QWidget* parent = nullptr);
~PasskeyImportDialog() override; ~PasskeyImportDialog() override;
void setInfo(const QString& url, const QString& username, const QSharedPointer<Database>& database); void setInfo(const QString& url, const QString& username, const QSharedPointer<Database>& database, bool isEntry);
QSharedPointer<Database> getSelectedDatabase(); QSharedPointer<Database> getSelectedDatabase() const;
QUuid getSelectedGroupUuid(); QUuid getSelectedEntryUuid() const;
bool useDefaultGroup(); QUuid getSelectedGroupUuid() const;
bool useDefaultGroup() const;
bool createNewEntry() const;
private: private:
QString getDatabaseName(const QSharedPointer<Database>& database) const; void addDatabases();
void addGroups(const QSharedPointer<Database>& database);
signals:
void updateEntries();
void updateGroups();
private slots: private slots:
void selectDatabase(); void addEntries();
void addGroups();
void changeDatabase(int index);
void changeEntry(int index);
void changeGroup(int index); void changeGroup(int index);
void useDefaultGroupChanged();
private: private:
QScopedPointer<Ui::PasskeyImportDialog> m_ui; QScopedPointer<Ui::PasskeyImportDialog> m_ui;
QSharedPointer<Database> m_selectedDatabase; QSharedPointer<Database> m_selectedDatabase;
QUuid m_selectedDatabaseUuid;
QUuid m_selectedEntryUuid;
QUuid m_selectedGroupUuid; QUuid m_selectedGroupUuid;
bool m_useDefaultGroup;
}; };
#endif // KEEPASSXC_PASSKEYIMPORTDIALOG_H #endif // KEEPASSXC_PASSKEYIMPORTDIALOG_H

View File

@ -6,10 +6,22 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>405</width> <width>500</width>
<height>227</height> <height>300</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>KeePassXC - Passkey Import</string> <string>KeePassXC - Passkey Import</string>
</property> </property>
@ -24,7 +36,7 @@
</font> </font>
</property> </property>
<property name="text"> <property name="text">
<string>Do you want to import the Passkey?</string> <string>Import the following Passkey:</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
@ -52,80 +64,62 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="useDefaultGroupCheckbox"> <spacer name="verticalSpacer">
<property name="text"> <property name="orientation">
<string>Use default group (Imported Passkeys)</string> <enum>Qt::Vertical</enum>
</property> </property>
<property name="checked"> <property name="sizeHint" stdset="0">
<bool>false</bool> <size>
<width>20</width>
<height>10</height>
</size>
</property> </property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QFormLayout" name="selectGroupHorLayout">
<item row="0" column="0">
<widget class="QLabel" name="databaseLabel">
<property name="text">
<string>Database</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="selectDatabaseCombobBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="groupLabel">
<property name="text">
<string>Group</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="selectGroupComboBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="entryLabel">
<property name="text">
<string>Entry</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="selectEntryComboBox"/>
</item>
</layout>
</item>
</layout>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="selectGroupHorLayout">
<item>
<widget class="QLabel" name="selectGroupLabel">
<property name="text">
<string>Group</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="selectGroupComboBox"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="selectDatabaseHorLayout">
<item>
<widget class="QLabel" name="selectDatabaseLabel">
<property name="text">
<string>Database</string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="selectDatabaseButton">
<property name="text">
<string>Select Database</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -22,6 +22,7 @@
#include "browser/BrowserService.h" #include "browser/BrowserService.h"
#include "core/Entry.h" #include "core/Entry.h"
#include "core/Group.h" #include "core/Group.h"
#include "core/Tools.h"
#include "gui/FileDialog.h" #include "gui/FileDialog.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include <QFileInfo> #include <QFileInfo>
@ -29,7 +30,7 @@
static const QString IMPORTED_PASSKEYS_GROUP = QStringLiteral("Imported Passkeys"); static const QString IMPORTED_PASSKEYS_GROUP = QStringLiteral("Imported Passkeys");
void PasskeyImporter::importPasskey(QSharedPointer<Database>& database) void PasskeyImporter::importPasskey(QSharedPointer<Database>& database, Entry* entry)
{ {
auto filter = QString("%1 (*.passkey);;%2 (*)").arg(tr("Passkey file"), tr("All files")); auto filter = QString("%1 (*.passkey);;%2 (*)").arg(tr("Passkey file"), tr("All files"));
auto fileName = auto fileName =
@ -47,10 +48,10 @@ void PasskeyImporter::importPasskey(QSharedPointer<Database>& database)
return; return;
} }
importSelectedFile(file, database); importSelectedFile(file, database, entry);
} }
void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>& database) void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>& database, Entry* entry)
{ {
const auto fileData = file.readAll(); const auto fileData = file.readAll();
const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData); const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData);
@ -61,18 +62,20 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>&
return; return;
} }
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();
const auto privateKey = passkeyObject["privateKey"].toString(); const auto privateKey = passkeyObject["privateKey"].toString();
const auto missingKeys = Tools::getMissingValuesFromList<QString>(passkeyObject.keys(),
QStringList() << "relyingParty"
<< "url"
<< "username"
<< "credentialId"
<< "userHandle"
<< "privateKey");
if (relyingParty.isEmpty() || username.isEmpty() || credentialId.isEmpty() || userHandle.isEmpty() if (!missingKeys.isEmpty()) {
|| privateKey.isEmpty()) {
MessageBox::information(nullptr, MessageBox::information(nullptr,
tr("Cannot import Passkey"), tr("Cannot import Passkey"),
tr("Cannot import Passkey file \"%1\". Data is missing.").arg(file.fileName())); 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-----") } else if (!privateKey.startsWith("-----BEGIN PRIVATE KEY-----")
|| !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) { || !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) {
MessageBox::information( MessageBox::information(
@ -80,7 +83,12 @@ void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>&
tr("Cannot import Passkey"), tr("Cannot import Passkey"),
tr("Cannot import Passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName())); tr("Cannot import Passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName()));
} else { } else {
showImportDialog(database, url, relyingParty, username, credentialId, userHandle, privateKey); 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);
} }
} }
@ -90,10 +98,11 @@ void PasskeyImporter::showImportDialog(QSharedPointer<Database>& database,
const QString& username, const QString& username,
const QString& credentialId, const QString& credentialId,
const QString& userHandle, const QString& userHandle,
const QString& privateKey) const QString& privateKey,
Entry* entry)
{ {
PasskeyImportDialog passkeyImportDialog; PasskeyImportDialog passkeyImportDialog;
passkeyImportDialog.setInfo(relyingParty, username, database); passkeyImportDialog.setInfo(relyingParty, username, database, entry != nullptr);
auto ret = passkeyImportDialog.exec(); auto ret = passkeyImportDialog.exec();
if (ret != QDialog::Accepted) { if (ret != QDialog::Accepted) {
@ -105,6 +114,29 @@ void PasskeyImporter::showImportDialog(QSharedPointer<Database>& database,
db = database; db = database;
} }
// Store to entry if given directly
if (entry) {
browserService()->addPasskeyToEntry(
entry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
return;
}
// Import to entry selected instead of creating a new one
if (!passkeyImportDialog.createNewEntry()) {
auto groupUuid = passkeyImportDialog.getSelectedGroupUuid();
auto group = db->rootGroup()->findGroupByUuid(groupUuid);
if (group) {
auto selectedEntry = group->findEntryByUuid(passkeyImportDialog.getSelectedEntryUuid());
if (selectedEntry) {
browserService()->addPasskeyToEntry(
selectedEntry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
}
}
return;
}
// Group settings. Use default group "Imported Passkeys" if user did not select a specific one. // Group settings. Use default group "Imported Passkeys" if user did not select a specific one.
Group* group = nullptr; Group* group = nullptr;
@ -123,7 +155,7 @@ void PasskeyImporter::showImportDialog(QSharedPointer<Database>& database,
group, url, relyingParty, relyingParty, username, credentialId, userHandle, privateKey); group, url, relyingParty, relyingParty, username, credentialId, userHandle, privateKey);
} }
Group* PasskeyImporter::getDefaultGroup(QSharedPointer<Database>& database) Group* PasskeyImporter::getDefaultGroup(QSharedPointer<Database>& database) const
{ {
auto defaultGroup = database->rootGroup()->findGroupByPath(IMPORTED_PASSKEYS_GROUP); auto defaultGroup = database->rootGroup()->findGroupByPath(IMPORTED_PASSKEYS_GROUP);

View File

@ -21,6 +21,7 @@
#include "core/Database.h" #include "core/Database.h"
#include <QFile> #include <QFile>
#include <QObject> #include <QObject>
#include <QUuid>
class Entry; class Entry;
@ -31,18 +32,19 @@ class PasskeyImporter : public QObject
public: public:
explicit PasskeyImporter() = default; explicit PasskeyImporter() = default;
void importPasskey(QSharedPointer<Database>& database); void importPasskey(QSharedPointer<Database>& database, Entry* entry = nullptr);
private: private:
void importSelectedFile(QFile& file, QSharedPointer<Database>& database); void importSelectedFile(QFile& file, QSharedPointer<Database>& database, Entry* entry);
void showImportDialog(QSharedPointer<Database>& database, void showImportDialog(QSharedPointer<Database>& database,
const QString& url, const QString& url,
const QString& relyingParty, const QString& relyingParty,
const QString& username, const QString& username,
const QString& credentialId, const QString& credentialId,
const QString& userHandle, const QString& userHandle,
const QString& privateKey); const QString& privateKey,
Group* getDefaultGroup(QSharedPointer<Database>& database); Entry* entry);
Group* getDefaultGroup(QSharedPointer<Database>& database) const;
}; };
#endif // KEEPASSXC_PASSKEYIMPORTER_H #endif // KEEPASSXC_PASSKEYIMPORTER_H

View File

@ -341,8 +341,8 @@ void TestBrowser::testSearchEntriesByReference()
auto secondEntryUuid = entries[1]->uuidToHex(); auto secondEntryUuid = entries[1]->uuidToHex();
auto fullReference = QString("{REF:A@I:%1}").arg(firstEntryUuid); auto fullReference = QString("{REF:A@I:%1}").arg(firstEntryUuid);
auto partialReference = QString("https://subdomain.{REF:A@I:%1}").arg(secondEntryUuid); auto partialReference = QString("https://subdomain.{REF:A@I:%1}").arg(secondEntryUuid);
entries[2]->attributes()->set(BrowserService::ADDITIONAL_URL, fullReference); entries[2]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, fullReference);
entries[3]->attributes()->set(BrowserService::ADDITIONAL_URL, partialReference); entries[3]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, partialReference);
entries[4]->setUrl(fullReference); entries[4]->setUrl(fullReference);
entries[5]->setUrl(partialReference); entries[5]->setUrl(partialReference);
@ -351,11 +351,13 @@ void TestBrowser::testSearchEntriesByReference()
QCOMPARE(result[0]->url(), urls[0]); QCOMPARE(result[0]->url(), urls[0]);
QCOMPARE(result[1]->url(), urls[1]); QCOMPARE(result[1]->url(), urls[1]);
QCOMPARE(result[2]->url(), urls[2]); QCOMPARE(result[2]->url(), urls[2]);
QCOMPARE(result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(BrowserService::ADDITIONAL_URL)), QCOMPARE(
urls[0]); result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
urls[0]);
QCOMPARE(result[3]->url(), urls[3]); QCOMPARE(result[3]->url(), urls[3]);
QCOMPARE(result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(BrowserService::ADDITIONAL_URL)), QCOMPARE(
urls[0]); result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
urls[0]);
QCOMPARE(result[4]->url(), fullReference); QCOMPARE(result[4]->url(), fullReference);
QCOMPARE(result[4]->resolveMultiplePlaceholders(result[4]->url()), urls[0]); // Should be resolved to the main entry QCOMPARE(result[4]->resolveMultiplePlaceholders(result[4]->url()), urls[0]); // Should be resolved to the main entry
QCOMPARE(result[5]->url(), partialReference); QCOMPARE(result[5]->url(), partialReference);
@ -386,7 +388,7 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
auto entries = createEntries(urls, root); auto entries = createEntries(urls, root);
// Add an additional URL to the first entry // Add an additional URL to the first entry
entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org"); entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://keepassxc.org");
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1); QCOMPARE(result.length(), 1);
@ -663,7 +665,7 @@ void TestBrowser::testBestMatchingWithAdditionalURLs()
browserSettings()->setBestMatchOnly(true); browserSettings()->setBestMatchOnly(true);
// Add an additional URL to the first entry // Add an additional URL to the first entry
entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://test.github.com/anotherpage"); entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://test.github.com/anotherpage");
// The first entry should be triggered // The first entry should be triggered
auto result = m_browserService->searchEntries( auto result = m_browserService->searchEntries(

View File

@ -19,6 +19,9 @@
#include "browser/BrowserCbor.h" #include "browser/BrowserCbor.h"
#include "browser/BrowserMessageBuilder.h" #include "browser/BrowserMessageBuilder.h"
#include "browser/BrowserService.h" #include "browser/BrowserService.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "crypto/Crypto.h" #include "crypto/Crypto.h"
#include <QJsonArray> #include <QJsonArray>
@ -469,3 +472,27 @@ void TestPasskeys::testSetFlags()
auto discouragedResult = browserPasskeys()->setFlagsFromJson(discouragedJson); auto discouragedResult = browserPasskeys()->setFlagsFromJson(discouragedJson);
QCOMPARE(discouragedResult, 0x01); QCOMPARE(discouragedResult, 0x01);
} }
void TestPasskeys::testEntry()
{
Database db;
auto* root = db.rootGroup();
root->setUuid(QUuid::createUuid());
auto* group1 = new Group();
group1->setUuid(QUuid::createUuid());
group1->setParent(root);
auto* entry = new Entry();
entry->setGroup(root);
browserService()->addPasskeyToEntry(entry,
QString("example.com"),
QString("example.com"),
QString("username"),
QString("userId"),
QString("userHandle"),
QString("privateKey"));
QVERIFY(entry->hasPasskey());
}

View File

@ -43,5 +43,7 @@ private slots:
void testExtensions(); void testExtensions();
void testParseFlags(); void testParseFlags();
void testSetFlags(); void testSetFlags();
void testEntry();
}; };
#endif // KEEPASSXC_TESTPASSKEYS_H #endif // KEEPASSXC_TESTPASSKEYS_H

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -239,3 +239,30 @@ void TestTools::testConvertToRegex_data()
<< input << static_cast<int>(Tools::RegexConvertOpts::WILDCARD_UNLIMITED_MATCH) << input << static_cast<int>(Tools::RegexConvertOpts::WILDCARD_UNLIMITED_MATCH)
<< QString(R"(te\|st.*t\?\[5\]\^\(test\)\;\'\,\.)"); << QString(R"(te\|st.*t\?\[5\]\^\(test\)\;\'\,\.)");
} }
void TestTools::testArrayContainsValues()
{
const auto values = QStringList() << "first"
<< "second"
<< "third";
// One missing
const auto result1 = Tools::getMissingValuesFromList<QString>(values,
QStringList() << "first"
<< "second"
<< "none");
QCOMPARE(result1.length(), 1);
QCOMPARE(result1.first(), QString("none"));
// All found
const auto result2 = Tools::getMissingValuesFromList<QString>(values,
QStringList() << "first"
<< "second"
<< "third");
QCOMPARE(result2.length(), 0);
// None are found
const auto numberValues = QList<int>({1, 2, 3, 4, 5});
const auto result3 = Tools::getMissingValuesFromList<int>(numberValues, QList<int>({6, 7, 8}));
QCOMPARE(result3.length(), 3);
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -35,6 +35,7 @@ private slots:
void testEscapeRegex_data(); void testEscapeRegex_data();
void testConvertToRegex(); void testConvertToRegex();
void testConvertToRegex_data(); void testConvertToRegex_data();
void testArrayContainsValues();
}; };
#endif // KEEPASSX_TESTTOOLS_H #endif // KEEPASSX_TESTTOOLS_H