Skip to content

Commit

Permalink
Passkeys: Add support for importing Passkey to entry (#9987)
Browse files Browse the repository at this point in the history
---------
Co-authored-by: Jonathan White <[email protected]>
  • Loading branch information
droidmonkey committed Jan 30, 2024
1 parent 7371589 commit c477f43
Show file tree
Hide file tree
Showing 27 changed files with 455 additions and 172 deletions.
39 changes: 27 additions & 12 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,15 @@ Do you want to delete the entry?
<source>%1 (Passkey)</source>
<translation type="unfinished"></translation>
</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>
<message>
<source>Converting attributes to custom data…</source>
<translation type="unfinished"></translation>
Expand Down Expand Up @@ -6018,51 +6027,51 @@ Do you want to overwrite it?
<translation type="unfinished"></translation>
</message>
<message>
<source>Do you want to import the Passkey?</source>
<source>URL: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>URL: %1</source>
<source>Username: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Username: %1</source>
<source>Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use default group (Imported Passkeys)</source>
<source>Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Group</source>
<source>Import Passkey</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database</source>
<source>Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select Database</source>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import Passkey</source>
<source>Import the following Passkey:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import</source>
<source>Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cancel</source>
<source>Import the following Passkey to this entry:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database: %1</source>
<source>Create new entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Group:</source>
<source>Default Passkeys group (Imported Passkeys)</source>
<translation type="unfinished"></translation>
</message>
</context>
Expand Down Expand Up @@ -6100,6 +6109,12 @@ Do you want to overwrite it?
<source>Cannot import Passkey file &quot;%1&quot;. Private key is missing or malformed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot import Passkey file &quot;%1&quot;.
The following data is missing:
%2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PasswordEditWidget</name>
Expand Down
47 changes: 42 additions & 5 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
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);

Expand Down Expand Up @@ -727,6 +725,20 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
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->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
Expand Down Expand Up @@ -1110,7 +1122,13 @@ void BrowserService::denyEntry(Entry* entry, const QString& siteHost, const QStr
QJsonObject BrowserService::prepareEntry(const Entry* entry)
{
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());
#endif
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
Expand Down Expand Up @@ -1317,8 +1335,7 @@ QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const Strin
{
QList<Entry*> entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
if (entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
entries << entry;
}
}
Expand Down Expand Up @@ -1445,14 +1462,34 @@ bool BrowserService::handleURL(const QString& entryUrl,
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) {
return m_currentDatabaseWidget->database();
}
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()
{
QList<DatabaseWidget*> databaseWidgets;
Expand Down
4 changes: 2 additions & 2 deletions src/browser/BrowserService.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ class BrowserService : public QObject
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const;
QSharedPointer<Database> getDatabase(const QUuid& rootGroupUuid = {});
QSharedPointer<Database> selectedDatabase();
QList<QSharedPointer<Database>> getOpenDatabases();
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
Expand Down Expand Up @@ -124,7 +126,6 @@ class BrowserService : public QObject
static const QString OPTION_ONLY_HTTP_AUTH;
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;

signals:
void requestUnlock();
Expand Down Expand Up @@ -191,7 +192,6 @@ private slots:
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
QSharedPointer<Database> getDatabase();
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();
bool checkLegacySettings(QSharedPointer<Database> db);
Expand Down
8 changes: 7 additions & 1 deletion src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ QStringList Entry::getAllUrls() const
}

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);
if (!additionalUrl.isEmpty()) {
urlList << resolveMultiplePlaceholders(additionalUrl);
Expand Down Expand Up @@ -545,6 +546,11 @@ bool Entry::hasTotp() const
return !m_data.totpSettings.isNull();
}

bool Entry::hasPasskey() const
{
return m_attributes->hasPasskey();
}

