2
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "OPUXReader.h"
20
#include "core/Database.h"
21
#include "core/Entry.h"
22
#include "core/Group.h"
23
#include "core/Metadata.h"
28
#include <QJsonDocument>
30
#include <QScopedPointer>
33
#include <minizip/unzip.h>
37
QByteArray extractFile(unzFile uf, QString filename)
39
if (unzLocateFile(uf, filename.toLatin1(), 2) != UNZ_OK) {
40
qWarning("Failed to extract 1PUX document: %s", qPrintable(filename));
44
// Read export.data into memory
45
int bytes, bytesRead = 0;
47
unzOpenCurrentFile(uf);
49
data.resize(data.size() + 8192);
50
bytes = unzReadCurrentFile(uf, data.data() + bytesRead, 8192);
55
unzCloseCurrentFile(uf);
56
data.truncate(bytesRead);
61
Entry* readItem(const QJsonObject& item, unzFile uf = nullptr)
63
const auto itemMap = item.toVariantMap();
64
const auto overviewMap = itemMap.value("overview").toMap();
65
const auto detailsMap = itemMap.value("details").toMap();
67
// Create entry and assign basic values
68
QScopedPointer<Entry> entry(new Entry());
69
entry->setUuid(QUuid::createUuid());
70
entry->setTitle(overviewMap.value("title").toString());
71
entry->setUrl(overviewMap.value("url").toString());
72
if (overviewMap.contains("urls")) {
74
for (const auto& urlRaw : overviewMap.value("urls").toList()) {
75
const auto urlMap = urlRaw.toMap();
76
const auto url = urlMap.value("url").toString();
77
if (entry->url() != url) {
78
entry->attributes()->set(
79
QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url);
84
if (overviewMap.contains("tags")) {
85
entry->setTags(overviewMap.value("tags").toStringList().join(","));
87
if (itemMap.value("favIndex").toString() == "1") {
88
entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
90
if (itemMap.value("state").toString() == "archived") {
91
entry->addTag(QObject::tr("Archived", "Tag for archived entries"));
94
// Parse the details map by setting the username, password, and notes first
95
const auto loginFields = detailsMap.value("loginFields").toList();
96
for (const auto& field : loginFields) {
97
const auto fieldMap = field.toMap();
98
const auto designation = fieldMap.value("designation").toString();
99
if (designation.compare("username", Qt::CaseInsensitive) == 0) {
100
entry->setUsername(fieldMap.value("value").toString());
101
} else if (designation.compare("password", Qt::CaseInsensitive) == 0) {
102
entry->setPassword(fieldMap.value("value").toString());
105
entry->setNotes(detailsMap.value("notesPlain").toString());
107
// Dive into the item sections to pull out advanced attributes
108
const auto sections = detailsMap.value("sections").toList();
109
for (const auto& section : sections) {
110
// Derive a prefix for attribute names using the title or uuid if missing
111
const auto sectionMap = section.toMap();
112
auto prefix = sectionMap.value("title").toString();
113
if (prefix.isEmpty()) {
114
prefix = QUuid::createUuid().toString().mid(1, 5);
117
for (const auto& field : sectionMap.value("fields").toList()) {
118
// Form the name of the attribute using the prefix and title or id
119
const auto fieldMap = field.toMap();
120
auto name = fieldMap.value("title").toString();
121
if (name.isEmpty()) {
122
name = fieldMap.value("id").toString();
124
name = QString("%1_%2").arg(prefix, name);
126
const auto valueMap = fieldMap.value("value").toMap();
127
const auto key = valueMap.firstKey();
129
auto totp = valueMap.value(key).toString();
130
if (!totp.startsWith("otpauth://")) {
132
QUrl url(QString("otpauth://totp/%1:%2?secret=%3")
133
.arg(QString(QUrl::toPercentEncoding(entry->title())),
134
QString(QUrl::toPercentEncoding(entry->username())),
135
QString(QUrl::toPercentEncoding(totp))));
136
totp = url.toString(QUrl::FullyEncoded);
139
if (entry->hasTotp()) {
140
// Store multiple TOTP definitions as additional otp attributes
143
const auto attributes = entry->attributes()->keys();
144
while (attributes.contains(name)) {
145
name = QString("otp_%1").arg(++i);
147
entry->attributes()->set(name, totp, true);
149
// First otp value encountered gets formal storage
150
entry->setTotp(Totp::parseSettings(totp));
152
} else if (key == "file") {
153
// Add a file to the entry attachments
154
const auto fileMap = valueMap.value(key).toMap();
155
const auto fileName = fileMap.value("fileName").toString();
156
const auto docId = fileMap.value("documentId").toString();
157
const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName));
158
if (!data.isNull()) {
159
entry->attachments()->set(fileName, data);
162
auto value = valueMap.value(key).toString();
164
// Convert date fields from Unix time
165
value = QDateTime::fromSecsSinceEpoch(valueMap.value(key).toULongLong(), Qt::UTC).toString();
166
} else if (key == "email") {
167
// Email address is buried in a sub-value
168
value = valueMap.value(key).toMap().value("email_address").toString();
169
} else if (key == "address") {
170
// Combine all the address attributes into a fully formed structure
171
const auto address = valueMap.value(key).toMap();
172
value = address.value("street").toString() + "\n" + address.value("city").toString() + ", "
173
+ address.value("state").toString() + " " + address.value("zip").toString() + "\n"
174
+ address.value("country").toString();
177
if (!value.isEmpty()) {
178
entry->attributes()->set(name, value, key == "concealed");
184
// Add a document attachment if defined
185
if (detailsMap.contains("documentAttributes")) {
186
const auto document = detailsMap.value("documentAttributes").toMap();
187
const auto fileName = document.value("fileName").toString();
188
const auto docId = document.value("documentId").toString();
189
const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName));
190
if (!data.isNull()) {
191
entry->attachments()->set(fileName, data);
195
// Collapse any accumulated history
196
entry->removeHistoryItems(entry->historyItems());
198
// Adjust the created and modified times
199
auto timeInfo = entry->timeInfo();
200
const auto createdTime = QDateTime::fromSecsSinceEpoch(itemMap.value("createdAt").toULongLong(), Qt::UTC);
201
const auto modifiedTime = QDateTime::fromSecsSinceEpoch(itemMap.value("updatedAt").toULongLong(), Qt::UTC);
202
timeInfo.setCreationTime(createdTime);
203
timeInfo.setLastModificationTime(modifiedTime);
204
timeInfo.setLastAccessTime(modifiedTime);
205
entry->setTimeInfo(timeInfo);
210
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db, unzFile uf = nullptr)
212
if (!vault.contains("attrs") || !vault.contains("items")) {
213
// Early out if the vault is missing critical items
217
const auto attr = vault.value("attrs").toObject().toVariantMap();
219
// Create group and assign basic values
220
auto group = new Group();
221
group->setUuid(QUuid::createUuid());
222
group->setName(attr.value("name").toString());
223
group->setParent(db->rootGroup());
225
const auto items = vault.value("items").toArray();
226
for (const auto& item : items) {
227
auto entry = readItem(item.toObject(), uf);
229
entry->setGroup(group, false);
233
// Add the group icon if present
234
const auto icon = attr.value("avatar").toString();
235
if (!icon.isEmpty()) {
236
auto data = extractFile(uf, QString("files/%1").arg(icon));
237
if (!data.isNull()) {
238
const auto uuid = QUuid::createUuid();
239
db->metadata()->addCustomIcon(uuid, data);
240
group->setIcon(uuid);
246
bool OPUXReader::hasError()
248
return !m_error.isEmpty();
251
QString OPUXReader::errorString()
256
QSharedPointer<Database> OPUXReader::convert(const QString& path)
260
QFileInfo fileinfo(path);
261
if (!fileinfo.exists()) {
262
m_error = QObject::tr("File does not exist.").arg(path);
266
// 1PUX is a zip file format, open it and process the contents in memory
267
auto uf = unzOpen64(fileinfo.absoluteFilePath().toLatin1().constData());
269
m_error = QObject::tr("Invalid 1PUX file format: Not a valid ZIP file.");
273
// Find the export.data file, if not found this isn't a 1PUX file
274
auto data = extractFile(uf, "export.data");
276
m_error = QObject::tr("Invalid 1PUX file format: Missing export.data");
281
auto db = QSharedPointer<Database>::create();
282
db->rootGroup()->setName(QObject::tr("1Password Import"));
283
const auto json = QJsonDocument::fromJson(data);
285
const auto account = json.object().value("accounts").toArray().first().toObject();
286
const auto vaults = account.value("vaults").toArray();
288
for (const auto& vault : vaults) {
289
writeVaultToDatabase(vault.toObject(), db, uf);