diff options
Diffstat (limited to 'src/format/KdbxXmlWriter.cpp')
-rw-r--r-- | src/format/KdbxXmlWriter.cpp | 608 |
1 files changed, 608 insertions, 0 deletions
diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp new file mode 100644 index 000000000..8f405aca0 --- /dev/null +++ b/src/format/KdbxXmlWriter.cpp @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2017 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 "KdbxXmlWriter.h" + +#include <QBuffer> +#include <QFile> + +#include "core/Endian.h" +#include "core/Metadata.h" +#include "format/KeePass2RandomStream.h" +#include "streams/QtIOCompressor" + +/** + * @param version KDBX version + */ +KdbxXmlWriter::KdbxXmlWriter(quint32 version) + : m_kdbxVersion(version) +{ +} + +void KdbxXmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream, const QByteArray& headerHash) +{ + m_db = db; + m_meta = db->metadata(); + m_randomStream = randomStream; + m_headerHash = headerHash; + + m_xml.setAutoFormatting(true); + m_xml.setAutoFormattingIndent(-1); // 1 tab + m_xml.setCodec("UTF-8"); + + generateIdMap(); + + m_xml.setDevice(device); + m_xml.writeStartDocument("1.0", true); + m_xml.writeStartElement("KeePassFile"); + + writeMetadata(); + writeRoot(); + + m_xml.writeEndElement(); + m_xml.writeEndDocument(); + + if (m_xml.hasError()) { + raiseError(device->errorString()); + } +} + +void KdbxXmlWriter::writeDatabase(const QString& filename, Database* db) +{ + QFile file(filename); + file.open(QIODevice::WriteOnly|QIODevice::Truncate); + writeDatabase(&file, db); +} + +bool KdbxXmlWriter::hasError() +{ + return m_error; +} + +QString KdbxXmlWriter::errorString() +{ + return m_errorStr; +} + +void KdbxXmlWriter::generateIdMap() +{ + const QList<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true); + int nextId = 0; + + for (Entry* entry : allEntries) { + const QList<QString> attachmentKeys = entry->attachments()->keys(); + for (const QString& key : attachmentKeys) { + QByteArray data = entry->attachments()->value(key); + if (!m_idMap.contains(data)) { + m_idMap.insert(data, nextId++); + } + } + } +} + +void KdbxXmlWriter::writeMetadata() +{ + m_xml.writeStartElement("Meta"); + writeString("Generator", m_meta->generator()); + if (m_kdbxVersion < KeePass2::FILE_VERSION_4 && !m_headerHash.isEmpty()) { + writeBinary("HeaderHash", m_headerHash); + } + writeString("DatabaseName", m_meta->name()); + writeDateTime("DatabaseNameChanged", m_meta->nameChanged()); + writeString("DatabaseDescription", m_meta->description()); + writeDateTime("DatabaseDescriptionChanged", m_meta->descriptionChanged()); + writeString("DefaultUserName", m_meta->defaultUserName()); + writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged()); + writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays()); + writeColor("Color", m_meta->color()); + writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged()); + writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec()); + writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce()); + writeMemoryProtection(); + writeCustomIcons(); + writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled()); + writeUuid("RecycleBinUUID", m_meta->recycleBin()); + writeDateTime("RecycleBinChanged", m_meta->recycleBinChanged()); + writeUuid("EntryTemplatesGroup", m_meta->entryTemplatesGroup()); + writeDateTime("EntryTemplatesGroupChanged", m_meta->entryTemplatesGroupChanged()); + writeUuid("LastSelectedGroup", m_meta->lastSelectedGroup()); + writeUuid("LastTopVisibleGroup", m_meta->lastTopVisibleGroup()); + writeNumber("HistoryMaxItems", m_meta->historyMaxItems()); + writeNumber("HistoryMaxSize", m_meta->historyMaxSize()); + if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) { + writeDateTime("SettingsChanged", m_meta->settingsChanged()); + } + if (m_kdbxVersion < KeePass2::FILE_VERSION_4) { + writeBinaries(); + } + if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) { + writeCustomData(m_meta->customData()); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeMemoryProtection() +{ + m_xml.writeStartElement("MemoryProtection"); + + writeBool("ProtectTitle", m_meta->protectTitle()); + writeBool("ProtectUserName", m_meta->protectUsername()); + writeBool("ProtectPassword", m_meta->protectPassword()); + writeBool("ProtectURL", m_meta->protectUrl()); + writeBool("ProtectNotes", m_meta->protectNotes()); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeCustomIcons() +{ + m_xml.writeStartElement("CustomIcons"); + + const QList<Uuid> customIconsOrder = m_meta->customIconsOrder(); + for (const Uuid& uuid : customIconsOrder) { + writeIcon(uuid, m_meta->customIcon(uuid)); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeIcon(const Uuid& uuid, const QImage& icon) +{ + m_xml.writeStartElement("Icon"); + + writeUuid("UUID", uuid); + + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + // TODO: check !icon.save() + icon.save(&buffer, "PNG"); + buffer.close(); + writeBinary("Data", ba); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeBinaries() +{ + m_xml.writeStartElement("Binaries"); + + QHash<QByteArray, int>::const_iterator i; + for (i = m_idMap.constBegin(); i != m_idMap.constEnd(); ++i) { + m_xml.writeStartElement("Binary"); + + m_xml.writeAttribute("ID", QString::number(i.value())); + + QByteArray data; + if (m_db->compressionAlgo() == Database::CompressionGZip) { + m_xml.writeAttribute("Compressed", "True"); + + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + + QtIOCompressor compressor(&buffer); + compressor.setStreamFormat(QtIOCompressor::GzipFormat); + compressor.open(QIODevice::WriteOnly); + + qint64 bytesWritten = compressor.write(i.key()); + Q_ASSERT(bytesWritten == i.key().size()); + Q_UNUSED(bytesWritten); + compressor.close(); + + buffer.seek(0); + data = buffer.readAll(); + } + else { + data = i.key(); + } + + if (!data.isEmpty()) { + m_xml.writeCharacters(QString::fromLatin1(data.toBase64())); + } + m_xml.writeEndElement(); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeCustomData(const CustomData* customData) +{ + if (customData->isEmpty()) { + return; + } + m_xml.writeStartElement("CustomData"); + + const QList<QString> keyList = customData->keys(); + for (const QString& key : keyList) { + writeCustomDataItem(key, customData->value(key)); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeCustomDataItem(const QString& key, const QString& value) +{ + m_xml.writeStartElement("Item"); + + writeString("Key", key); + writeString("Value", value); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeRoot() +{ + Q_ASSERT(m_db->rootGroup()); + + m_xml.writeStartElement("Root"); + + writeGroup(m_db->rootGroup()); + writeDeletedObjects(); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeGroup(const Group* group) +{ + Q_ASSERT(!group->uuid().isNull()); + + m_xml.writeStartElement("Group"); + + writeUuid("UUID", group->uuid()); + writeString("Name", group->name()); + writeString("Notes", group->notes()); + writeNumber("IconID", group->iconNumber()); + + if (!group->iconUuid().isNull()) { + writeUuid("CustomIconUUID", group->iconUuid()); + } + writeTimes(group->timeInfo()); + writeBool("IsExpanded", group->isExpanded()); + writeString("DefaultAutoTypeSequence", group->defaultAutoTypeSequence()); + + writeTriState("EnableAutoType", group->autoTypeEnabled()); + + writeTriState("EnableSearching", group->searchingEnabled()); + + writeUuid("LastTopVisibleEntry", group->lastTopVisibleEntry()); + + if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) { + writeCustomData(group->customData()); + } + + const QList<Entry*>& entryList = group->entries(); + for (const Entry* entry : entryList) { + writeEntry(entry); + } + + const QList<Group*>& children = group->children(); + for (const Group* child : children) { + writeGroup(child); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeTimes(const TimeInfo& ti) +{ + m_xml.writeStartElement("Times"); + + writeDateTime("LastModificationTime", ti.lastModificationTime()); + writeDateTime("CreationTime", ti.creationTime()); + writeDateTime("LastAccessTime", ti.lastAccessTime()); + writeDateTime("ExpiryTime", ti.expiryTime()); + writeBool("Expires", ti.expires()); + writeNumber("UsageCount", ti.usageCount()); + writeDateTime("LocationChanged", ti.locationChanged()); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeDeletedObjects() +{ + m_xml.writeStartElement("DeletedObjects"); + + const QList<DeletedObject> delObjList = m_db->deletedObjects(); + for (const DeletedObject& delObj : delObjList) { + writeDeletedObject(delObj); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeDeletedObject(const DeletedObject& delObj) +{ + m_xml.writeStartElement("DeletedObject"); + + writeUuid("UUID", delObj.uuid); + writeDateTime("DeletionTime", delObj.deletionTime); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeEntry(const Entry* entry) +{ + Q_ASSERT(!entry->uuid().isNull()); + + m_xml.writeStartElement("Entry"); + + writeUuid("UUID", entry->uuid()); + writeNumber("IconID", entry->iconNumber()); + if (!entry->iconUuid().isNull()) { + writeUuid("CustomIconUUID", entry->iconUuid()); + } + writeColor("ForegroundColor", entry->foregroundColor()); + writeColor("BackgroundColor", entry->backgroundColor()); + writeString("OverrideURL", entry->overrideUrl()); + writeString("Tags", entry->tags()); + writeTimes(entry->timeInfo()); + + const QList<QString> attributesKeyList = entry->attributes()->keys(); + for (const QString& key : attributesKeyList) { + m_xml.writeStartElement("String"); + + bool protect = ( ((key == "Title") && m_meta->protectTitle()) || + ((key == "UserName") && m_meta->protectUsername()) || + ((key == "Password") && m_meta->protectPassword()) || + ((key == "URL") && m_meta->protectUrl()) || + ((key == "Notes") && m_meta->protectNotes()) || + entry->attributes()->isProtected(key) ); + + writeString("Key", key); + + m_xml.writeStartElement("Value"); + QString value; + + if (protect) { + if (m_randomStream) { + m_xml.writeAttribute("Protected", "True"); + bool ok; + QByteArray rawData = m_randomStream->process(entry->attributes()->value(key).toUtf8(), &ok); + if (!ok) { + raiseError(m_randomStream->errorString()); + } + value = QString::fromLatin1(rawData.toBase64()); + } + else { + m_xml.writeAttribute("ProtectInMemory", "True"); + value = entry->attributes()->value(key); + } + } + else { + value = entry->attributes()->value(key); + } + + if (!value.isEmpty()) { + m_xml.writeCharacters(stripInvalidXml10Chars(value)); + } + m_xml.writeEndElement(); + + m_xml.writeEndElement(); + } + + const QList<QString> attachmentsKeyList = entry->attachments()->keys(); + for (const QString& key : attachmentsKeyList) { + m_xml.writeStartElement("Binary"); + + writeString("Key", key); + + m_xml.writeStartElement("Value"); + m_xml.writeAttribute("Ref", QString::number(m_idMap[entry->attachments()->value(key)])); + m_xml.writeEndElement(); + + m_xml.writeEndElement(); + } + + writeAutoType(entry); + + if (m_kdbxVersion >= KeePass2::FILE_VERSION_4) { + writeCustomData(entry->customData()); + } + + // write history only for entries that are not history items + if (entry->parent()) { + writeEntryHistory(entry); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeAutoType(const Entry* entry) +{ + m_xml.writeStartElement("AutoType"); + + writeBool("Enabled", entry->autoTypeEnabled()); + writeNumber("DataTransferObfuscation", entry->autoTypeObfuscation()); + writeString("DefaultSequence", entry->defaultAutoTypeSequence()); + + const QList<AutoTypeAssociations::Association> autoTypeAssociations = entry->autoTypeAssociations()->getAll(); + for (const AutoTypeAssociations::Association& assoc : autoTypeAssociations) { + writeAutoTypeAssoc(assoc); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc) +{ + m_xml.writeStartElement("Association"); + + writeString("Window", assoc.window); + writeString("KeystrokeSequence", assoc.sequence); + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeEntryHistory(const Entry* entry) +{ + m_xml.writeStartElement("History"); + + const QList<Entry*>& historyItems = entry->historyItems(); + for (const Entry* item : historyItems) { + writeEntry(item); + } + + m_xml.writeEndElement(); +} + +void KdbxXmlWriter::writeString(const QString& qualifiedName, const QString& string) +{ + if (string.isEmpty()) { + m_xml.writeEmptyElement(qualifiedName); + } + else { + m_xml.writeTextElement(qualifiedName, stripInvalidXml10Chars(string)); + } +} + +void KdbxXmlWriter::writeNumber(const QString& qualifiedName, int number) +{ + writeString(qualifiedName, QString::number(number)); +} + +void KdbxXmlWriter::writeBool(const QString& qualifiedName, bool b) +{ + if (b) { + writeString(qualifiedName, "True"); + } + else { + writeString(qualifiedName, "False"); + } +} + +void KdbxXmlWriter::writeDateTime(const QString& qualifiedName, const QDateTime& dateTime) +{ + Q_ASSERT(dateTime.isValid()); + Q_ASSERT(dateTime.timeSpec() == Qt::UTC); + + QString dateTimeStr; + if (m_kdbxVersion < KeePass2::FILE_VERSION_4) { + dateTimeStr = dateTime.toString(Qt::ISODate); + + // Qt < 4.8 doesn't append a 'Z' at the end + if (!dateTimeStr.isEmpty() && dateTimeStr[dateTimeStr.size() - 1] != 'Z') { + dateTimeStr.append('Z'); + } + } else { + qint64 secs = QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).secsTo(dateTime); + QByteArray secsBytes = Endian::sizedIntToBytes(secs, KeePass2::BYTEORDER); + dateTimeStr = QString::fromLatin1(secsBytes.toBase64()); + } + writeString(qualifiedName, dateTimeStr); +} + +void KdbxXmlWriter::writeUuid(const QString& qualifiedName, const Uuid& uuid) +{ + writeString(qualifiedName, uuid.toBase64()); +} + +void KdbxXmlWriter::writeUuid(const QString& qualifiedName, const Group* group) +{ + if (group) { + writeUuid(qualifiedName, group->uuid()); + } + else { + writeUuid(qualifiedName, Uuid()); + } +} + +void KdbxXmlWriter::writeUuid(const QString& qualifiedName, const Entry* entry) +{ + if (entry) { + writeUuid(qualifiedName, entry->uuid()); + } + else { + writeUuid(qualifiedName, Uuid()); + } +} + +void KdbxXmlWriter::writeBinary(const QString& qualifiedName, const QByteArray& ba) +{ + writeString(qualifiedName, QString::fromLatin1(ba.toBase64())); +} + +void KdbxXmlWriter::writeColor(const QString& qualifiedName, const QColor& color) +{ + QString colorStr; + + if (color.isValid()) { + colorStr = QString("#%1%2%3").arg(colorPartToString(color.red()), + colorPartToString(color.green()), + colorPartToString(color.blue())); + } + + writeString(qualifiedName, colorStr); +} + +void KdbxXmlWriter::writeTriState(const QString& qualifiedName, Group::TriState triState) +{ + QString value; + + if (triState == Group::Inherit) { + value = "null"; + } + else if (triState == Group::Enable) { + value = "true"; + } + else { + value = "false"; + } + + writeString(qualifiedName, value); +} + +QString KdbxXmlWriter::colorPartToString(int value) +{ + QString str = QString::number(value, 16).toUpper(); + if (str.length() == 1) { + str.prepend("0"); + } + + return str; +} + +QString KdbxXmlWriter::stripInvalidXml10Chars(QString str) +{ + for (int i = str.size() - 1; i >= 0; i--) { + const QChar ch = str.at(i); + const ushort uc = ch.unicode(); + + if (ch.isLowSurrogate() && i != 0 && str.at(i - 1).isHighSurrogate()) { + // keep valid surrogate pair + i--; + } + else if ((uc < 0x20 && uc != 0x09 && uc != 0x0A && uc != 0x0D) // control characters + || (uc >= 0x7F && uc <= 0x84) // control characters, valid but discouraged by XML + || (uc >= 0x86 && uc <= 0x9F) // control characters, valid but discouraged by XML + || (uc > 0xFFFD) // noncharacter + || ch.isLowSurrogate() // single low surrogate + || ch.isHighSurrogate()) // single high surrogate + { + qWarning("Stripping invalid XML 1.0 codepoint %x", uc); + str.remove(i, 1); + } + } + + return str; +} + +void KdbxXmlWriter::raiseError(const QString& errorMessage) +{ + m_error = true; + m_errorStr = errorMessage; +} |