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