diff options
Diffstat (limited to 'src/format/KdbxXmlReader.cpp')
-rw-r--r-- | src/format/KdbxXmlReader.cpp | 1178 |
1 files changed, 1178 insertions, 0 deletions
diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp new file mode 100644 index 000000000..8f6bd2835 --- /dev/null +++ b/src/format/KdbxXmlReader.cpp @@ -0,0 +1,1178 @@ +/* + * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "KdbxXmlReader.h" +#include "KeePass2RandomStream.h" +#include "core/Global.h" +#include "core/Tools.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/DatabaseIcons.h" +#include "core/Endian.h" +#include "streams/QtIOCompressor" + +#include <QFile> +#include <QBuffer> + +/** + * @param version KDBX version + */ +KdbxXmlReader::KdbxXmlReader(quint32 version) + : m_kdbxVersion(version) +{ +} + +/** + * @param version KDBX version + * @param binaryPool binary pool + */ +KdbxXmlReader::KdbxXmlReader(quint32 version, const QHash<QString, QByteArray>& binaryPool) + : m_kdbxVersion(version) + , m_binaryPool(binaryPool) +{ +} + +/** + * Read XML contents from a file into a new database. + * + * @param device input file + * @return pointer to the new database + */ +Database* KdbxXmlReader::readDatabase(const QString& filename) +{ + QFile file(filename); + file.open(QIODevice::ReadOnly); + return readDatabase(&file); +} + +/** + * Read XML stream from a device into a new database. + * + * @param device input device + * @return pointer to the new database + */ +Database* KdbxXmlReader::readDatabase(QIODevice* device) +{ + auto db = new Database(); + readDatabase(device, db); + return db; +} + +/** + * Read XML contents from a device into a given database using a \link KeePass2RandomStream. + * + * @param device input device + * @param db database to read into + * @param randomStream random stream to use for decryption + */ +#include "QDebug" +void KdbxXmlReader::readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream) +{ + m_error = false; + m_errorStr.clear(); + + m_xml.clear(); + m_xml.setDevice(device); + + m_db = db; + m_meta = m_db->metadata(); + m_meta->setUpdateDatetime(false); + + m_randomStream = randomStream; + m_headerHash.clear(); + + m_tmpParent.reset(new Group()); + + bool rootGroupParsed = false; + + if (m_xml.hasError()) { + raiseError(tr("XML parsing failure: %1").arg(m_xml.error())); + return; + } + + if (m_xml.readNextStartElement() && m_xml.name() == "KeePassFile") { + rootGroupParsed = parseKeePassFile(); + } + + if (!rootGroupParsed) { + raiseError(tr("No root group")); + return; + } + + if (!m_tmpParent->children().isEmpty()) { + qWarning("KdbxXmlReader::readDatabase: found %d invalid group reference(s)", + m_tmpParent->children().size()); + } + + if (!m_tmpParent->entries().isEmpty()) { + qWarning("KdbxXmlReader::readDatabase: found %d invalid entry reference(s)", + m_tmpParent->children().size()); + } + + const QSet<QString> poolKeys = m_binaryPool.keys().toSet(); + const QSet<QString> entryKeys = m_binaryMap.keys().toSet(); + const QSet<QString> unmappedKeys = entryKeys - poolKeys; + const QSet<QString> unusedKeys = poolKeys - entryKeys; + + if (!unmappedKeys.isEmpty()) { + qWarning("Unmapped keys left."); + } + + for (const QString& key : unusedKeys) { + qWarning("KdbxXmlReader::readDatabase: found unused key \"%s\"", qPrintable(key)); + } + + QHash<QString, QPair<Entry*, QString> >::const_iterator i; + for (i = m_binaryMap.constBegin(); i != m_binaryMap.constEnd(); ++i) { + const QPair<Entry*, QString>& target = i.value(); + target.first->attachments()->set(target.second, m_binaryPool[i.key()]); + } + + m_meta->setUpdateDatetime(true); + + QHash<Uuid, Group*>::const_iterator iGroup; + for (iGroup = m_groups.constBegin(); iGroup != m_groups.constEnd(); ++iGroup) { + iGroup.value()->setUpdateTimeinfo(true); + } + + QHash<Uuid, Entry*>::const_iterator iEntry; + for (iEntry = m_entries.constBegin(); iEntry != m_entries.constEnd(); ++iEntry) { + iEntry.value()->setUpdateTimeinfo(true); + + const QList<Entry*> historyItems = iEntry.value()->historyItems(); + for (Entry* histEntry : historyItems) { + histEntry->setUpdateTimeinfo(true); + } + } +} + +bool KdbxXmlReader::strictMode() const +{ + return m_strictMode; +} + +void KdbxXmlReader::setStrictMode(bool strictMode) +{ + m_strictMode = strictMode; +} + +bool KdbxXmlReader::hasError() const +{ + return m_error || m_xml.hasError(); +} + +QString KdbxXmlReader::errorString() const +{ + if (m_error) { + return m_errorStr; + }if (m_xml.hasError()) { + return QString("XML error:\n%1\nLine %2, column %3") + .arg(m_xml.errorString()) + .arg(m_xml.lineNumber()) + .arg(m_xml.columnNumber()); + } + return QString(); +} + +void KdbxXmlReader::raiseError(const QString& errorMessage) +{ + m_error = true; + m_errorStr = errorMessage; +} + +QByteArray KdbxXmlReader::headerHash() const +{ + return m_headerHash; +} + +bool KdbxXmlReader::parseKeePassFile() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "KeePassFile"); + + bool rootElementFound = false; + bool rootParsedSuccessfully = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Meta") { + parseMeta(); + continue; + } + + if (m_xml.name() == "Root") { + if (rootElementFound) { + rootParsedSuccessfully = false; + qWarning("Multiple root elements"); + } else { + rootParsedSuccessfully = parseRoot(); + rootElementFound = true; + } + continue; + } + + skipCurrentElement(); + } + + return rootParsedSuccessfully; +} + +void KdbxXmlReader::parseMeta() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Meta"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Generator") { + m_meta->setGenerator(readString()); + } else if (m_xml.name() == "HeaderHash") { + m_headerHash = readBinary(); + } else if (m_xml.name() == "DatabaseName") { + m_meta->setName(readString()); + } else if (m_xml.name() == "DatabaseNameChanged") { + m_meta->setNameChanged(readDateTime()); + } else if (m_xml.name() == "DatabaseDescription") { + m_meta->setDescription(readString()); + } else if (m_xml.name() == "DatabaseDescriptionChanged") { + m_meta->setDescriptionChanged(readDateTime()); + } else if (m_xml.name() == "DefaultUserName") { + m_meta->setDefaultUserName(readString()); + } else if (m_xml.name() == "DefaultUserNameChanged") { + m_meta->setDefaultUserNameChanged(readDateTime()); + } else if (m_xml.name() == "MaintenanceHistoryDays") { + m_meta->setMaintenanceHistoryDays(readNumber()); + } else if (m_xml.name() == "Color") { + m_meta->setColor(readColor()); + } else if (m_xml.name() == "MasterKeyChanged") { + m_meta->setMasterKeyChanged(readDateTime()); + } else if (m_xml.name() == "MasterKeyChangeRec") { + m_meta->setMasterKeyChangeRec(readNumber()); + } else if (m_xml.name() == "MasterKeyChangeForce") { + m_meta->setMasterKeyChangeForce(readNumber()); + } else if (m_xml.name() == "MemoryProtection") { + parseMemoryProtection(); + } else if (m_xml.name() == "CustomIcons") { + parseCustomIcons(); + } else if (m_xml.name() == "RecycleBinEnabled") { + m_meta->setRecycleBinEnabled(readBool()); + } else if (m_xml.name() == "RecycleBinUUID") { + m_meta->setRecycleBin(getGroup(readUuid())); + } else if (m_xml.name() == "RecycleBinChanged") { + m_meta->setRecycleBinChanged(readDateTime()); + } else if (m_xml.name() == "EntryTemplatesGroup") { + m_meta->setEntryTemplatesGroup(getGroup(readUuid())); + } else if (m_xml.name() == "EntryTemplatesGroupChanged") { + m_meta->setEntryTemplatesGroupChanged(readDateTime()); + } else if (m_xml.name() == "LastSelectedGroup") { + m_meta->setLastSelectedGroup(getGroup(readUuid())); + } else if (m_xml.name() == "LastTopVisibleGroup") { + m_meta->setLastTopVisibleGroup(getGroup(readUuid())); + } else if (m_xml.name() == "HistoryMaxItems") { + int value = readNumber(); + if (value >= -1) { + m_meta->setHistoryMaxItems(value); + } else { + qWarning("HistoryMaxItems invalid number"); + } + } else if (m_xml.name() == "HistoryMaxSize") { + int value = readNumber(); + if (value >= -1) { + m_meta->setHistoryMaxSize(value); + } else { + qWarning("HistoryMaxSize invalid number"); + } + } else if (m_xml.name() == "Binaries") { + parseBinaries(); + } else if (m_xml.name() == "CustomData") { + parseCustomData(m_meta->customData()); + } else if (m_xml.name() == "SettingsChanged") { + m_meta->setSettingsChanged(readDateTime()); + } else { + skipCurrentElement(); + } + } +} + +void KdbxXmlReader::parseMemoryProtection() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "MemoryProtection"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "ProtectTitle") { + m_meta->setProtectTitle(readBool()); + } else if (m_xml.name() == "ProtectUserName") { + m_meta->setProtectUsername(readBool()); + } else if (m_xml.name() == "ProtectPassword") { + m_meta->setProtectPassword(readBool()); + } else if (m_xml.name() == "ProtectURL") { + m_meta->setProtectUrl(readBool()); + } else if (m_xml.name() == "ProtectNotes") { + m_meta->setProtectNotes(readBool()); + } else { + skipCurrentElement(); + } + } +} + +void KdbxXmlReader::parseCustomIcons() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomIcons"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Icon") { + parseIcon(); + } else { + skipCurrentElement(); + } + } +} + +void KdbxXmlReader::parseIcon() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Icon"); + + Uuid uuid; + QImage icon; + bool uuidSet = false; + bool iconSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + uuid = readUuid(); + uuidSet = !uuid.isNull(); + } else if (m_xml.name() == "Data") { + icon.loadFromData(readBinary()); + iconSet = true; + } else { + skipCurrentElement(); + } + } + + if (uuidSet && iconSet) { + m_meta->addCustomIcon(uuid, icon); + return; + } + + raiseError(tr("Missing icon uuid or data")); +} + +void KdbxXmlReader::parseBinaries() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binaries"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() != "Binary") { + skipCurrentElement(); + continue; + } + + QXmlStreamAttributes attr = m_xml.attributes(); + + QString id = attr.value("ID").toString(); + + QByteArray data; + if (attr.value("Compressed").compare(QLatin1String("True"), Qt::CaseInsensitive) == 0) { + data = readCompressedBinary(); + } else { + data = readBinary(); + } + + if (m_binaryPool.contains(id)) { + qWarning("KdbxXmlReader::parseBinaries: overwriting binary item \"%s\"", + qPrintable(id)); + } + + m_binaryPool.insert(id, data); + } +} + +void KdbxXmlReader::parseCustomData(CustomData* customData) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "CustomData"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Item") { + parseCustomDataItem(customData); + continue; + } + skipCurrentElement(); + } +} + +void KdbxXmlReader::parseCustomDataItem(CustomData* customData) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Item"); + + QString key; + QString value; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + } else if (m_xml.name() == "Value") { + value = readString(); + valueSet = true; + } else { + skipCurrentElement(); + } + } + + if (keySet && valueSet) { + customData->set(key, value); + return; + } + + raiseError(tr("Missing custom data key or value")); +} + +bool KdbxXmlReader::parseRoot() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Root"); + + bool groupElementFound = false; + bool groupParsedSuccessfully = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Group") { + if (groupElementFound) { + groupParsedSuccessfully = false; + raiseError(tr("Multiple group elements")); + continue; + } + + Group* rootGroup = parseGroup(); + if (rootGroup) { + Group* oldRoot = m_db->rootGroup(); + m_db->setRootGroup(rootGroup); + delete oldRoot; + groupParsedSuccessfully = true; + } + + groupElementFound = true; + } else if (m_xml.name() == "DeletedObjects") { + parseDeletedObjects(); + } else { + skipCurrentElement(); + } + } + + return groupParsedSuccessfully; +} + +Group* KdbxXmlReader::parseGroup() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group"); + + auto group = new Group(); + group->setUpdateTimeinfo(false); + QList<Group*> children; + QList<Entry*> entries; + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError(tr("Null group uuid")); + } else { + group->setUuid(Uuid::random()); + } + } else { + group->setUuid(uuid); + } + continue; + } + if (m_xml.name() == "Name") { + group->setName(readString()); + continue; + } + if (m_xml.name() == "Notes") { + group->setNotes(readString()); + continue; + } + if (m_xml.name() == "IconID") { + int iconId = readNumber(); + if (iconId < 0) { + if (m_strictMode) { + raiseError(tr("Invalid group icon number")); + } + iconId = 0; + } else if (iconId >= DatabaseIcons::IconCount) { + qWarning("KdbxXmlReader::parseGroup: icon id \"%d\" not supported", iconId); + iconId = DatabaseIcons::IconCount - 1; + } + + group->setIcon(iconId); + continue; + } + if (m_xml.name() == "CustomIconUUID") { + Uuid uuid = readUuid(); + if (!uuid.isNull()) { + group->setIcon(uuid); + } + continue; + } + if (m_xml.name() == "Times") { + group->setTimeInfo(parseTimes()); + continue; + } + if (m_xml.name() == "IsExpanded") { + group->setExpanded(readBool()); + continue; + } + if (m_xml.name() == "DefaultAutoTypeSequence") { + group->setDefaultAutoTypeSequence(readString()); + continue; + } + if (m_xml.name() == "EnableAutoType") { + QString str = readString(); + + if (str.compare("null", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Inherit); + } else if (str.compare("true", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Enable); + } else if (str.compare("false", Qt::CaseInsensitive) == 0) { + group->setAutoTypeEnabled(Group::Disable); + } else { + raiseError(tr("Invalid EnableAutoType value")); + } + continue; + } + if (m_xml.name() == "EnableSearching") { + QString str = readString(); + + if (str.compare("null", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Inherit); + } else if (str.compare("true", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Enable); + } else if (str.compare("false", Qt::CaseInsensitive) == 0) { + group->setSearchingEnabled(Group::Disable); + } else { + raiseError(tr("Invalid EnableSearching value")); + } + continue; + } + if (m_xml.name() == "LastTopVisibleEntry") { + group->setLastTopVisibleEntry(getEntry(readUuid())); + continue; + } + if (m_xml.name() == "Group") { + Group* newGroup = parseGroup(); + if (newGroup) { + children.append(newGroup); + } + continue; + } + if (m_xml.name() == "Entry") { + Entry* newEntry = parseEntry(false); + if (newEntry) { + entries.append(newEntry); + } + continue; + } + if (m_xml.name() == "CustomData") { + parseCustomData(group->customData()); + continue; + } + + skipCurrentElement(); + } + + if (group->uuid().isNull() && !m_strictMode) { + group->setUuid(Uuid::random()); + } + + if (!group->uuid().isNull()) { + Group* tmpGroup = group; + group = getGroup(tmpGroup->uuid()); + group->copyDataFrom(tmpGroup); + group->setUpdateTimeinfo(false); + delete tmpGroup; + } else if (!hasError()) { + raiseError(tr("No group uuid found")); + } + + for (Group* child : asConst(children)) { + child->setParent(group); + } + + for (Entry* entry : asConst(entries)) { + entry->setGroup(group); + } + + return group; +} + +void KdbxXmlReader::parseDeletedObjects() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObjects"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "DeletedObject") { + parseDeletedObject(); + } else { + skipCurrentElement(); + } + } +} + +void KdbxXmlReader::parseDeletedObject() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "DeletedObject"); + + DeletedObject delObj{{}, {}}; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError(tr("Null DeleteObject uuid")); + } + continue; + } + delObj.uuid = uuid; + continue; + } + if (m_xml.name() == "DeletionTime") { + delObj.deletionTime = readDateTime(); + continue; + } + skipCurrentElement(); + } + + if (!delObj.uuid.isNull() && !delObj.deletionTime.isNull()) { + m_db->addDeletedObject(delObj); + return; + } + + if (m_strictMode) { + raiseError(tr("Missing DeletedObject uuid or time")); + } +} + +Entry* KdbxXmlReader::parseEntry(bool history) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Entry"); + + auto entry = new Entry(); + entry->setUpdateTimeinfo(false); + QList<Entry*> historyItems; + QList<StringPair> binaryRefs; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "UUID") { + Uuid uuid = readUuid(); + if (uuid.isNull()) { + if (m_strictMode) { + raiseError(tr("Null entry uuid")); + } else { + entry->setUuid(Uuid::random()); + } + } else { + entry->setUuid(uuid); + } + continue; + } + if (m_xml.name() == "IconID") { + int iconId = readNumber(); + if (iconId < 0) { + if (m_strictMode) { + raiseError(tr("Invalid entry icon number")); + } + iconId = 0; + } + entry->setIcon(iconId); + continue; + } + if (m_xml.name() == "CustomIconUUID") { + Uuid uuid = readUuid(); + if (!uuid.isNull()) { + entry->setIcon(uuid); + } + continue; + } + if (m_xml.name() == "ForegroundColor") { + entry->setForegroundColor(readColor()); + continue; + } + if (m_xml.name() == "BackgroundColor") { + entry->setBackgroundColor(readColor()); + continue; + } + if (m_xml.name() == "OverrideURL") { + entry->setOverrideUrl(readString()); + continue; + } + if (m_xml.name() == "Tags") { + entry->setTags(readString()); + continue; + } + if (m_xml.name() == "Times") { + entry->setTimeInfo(parseTimes()); + continue; + } + if (m_xml.name() == "String") { + parseEntryString(entry); + continue; + } + if (m_xml.name() == "Binary") { + QPair<QString, QString> ref = parseEntryBinary(entry); + if (!ref.first.isNull() && !ref.second.isNull()) { + binaryRefs.append(ref); + } + continue; + } + if (m_xml.name() == "AutoType") { + parseAutoType(entry); + continue; + } + if (m_xml.name() == "History") { + if (history) { + raiseError(tr("History element in history entry")); + } else { + historyItems = parseEntryHistory(); + } + continue; + } + if (m_xml.name() == "CustomData") { + parseCustomData(entry->customData()); + continue; + } + skipCurrentElement(); + } + + if (entry->uuid().isNull() && !m_strictMode) { + entry->setUuid(Uuid::random()); + } + + if (!entry->uuid().isNull()) { + if (history) { + entry->setUpdateTimeinfo(false); + } else { + Entry* tmpEntry = entry; + + entry = getEntry(tmpEntry->uuid()); + entry->copyDataFrom(tmpEntry); + entry->setUpdateTimeinfo(false); + + delete tmpEntry; + } + } else if (!hasError()) { + raiseError(tr("No entry uuid found")); + } + + for (Entry* historyItem : asConst(historyItems)) { + if (historyItem->uuid() != entry->uuid()) { + if (m_strictMode) { + raiseError(tr("History element with different uuid")); + } else { + historyItem->setUuid(entry->uuid()); + } + } + entry->addHistoryItem(historyItem); + } + + for (const StringPair& ref : asConst(binaryRefs)) { + m_binaryMap.insertMulti(ref.first, qMakePair(entry, ref.second)); + } + + return entry; +} + +void KdbxXmlReader::parseEntryString(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "String"); + + QString key; + QString value; + bool protect = false; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + continue; + } + + if (m_xml.name() == "Value") { + QXmlStreamAttributes attr = m_xml.attributes(); + value = readString(); + + bool isProtected = attr.value("Protected") == "True"; + bool protectInMemory = attr.value("ProtectInMemory") == "True"; + + if (isProtected && !value.isEmpty()) { + if (m_randomStream) { + QByteArray ciphertext = QByteArray::fromBase64(value.toLatin1()); + bool ok; + QByteArray plaintext = m_randomStream->process(ciphertext, &ok); + if (!ok) { + value.clear(); + raiseError(m_randomStream->errorString()); + } else { + value = QString::fromUtf8(plaintext); + } + } else { + raiseError(tr("Unable to decrypt entry string")); + continue; + } + } + + protect = isProtected || protectInMemory; + valueSet = true; + continue; + } + + skipCurrentElement(); + } + + if (keySet && valueSet) { + // the default attributes are always there so additionally check if it's empty + if (entry->attributes()->hasKey(key) && !entry->attributes()->value(key).isEmpty()) { + raiseError(tr("Duplicate custom attribute found")); + return; + } + entry->attributes()->set(key, value, protect); + return; + } + + raiseError(tr("Entry string key or value missing")); +} + +QPair<QString, QString> KdbxXmlReader::parseEntryBinary(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Binary"); + + QPair<QString, QString> poolRef; + + QString key; + QByteArray value; + bool keySet = false; + bool valueSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Key") { + key = readString(); + keySet = true; + continue; + } + if (m_xml.name() == "Value") { + QXmlStreamAttributes attr = m_xml.attributes(); + + if (attr.hasAttribute("Ref")) { + poolRef = qMakePair(attr.value("Ref").toString(), key); + m_xml.skipCurrentElement(); + } else { + // format compatibility + value = readBinary(); + bool isProtected = attr.hasAttribute("Protected") + && (attr.value("Protected") == "True"); + + if (isProtected && !value.isEmpty()) { + if (!m_randomStream->processInPlace(value)) { + raiseError(m_randomStream->errorString()); + } + } + } + + valueSet = true; + continue; + } + skipCurrentElement(); + } + + if (keySet && valueSet) { + if (entry->attachments()->hasKey(key)) { + raiseError(tr("Duplicate attachment found")); + } else { + entry->attachments()->set(key, value); + } + } else { + raiseError(tr("Entry binary key or value missing")); + } + + return poolRef; +} + +void KdbxXmlReader::parseAutoType(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "AutoType"); + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Enabled") { + entry->setAutoTypeEnabled(readBool()); + } else if (m_xml.name() == "DataTransferObfuscation") { + entry->setAutoTypeObfuscation(readNumber()); + } else if (m_xml.name() == "DefaultSequence") { + entry->setDefaultAutoTypeSequence(readString()); + } else if (m_xml.name() == "Association") { + parseAutoTypeAssoc(entry); + } else { + skipCurrentElement(); + } + } +} + +void KdbxXmlReader::parseAutoTypeAssoc(Entry* entry) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Association"); + + AutoTypeAssociations::Association assoc; + bool windowSet = false; + bool sequenceSet = false; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Window") { + assoc.window = readString(); + windowSet = true; + } else if (m_xml.name() == "KeystrokeSequence") { + assoc.sequence = readString(); + sequenceSet = true; + } else { + skipCurrentElement(); + } + } + + if (windowSet && sequenceSet) { + entry->autoTypeAssociations()->add(assoc); + return; + } + raiseError(tr("Auto-type association window or sequence missing")); +} + +QList<Entry*> KdbxXmlReader::parseEntryHistory() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "History"); + + QList<Entry*> historyItems; + + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "Entry") { + historyItems.append(parseEntry(true)); + } else { + skipCurrentElement(); + } + } + + return historyItems; +} + +TimeInfo KdbxXmlReader::parseTimes() +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Times"); + + TimeInfo timeInfo; + while (!m_xml.hasError() && m_xml.readNextStartElement()) { + if (m_xml.name() == "LastModificationTime") { + timeInfo.setLastModificationTime(readDateTime()); + } else if (m_xml.name() == "CreationTime") { + timeInfo.setCreationTime(readDateTime()); + } else if (m_xml.name() == "LastAccessTime") { + timeInfo.setLastAccessTime(readDateTime()); + } else if (m_xml.name() == "ExpiryTime") { + timeInfo.setExpiryTime(readDateTime()); + } else if (m_xml.name() == "Expires") { + timeInfo.setExpires(readBool()); + } else if (m_xml.name() == "UsageCount") { + timeInfo.setUsageCount(readNumber()); + } else if (m_xml.name() == "LocationChanged") { + timeInfo.setLocationChanged(readDateTime()); + } else { + skipCurrentElement(); + } + } + + return timeInfo; +} + +QString KdbxXmlReader::readString() +{ + return m_xml.readElementText(); +} + +bool KdbxXmlReader::readBool() +{ + QString str = readString(); + + if (str.compare("True", Qt::CaseInsensitive) == 0) { + return true; + } + if (str.compare("False", Qt::CaseInsensitive) == 0) { + return false; + } + if (str.length() == 0) { + return false; + } + raiseError(tr("Invalid bool value")); + return false; +} + +QDateTime KdbxXmlReader::readDateTime() +{ + static QRegularExpression b64regex("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"); + QString str = readString(); + + if (b64regex.match(str).hasMatch()) { + QByteArray secsBytes = QByteArray::fromBase64(str.toUtf8()).leftJustified(8, '\0', true).left(8); + qint64 secs = Endian::bytesToSizedInt<quint64>(secsBytes, KeePass2::BYTEORDER); + return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs); + } + + QDateTime dt = QDateTime::fromString(str, Qt::ISODate); + if (dt.isValid()) { + return dt; + } + + if (m_strictMode) { + raiseError(tr("Invalid date time value")); + } + + return QDateTime::currentDateTimeUtc(); +} + +QColor KdbxXmlReader::readColor() +{ + QString colorStr = readString(); + + if (colorStr.isEmpty()) { + return {}; + } + + if (colorStr.length() != 7 || colorStr[0] != '#') { + if (m_strictMode) { + raiseError(tr("Invalid color value")); + } + return {}; + } + + QColor color; + for (int i = 0; i <= 2; ++i) { + QString rgbPartStr = colorStr.mid(1 + 2 * i, 2); + bool ok; + int rgbPart = rgbPartStr.toInt(&ok, 16); + if (!ok || rgbPart > 255) { + if (m_strictMode) { + raiseError(tr("Invalid color rgb part")); + } + return {}; + } + + if (i == 0) { + color.setRed(rgbPart); + } else if (i == 1) { + color.setGreen(rgbPart); + } else { + color.setBlue(rgbPart); + } + } + + return color; +} + +int KdbxXmlReader::readNumber() +{ + bool ok; + int result = readString().toInt(&ok); + if (!ok) { + raiseError(tr("Invalid number value")); + } + return result; +} + +Uuid KdbxXmlReader::readUuid() +{ + QByteArray uuidBin = readBinary(); + if (uuidBin.isEmpty()) { + return {}; + } + if (uuidBin.length() != Uuid::Length) { + if (m_strictMode) { + raiseError(tr("Invalid uuid value")); + } + return {}; + } + return Uuid(uuidBin); +} + +QByteArray KdbxXmlReader::readBinary() +{ + return QByteArray::fromBase64(readString().toLatin1()); +} + +QByteArray KdbxXmlReader::readCompressedBinary() +{ + QByteArray rawData = readBinary(); + + QBuffer buffer(&rawData); + buffer.open(QIODevice::ReadOnly); + + QtIOCompressor compressor(&buffer); + compressor.setStreamFormat(QtIOCompressor::GzipFormat); + compressor.open(QIODevice::ReadOnly); + + QByteArray result; + if (!Tools::readAllFromDevice(&compressor, result)) { + //: Translator meant is a binary data inside an entry + raiseError(tr("Unable to decompress binary")); + } + return result; +} + +Group* KdbxXmlReader::getGroup(const Uuid& uuid) +{ + if (uuid.isNull()) { + return nullptr; + } + + if (m_groups.contains(uuid)) { + return m_groups.value(uuid); + } + + auto group = new Group(); + group->setUpdateTimeinfo(false); + group->setUuid(uuid); + group->setParent(m_tmpParent.data()); + m_groups.insert(uuid, group); + return group; +} + +Entry* KdbxXmlReader::getEntry(const Uuid& uuid) +{ + if (uuid.isNull()) { + return nullptr; + } + + if (m_entries.contains(uuid)) { + return m_entries.value(uuid); + } + + auto entry = new Entry(); + entry->setUpdateTimeinfo(false); + entry->setUuid(uuid); + entry->setGroup(m_tmpParent.data()); + m_entries.insert(uuid, entry); + return entry; +} + +void KdbxXmlReader::skipCurrentElement() +{ + qWarning("KdbxXmlReader::skipCurrentElement: skip element \"%s\"", qPrintable(m_xml.name().toString())); + m_xml.skipCurrentElement(); +} + |