diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp index 5f729aa77..43b2a34f3 100644 --- a/src/format/BitwardenReader.cpp +++ b/src/format/BitwardenReader.cpp @@ -56,6 +56,7 @@ namespace // Create entry and assign basic values QScopedPointer entry(new Entry()); + entry->setEmitModified(false); entry->setUuid(QUuid::createUuid()); entry->setTitle(itemMap.value("name").toString()); entry->setNotes(itemMap.value("notes").toString()); @@ -205,9 +206,58 @@ namespace entry->attributes()->set(name, value, type == 1); } + // Parse timestamps + auto timeInfo = entry->timeInfo(); + if (itemMap.contains("creationDate")) { + const auto creationDate = QDateTime::fromString(itemMap.value("creationDate").toString(), Qt::ISODate); + if (creationDate.isValid()) { + timeInfo.setCreationTime(creationDate); + } + } + if (itemMap.contains("revisionDate")) { + const auto revisionDate = QDateTime::fromString(itemMap.value("revisionDate").toString(), Qt::ISODate); + if (revisionDate.isValid()) { + timeInfo.setLastModificationTime(revisionDate); + timeInfo.setLastAccessTime(revisionDate); + } + } + entry->setTimeInfo(timeInfo); + // Collapse any accumulated history entry->removeHistoryItems(entry->historyItems()); + // Parse password history, if present + if (itemMap.contains("passwordHistory")) { + const auto passwordHistory = itemMap.value("passwordHistory").toList(); + for (const auto& historyItem : passwordHistory) { + const auto historyMap = historyItem.toMap(); + const auto password = historyMap.value("password").toString(); + const auto lastUsedDate = + QDateTime::fromString(historyMap.value("lastUsedDate").toString(), Qt::ISODate); + + if (!password.isEmpty() && lastUsedDate.isValid()) { + // Create a history entry with the old password + auto historyEntry = new Entry(); + historyEntry->setUuid(entry->uuid()); + historyEntry->setTitle(entry->title()); + historyEntry->setUsername(entry->username()); + historyEntry->setPassword(password); + historyEntry->setUrl(entry->url()); + historyEntry->setNotes(entry->notes()); + + // Set the timestamp for this history item + auto historyTimeInfo = historyEntry->timeInfo(); + historyTimeInfo.setCreationTime(entry->timeInfo().creationTime()); + historyTimeInfo.setLastModificationTime(lastUsedDate); + historyTimeInfo.setLastAccessTime(lastUsedDate); + historyEntry->setTimeInfo(historyTimeInfo); + + entry->addHistoryItem(historyEntry); + } + } + } + + entry->setEmitModified(true); return entry.take(); } @@ -240,7 +290,9 @@ namespace for (const auto& item : items) { auto entry = readItem(item.toObject(), folderId); if (entry) { + entry->setUpdateTimeinfo(false); entry->setGroup(folderMap.value(folderId, db->rootGroup()), false); + entry->setUpdateTimeinfo(true); } } } diff --git a/tests/TestImports.cpp b/tests/TestImports.cpp index 17ec2bef5..d17beb389 100644 --- a/tests/TestImports.cpp +++ b/tests/TestImports.cpp @@ -228,6 +228,16 @@ void TestImports::testBitwarden() QCOMPARE(entry->attribute("KP2A_URL_2"), QStringLiteral("https://gmail.com")); // Check TOTP QVERIFY(entry->hasTotp()); + // Check Modified and Created timestamps + QCOMPARE(entry->timeInfo().lastModificationTime(), + QDateTime::fromString(QStringLiteral("2024-12-25T12:00:00Z"), Qt::ISODate)); + QCOMPARE(entry->timeInfo().creationTime(), + QDateTime::fromString(QStringLiteral("2024-12-01T12:00:00Z"), Qt::ISODate)); + // Check Password History + QCOMPARE(entry->historyItems().size(), 1); + QCOMPARE(entry->historyItems().first()->password(), QStringLiteral("oldpassword")); + QCOMPARE(entry->historyItems().first()->timeInfo().lastModificationTime(), + QDateTime::fromString(QStringLiteral("2024-12-01T12:00:00Z"), Qt::ISODate)); // NOTE: Bitwarden does not export attachments // NOTE: Bitwarden does not export expiration dates diff --git a/tests/data/bitwarden_export.json b/tests/data/bitwarden_export.json index 7d6a5de21..7a4f0562d 100644 --- a/tests/data/bitwarden_export.json +++ b/tests/data/bitwarden_export.json @@ -11,6 +11,9 @@ ], "items": [ { + "revisionDate": "2024-12-25T12:00:00.000Z", + "creationDate": "2024-12-01T12:00:00.000Z", + "deletedDate": null, "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa", "organizationId": null, "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", @@ -43,6 +46,9 @@ ] }, { + "revisionDate": "2024-12-25T12:00:00.000Z", + "creationDate": "2024-12-01T12:00:00.000Z", + "deletedDate": null, "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "organizationId": null, "folderId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", @@ -80,6 +86,9 @@ ] }, { + "revisionDate": "2024-12-25T12:00:00.000Z", + "creationDate": "2024-12-01T12:00:00.000Z", + "deletedDate": null, "id": "cccccccc-cccc-cccc-cccc-cccccccccccc", "organizationId": null, "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", @@ -129,6 +138,15 @@ ] }, { + "passwordHistory": [ + { + "lastUsedDate": "2024-12-01T12:00:00.000Z", + "password": "oldpassword" + } + ], + "revisionDate": "2024-12-25T12:00:00.000Z", + "creationDate": "2024-12-01T12:00:00.000Z", + "deletedDate": null, "id": "dddddddd-dddd-dddd-dddd-dddddddddddd", "organizationId": null, "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",