/* * 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 * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PasskeyImporter.h" #include "PasskeyImportDialog.h" #include "browser/BrowserMessageBuilder.h" #include "browser/BrowserPasskeys.h" #include "browser/BrowserService.h" #include "core/Entry.h" #include "core/Group.h" #include "core/Tools.h" #include "gui/FileDialog.h" #include "gui/MessageBox.h" #include #include static const QString IMPORTED_PASSKEYS_GROUP = QStringLiteral("Imported Passkeys"); PasskeyImporter::PasskeyImporter(QWidget* parent) : m_parent(parent) { } void PasskeyImporter::importPasskey(QSharedPointer& database, Entry* entry) { 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); if (fileName.isEmpty()) { return; } FileDialog::saveLastDir("passkey", fileName, true); QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { MessageBox::information( nullptr, tr("Cannot open file"), tr("Cannot open file \"%1\" for reading.").arg(fileName)); return; } importSelectedFile(file, database, entry); } void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer& database, Entry* entry) { const auto fileData = file.readAll(); const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData); if (passkeyObject.isEmpty()) { MessageBox::information(m_parent, tr("Cannot import passkey"), tr("Cannot import passkey file \"%1\". Data is missing.").arg(file.fileName())); return; } const auto privateKey = passkeyObject["privateKey"].toString(); const auto missingKeys = Tools::getMissingValuesFromList(passkeyObject.keys(), QStringList() << "relyingParty" << "url" << "username" << "credentialId" << "userHandle" << "privateKey"); if (!missingKeys.isEmpty()) { MessageBox::information(m_parent, 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( m_parent, 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, entry, url, relyingParty, username, credentialId, userHandle, privateKey); } } 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, const QString& titleText, const QString& infoText, const QString& importButtonText) { PasskeyImportDialog passkeyImportDialog(m_parent); passkeyImportDialog.setInfo( relyingParty, username, database, entry != nullptr, titleText, infoText, importButtonText); auto ret = passkeyImportDialog.exec(); if (ret != QDialog::Accepted) { return false; } auto db = passkeyImportDialog.getSelectedDatabase(); if (!db) { db = database; } // Store to entry if given directly if (entry) { browserService()->addPasskeyToEntry( entry, relyingParty, relyingParty, username, credentialId, userHandle, privateKey); return true; } // 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 true; } // Group settings. Use default group "Imported Passkeys" if user did not select a specific one. Group* group = nullptr; // Attempt to use the selected group if (!passkeyImportDialog.useDefaultGroup()) { auto groupUuid = passkeyImportDialog.getSelectedGroupUuid(); group = db->rootGroup()->findGroupByUuid(groupUuid); } // Use default group if requested or if the selected group does not exist if (!group) { group = getDefaultGroup(db); } browserService()->addPasskeyToGroup( db, group, url, relyingParty, relyingParty, username, credentialId, userHandle, privateKey); return true; } Group* PasskeyImporter::getDefaultGroup(QSharedPointer& database) const { auto defaultGroup = database->rootGroup()->findGroupByPath(IMPORTED_PASSKEYS_GROUP); // Create the default group if it does not exist if (!defaultGroup) { defaultGroup = new Group(); defaultGroup->setName(IMPORTED_PASSKEYS_GROUP); defaultGroup->setUuid(QUuid::createUuid()); defaultGroup->setParent(database->rootGroup()); } return defaultGroup; }