1 /*
2 * Copyright (C) 2018 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 "KdbxXmlReader.h"
19 #include "KeePass2RandomStream.h"
20 #include "core/Clock.h"
21 #include "core/DatabaseIcons.h"
22 #include "core/Endian.h"
23 #include "core/Entry.h"
24 #include "core/Global.h"
25 #include "core/Group.h"
26 #include "core/Tools.h"
27 #include "streams/QtIOCompressor"
28
29 #include <QBuffer>
30 #include <QFile>
31 #include <utility>
32
33 #define UUID_LENGTH 16
34
35 /**
36 * @param version KDBX version
37 */
KdbxXmlReader(quint32 version)38 KdbxXmlReader::KdbxXmlReader(quint32 version)
39 : m_kdbxVersion(version)
40 {
41 }
42
43 /**
44 * @param version KDBX version
45 * @param binaryPool binary pool
46 */
KdbxXmlReader(quint32 version,QHash<QString,QByteArray> binaryPool)47 KdbxXmlReader::KdbxXmlReader(quint32 version, QHash<QString, QByteArray> binaryPool)
48 : m_kdbxVersion(version)
49 , m_binaryPool(std::move(binaryPool))
50 {
51 }
52
53 /**
54 * Read XML contents from a file into a new database.
55 *
56 * @param device input file
57 * @return pointer to the new database
58 */
readDatabase(const QString & filename)59 QSharedPointer<Database> KdbxXmlReader::readDatabase(const QString& filename)
60 {
61 QFile file(filename);
62 file.open(QIODevice::ReadOnly);
63 return readDatabase(&file);
64 }
65
66 /**
67 * Read XML stream from a device into a new database.
68 *
69 * @param device input device
70 * @return pointer to the new database
71 */
readDatabase(QIODevice * device)72 QSharedPointer<Database> KdbxXmlReader::readDatabase(QIODevice* device)
73 {
74 auto db = QSharedPointer<Database>::create();
75 readDatabase(device, db.data());
76 return db;
77 }
78
79 /**
80 * Read XML contents from a device into a given database using a \link KeePass2RandomStream.
81 *
82 * @param device input device
83 * @param db database to read into
84 * @param randomStream random stream to use for decryption
85 */
readDatabase(QIODevice * device,Database * db,KeePass2RandomStream * randomStream)86 void KdbxXmlReader::readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream)
87 {
88 m_error = false;
89 m_errorStr.clear();
90
91 m_xml.clear();
92 m_xml.setDevice(device);
93
94 m_db = db;
95 m_meta = m_db->metadata();
96 m_meta->setUpdateDatetime(false);
97
98 m_randomStream = randomStream;
99 m_headerHash.clear();
100
101 m_tmpParent.reset(new Group());
102
103 bool rootGroupParsed = false;
104
105 if (m_xml.hasError()) {
106 raiseError(tr("XML parsing failure: %1").arg(m_xml.error()));
107 return;
108 }
109
110 if (m_xml.readNextStartElement() && m_xml.name() == "KeePassFile") {
111 rootGroupParsed = parseKeePassFile();
112 }
113
114 if (!rootGroupParsed) {
115 raiseError(tr("No root group"));
116 return;
117 }
118
119 if (!m_tmpParent->children().isEmpty()) {
120 qWarning("KdbxXmlReader::readDatabase: found %d invalid group reference(s)", m_tmpParent->children().size());
121 }
122
123 if (!m_tmpParent->entries().isEmpty()) {
124 qWarning("KdbxXmlReader::readDatabase: found %d invalid entry reference(s)", m_tmpParent->children().size());
125 }
126
127 const QSet<QString> poolKeys = asConst(m_binaryPool).keys().toSet();
128 const QSet<QString> entryKeys = asConst(m_binaryMap).keys().toSet();
129 const QSet<QString> unmappedKeys = entryKeys - poolKeys;
130 const QSet<QString> unusedKeys = poolKeys - entryKeys;
131
132 if (!unmappedKeys.isEmpty()) {
133 qWarning("Unmapped keys left.");
134 }
135
136 for (const QString& key : unusedKeys) {
137 qWarning("KdbxXmlReader::readDatabase: found unused key \"%s\"", qPrintable(key));
138 }
139
140 QHash<QString, QPair<Entry*, QString>>::const_iterator i;
141 for (i = m_binaryMap.constBegin(); i != m_binaryMap.constEnd(); ++i) {
142 const QPair<Entry*, QString>& target = i.value();
143 target.first->attachments()->set(target.second, m_binaryPool[i.key()]);
144 }
145
146 m_meta->setUpdateDatetime(true);
147
148 QHash<QUuid, Group*>::const_iterator iGroup;
149 for (iGroup = m_groups.constBegin(); iGroup != m_groups.constEnd(); ++iGroup) {
150 iGroup.value()->setUpdateTimeinfo(true);
151 }
152
153 QHash<QUuid, Entry*>::const_iterator iEntry;
154 for (iEntry = m_entries.constBegin(); iEntry != m_entries.constEnd(); ++iEntry) {
155 iEntry.value()->setUpdateTimeinfo(true);
156
157 const QList<Entry*> historyItems = iEntry.value()->historyItems();
158 for (Entry* histEntry : historyItems) {
159 histEntry->setUpdateTimeinfo(true);
160 }
161 }
162 }
163
strictMode() const164 bool KdbxXmlReader::strictMode() const
165 {
166 return m_strictMode;
167 }
168
setStrictMode(bool strictMode)169 void KdbxXmlReader::setStrictMode(bool strictMode)
170 {
171 m_strictMode = strictMode;
172 }
173
hasError() const174 bool KdbxXmlReader::hasError() const
175 {
176 return m_error || m_xml.hasError();
177 }
178
errorString() const179 QString KdbxXmlReader::errorString() const
180 {
181 if (m_error) {
182 return m_errorStr;
183 }
184 if (m_xml.hasError()) {
185 return tr("XML error:\n%1\nLine %2, column %3")
186 .arg(m_xml.errorString())
187 .arg(m_xml.lineNumber())
188 .arg(m_xml.columnNumber());
189 }
190 return QString();
191 }
192
isTrueValue(const QStringRef & value)193 bool KdbxXmlReader::isTrueValue(const QStringRef& value)
194 {
195 return value.compare(QLatin1String("true"), Qt::CaseInsensitive) == 0 || value == "1";
196 }
197
raiseError(const QString & errorMessage)198 void KdbxXmlReader::raiseError(const QString& errorMessage)
199 {
200 m_error = true;
201 m_errorStr = errorMessage;
202 }
203
headerHash() const204 QByteArray KdbxXmlReader::headerHash() const
205 {
206 return m_headerHash;
207 }
208
parseKeePassFile()209 bool KdbxXmlReader::parseKeePassFile()
210 {
211 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile");
212
213 bool rootElementFound = false;
214 bool rootParsedSuccessfully = false;
215
216 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
217 if (m_xml.name() == "Meta") {
218 parseMeta();
219 continue;
220 }
221
222 if (m_xml.name() == "Root") {
223 if (rootElementFound) {
224 rootParsedSuccessfully = false;
225 qWarning("Multiple root elements");
226 } else {
227 rootParsedSuccessfully = parseRoot();
228 rootElementFound = true;
229 }
230 continue;
231 }
232
233 skipCurrentElement();
234 }
235
236 return rootParsedSuccessfully;
237 }
238
parseMeta()239 void KdbxXmlReader::parseMeta()
240 {
241 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Meta");
242
243 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
244 if (m_xml.name() == "Generator") {
245 m_meta->setGenerator(readString());
246 } else if (m_xml.name() == "HeaderHash") {
247 m_headerHash = readBinary();
248 } else if (m_xml.name() == "DatabaseName") {
249 m_meta->setName(readString());
250 } else if (m_xml.name() == "DatabaseNameChanged") {
251 m_meta->setNameChanged(readDateTime());
252 } else if (m_xml.name() == "DatabaseDescription") {
253 m_meta->setDescription(readString());
254 } else if (m_xml.name() == "DatabaseDescriptionChanged") {
255 m_meta->setDescriptionChanged(readDateTime());
256 } else if (m_xml.name() == "DefaultUserName") {
257 m_meta->setDefaultUserName(readString());
258 } else if (m_xml.name() == "DefaultUserNameChanged") {
259 m_meta->setDefaultUserNameChanged(readDateTime());
260 } else if (m_xml.name() == "MaintenanceHistoryDays") {
261 m_meta->setMaintenanceHistoryDays(readNumber());
262 } else if (m_xml.name() == "Color") {
263 m_meta->setColor(readColor());
264 } else if (m_xml.name() == "MasterKeyChanged") {
265 m_meta->setDatabaseKeyChanged(readDateTime());
266 } else if (m_xml.name() == "MasterKeyChangeRec") {
267 m_meta->setMasterKeyChangeRec(readNumber());
268 } else if (m_xml.name() == "MasterKeyChangeForce") {
269 m_meta->setMasterKeyChangeForce(readNumber());
270 } else if (m_xml.name() == "MemoryProtection") {
271 parseMemoryProtection();
272 } else if (m_xml.name() == "CustomIcons") {
273 parseCustomIcons();
274 } else if (m_xml.name() == "RecycleBinEnabled") {
275 m_meta->setRecycleBinEnabled(readBool());
276 } else if (m_xml.name() == "RecycleBinUUID") {
277 m_meta->setRecycleBin(getGroup(readUuid()));
278 } else if (m_xml.name() == "RecycleBinChanged") {
279 m_meta->setRecycleBinChanged(readDateTime());
280 } else if (m_xml.name() == "EntryTemplatesGroup") {
281 m_meta->setEntryTemplatesGroup(getGroup(readUuid()));
282 } else if (m_xml.name() == "EntryTemplatesGroupChanged") {
283 m_meta->setEntryTemplatesGroupChanged(readDateTime());
284 } else if (m_xml.name() == "LastSelectedGroup") {
285 m_meta->setLastSelectedGroup(getGroup(readUuid()));
286 } else if (m_xml.name() == "LastTopVisibleGroup") {
287 m_meta->setLastTopVisibleGroup(getGroup(readUuid()));
288 } else if (m_xml.name() == "HistoryMaxItems") {
289 int value = readNumber();
290 if (value >= -1) {
291 m_meta->setHistoryMaxItems(value);
292 } else {
293 qWarning("HistoryMaxItems invalid number");
294 }
295 } else if (m_xml.name() == "HistoryMaxSize") {
296 int value = readNumber();
297 if (value >= -1) {
298 m_meta->setHistoryMaxSize(value);
299 } else {
300 qWarning("HistoryMaxSize invalid number");
301 }
302 } else if (m_xml.name() == "Binaries") {
303 parseBinaries();
304 } else if (m_xml.name() == "CustomData") {
305 parseCustomData(m_meta->customData());
306 } else if (m_xml.name() == "SettingsChanged") {
307 m_meta->setSettingsChanged(readDateTime());
308 } else {
309 skipCurrentElement();
310 }
311 }
312 }
313
parseMemoryProtection()314 void KdbxXmlReader::parseMemoryProtection()
315 {
316 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "MemoryProtection");
317
318 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
319 if (m_xml.name() == "ProtectTitle") {
320 m_meta->setProtectTitle(readBool());
321 } else if (m_xml.name() == "ProtectUserName") {
322 m_meta->setProtectUsername(readBool());
323 } else if (m_xml.name() == "ProtectPassword") {
324 m_meta->setProtectPassword(readBool());
325 } else if (m_xml.name() == "ProtectURL") {
326 m_meta->setProtectUrl(readBool());
327 } else if (m_xml.name() == "ProtectNotes") {
328 m_meta->setProtectNotes(readBool());
329 } else {
330 skipCurrentElement();
331 }
332 }
333 }
334
parseCustomIcons()335 void KdbxXmlReader::parseCustomIcons()
336 {
337 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomIcons");
338
339 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
340 if (m_xml.name() == "Icon") {
341 parseIcon();
342 } else {
343 skipCurrentElement();
344 }
345 }
346 }
347
parseIcon()348 void KdbxXmlReader::parseIcon()
349 {
350 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Icon");
351
352 QUuid uuid;
353 QImage icon;
354 bool uuidSet = false;
355 bool iconSet = false;
356
357 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
358 if (m_xml.name() == "UUID") {
359 uuid = readUuid();
360 uuidSet = !uuid.isNull();
361 } else if (m_xml.name() == "Data") {
362 icon.loadFromData(readBinary());
363 iconSet = true;
364 } else {
365 skipCurrentElement();
366 }
367 }
368
369 if (uuidSet && iconSet) {
370 // Check for duplicate UUID (corruption)
371 if (m_meta->hasCustomIcon(uuid)) {
372 uuid = QUuid::createUuid();
373 }
374 m_meta->addCustomIcon(uuid, icon);
375 return;
376 }
377
378 raiseError(tr("Missing icon uuid or data"));
379 }
380
parseBinaries()381 void KdbxXmlReader::parseBinaries()
382 {
383 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binaries");
384
385 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
386 if (m_xml.name() != "Binary") {
387 skipCurrentElement();
388 continue;
389 }
390
391 QXmlStreamAttributes attr = m_xml.attributes();
392 QString id = attr.value("ID").toString();
393 QByteArray data = isTrueValue(attr.value("Compressed")) ? readCompressedBinary() : readBinary();
394
395 if (m_binaryPool.contains(id)) {
396 qWarning("KdbxXmlReader::parseBinaries: overwriting binary item \"%s\"", qPrintable(id));
397 }
398
399 m_binaryPool.insert(id, data);
400 }
401 }
402
parseCustomData(CustomData * customData)403 void KdbxXmlReader::parseCustomData(CustomData* customData)
404 {
405 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomData");
406
407 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
408 if (m_xml.name() == "Item") {
409 parseCustomDataItem(customData);
410 continue;
411 }
412 skipCurrentElement();
413 }
414 }
415
parseCustomDataItem(CustomData * customData)416 void KdbxXmlReader::parseCustomDataItem(CustomData* customData)
417 {
418 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item");
419
420 QString key;
421 QString value;
422 bool keySet = false;
423 bool valueSet = false;
424
425 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
426 if (m_xml.name() == "Key") {
427 key = readString();
428 keySet = true;
429 } else if (m_xml.name() == "Value") {
430 value = readString();
431 valueSet = true;
432 } else {
433 skipCurrentElement();
434 }
435 }
436
437 if (keySet && valueSet) {
438 customData->set(key, value);
439 return;
440 }
441
442 raiseError(tr("Missing custom data key or value"));
443 }
444
parseRoot()445 bool KdbxXmlReader::parseRoot()
446 {
447 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Root");
448
449 bool groupElementFound = false;
450 bool groupParsedSuccessfully = false;
451
452 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
453 if (m_xml.name() == "Group") {
454 if (groupElementFound) {
455 groupParsedSuccessfully = false;
456 raiseError(tr("Multiple group elements"));
457 continue;
458 }
459
460 Group* rootGroup = parseGroup();
461 if (rootGroup) {
462 Group* oldRoot = m_db->rootGroup();
463 m_db->setRootGroup(rootGroup);
464 delete oldRoot;
465 groupParsedSuccessfully = true;
466 }
467
468 groupElementFound = true;
469 } else if (m_xml.name() == "DeletedObjects") {
470 parseDeletedObjects();
471 } else {
472 skipCurrentElement();
473 }
474 }
475
476 return groupParsedSuccessfully;
477 }
478
parseGroup()479 Group* KdbxXmlReader::parseGroup()
480 {
481 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group");
482
483 auto group = new Group();
484 group->setUpdateTimeinfo(false);
485 QList<Group*> children;
486 QList<Entry*> entries;
487 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
488 if (m_xml.name() == "UUID") {
489 QUuid uuid = readUuid();
490 if (uuid.isNull()) {
491 if (m_strictMode) {
492 raiseError(tr("Null group uuid"));
493 } else {
494 group->setUuid(QUuid::createUuid());
495 }
496 } else {
497 group->setUuid(uuid);
498 }
499 continue;
500 }
501 if (m_xml.name() == "Name") {
502 group->setName(readString());
503 continue;
504 }
505 if (m_xml.name() == "Notes") {
506 group->setNotes(readString());
507 continue;
508 }
509 if (m_xml.name() == "IconID") {
510 int iconId = readNumber();
511 if (iconId < 0) {
512 if (m_strictMode) {
513 raiseError(tr("Invalid group icon number"));
514 }
515 iconId = 0;
516 } else if (iconId >= databaseIcons()->count()) {
517 qWarning("KdbxXmlReader::parseGroup: icon id \"%d\" not supported", iconId);
518 iconId = databaseIcons()->count() - 1;
519 }
520
521 group->setIcon(iconId);
522 continue;
523 }
524 if (m_xml.name() == "CustomIconUUID") {
525 QUuid uuid = readUuid();
526 if (!uuid.isNull()) {
527 group->setIcon(uuid);
528 }
529 continue;
530 }
531 if (m_xml.name() == "Times") {
532 group->setTimeInfo(parseTimes());
533 continue;
534 }
535 if (m_xml.name() == "IsExpanded") {
536 group->setExpanded(readBool());
537 continue;
538 }
539 if (m_xml.name() == "DefaultAutoTypeSequence") {
540 group->setDefaultAutoTypeSequence(readString());
541 continue;
542 }
543 if (m_xml.name() == "EnableAutoType") {
544 QString str = readString();
545
546 if (str.compare("null", Qt::CaseInsensitive) == 0) {
547 group->setAutoTypeEnabled(Group::Inherit);
548 } else if (str.compare("true", Qt::CaseInsensitive) == 0) {
549 group->setAutoTypeEnabled(Group::Enable);
550 } else if (str.compare("false", Qt::CaseInsensitive) == 0) {
551 group->setAutoTypeEnabled(Group::Disable);
552 } else {
553 raiseError(tr("Invalid EnableAutoType value"));
554 }
555 continue;
556 }
557 if (m_xml.name() == "EnableSearching") {
558 QString str = readString();
559
560 if (str.compare("null", Qt::CaseInsensitive) == 0) {
561 group->setSearchingEnabled(Group::Inherit);
562 } else if (str.compare("true", Qt::CaseInsensitive) == 0) {
563 group->setSearchingEnabled(Group::Enable);
564 } else if (str.compare("false", Qt::CaseInsensitive) == 0) {
565 group->setSearchingEnabled(Group::Disable);
566 } else {
567 raiseError(tr("Invalid EnableSearching value"));
568 }
569 continue;
570 }
571 if (m_xml.name() == "LastTopVisibleEntry") {
572 group->setLastTopVisibleEntry(getEntry(readUuid()));
573 continue;
574 }
575 if (m_xml.name() == "Group") {
576 Group* newGroup = parseGroup();
577 if (newGroup) {
578 children.append(newGroup);
579 }
580 continue;
581 }
582 if (m_xml.name() == "Entry") {
583 Entry* newEntry = parseEntry(false);
584 if (newEntry) {
585 entries.append(newEntry);
586 }
587 continue;
588 }
589 if (m_xml.name() == "CustomData") {
590 parseCustomData(group->customData());
591 continue;
592 }
593
594 skipCurrentElement();
595 }
596
597 if (group->uuid().isNull() && !m_strictMode) {
598 group->setUuid(QUuid::createUuid());
599 }
600
601 if (!group->uuid().isNull()) {
602 Group* tmpGroup = group;
603 group = getGroup(tmpGroup->uuid());
604 group->copyDataFrom(tmpGroup);
605 group->setUpdateTimeinfo(false);
606 delete tmpGroup;
607 } else if (!hasError()) {
608 raiseError(tr("No group uuid found"));
609 }
610
611 for (Group* child : asConst(children)) {
612 child->setParent(group);
613 }
614
615 for (Entry* entry : asConst(entries)) {
616 entry->setGroup(group);
617 }
618
619 return group;
620 }
621
parseDeletedObjects()622 void KdbxXmlReader::parseDeletedObjects()
623 {
624 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObjects");
625
626 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
627 if (m_xml.name() == "DeletedObject") {
628 parseDeletedObject();
629 } else {
630 skipCurrentElement();
631 }
632 }
633 }
634
parseDeletedObject()635 void KdbxXmlReader::parseDeletedObject()
636 {
637 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObject");
638
639 DeletedObject delObj{{}, {}};
640
641 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
642 if (m_xml.name() == "UUID") {
643 QUuid uuid = readUuid();
644 if (uuid.isNull()) {
645 if (m_strictMode) {
646 raiseError(tr("Null DeleteObject uuid"));
647 return;
648 }
649 continue;
650 }
651 delObj.uuid = uuid;
652 continue;
653 }
654 if (m_xml.name() == "DeletionTime") {
655 delObj.deletionTime = readDateTime();
656 continue;
657 }
658 skipCurrentElement();
659 }
660
661 if (!delObj.uuid.isNull() && !delObj.deletionTime.isNull()) {
662 m_db->addDeletedObject(delObj);
663 return;
664 }
665
666 if (m_strictMode) {
667 raiseError(tr("Missing DeletedObject uuid or time"));
668 }
669 }
670
parseEntry(bool history)671 Entry* KdbxXmlReader::parseEntry(bool history)
672 {
673 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Entry");
674
675 auto entry = new Entry();
676 entry->setUpdateTimeinfo(false);
677 QList<Entry*> historyItems;
678 QList<StringPair> binaryRefs;
679
680 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
681 if (m_xml.name() == "UUID") {
682 QUuid uuid = readUuid();
683 if (uuid.isNull()) {
684 if (m_strictMode) {
685 raiseError(tr("Null entry uuid"));
686 } else {
687 entry->setUuid(QUuid::createUuid());
688 }
689 } else {
690 entry->setUuid(uuid);
691 }
692 continue;
693 }
694 if (m_xml.name() == "IconID") {
695 int iconId = readNumber();
696 if (iconId < 0) {
697 if (m_strictMode) {
698 raiseError(tr("Invalid entry icon number"));
699 }
700 iconId = 0;
701 }
702 entry->setIcon(iconId);
703 continue;
704 }
705 if (m_xml.name() == "CustomIconUUID") {
706 QUuid uuid = readUuid();
707 if (!uuid.isNull()) {
708 entry->setIcon(uuid);
709 }
710 continue;
711 }
712 if (m_xml.name() == "ForegroundColor") {
713 entry->setForegroundColor(readColor());
714 continue;
715 }
716 if (m_xml.name() == "BackgroundColor") {
717 entry->setBackgroundColor(readColor());
718 continue;
719 }
720 if (m_xml.name() == "OverrideURL") {
721 entry->setOverrideUrl(readString());
722 continue;
723 }
724 if (m_xml.name() == "Tags") {
725 entry->setTags(readString());
726 continue;
727 }
728 if (m_xml.name() == "Times") {
729 entry->setTimeInfo(parseTimes());
730 continue;
731 }
732 if (m_xml.name() == "String") {
733 parseEntryString(entry);
734 continue;
735 }
736 if (m_xml.name() == "Binary") {
737 QPair<QString, QString> ref = parseEntryBinary(entry);
738 if (!ref.first.isEmpty() && !ref.second.isEmpty()) {
739 binaryRefs.append(ref);
740 }
741 continue;
742 }
743 if (m_xml.name() == "AutoType") {
744 parseAutoType(entry);
745 continue;
746 }
747 if (m_xml.name() == "History") {
748 if (history) {
749 raiseError(tr("History element in history entry"));
750 } else {
751 historyItems = parseEntryHistory();
752 }
753 continue;
754 }
755 if (m_xml.name() == "CustomData") {
756 parseCustomData(entry->customData());
757 continue;
758 }
759 skipCurrentElement();
760 }
761
762 if (entry->uuid().isNull() && !m_strictMode) {
763 entry->setUuid(QUuid::createUuid());
764 }
765
766 if (!entry->uuid().isNull()) {
767 if (history) {
768 entry->setUpdateTimeinfo(false);
769 } else {
770 Entry* tmpEntry = entry;
771
772 entry = getEntry(tmpEntry->uuid());
773 entry->copyDataFrom(tmpEntry);
774 entry->setUpdateTimeinfo(false);
775
776 delete tmpEntry;
777 }
778 } else if (!hasError()) {
779 raiseError(tr("No entry uuid found"));
780 }
781
782 for (Entry* historyItem : asConst(historyItems)) {
783 if (historyItem->uuid() != entry->uuid()) {
784 if (m_strictMode) {
785 raiseError(tr("History element with different uuid"));
786 } else {
787 historyItem->setUuid(entry->uuid());
788 }
789 }
790 entry->addHistoryItem(historyItem);
791 }
792
793 for (const StringPair& ref : asConst(binaryRefs)) {
794 m_binaryMap.insertMulti(ref.first, qMakePair(entry, ref.second));
795 }
796
797 return entry;
798 }
799
parseEntryString(Entry * entry)800 void KdbxXmlReader::parseEntryString(Entry* entry)
801 {
802 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "String");
803
804 QString key;
805 QString value;
806 bool protect = false;
807 bool keySet = false;
808 bool valueSet = false;
809
810 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
811 if (m_xml.name() == "Key") {
812 key = readString();
813 keySet = true;
814 continue;
815 }
816
817 if (m_xml.name() == "Value") {
818 QXmlStreamAttributes attr = m_xml.attributes();
819 bool isProtected;
820 bool protectInMemory;
821 value = readString(isProtected, protectInMemory);
822 protect = isProtected || protectInMemory;
823 valueSet = true;
824 continue;
825 }
826
827 skipCurrentElement();
828 }
829
830 if (keySet && valueSet) {
831 // the default attributes are always there so additionally check if it's empty
832 if (entry->attributes()->hasKey(key) && !entry->attributes()->value(key).isEmpty()) {
833 raiseError(tr("Duplicate custom attribute found"));
834 return;
835 }
836 entry->attributes()->set(key, value, protect);
837 return;
838 }
839
840 raiseError(tr("Entry string key or value missing"));
841 }
842
parseEntryBinary(Entry * entry)843 QPair<QString, QString> KdbxXmlReader::parseEntryBinary(Entry* entry)
844 {
845 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binary");
846
847 QPair<QString, QString> poolRef;
848
849 QString key;
850 QByteArray value;
851 bool keySet = false;
852 bool valueSet = false;
853
854 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
855 if (m_xml.name() == "Key") {
856 key = readString();
857 keySet = true;
858 continue;
859 }
860 if (m_xml.name() == "Value") {
861 QXmlStreamAttributes attr = m_xml.attributes();
862
863 if (attr.hasAttribute("Ref")) {
864 poolRef = qMakePair(attr.value("Ref").toString(), key);
865 m_xml.skipCurrentElement();
866 } else {
867 // format compatibility
868 value = readBinary();
869 }
870
871 valueSet = true;
872 continue;
873 }
874 skipCurrentElement();
875 }
876
877 if (keySet && valueSet) {
878 if (entry->attachments()->hasKey(key) && entry->attachments()->value(key) != value) {
879 // NOTE: This only impacts KDBX 3.x databases
880 // Prepend a random string to the key to make it unique and prevent data loss
881 key = key.prepend(QUuid::createUuid().toString().mid(1, 8) + "_");
882 qWarning("Duplicate attachment name found, renamed to: %s", qPrintable(key));
883 }
884 entry->attachments()->set(key, value);
885 } else {
886 raiseError(tr("Entry binary key or value missing"));
887 }
888
889 return poolRef;
890 }
891
parseAutoType(Entry * entry)892 void KdbxXmlReader::parseAutoType(Entry* entry)
893 {
894 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "AutoType");
895
896 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
897 if (m_xml.name() == "Enabled") {
898 entry->setAutoTypeEnabled(readBool());
899 } else if (m_xml.name() == "DataTransferObfuscation") {
900 entry->setAutoTypeObfuscation(readNumber());
901 } else if (m_xml.name() == "DefaultSequence") {
902 entry->setDefaultAutoTypeSequence(readString());
903 } else if (m_xml.name() == "Association") {
904 parseAutoTypeAssoc(entry);
905 } else {
906 skipCurrentElement();
907 }
908 }
909 }
910
parseAutoTypeAssoc(Entry * entry)911 void KdbxXmlReader::parseAutoTypeAssoc(Entry* entry)
912 {
913 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Association");
914
915 AutoTypeAssociations::Association assoc;
916 bool windowSet = false;
917 bool sequenceSet = false;
918
919 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
920 if (m_xml.name() == "Window") {
921 assoc.window = readString();
922 windowSet = true;
923 } else if (m_xml.name() == "KeystrokeSequence") {
924 assoc.sequence = readString();
925 sequenceSet = true;
926 } else {
927 skipCurrentElement();
928 }
929 }
930
931 if (windowSet && sequenceSet) {
932 entry->autoTypeAssociations()->add(assoc);
933 return;
934 }
935 raiseError(tr("Auto-type association window or sequence missing"));
936 }
937
parseEntryHistory()938 QList<Entry*> KdbxXmlReader::parseEntryHistory()
939 {
940 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "History");
941
942 QList<Entry*> historyItems;
943
944 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
945 if (m_xml.name() == "Entry") {
946 historyItems.append(parseEntry(true));
947 } else {
948 skipCurrentElement();
949 }
950 }
951
952 return historyItems;
953 }
954
parseTimes()955 TimeInfo KdbxXmlReader::parseTimes()
956 {
957 Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Times");
958
959 TimeInfo timeInfo;
960 while (!m_xml.hasError() && m_xml.readNextStartElement()) {
961 if (m_xml.name() == "LastModificationTime") {
962 timeInfo.setLastModificationTime(readDateTime());
963 } else if (m_xml.name() == "CreationTime") {
964 timeInfo.setCreationTime(readDateTime());
965 } else if (m_xml.name() == "LastAccessTime") {
966 timeInfo.setLastAccessTime(readDateTime());
967 } else if (m_xml.name() == "ExpiryTime") {
968 timeInfo.setExpiryTime(readDateTime());
969 } else if (m_xml.name() == "Expires") {
970 timeInfo.setExpires(readBool());
971 } else if (m_xml.name() == "UsageCount") {
972 timeInfo.setUsageCount(readNumber());
973 } else if (m_xml.name() == "LocationChanged") {
974 timeInfo.setLocationChanged(readDateTime());
975 } else {
976 skipCurrentElement();
977 }
978 }
979
980 return timeInfo;
981 }
982
readString()983 QString KdbxXmlReader::readString()
984 {
985 bool isProtected;
986 bool protectInMemory;
987
988 return readString(isProtected, protectInMemory);
989 }
990
readString(bool & isProtected,bool & protectInMemory)991 QString KdbxXmlReader::readString(bool& isProtected, bool& protectInMemory)
992 {
993 QXmlStreamAttributes attr = m_xml.attributes();
994 isProtected = isTrueValue(attr.value("Protected"));
995 protectInMemory = isTrueValue(attr.value("ProtectInMemory"));
996 QString value = m_xml.readElementText();
997
998 if (isProtected && !value.isEmpty()) {
999 QByteArray ciphertext = QByteArray::fromBase64(value.toLatin1());
1000 bool ok;
1001 QByteArray plaintext = m_randomStream->process(ciphertext, &ok);
1002 if (!ok) {
1003 value.clear();
1004 raiseError(m_randomStream->errorString());
1005 return value;
1006 }
1007
1008 value = QString::fromUtf8(plaintext);
1009 }
1010
1011 return value;
1012 }
1013
readBool()1014 bool KdbxXmlReader::readBool()
1015 {
1016 QString str = readString();
1017
1018 if (str.compare("true", Qt::CaseInsensitive) == 0) {
1019 return true;
1020 }
1021 if (str.compare("false", Qt::CaseInsensitive) == 0) {
1022 return false;
1023 }
1024 if (str.length() == 0) {
1025 return false;
1026 }
1027 raiseError(tr("Invalid bool value"));
1028 return false;
1029 }
1030
readDateTime()1031 QDateTime KdbxXmlReader::readDateTime()
1032 {
1033 QString str = readString();
1034 if (Tools::isBase64(str.toLatin1())) {
1035 QByteArray secsBytes = QByteArray::fromBase64(str.toUtf8()).leftJustified(8, '\0', true).left(8);
1036 qint64 secs = Endian::bytesToSizedInt<quint64>(secsBytes, KeePass2::BYTEORDER);
1037 return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs);
1038 }
1039
1040 QDateTime dt = Clock::parse(str, Qt::ISODate);
1041 if (dt.isValid()) {
1042 return dt;
1043 }
1044
1045 if (m_strictMode) {
1046 raiseError(tr("Invalid date time value"));
1047 }
1048
1049 return Clock::currentDateTimeUtc();
1050 }
1051
readColor()1052 QString KdbxXmlReader::readColor()
1053 {
1054 QString colorStr = readString();
1055
1056 if (colorStr.isEmpty()) {
1057 return colorStr;
1058 }
1059
1060 if (colorStr.length() != 7 || colorStr[0] != '#') {
1061 if (m_strictMode) {
1062 raiseError(tr("Invalid color value"));
1063 }
1064 return colorStr;
1065 }
1066
1067 for (int i = 0; i <= 2; ++i) {
1068 QString rgbPartStr = colorStr.mid(1 + 2 * i, 2);
1069 bool ok;
1070 int rgbPart = rgbPartStr.toInt(&ok, 16);
1071 if (!ok || rgbPart > 255) {
1072 if (m_strictMode) {
1073 raiseError(tr("Invalid color rgb part"));
1074 }
1075 return colorStr;
1076 }
1077 }
1078
1079 return colorStr;
1080 }
1081
readNumber()1082 int KdbxXmlReader::readNumber()
1083 {
1084 bool ok;
1085 int result = readString().toInt(&ok);
1086 if (!ok) {
1087 raiseError(tr("Invalid number value"));
1088 }
1089 return result;
1090 }
1091
readUuid()1092 QUuid KdbxXmlReader::readUuid()
1093 {
1094 QByteArray uuidBin = readBinary();
1095 if (uuidBin.isEmpty()) {
1096 return QUuid();
1097 }
1098 if (uuidBin.length() != UUID_LENGTH) {
1099 if (m_strictMode) {
1100 raiseError(tr("Invalid uuid value"));
1101 }
1102 return QUuid();
1103 }
1104 return QUuid::fromRfc4122(uuidBin);
1105 }
1106
readBinary()1107 QByteArray KdbxXmlReader::readBinary()
1108 {
1109 QXmlStreamAttributes attr = m_xml.attributes();
1110 bool isProtected = isTrueValue(attr.value("Protected"));
1111 QString value = m_xml.readElementText();
1112 QByteArray data = QByteArray::fromBase64(value.toLatin1());
1113
1114 if (isProtected && !data.isEmpty()) {
1115 bool ok;
1116 QByteArray plaintext = m_randomStream->process(data, &ok);
1117 if (!ok) {
1118 data.clear();
1119 raiseError(m_randomStream->errorString());
1120 return data;
1121 }
1122
1123 data = plaintext;
1124 }
1125
1126 return data;
1127 }
1128
readCompressedBinary()1129 QByteArray KdbxXmlReader::readCompressedBinary()
1130 {
1131 QByteArray rawData = readBinary();
1132
1133 QBuffer buffer(&rawData);
1134 buffer.open(QIODevice::ReadOnly);
1135
1136 QtIOCompressor compressor(&buffer);
1137 compressor.setStreamFormat(QtIOCompressor::GzipFormat);
1138 compressor.open(QIODevice::ReadOnly);
1139
1140 QByteArray result;
1141 if (!Tools::readAllFromDevice(&compressor, result)) {
1142 //: Translator meant is a binary data inside an entry
1143 raiseError(tr("Unable to decompress binary"));
1144 }
1145 return result;
1146 }
1147
getGroup(const QUuid & uuid)1148 Group* KdbxXmlReader::getGroup(const QUuid& uuid)
1149 {
1150 if (uuid.isNull()) {
1151 return nullptr;
1152 }
1153
1154 if (m_groups.contains(uuid)) {
1155 return m_groups.value(uuid);
1156 }
1157
1158 auto group = new Group();
1159 group->setUpdateTimeinfo(false);
1160 group->setUuid(uuid);
1161 group->setParent(m_tmpParent.data());
1162 m_groups.insert(uuid, group);
1163 return group;
1164 }
1165
getEntry(const QUuid & uuid)1166 Entry* KdbxXmlReader::getEntry(const QUuid& uuid)
1167 {
1168 if (uuid.isNull()) {
1169 return nullptr;
1170 }
1171
1172 if (m_entries.contains(uuid)) {
1173 return m_entries.value(uuid);
1174 }
1175
1176 auto entry = new Entry();
1177 entry->setUpdateTimeinfo(false);
1178 entry->setUuid(uuid);
1179 entry->setGroup(m_tmpParent.data());
1180 m_entries.insert(uuid, entry);
1181 return entry;
1182 }
1183
skipCurrentElement()1184 void KdbxXmlReader::skipCurrentElement()
1185 {
1186 qWarning("KdbxXmlReader::skipCurrentElement: skip element \"%s\"", qPrintable(m_xml.name().toString()));
1187 m_xml.skipCurrentElement();
1188 }
1189