Add Proton Pass importer

* Closes #10465
This commit is contained in:
Jonathan White 2024-08-25 08:17:16 -04:00
parent c1a66a8be9
commit 6bbf9d1be2
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
15 changed files with 556 additions and 10 deletions

View File

@ -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

View 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

View File

@ -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>

View 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>

View File

@ -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

View 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;
}

View 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

View File

@ -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);

View File

@ -48,6 +48,7 @@ public:
IMPORT_OPVAULT,
IMPORT_OPUX,
IMPORT_BITWARDEN,
IMPORT_PROTONPASS,
IMPORT_KEEPASS1
};

View File

@ -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;
}

View File

@ -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();

View File

@ -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:

View File

@ -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);
}

View File

@ -30,6 +30,7 @@ private slots:
void testOPVault();
void testBitwarden();
void testBitwardenEncrypted();
void testProtonPass();
};
#endif /* TEST_IMPORTS_H */

View 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
}
]
}
}
}