mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
parent
c1a66a8be9
commit
6bbf9d1be2
1
COPYING
1
COPYING
@ -137,6 +137,7 @@ Files: share/icons/badges/2_Expired.svg
|
||||
share/icons/database/C46_Help.svg
|
||||
share/icons/database/C53_Apply.svg
|
||||
share/icons/database/C61_Services.svg
|
||||
share/icons/application/scalable/actions/proton.svg
|
||||
Copyright: 2022 KeePassXC Team <team@keepassxc.org>
|
||||
License: MIT
|
||||
|
||||
|
1
share/icons/application/scalable/actions/proton.svg
Normal file
1
share/icons/application/scalable/actions/proton.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M22.5 101.382V88.3094c0-9.1756 7.4383-16.6154 16.6154-16.6154l11.528.1398c12.5786 0 21.5115-2.3424 26.7973-7.0258 5.2858-4.6835 7.9851-8.4576 7.9851-16.9566 0-6.1551-1.6287-11.224-4.7734-15.5733-3.0775-4.4165-7.1586-7.3271-12.2445-8.7317-3.2789-.8707-9.334-1.3047-18.1656-1.3047l-9.1856-.1284v28.1362H22.5V4.75l26.1364.1284c9.768 0 17.2292.4682 22.3808 1.4046 7.2271 1.2048 13.2823 3.513 18.167 6.926 4.8847 3.3445 8.7988 8.0622 11.7422 14.1516 3.0119 6.088 4.5735 12.5958 4.5735 19.89 0 12.5115-4.0382 20.301-12.0005 28.9998-7.9623 8.6303-22.9161 12.4345-43.7268 12.4345 0 0-7.5968.1384-8.716.1384-8.4061 0-15.6061 5.2016-18.5566 12.5586ZM22.5 124.2501c0-13.2305 8.192-24.0806 18.5567-24.9993v24.902L22.5 124.25Z"/></svg>
|
After Width: | Height: | Size: 900 B |
@ -70,6 +70,7 @@
|
||||
<file>application/scalable/actions/password-generator.svg</file>
|
||||
<file>application/scalable/actions/password-show-off.svg</file>
|
||||
<file>application/scalable/actions/password-show-on.svg</file>
|
||||
<file>application/scalable/actions/proton.svg</file>
|
||||
<file>application/scalable/actions/qrcode.svg</file>
|
||||
<file>application/scalable/actions/refresh.svg</file>
|
||||
<file>application/scalable/actions/remote-sync.svg</file>
|
||||
|
@ -4585,6 +4585,14 @@ You can enable the DuckDuckGo website icon service in the security section of th
|
||||
<source>KeePass1 Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Proton Pass (.json)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Proton Pass JSON Export</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>KMessageWidget</name>
|
||||
@ -8902,6 +8910,14 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||
<source>Cannot generate valid passphrases because the wordlist is too short</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Encrypted files are not supported.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Proton Pass Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Delete plugin data?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -90,6 +90,7 @@ set(core_SOURCES
|
||||
format/OpVaultReaderAttachments.cpp
|
||||
format/OpVaultReaderBandEntry.cpp
|
||||
format/OpVaultReaderSections.cpp
|
||||
format/ProtonPassReader.cpp
|
||||
keys/CompositeKey.cpp
|
||||
keys/FileKey.cpp
|
||||
keys/PasswordKey.cpp
|
||||
|
221
src/format/ProtonPassReader.cpp
Normal file
221
src/format/ProtonPassReader.cpp
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ProtonPassReader.h"
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "core/Entry.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "core/Tools.h"
|
||||
#include "core/Totp.h"
|
||||
#include "crypto/CryptoHash.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QMap>
|
||||
#include <QScopedPointer>
|
||||
#include <QUrl>
|
||||
|
||||
namespace
|
||||
{
|
||||
Entry* readItem(const QJsonObject& item)
|
||||
{
|
||||
const auto itemMap = item.toVariantMap();
|
||||
const auto dataMap = itemMap.value("data").toMap();
|
||||
const auto metadataMap = dataMap.value("metadata").toMap();
|
||||
|
||||
// Create entry and assign basic values
|
||||
QScopedPointer<Entry> entry(new Entry());
|
||||
entry->setUuid(QUuid::createUuid());
|
||||
entry->setTitle(metadataMap.value("name").toString());
|
||||
entry->setNotes(metadataMap.value("note").toString());
|
||||
|
||||
if (itemMap.value("pinned").toBool()) {
|
||||
entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
|
||||
}
|
||||
|
||||
// Handle specific item types
|
||||
auto type = dataMap.value("type").toString();
|
||||
|
||||
// Login
|
||||
if (type.compare("login", Qt::CaseInsensitive) == 0) {
|
||||
const auto loginMap = dataMap.value("content").toMap();
|
||||
entry->setUsername(loginMap.value("itemUsername").toString());
|
||||
entry->setPassword(loginMap.value("password").toString());
|
||||
if (loginMap.contains("totpUri")) {
|
||||
auto totp = loginMap.value("totpUri").toString();
|
||||
if (!totp.startsWith("otpauth://")) {
|
||||
QUrl url(QString("otpauth://totp/%1:%2?secret=%3")
|
||||
.arg(QString(QUrl::toPercentEncoding(entry->title())),
|
||||
QString(QUrl::toPercentEncoding(entry->username())),
|
||||
QString(QUrl::toPercentEncoding(totp))));
|
||||
totp = url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
entry->setTotp(Totp::parseSettings(totp));
|
||||
}
|
||||
|
||||
if (loginMap.contains("itemEmail")) {
|
||||
entry->attributes()->set("login_email", loginMap.value("itemEmail").toString());
|
||||
}
|
||||
|
||||
// Set the entry url(s)
|
||||
int i = 1;
|
||||
for (const auto& urlObj : loginMap.value("urls").toList()) {
|
||||
const auto url = urlObj.toString();
|
||||
if (entry->url().isEmpty()) {
|
||||
// First url encountered is set as the primary url
|
||||
entry->setUrl(url);
|
||||
} else {
|
||||
// Subsequent urls
|
||||
entry->attributes()->set(
|
||||
QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Credit Card
|
||||
else if (type.compare("creditCard", Qt::CaseInsensitive) == 0) {
|
||||
const auto cardMap = dataMap.value("content").toMap();
|
||||
entry->setUsername(cardMap.value("number").toString());
|
||||
entry->setPassword(cardMap.value("verificationNumber").toString());
|
||||
const QStringList attrs({"cardholderName", "pin", "expirationDate"});
|
||||
const QStringList sensitive({"pin"});
|
||||
for (const auto& attr : attrs) {
|
||||
auto value = cardMap.value(attr).toString();
|
||||
if (!value.isEmpty()) {
|
||||
entry->attributes()->set("card_" + attr, value, sensitive.contains(attr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse extra fields
|
||||
for (const auto& field : dataMap.value("extraFields").toList()) {
|
||||
// Derive a prefix for attribute names using the title or uuid if missing
|
||||
const auto fieldMap = field.toMap();
|
||||
auto name = fieldMap.value("fieldName").toString();
|
||||
if (entry->attributes()->hasKey(name)) {
|
||||
name = QString("%1_%2").arg(name, QUuid::createUuid().toString().mid(1, 5));
|
||||
}
|
||||
|
||||
QString value;
|
||||
const auto fieldType = fieldMap.value("type").toString();
|
||||
if (fieldType.compare("totp", Qt::CaseInsensitive) == 0) {
|
||||
value = fieldMap.value("data").toJsonObject().value("totpUri").toString();
|
||||
} else {
|
||||
value = fieldMap.value("data").toJsonObject().value("content").toString();
|
||||
}
|
||||
|
||||
entry->attributes()->set(name, value, fieldType.compare("hidden", Qt::CaseInsensitive) == 0);
|
||||
}
|
||||
|
||||
// Checked expired/deleted state
|
||||
if (itemMap.value("state").toInt() == 2) {
|
||||
entry->setExpires(true);
|
||||
entry->setExpiryTime(QDateTime::currentDateTimeUtc());
|
||||
}
|
||||
|
||||
// Collapse any accumulated history
|
||||
entry->removeHistoryItems(entry->historyItems());
|
||||
|
||||
// Adjust the created and modified times
|
||||
auto timeInfo = entry->timeInfo();
|
||||
const auto createdTime = QDateTime::fromSecsSinceEpoch(itemMap.value("createTime").toULongLong(), Qt::UTC);
|
||||
const auto modifiedTime = QDateTime::fromSecsSinceEpoch(itemMap.value("modifyTime").toULongLong(), Qt::UTC);
|
||||
timeInfo.setCreationTime(createdTime);
|
||||
timeInfo.setLastModificationTime(modifiedTime);
|
||||
timeInfo.setLastAccessTime(modifiedTime);
|
||||
entry->setTimeInfo(timeInfo);
|
||||
|
||||
return entry.take();
|
||||
}
|
||||
|
||||
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
|
||||
{
|
||||
// Create groups from vaults and store a temporary map of id -> uuid
|
||||
const auto vaults = vault.value("vaults").toObject().toVariantMap();
|
||||
for (const auto& vaultId : vaults.keys()) {
|
||||
auto vaultObj = vaults.value(vaultId).toJsonObject();
|
||||
auto group = new Group();
|
||||
group->setUuid(QUuid::createUuid());
|
||||
group->setName(vaultObj.value("name").toString());
|
||||
group->setNotes(vaultObj.value("description").toString());
|
||||
group->setParent(db->rootGroup());
|
||||
|
||||
const auto items = vaultObj.value("items").toArray();
|
||||
for (const auto& item : items) {
|
||||
auto entry = readItem(item.toObject());
|
||||
if (entry) {
|
||||
entry->setGroup(group, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ProtonPassReader::hasError()
|
||||
{
|
||||
return !m_error.isEmpty();
|
||||
}
|
||||
|
||||
QString ProtonPassReader::errorString()
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
QSharedPointer<Database> ProtonPassReader::convert(const QString& path)
|
||||
{
|
||||
m_error.clear();
|
||||
|
||||
QFileInfo fileinfo(path);
|
||||
if (!fileinfo.exists()) {
|
||||
m_error = QObject::tr("File does not exist.").arg(path);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Bitwarden uses a json file format
|
||||
QFile file(fileinfo.absoluteFilePath());
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
m_error = QObject::tr("Cannot open file: %1").arg(file.errorString());
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
auto json = QJsonDocument::fromJson(file.readAll(), &error).object();
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
m_error =
|
||||
QObject::tr("Cannot parse file: %1 at position %2").arg(error.errorString(), QString::number(error.offset));
|
||||
return {};
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
if (json.value("encrypted").toBool()) {
|
||||
m_error = QObject::tr("Encrypted files are not supported.");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto db = QSharedPointer<Database>::create();
|
||||
db->rootGroup()->setName(QObject::tr("Proton Pass Import"));
|
||||
|
||||
writeVaultToDatabase(json, db);
|
||||
|
||||
return db;
|
||||
}
|
43
src/format/ProtonPassReader.h
Normal file
43
src/format/ProtonPassReader.h
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef PROTONPASS_READER_H
|
||||
#define PROTONPASS_READER_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
|
||||
class Database;
|
||||
|
||||
/*!
|
||||
* Imports a Proton Pass vault in JSON format: https://proton.me/support/pass-export
|
||||
*/
|
||||
class ProtonPassReader
|
||||
{
|
||||
public:
|
||||
explicit ProtonPassReader() = default;
|
||||
~ProtonPassReader() = default;
|
||||
|
||||
QSharedPointer<Database> convert(const QString& path);
|
||||
|
||||
bool hasError();
|
||||
QString errorString();
|
||||
|
||||
private:
|
||||
QString m_error;
|
||||
};
|
||||
|
||||
#endif // PROTONPASS_READER_H
|
@ -278,6 +278,9 @@ DatabaseWidget* DatabaseTabWidget::importFile()
|
||||
Merger merger(db.data(), newDb.data());
|
||||
merger.setSkipDatabaseCustomData(true);
|
||||
merger.merge();
|
||||
// Transfer the root group data
|
||||
newDb->rootGroup()->setName(db->rootGroup()->name());
|
||||
newDb->rootGroup()->setNotes(db->rootGroup()->notes());
|
||||
// Show the new database
|
||||
auto dbWidget = new DatabaseWidget(newDb, this);
|
||||
addDatabaseTab(dbWidget);
|
||||
|
@ -48,6 +48,7 @@ public:
|
||||
IMPORT_OPVAULT,
|
||||
IMPORT_OPUX,
|
||||
IMPORT_BITWARDEN,
|
||||
IMPORT_PROTONPASS,
|
||||
IMPORT_KEEPASS1
|
||||
};
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "format/KeePass1Reader.h"
|
||||
#include "format/OPUXReader.h"
|
||||
#include "format/OpVaultReader.h"
|
||||
#include "format/ProtonPassReader.h"
|
||||
#include "gui/csvImport/CsvImportWidget.h"
|
||||
#include "gui/wizard/ImportWizard.h"
|
||||
|
||||
@ -66,28 +67,29 @@ void ImportWizardPageReview::initializePage()
|
||||
break;
|
||||
case ImportWizard::IMPORT_OPVAULT:
|
||||
m_db = importOPVault(filename, field("ImportPassword").toString());
|
||||
setupDatabasePreview();
|
||||
break;
|
||||
case ImportWizard::IMPORT_OPUX:
|
||||
m_db = importOPUX(filename);
|
||||
setupDatabasePreview();
|
||||
break;
|
||||
case ImportWizard::IMPORT_KEEPASS1:
|
||||
m_db = importKeePass1(filename, field("ImportPassword").toString(), field("ImportKeyFile").toString());
|
||||
setupDatabasePreview();
|
||||
break;
|
||||
case ImportWizard::IMPORT_BITWARDEN:
|
||||
m_db = importBitwarden(filename, field("ImportPassword").toString());
|
||||
setupDatabasePreview();
|
||||
break;
|
||||
case ImportWizard::IMPORT_PROTONPASS:
|
||||
m_db = importProtonPass(filename);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
setupDatabasePreview();
|
||||
}
|
||||
|
||||
bool ImportWizardPageReview::validatePage()
|
||||
{
|
||||
if (m_csvWidget && field("ImportType").toInt() == ImportWizard::IMPORT_CSV) {
|
||||
if (isCsvImport()) {
|
||||
m_db = m_csvWidget->buildDatabase();
|
||||
}
|
||||
return !m_db.isNull();
|
||||
@ -109,14 +111,18 @@ void ImportWizardPageReview::setupCsvImport(const QString& filename)
|
||||
});
|
||||
|
||||
m_csvWidget->load(filename);
|
||||
|
||||
// Qt does not automatically resize a QScrollWidget in a QWizard...
|
||||
m_ui->scrollAreaContents->layout()->addWidget(m_csvWidget);
|
||||
m_ui->scrollArea->setMinimumSize(m_csvWidget->width() + 50, m_csvWidget->height() + 100);
|
||||
}
|
||||
|
||||
void ImportWizardPageReview::setupDatabasePreview()
|
||||
{
|
||||
// CSV preview is handled by the import widget
|
||||
if (isCsvImport()) {
|
||||
// Qt does not automatically resize a QScrollWidget in a QWizard...
|
||||
m_ui->scrollAreaContents->layout()->addWidget(m_csvWidget);
|
||||
m_ui->scrollArea->setMinimumSize(m_csvWidget->width() + 50, m_csvWidget->height() + 100);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_db) {
|
||||
m_ui->scrollArea->setVisible(false);
|
||||
return;
|
||||
@ -200,3 +206,18 @@ ImportWizardPageReview::importKeePass1(const QString& filename, const QString& p
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
QSharedPointer<Database> ImportWizardPageReview::importProtonPass(const QString& filename)
|
||||
{
|
||||
ProtonPassReader reader;
|
||||
auto db = reader.convert(filename);
|
||||
if (reader.hasError()) {
|
||||
m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
bool ImportWizardPageReview::isCsvImport() const
|
||||
{
|
||||
return m_csvWidget && field("ImportType").toInt() == ImportWizard::IMPORT_CSV;
|
||||
}
|
||||
|
@ -43,11 +43,13 @@ public:
|
||||
QSharedPointer<Database> database();
|
||||
|
||||
private:
|
||||
bool isCsvImport() const;
|
||||
void setupCsvImport(const QString& filename);
|
||||
QSharedPointer<Database> importOPUX(const QString& filename);
|
||||
QSharedPointer<Database> importBitwarden(const QString& filename, const QString& password);
|
||||
QSharedPointer<Database> importOPVault(const QString& filename, const QString& password);
|
||||
QSharedPointer<Database> importKeePass1(const QString& filename, const QString& password, const QString& keyfile);
|
||||
QSharedPointer<Database> importProtonPass(const QString& filename);
|
||||
|
||||
void setupDatabasePreview();
|
||||
|
||||
|
@ -35,13 +35,15 @@ ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent)
|
||||
new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Export (.1pux)"), m_ui->importTypeList);
|
||||
new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Vault (.opvault)"), m_ui->importTypeList);
|
||||
new QListWidgetItem(icons()->icon("bitwarden"), tr("Bitwarden (.json)"), m_ui->importTypeList);
|
||||
new QListWidgetItem(icons()->icon("proton"), tr("Proton Pass (.json)"), m_ui->importTypeList);
|
||||
new QListWidgetItem(icons()->icon("object-locked"), tr("KeePass 1 Database (.kdb)"), m_ui->importTypeList);
|
||||
|
||||
m_ui->importTypeList->item(0)->setData(Qt::UserRole, ImportWizard::IMPORT_CSV);
|
||||
m_ui->importTypeList->item(1)->setData(Qt::UserRole, ImportWizard::IMPORT_OPUX);
|
||||
m_ui->importTypeList->item(2)->setData(Qt::UserRole, ImportWizard::IMPORT_OPVAULT);
|
||||
m_ui->importTypeList->item(3)->setData(Qt::UserRole, ImportWizard::IMPORT_BITWARDEN);
|
||||
m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1);
|
||||
m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_PROTONPASS);
|
||||
m_ui->importTypeList->item(5)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1);
|
||||
|
||||
connect(m_ui->importTypeList, &QListWidget::currentItemChanged, this, &ImportWizardPageSelect::itemSelected);
|
||||
m_ui->importTypeList->setCurrentRow(0);
|
||||
@ -104,6 +106,7 @@ void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetI
|
||||
// Unencrypted types
|
||||
case ImportWizard::IMPORT_CSV:
|
||||
case ImportWizard::IMPORT_OPUX:
|
||||
case ImportWizard::IMPORT_PROTONPASS:
|
||||
setCredentialState(false);
|
||||
break;
|
||||
// Password may be required
|
||||
@ -237,6 +240,8 @@ QString ImportWizardPageSelect::importFileFilter()
|
||||
return QString("%1 (*.1pux)").arg(tr("1Password Export"));
|
||||
case ImportWizard::IMPORT_BITWARDEN:
|
||||
return QString("%1 (*.json)").arg(tr("Bitwarden JSON Export"));
|
||||
case ImportWizard::IMPORT_PROTONPASS:
|
||||
return QString("%1 (*.json)").arg(tr("Proton Pass JSON Export"));
|
||||
case ImportWizard::IMPORT_OPVAULT:
|
||||
return QString("%1 (*.opvault)").arg(tr("1Password Vault"));
|
||||
case ImportWizard::IMPORT_KEEPASS1:
|
||||
|
@ -25,6 +25,7 @@
|
||||
#include "format/BitwardenReader.h"
|
||||
#include "format/OPUXReader.h"
|
||||
#include "format/OpVaultReader.h"
|
||||
#include "format/ProtonPassReader.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
@ -277,3 +278,58 @@ void TestImports::testBitwardenEncrypted()
|
||||
}
|
||||
QVERIFY(db);
|
||||
}
|
||||
|
||||
void TestImports::testProtonPass()
|
||||
{
|
||||
auto protonPassPath =
|
||||
QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/protonpass_export.json"));
|
||||
|
||||
ProtonPassReader reader;
|
||||
auto db = reader.convert(protonPassPath);
|
||||
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
|
||||
QVERIFY(db);
|
||||
|
||||
// Confirm Login fields
|
||||
auto entry = db->rootGroup()->findEntryByPath("/Personal/Test Login");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->title(), QStringLiteral("Test Login"));
|
||||
QCOMPARE(entry->username(), QStringLiteral("Username"));
|
||||
QCOMPARE(entry->password(), QStringLiteral("Password"));
|
||||
QCOMPARE(entry->url(), QStringLiteral("https://example.com/"));
|
||||
QCOMPARE(entry->notes(), QStringLiteral("My login secure note."));
|
||||
// Check extra URL's
|
||||
QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://example2.com/"));
|
||||
// Check TOTP
|
||||
QVERIFY(entry->hasTotp());
|
||||
// Check attributes
|
||||
auto attr = entry->attributes();
|
||||
QVERIFY(attr->isProtected("hidden field"));
|
||||
QCOMPARE(attr->value("second 2fa secret"), QStringLiteral("TOTPCODE"));
|
||||
// NOTE: Proton Pass does not export attachments
|
||||
// NOTE: Proton Pass does not export expiration dates
|
||||
|
||||
// Confirm Secure Note
|
||||
entry = db->rootGroup()->findEntryByPath("/Personal/My Secure Note");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->notes(), QStringLiteral("Secure note contents."));
|
||||
|
||||
// Confirm Credit Card
|
||||
entry = db->rootGroup()->findEntryByPath("/Personal/Test Card");
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->username(), QStringLiteral("1234222233334444"));
|
||||
QCOMPARE(entry->password(), QStringLiteral("333"));
|
||||
attr = entry->attributes();
|
||||
QCOMPARE(attr->value("card_cardholderName"), QStringLiteral("Test name"));
|
||||
QCOMPARE(attr->value("card_expirationDate"), QStringLiteral("2025-01"));
|
||||
QCOMPARE(attr->value("card_pin"), QStringLiteral("1234"));
|
||||
QVERIFY(attr->isProtected("card_pin"));
|
||||
|
||||
// Confirm Expired (deleted) entry
|
||||
entry = db->rootGroup()->findEntryByPath("/Personal/My Deleted Note");
|
||||
QVERIFY(entry);
|
||||
QTRY_VERIFY(entry->isExpired());
|
||||
|
||||
// Confirm second group (vault)
|
||||
entry = db->rootGroup()->findEntryByPath("/Test/Other vault login");
|
||||
QVERIFY(entry);
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ private slots:
|
||||
void testOPVault();
|
||||
void testBitwarden();
|
||||
void testBitwardenEncrypted();
|
||||
void testProtonPass();
|
||||
};
|
||||
|
||||
#endif /* TEST_IMPORTS_H */
|
||||
|
173
tests/data/protonpass_export.json
Normal file
173
tests/data/protonpass_export.json
Normal file
@ -0,0 +1,173 @@
|
||||
{
|
||||
"version": "1.21.2",
|
||||
"userId": "USER_ID",
|
||||
"encrypted": false,
|
||||
"vaults": {
|
||||
"VAULT_A": {
|
||||
"name": "Personal",
|
||||
"description": "Personal vault",
|
||||
"display": {
|
||||
"color": 0,
|
||||
"icon": 0
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"itemId": "yZENmDjtmZGODNy3Q_CZiPAF_IgINq8w-R-qazrOh-Nt9YJeVF3gu07ovzDS4jhYHoMdOebTw5JkYPGgIL1mwQ==",
|
||||
"shareId": "SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||
"data": {
|
||||
"metadata": {
|
||||
"name": "Test Login",
|
||||
"note": "My login secure note.",
|
||||
"itemUuid": "e8ee1a0c"
|
||||
},
|
||||
"extraFields": [
|
||||
{
|
||||
"fieldName": "non-hidden field",
|
||||
"type": "text",
|
||||
"data": {
|
||||
"content": "non-hidden field content"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldName": "hidden field",
|
||||
"type": "hidden",
|
||||
"data": {
|
||||
"content": "hidden field content"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldName": "second 2fa secret",
|
||||
"type": "totp",
|
||||
"data": {
|
||||
"totpUri": "TOTPCODE"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "login",
|
||||
"content": {
|
||||
"itemEmail": "Email",
|
||||
"password": "Password",
|
||||
"urls": [
|
||||
"https://example.com/",
|
||||
"https://example2.com/"
|
||||
],
|
||||
"totpUri": "otpauth://totp/Test%20Login%20-%20Personal%20Vault:Username?issuer=Test%20Login%20-%20Personal%20Vault&secret=TOTPCODE&algorithm=SHA1&digits=6&period=30",
|
||||
"passkeys": [],
|
||||
"itemUsername": "Username"
|
||||
}
|
||||
},
|
||||
"state": 1,
|
||||
"aliasEmail": null,
|
||||
"contentFormatVersion": 1,
|
||||
"createTime": 1689182868,
|
||||
"modifyTime": 1689182868,
|
||||
"pinned": true
|
||||
},
|
||||
{
|
||||
"itemId": "xqq_Bh8RxNMBerkiMvRdH427yswZznjYwps-f6C5D8tmKiPgMxCSPNz1BOd4nRJ309gciDiPhXcCVWOyfJ66ZA==",
|
||||
"shareId": "SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||
"data": {
|
||||
"metadata": {
|
||||
"name": "My Secure Note",
|
||||
"note": "Secure note contents.",
|
||||
"itemUuid": "ad618070"
|
||||
},
|
||||
"extraFields": [],
|
||||
"type": "note",
|
||||
"content": {}
|
||||
},
|
||||
"state": 1,
|
||||
"aliasEmail": null,
|
||||
"contentFormatVersion": 1,
|
||||
"createTime": 1689182908,
|
||||
"modifyTime": 1689182908,
|
||||
"pinned": false
|
||||
},
|
||||
{
|
||||
"itemId": "ZmGzd-HNQYTr6wmfWlSfiStXQLqGic_PYB2Q2T_hmuRM2JIA4pKAPJcmFafxJrDpXxLZ2EPjgD6Noc9a0U6AVQ==",
|
||||
"shareId": "SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||
"data": {
|
||||
"metadata": {
|
||||
"name": "Test Card",
|
||||
"note": "Credit Card Note",
|
||||
"itemUuid": "d8f45370"
|
||||
},
|
||||
"extraFields": [],
|
||||
"type": "creditCard",
|
||||
"content": {
|
||||
"cardholderName": "Test name",
|
||||
"cardType": 0,
|
||||
"number": "1234222233334444",
|
||||
"verificationNumber": "333",
|
||||
"expirationDate": "2025-01",
|
||||
"pin": "1234"
|
||||
}
|
||||
},
|
||||
"state": 1,
|
||||
"aliasEmail": null,
|
||||
"contentFormatVersion": 1,
|
||||
"createTime": 1691001643,
|
||||
"modifyTime": 1691001643,
|
||||
"pinned": true
|
||||
},
|
||||
{
|
||||
"itemId": "xqq_Bh8RxNMBerkiMvRdH427yswZznjYwps-f6C5D8tmKiPgMxCSPNz1BOd4nRJ309gciDiPhXcCVWOyfJ66ZA==",
|
||||
"shareId": "SN5uWo4WZF2uT5wIDqtbdpkjuxCbNTOIdf-JQ_DYZcKYKURHiZB5csS1a1p9lklvju9ni42l08IKzwQG0B2ySg==",
|
||||
"data": {
|
||||
"metadata": {
|
||||
"name": "My Deleted Note",
|
||||
"note": "Secure note contents.",
|
||||
"itemUuid": "ad618070"
|
||||
},
|
||||
"extraFields": [],
|
||||
"type": "note",
|
||||
"content": {}
|
||||
},
|
||||
"state": 2,
|
||||
"aliasEmail": null,
|
||||
"contentFormatVersion": 1,
|
||||
"createTime": 1689182908,
|
||||
"modifyTime": 1689182908,
|
||||
"pinned": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"VAULT_B": {
|
||||
"name": "Test",
|
||||
"description": "",
|
||||
"display": {
|
||||
"color": 4,
|
||||
"icon": 2
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"itemId": "U_J8-eUR15sC-PjUhjVcixDcayhjGuoerUZCr560RlAi0ZjBNkSaSKAytVzZn4E0hiFX1_y4qZbUetl6jO3aJw==",
|
||||
"shareId": "OJz-4MnPqAuYnyemhctcGDlSLJrzsTnf2FnFSwxh1QP_oth9xyGDc2ZAqCv5FnqkVgTNHT5aPj62zcekNemfNw==",
|
||||
"data": {
|
||||
"metadata": {
|
||||
"name": "Other vault login",
|
||||
"note": "",
|
||||
"itemUuid": "f3429d44"
|
||||
},
|
||||
"extraFields": [],
|
||||
"type": "login",
|
||||
"content": {
|
||||
"itemEmail": "other vault username",
|
||||
"password": "other vault password",
|
||||
"urls": [],
|
||||
"totpUri": "JBSWY3DPEHPK3PXP",
|
||||
"passkeys": [],
|
||||
"itemUsername": ""
|
||||
}
|
||||
},
|
||||
"state": 1,
|
||||
"aliasEmail": null,
|
||||
"contentFormatVersion": 1,
|
||||
"createTime": 1689182949,
|
||||
"modifyTime": 1689182949,
|
||||
"pinned": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user