123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- /*
- * 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 "TestImports.h"
- #include "config-keepassx-tests.h"
- #include "core/Group.h"
- #include "core/Metadata.h"
- #include "core/Totp.h"
- #include "crypto/Crypto.h"
- #include "format/BitwardenReader.h"
- #include "format/OPUXReader.h"
- #include "format/OpVaultReader.h"
- #include <QJsonObject>
- #include <QList>
- #include <QTest>
- QTEST_GUILESS_MAIN(TestImports)
- void TestImports::initTestCase()
- {
- QVERIFY(Crypto::init());
- }
- void TestImports::testOPUX()
- {
- auto opuxPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/1PasswordExport.1pux"));
- OPUXReader reader;
- auto db = reader.convert(opuxPath);
- QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
- QVERIFY(db);
- // Confirm specific entry details are valid
- auto entry = db->rootGroup()->findEntryByPath("/Personal/Login");
- QVERIFY(entry);
- QCOMPARE(entry->title(), QStringLiteral("Login"));
- QCOMPARE(entry->username(), QStringLiteral("team@keepassxc.org"));
- QCOMPARE(entry->password(), QStringLiteral("password"));
- QCOMPARE(entry->url(), QStringLiteral("https://keepassxc.org"));
- QCOMPARE(entry->notes(), QStringLiteral("Note to self"));
- // Check extra URL's
- QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://twitter.com"));
- // Check TOTP
- QVERIFY(entry->hasTotp());
- QVERIFY(!entry->attribute("otp_1").isEmpty());
- // Check tags
- QVERIFY(entry->tagList().contains("Favorite"));
- QVERIFY(entry->tagList().contains("website"));
- // Check attachments
- entry = db->rootGroup()->findEntryByPath("/Personal/KeePassXC Logo");
- auto attachments = entry->attachments();
- QCOMPARE(attachments->keys().count(), 1);
- QCOMPARE(attachments->keys()[0], QString("keepassxc.png"));
- // Confirm advanced attributes
- // NOTE: 1PUX does not support an explicit expiration field
- entry = db->rootGroup()->findEntryByPath("/Personal/Credit Card");
- QVERIFY(entry);
- auto tmpl = QString("Credit Card Fields_%1");
- auto attr = entry->attributes();
- QCOMPARE(attr->value(tmpl.arg("cardholder name")), QStringLiteral("KeePassXC"));
- QCOMPARE(attr->value(tmpl.arg("expiry date")), QStringLiteral("202206"));
- QCOMPARE(attr->value(tmpl.arg("verification number")), QStringLiteral("123"));
- QVERIFY(attr->isProtected(tmpl.arg("verification number")));
- // Confirm address fields
- entry = db->rootGroup()->findEntryByPath("/Personal/Identity");
- QVERIFY(entry);
- attr = entry->attributes();
- QCOMPARE(attr->value("Address_address"), QStringLiteral("123 Avenue Rd\nBoston, MA 12345\nus"));
- // Check archived entries
- entry = db->rootGroup()->findEntryByPath("/Personal/Login Archived");
- QVERIFY(entry);
- QVERIFY(entry->tagList().contains("Archived"));
- // Check vault to group structure
- entry = db->rootGroup()->findEntryByPath("/Shared/Bank Account");
- QVERIFY(entry);
- // Check custom group icon
- QVERIFY(!entry->group()->iconUuid().isNull());
- // Check Category UUID 05 Passwords
- entry = db->rootGroup()->findEntryByPath("/Personal/UUID 005 Password");
- QVERIFY(entry);
- QCOMPARE(entry->password(), QStringLiteral("uuid005password"));
- }
- void TestImports::testOPVault()
- {
- auto opVaultPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/keepassxc.opvault"));
- auto categories = QStringList({QStringLiteral("Login"),
- QStringLiteral("Credit Card"),
- QStringLiteral("Secure Note"),
- QStringLiteral("Identity"),
- QStringLiteral("Password"),
- QStringLiteral("Tombstone"),
- QStringLiteral("Software License"),
- QStringLiteral("Bank Account"),
- QStringLiteral("Database"),
- QStringLiteral("Driver License"),
- QStringLiteral("Outdoor License"),
- QStringLiteral("Membership"),
- QStringLiteral("Passport"),
- QStringLiteral("Rewards"),
- QStringLiteral("SSN"),
- QStringLiteral("Router"),
- QStringLiteral("Server"),
- QStringLiteral("Email")});
- QDir opVaultDir(opVaultPath);
- OpVaultReader reader;
- auto db = reader.convert(opVaultDir, "a");
- QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
- QVERIFY(db);
- // Confirm specific entry details are valid
- auto entry = db->rootGroup()->findEntryByPath("/Login/KeePassXC");
- QVERIFY(entry);
- QCOMPARE(entry->title(), QStringLiteral("KeePassXC"));
- QCOMPARE(entry->username(), QStringLiteral("keepassxc"));
- QCOMPARE(entry->password(), QStringLiteral("opvault"));
- QCOMPARE(entry->url(), QStringLiteral("https://www.keepassxc.org"));
- QCOMPARE(entry->notes(), QStringLiteral("KeePassXC Account"));
- // Check extra URL's
- QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://snapshot.keepassxc.org"));
- // Check TOTP
- QVERIFY(entry->hasTotp());
- // Check attachments
- auto attachments = entry->attachments();
- QCOMPARE(attachments->keys().count(), 1);
- QCOMPARE(*attachments->values().begin(), QByteArray("attachment"));
- // Confirm expired entries
- entry = db->rootGroup()->findEntryByPath("/Login/Expired Login");
- QVERIFY(entry->isExpired());
- // Confirm advanced attributes
- entry = db->rootGroup()->findEntryByPath("/Credit Card/My Credit Card");
- QVERIFY(entry);
- auto attr = entry->attributes();
- QCOMPARE(attr->value("cardholder name"), QStringLiteral("Team KeePassXC"));
- QVERIFY(!attr->value("valid from").isEmpty());
- QCOMPARE(attr->value("Additional Details_PIN"), QStringLiteral("1234"));
- QVERIFY(attr->isProtected("Additional Details_PIN"));
- // Confirm address fields
- entry = db->rootGroup()->findEntryByPath("/Identity/Team KeePassXC");
- QVERIFY(entry);
- attr = entry->attributes();
- QCOMPARE(attr->value("address_street"), QStringLiteral("123 Password Lane"));
- // Confirm complex passwords
- entry = db->rootGroup()->findEntryByPath("/Password/Complex Password");
- QVERIFY(entry);
- QCOMPARE(entry->password(), QStringLiteral("HfgcHjEL}iO}^3N!?*cv~O:9GJZQ0>oC"));
- QVERIFY(entry->hasTotp());
- auto totpSettings = entry->totpSettings();
- QCOMPARE(totpSettings->digits, static_cast<unsigned int>(8));
- QCOMPARE(totpSettings->step, static_cast<unsigned int>(45));
- // Add another OTP to this entry to confirm it doesn't overwrite the existing one
- auto field = QJsonObject::fromVariantMap({{"n", "TOTP_SETTINGS"}, {"v", "otpauth://test.url?digits=6"}});
- reader.fillFromSectionField(entry, "", field);
- QVERIFY(entry->hasTotp());
- totpSettings = entry->totpSettings();
- QCOMPARE(totpSettings->digits, static_cast<unsigned int>(8));
- QCOMPARE(totpSettings->step, static_cast<unsigned int>(45));
- QVERIFY(entry->attributes()->contains("otp_1"));
- // Confirm trashed entries are sent to the recycle bin
- auto recycleBin = db->metadata()->recycleBin();
- QVERIFY(recycleBin);
- QVERIFY(!recycleBin->isEmpty());
- QVERIFY(recycleBin->findEntryByPath("Trashed Password"));
- // Confirm created groups align with category names
- for (const auto group : db->rootGroup()->children()) {
- if (group == recycleBin) {
- continue;
- }
- QVERIFY2(categories.contains(group->name()),
- qPrintable(QStringLiteral("Invalid group name: %1").arg(group->name())));
- // Confirm each group is not empty
- QVERIFY2(!group->isEmpty(), qPrintable(QStringLiteral("Group %1 is empty").arg(group->name())));
- }
- }
- void TestImports::testBitwarden()
- {
- auto bitwardenPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_export.json"));
- BitwardenReader reader;
- auto db = reader.convert(bitwardenPath);
- QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
- QVERIFY(db);
- // Confirm Login fields
- auto entry = db->rootGroup()->findEntryByPath("/My Folder/Login Name");
- QVERIFY(entry);
- QCOMPARE(entry->title(), QStringLiteral("Login Name"));
- QCOMPARE(entry->username(), QStringLiteral("myusername@gmail.com"));
- QCOMPARE(entry->password(), QStringLiteral("mypassword"));
- QCOMPARE(entry->url(), QStringLiteral("https://mail.google.com"));
- QCOMPARE(entry->notes(), QStringLiteral("1st line of note text\n2nd Line of note text"));
- // Check extra URL's
- QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://google.com"));
- QCOMPARE(entry->attribute("KP2A_URL_2"), QStringLiteral("https://gmail.com"));
- // Check TOTP
- QVERIFY(entry->hasTotp());
- // NOTE: Bitwarden does not export attachments
- // NOTE: Bitwarden does not export expiration dates
- // Confirm Identity fields
- entry = db->rootGroup()->findEntryByPath("/My Folder/My Identity");
- QVERIFY(entry);
- auto attr = entry->attributes();
- // NOTE: The extra spaces are deliberate to test unmodified ingest of data
- QCOMPARE(attr->value("identity_address"),
- QStringLiteral(" 1 North Calle Cesar Chavez \nSanta Barbara, CA 93103\nUnited States "));
- QCOMPARE(attr->value("identity_name"), QStringLiteral("Mrs Jane A Doe"));
- QCOMPARE(attr->value("identity_ssn"), QStringLiteral("123-12-1234"));
- QVERIFY(attr->isProtected("identity_ssn"));
- // Confirm Secure Note
- entry = db->rootGroup()->findEntryByPath("/My Folder/My Secure Note");
- QVERIFY(entry);
- QCOMPARE(entry->notes(),
- QStringLiteral("1st line of secure note\n2nd line of secure note\n3rd line of secure note"));
- // Confirm Credit Card
- entry = db->rootGroup()->findEntryByPath("/Second Folder/Card Name");
- QVERIFY(entry);
- attr = entry->attributes();
- QCOMPARE(attr->value("card_cardholderName"), QStringLiteral("Jane Doe"));
- QCOMPARE(attr->value("card_number"), QStringLiteral("1234567891011121"));
- QCOMPARE(attr->value("card_code"), QStringLiteral("123"));
- QVERIFY(attr->isProtected("card_code"));
- }
- void TestImports::testBitwardenEncrypted()
- {
- // We already tested the parser so just test that decryption works properly
- // First test PBKDF2 password stretching (KDF Type 0)
- auto bitwardenPath =
- QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_encrypted_export.json"));
- BitwardenReader reader;
- auto db = reader.convert(bitwardenPath, "a");
- if (reader.hasError()) {
- QFAIL(qPrintable(reader.errorString()));
- }
- QVERIFY(db);
- // Now test Argon2id password stretching (KDF Type 1)
- bitwardenPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR,
- QStringLiteral("/bitwarden_encrypted_argon2id_export.json"));
- db = reader.convert(bitwardenPath, "a");
- if (reader.hasError()) {
- QFAIL(qPrintable(reader.errorString()));
- }
- QVERIFY(db);
- }
- void TestImports::testBitwardenPasskey()
- {
- auto bitwardenPath =
- QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_passkey_export.json"));
- BitwardenReader reader;
- auto db = reader.convert(bitwardenPath);
- QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
- QVERIFY(db);
- // Confirm Login fields
- auto entry = db->rootGroup()->findEntryByPath("/webauthn.io");
- QVERIFY(entry);
- QCOMPARE(entry->title(), QStringLiteral("webauthn.io"));
- QCOMPARE(entry->username(), QStringLiteral("KPXC_BITWARDEN"));
- QCOMPARE(entry->url(), QStringLiteral("https://webauthn.io/"));
- // Confirm passkey attributes
- auto attr = entry->attributes();
- QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_CREDENTIAL_ID), QStringLiteral("o-FfiyfBQq6Qz6YVrYeFTw"));
- QCOMPARE(
- attr->value(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM),
- QStringLiteral(
- "-----BEGIN PRIVATE "
- "KEY-----"
- "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmr4GQQjerojFuf0ZouOuUllMvAwxZSZAfB6gwDYcLiehRANCAAT0WR5zVS"
- "p6ieusvjkLkzaGc7fjGBmwpiuLPxR/d+ZjqMI9L2DKh+takp6wGt2x0n4jzr1KA352NZg0vjZX9CHh-----END PRIVATE KEY-----"));
- QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_USERNAME), QStringLiteral("KPXC_BITWARDEN"));
- QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY), QStringLiteral("webauthn.io"));
- QCOMPARE(attr->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE),
- QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
- }
|