mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Bitwarden import: Add support for timestamps and password history (#12588)
* Closes #12587 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com> Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
@@ -56,6 +56,7 @@ namespace
|
|||||||
|
|
||||||
// Create entry and assign basic values
|
// Create entry and assign basic values
|
||||||
QScopedPointer<Entry> entry(new Entry());
|
QScopedPointer<Entry> entry(new Entry());
|
||||||
|
entry->setEmitModified(false);
|
||||||
entry->setUuid(QUuid::createUuid());
|
entry->setUuid(QUuid::createUuid());
|
||||||
entry->setTitle(itemMap.value("name").toString());
|
entry->setTitle(itemMap.value("name").toString());
|
||||||
entry->setNotes(itemMap.value("notes").toString());
|
entry->setNotes(itemMap.value("notes").toString());
|
||||||
@@ -205,9 +206,58 @@ namespace
|
|||||||
entry->attributes()->set(name, value, type == 1);
|
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
|
// Collapse any accumulated history
|
||||||
entry->removeHistoryItems(entry->historyItems());
|
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();
|
return entry.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +290,9 @@ namespace
|
|||||||
for (const auto& item : items) {
|
for (const auto& item : items) {
|
||||||
auto entry = readItem(item.toObject(), folderId);
|
auto entry = readItem(item.toObject(), folderId);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
entry->setUpdateTimeinfo(false);
|
||||||
entry->setGroup(folderMap.value(folderId, db->rootGroup()), false);
|
entry->setGroup(folderMap.value(folderId, db->rootGroup()), false);
|
||||||
|
entry->setUpdateTimeinfo(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,16 @@ void TestImports::testBitwarden()
|
|||||||
QCOMPARE(entry->attribute("KP2A_URL_2"), QStringLiteral("https://gmail.com"));
|
QCOMPARE(entry->attribute("KP2A_URL_2"), QStringLiteral("https://gmail.com"));
|
||||||
// Check TOTP
|
// Check TOTP
|
||||||
QVERIFY(entry->hasTotp());
|
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 attachments
|
||||||
// NOTE: Bitwarden does not export expiration dates
|
// NOTE: Bitwarden does not export expiration dates
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
],
|
],
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
"revisionDate": "2024-12-25T12:00:00.000Z",
|
||||||
|
"creationDate": "2024-12-01T12:00:00.000Z",
|
||||||
|
"deletedDate": null,
|
||||||
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa",
|
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa",
|
||||||
"organizationId": null,
|
"organizationId": null,
|
||||||
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
"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",
|
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||||
"organizationId": null,
|
"organizationId": null,
|
||||||
"folderId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
|
"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",
|
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
"organizationId": null,
|
"organizationId": null,
|
||||||
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
"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",
|
"id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||||
"organizationId": null,
|
"organizationId": null,
|
||||||
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
"folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
|
|||||||
Reference in New Issue
Block a user