QString Entry::totp() const
{
if (hasTotp()) {
Expand Down
1 change: 1 addition & 0 deletions src/core/Entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class Entry : public ModifiableObject
void setExcludeFromReports(bool state);

bool hasTotp() const;
bool hasPasskey() const;
bool isExpired() const;
bool willExpireInDays(int days) const;
bool isRecycled() const;
Expand Down
13 changes: 13 additions & 0 deletions src/core/EntryAttributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const QString EntryAttributes::SearchInGroupName = "SearchIn";
const QString EntryAttributes::SearchTextGroupName = "SearchText";

const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
const QString EntryAttributes::AdditionalUrlAttribute = "KP2A_URL";
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";

EntryAttributes::EntryAttributes(QObject* parent)
Expand All @@ -52,6 +53,18 @@ bool EntryAttributes::hasKey(const QString& key) const
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> customKeys;
Expand Down
4 changes: 3 additions & 1 deletion src/core/EntryAttributes.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
* Copyright (C) 2012 Felix Geyer <[email protected]>
* Copyright (C) 2017 KeePassXC Team <[email protected]>
*
* 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
Expand Down Expand Up @@ -33,6 +33,7 @@ class EntryAttributes : public ModifiableObject
explicit EntryAttributes(QObject* parent = nullptr);
QList<QString> keys() const;
bool hasKey(const QString& key) const;
bool hasPasskey() const;
QList<QString> customKeys() const;
QString value(const QString& key) const;
QList<QString> values(const QList<QString>& keys) const;
Expand Down Expand Up @@ -61,6 +62,7 @@ class EntryAttributes : public ModifiableObject
static const QString NotesKey;
static const QStringList DefaultAttributes;
static const QString RememberCmdExecAttr;
static const QString AdditionalUrlAttribute;
static const QString PasskeyAttribute;
static bool isDefaultAttribute(const QString& key);
static bool isPasskeyAttribute(const QString& key);
Expand Down
14 changes: 14 additions & 0 deletions src/core/Tools.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "core/Global.h"

#include <QDateTime>
#include <QList>
#include <QProcessEnvironment>

class QIODevice;
Expand Down Expand Up @@ -100,6 +101,19 @@ namespace Tools
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"});

QString substituteBackupFilePath(QString pattern, const QString& databasePath);
Expand Down
5 changes: 3 additions & 2 deletions src/format/OpVaultReaderBandEntry.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 KeePassXC Team <[email protected]>
* Copyright (C) 2023 KeePassXC Team <[email protected]>
*
* 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
Expand Down Expand Up @@ -243,7 +243,8 @@ bool OpVaultReader::fillAttributes(Entry* entry, const QJsonObject& bandEntry)
auto newUrl = urlObj["u"].toString();
if (newUrl != url) {
// 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;
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/gui/DatabaseTabWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,12 @@ void DatabaseTabWidget::showPasskeys()

void DatabaseTabWidget::importPasskey()
{
currentDatabaseWidget()->switchToImportPasskey();
currentDatabaseWidget()->showImportPasskeyDialog();
}

void DatabaseTabWidget::importPasskeyToEntry()
{
currentDatabaseWidget()->showImportPasskeyDialog(true);
}
#endif

Expand Down
1 change: 1 addition & 0 deletions src/gui/DatabaseTabWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public slots:
#ifdef WITH_XC_BROWSER_PASSKEYS
void showPasskeys();
void importPasskey();
void importPasskeyToEntry();
#endif
void performGlobalAutoType(const QString& search);
void performBrowserUnlock();
Expand Down
14 changes: 12 additions & 2 deletions src/gui/DatabaseWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1403,10 +1403,20 @@ void DatabaseWidget::switchToPasskeys()
m_reportsDialog->activatePasskeysPage();
}

void DatabaseWidget::switchToImportPasskey()
void DatabaseWidget::showImportPasskeyDialog(bool isEntry)
{
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

Expand Down
2 changes: 1 addition & 1 deletion src/gui/DatabaseWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public slots:
void switchToDatabaseSettings();
#ifdef WITH_XC_BROWSER_PASSKEYS
void switchToPasskeys();
void switchToImportPasskey();
void showImportPasskeyDialog(bool isEntry = false);
#endif
void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath);
Expand Down
Loading

0 comments on commit c477f43

Please sign in to comment.