keepassxc

Форк
0
/
OPUXReader.cpp 
294 строки · 11.8 Кб
1
/*
2
 *  Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
3
 *
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.
8
 *
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.
13
 *
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/>.
16
 */
17

18
#include "OPUXReader.h"
19

20
#include "core/Database.h"
21
#include "core/Entry.h"
22
#include "core/Group.h"
23
#include "core/Metadata.h"
24
#include "core/Totp.h"
25

26
#include <QFileInfo>
27
#include <QJsonArray>
28
#include <QJsonDocument>
29
#include <QJsonObject>
30
#include <QScopedPointer>
31
#include <QUrl>
32

33
#include <minizip/unzip.h>
34

35
namespace
36
{
37
    QByteArray extractFile(unzFile uf, QString filename)
38
    {
39
        if (unzLocateFile(uf, filename.toLatin1(), 2) != UNZ_OK) {
40
            qWarning("Failed to extract 1PUX document: %s", qPrintable(filename));
41
            return {};
42
        }
43

44
        // Read export.data into memory
45
        int bytes, bytesRead = 0;
46
        QByteArray data;
47
        unzOpenCurrentFile(uf);
48
        do {
49
            data.resize(data.size() + 8192);
50
            bytes = unzReadCurrentFile(uf, data.data() + bytesRead, 8192);
51
            if (bytes > 0) {
52
                bytesRead += bytes;
53
            }
54
        } while (bytes > 0);
55
        unzCloseCurrentFile(uf);
56
        data.truncate(bytesRead);
57

58
        return data;
59
    }
60

61
    Entry* readItem(const QJsonObject& item, unzFile uf = nullptr)
62
    {
63
        const auto itemMap = item.toVariantMap();
64
        const auto overviewMap = itemMap.value("overview").toMap();
65
        const auto detailsMap = itemMap.value("details").toMap();
66

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")) {
73
            int i = 1;
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);
80
                    ++i;
81
                }
82
            }
83
        }
84
        if (overviewMap.contains("tags")) {
85
            entry->setTags(overviewMap.value("tags").toStringList().join(","));
86
        }
87
        if (itemMap.value("favIndex").toString() == "1") {
88
            entry->addTag(QObject::tr("Favorite", "Tag for favorite entries"));
89
        }
90
        if (itemMap.value("state").toString() == "archived") {
91
            entry->addTag(QObject::tr("Archived", "Tag for archived entries"));
92
        }
93

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());
103
            }
104
        }
105
        entry->setNotes(detailsMap.value("notesPlain").toString());
106

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);
115
            }
116

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();
123
                }
124
                name = QString("%1_%2").arg(prefix, name);
125

126
                const auto valueMap = fieldMap.value("value").toMap();
127
                const auto key = valueMap.firstKey();
128
                if (key == "totp") {
129
                    auto totp = valueMap.value(key).toString();
130
                    if (!totp.startsWith("otpauth://")) {
131
                        // Build otpauth url
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);
137
                    }
138

139
                    if (entry->hasTotp()) {
140
                        // Store multiple TOTP definitions as additional otp attributes
141
                        int i = 0;
142
                        name = "otp";
143
                        const auto attributes = entry->attributes()->keys();
144
                        while (attributes.contains(name)) {
145
                            name = QString("otp_%1").arg(++i);
146
                        }
147
                        entry->attributes()->set(name, totp, true);
148
                    } else {
149
                        // First otp value encountered gets formal storage
150
                        entry->setTotp(Totp::parseSettings(totp));
151
                    }
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);
160
                    }
161
                } else {
162
                    auto value = valueMap.value(key).toString();
163
                    if (key == "date") {
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();
175
                    }
176

177
                    if (!value.isEmpty()) {
178
                        entry->attributes()->set(name, value, key == "concealed");
179
                    }
180
                }
181
            }
182
        }
183

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);
192
            }
193
        }
194

195
        // Collapse any accumulated history
196
        entry->removeHistoryItems(entry->historyItems());
197

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

207
        return entry.take();
208
    }
209

210
    void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db, unzFile uf = nullptr)
211
    {
212
        if (!vault.contains("attrs") || !vault.contains("items")) {
213
            // Early out if the vault is missing critical items
214
            return;
215
        }
216

217
        const auto attr = vault.value("attrs").toObject().toVariantMap();
218

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

225
        const auto items = vault.value("items").toArray();
226
        for (const auto& item : items) {
227
            auto entry = readItem(item.toObject(), uf);
228
            if (entry) {
229
                entry->setGroup(group, false);
230
            }
231
        }
232

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);
241
            }
242
        }
243
    }
244
} // namespace
245

246
bool OPUXReader::hasError()
247
{
248
    return !m_error.isEmpty();
249
}
250

251
QString OPUXReader::errorString()
252
{
253
    return m_error;
254
}
255

256
QSharedPointer<Database> OPUXReader::convert(const QString& path)
257
{
258
    m_error.clear();
259

260
    QFileInfo fileinfo(path);
261
    if (!fileinfo.exists()) {
262
        m_error = QObject::tr("File does not exist.").arg(path);
263
        return {};
264
    }
265

266
    // 1PUX is a zip file format, open it and process the contents in memory
267
    auto uf = unzOpen64(fileinfo.absoluteFilePath().toLatin1().constData());
268
    if (!uf) {
269
        m_error = QObject::tr("Invalid 1PUX file format: Not a valid ZIP file.");
270
        return {};
271
    }
272

273
    // Find the export.data file, if not found this isn't a 1PUX file
274
    auto data = extractFile(uf, "export.data");
275
    if (data.isNull()) {
276
        m_error = QObject::tr("Invalid 1PUX file format: Missing export.data");
277
        unzClose(uf);
278
        return {};
279
    }
280

281
    auto db = QSharedPointer<Database>::create();
282
    db->rootGroup()->setName(QObject::tr("1Password Import"));
283
    const auto json = QJsonDocument::fromJson(data);
284

285
    const auto account = json.object().value("accounts").toArray().first().toObject();
286
    const auto vaults = account.value("vaults").toArray();
287

288
    for (const auto& vault : vaults) {
289
        writeVaultToDatabase(vault.toObject(), db, uf);
290
    }
291

292
    unzClose(uf);
293
    return db;
294
}
295

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.