1 /*
2  *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
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 "KeePass2XmlWriter.h"
19 
20 #include <QBuffer>
21 #include <QFile>
22 
23 #include "core/Metadata.h"
24 #include "format/KeePass2RandomStream.h"
25 #include "streams/QtIOCompressor"
26 
KeePass2XmlWriter()27 KeePass2XmlWriter::KeePass2XmlWriter()
28     : m_db(nullptr)
29     , m_meta(nullptr)
30     , m_randomStream(nullptr)
31     , m_error(false)
32 {
33     m_xml.setAutoFormatting(true);
34     m_xml.setAutoFormattingIndent(-1); // 1 tab
35     m_xml.setCodec("UTF-8");
36 }
37 
writeDatabase(QIODevice * device,Database * db,KeePass2RandomStream * randomStream,const QByteArray & headerHash)38 void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream,
39                                       const QByteArray& headerHash)
40 {
41     m_db = db;
42     m_meta = db->metadata();
43     m_randomStream = randomStream;
44     m_headerHash = headerHash;
45 
46     generateIdMap();
47 
48     m_xml.setDevice(device);
49 
50     m_xml.writeStartDocument("1.0", true);
51 
52     m_xml.writeStartElement("KeePassFile");
53 
54     writeMetadata();
55     writeRoot();
56 
57     m_xml.writeEndElement();
58 
59     m_xml.writeEndDocument();
60 
61     if (m_xml.hasError()) {
62         raiseError(device->errorString());
63     }
64 }
65 
writeDatabase(const QString & filename,Database * db)66 void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db)
67 {
68     QFile file(filename);
69     file.open(QIODevice::WriteOnly|QIODevice::Truncate);
70     writeDatabase(&file, db);
71 }
72 
hasError()73 bool KeePass2XmlWriter::hasError()
74 {
75     return m_error;
76 }
77 
errorString()78 QString KeePass2XmlWriter::errorString()
79 {
80     return m_errorStr;
81 }
82 
generateIdMap()83 void KeePass2XmlWriter::generateIdMap()
84 {
85     const QList<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true);
86     int nextId = 0;
87 
88     for (Entry* entry : allEntries) {
89         const QList<QString> attachmentKeys = entry->attachments()->keys();
90         for (const QString& key : attachmentKeys) {
91             QByteArray data = entry->attachments()->value(key);
92             if (!m_idMap.contains(data)) {
93                 m_idMap.insert(data, nextId++);
94             }
95         }
96     }
97 }
98 
writeMetadata()99 void KeePass2XmlWriter::writeMetadata()
100 {
101     m_xml.writeStartElement("Meta");
102 
103     writeString("Generator", m_meta->generator());
104     if (!m_headerHash.isEmpty()) {
105         writeBinary("HeaderHash", m_headerHash);
106     }
107     writeString("DatabaseName", m_meta->name());
108     writeDateTime("DatabaseNameChanged", m_meta->nameChanged());
109     writeString("DatabaseDescription", m_meta->description());
110     writeDateTime("DatabaseDescriptionChanged", m_meta->descriptionChanged());
111     writeString("DefaultUserName", m_meta->defaultUserName());
112     writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged());
113     writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays());
114     writeColor("Color", m_meta->color());
115     writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged());
116     writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec());
117     writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce());
118     writeMemoryProtection();
119     writeCustomIcons();
120     writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled());
121     writeUuid("RecycleBinUUID", m_meta->recycleBin());
122     writeDateTime("RecycleBinChanged", m_meta->recycleBinChanged());
123     writeUuid("EntryTemplatesGroup", m_meta->entryTemplatesGroup());
124     writeDateTime("EntryTemplatesGroupChanged", m_meta->entryTemplatesGroupChanged());
125     writeUuid("LastSelectedGroup", m_meta->lastSelectedGroup());
126     writeUuid("LastTopVisibleGroup", m_meta->lastTopVisibleGroup());
127     writeNumber("HistoryMaxItems", m_meta->historyMaxItems());
128     writeNumber("HistoryMaxSize", m_meta->historyMaxSize());
129     writeBinaries();
130     writeCustomData();
131 
132     m_xml.writeEndElement();
133 }
134 
writeMemoryProtection()135 void KeePass2XmlWriter::writeMemoryProtection()
136 {
137     m_xml.writeStartElement("MemoryProtection");
138 
139     writeBool("ProtectTitle", m_meta->protectTitle());
140     writeBool("ProtectUserName", m_meta->protectUsername());
141     writeBool("ProtectPassword", m_meta->protectPassword());
142     writeBool("ProtectURL", m_meta->protectUrl());
143     writeBool("ProtectNotes", m_meta->protectNotes());
144     // writeBool("AutoEnableVisualHiding", m_meta->autoEnableVisualHiding());
145 
146     m_xml.writeEndElement();
147 }
148 
writeCustomIcons()149 void KeePass2XmlWriter::writeCustomIcons()
150 {
151     m_xml.writeStartElement("CustomIcons");
152 
153     const QList<Uuid> customIconsOrder = m_meta->customIconsOrder();
154     for (const Uuid& uuid : customIconsOrder) {
155         writeIcon(uuid, m_meta->customIcon(uuid));
156     }
157 
158     m_xml.writeEndElement();
159 }
160 
writeIcon(const Uuid & uuid,const QImage & icon)161 void KeePass2XmlWriter::writeIcon(const Uuid& uuid, const QImage& icon)
162 {
163     m_xml.writeStartElement("Icon");
164 
165     writeUuid("UUID", uuid);
166 
167     QByteArray ba;
168     QBuffer buffer(&ba);
169     buffer.open(QIODevice::WriteOnly);
170     // TODO: check !icon.save()
171     icon.save(&buffer, "PNG");
172     buffer.close();
173     writeBinary("Data", ba);
174 
175     m_xml.writeEndElement();
176 }
177 
writeBinaries()178 void KeePass2XmlWriter::writeBinaries()
179 {
180     m_xml.writeStartElement("Binaries");
181 
182     QHash<QByteArray, int>::const_iterator i;
183     for (i = m_idMap.constBegin(); i != m_idMap.constEnd(); ++i) {
184         m_xml.writeStartElement("Binary");
185 
186         m_xml.writeAttribute("ID", QString::number(i.value()));
187 
188         QByteArray data;
189         if (m_db->compressionAlgo() == Database::CompressionGZip) {
190             m_xml.writeAttribute("Compressed", "True");
191 
192             QBuffer buffer;
193             buffer.open(QIODevice::ReadWrite);
194 
195             QtIOCompressor compressor(&buffer);
196             compressor.setStreamFormat(QtIOCompressor::GzipFormat);
197             compressor.open(QIODevice::WriteOnly);
198 
199             qint64 bytesWritten = compressor.write(i.key());
200             Q_ASSERT(bytesWritten == i.key().size());
201             Q_UNUSED(bytesWritten);
202             compressor.close();
203 
204             buffer.seek(0);
205             data = buffer.readAll();
206         }
207         else {
208             data = i.key();
209         }
210 
211         if (!data.isEmpty()) {
212             m_xml.writeCharacters(QString::fromLatin1(data.toBase64()));
213         }
214         m_xml.writeEndElement();
215     }
216 
217     m_xml.writeEndElement();
218 }
219 
writeCustomData()220 void KeePass2XmlWriter::writeCustomData()
221 {
222     m_xml.writeStartElement("CustomData");
223 
224     QHash<QString, QString> customFields = m_meta->customFields();
225     const QList<QString> keyList = customFields.keys();
226     for (const QString& key : keyList) {
227         writeCustomDataItem(key, customFields.value(key));
228     }
229 
230     m_xml.writeEndElement();
231 }
232 
writeCustomDataItem(const QString & key,const QString & value)233 void KeePass2XmlWriter::writeCustomDataItem(const QString& key, const QString& value)
234 {
235     m_xml.writeStartElement("Item");
236 
237     writeString("Key", key);
238     writeString("Value", value);
239 
240     m_xml.writeEndElement();
241 }
242 
writeRoot()243 void KeePass2XmlWriter::writeRoot()
244 {
245     Q_ASSERT(m_db->rootGroup());
246 
247     m_xml.writeStartElement("Root");
248 
249     writeGroup(m_db->rootGroup());
250     writeDeletedObjects();
251 
252     m_xml.writeEndElement();
253 }
254 
writeGroup(const Group * group)255 void KeePass2XmlWriter::writeGroup(const Group* group)
256 {
257     Q_ASSERT(!group->uuid().isNull());
258 
259     m_xml.writeStartElement("Group");
260 
261     writeUuid("UUID", group->uuid());
262     writeString("Name", group->name());
263     writeString("Notes", group->notes());
264     writeNumber("IconID", group->iconNumber());
265 
266     if (!group->iconUuid().isNull()) {
267         writeUuid("CustomIconUUID", group->iconUuid());
268     }
269     writeTimes(group->timeInfo());
270     writeBool("IsExpanded", group->isExpanded());
271     writeString("DefaultAutoTypeSequence", group->defaultAutoTypeSequence());
272 
273     writeTriState("EnableAutoType", group->autoTypeEnabled());
274 
275     writeTriState("EnableSearching", group->searchingEnabled());
276 
277     writeUuid("LastTopVisibleEntry", group->lastTopVisibleEntry());
278 
279     const QList<Entry*> entryList = group->entries();
280     for (const Entry* entry : entryList) {
281         writeEntry(entry);
282     }
283 
284     const QList<Group*> children = group->children();
285     for (const Group* child : children) {
286         writeGroup(child);
287     }
288 
289     m_xml.writeEndElement();
290 }
291 
writeTimes(const TimeInfo & ti)292 void KeePass2XmlWriter::writeTimes(const TimeInfo& ti)
293 {
294     m_xml.writeStartElement("Times");
295 
296     writeDateTime("LastModificationTime", ti.lastModificationTime());
297     writeDateTime("CreationTime", ti.creationTime());
298     writeDateTime("LastAccessTime", ti.lastAccessTime());
299     writeDateTime("ExpiryTime", ti.expiryTime());
300     writeBool("Expires", ti.expires());
301     writeNumber("UsageCount", ti.usageCount());
302     writeDateTime("LocationChanged", ti.locationChanged());
303 
304     m_xml.writeEndElement();
305 }
306 
writeDeletedObjects()307 void KeePass2XmlWriter::writeDeletedObjects()
308 {
309     m_xml.writeStartElement("DeletedObjects");
310 
311     const QList<DeletedObject> delObjList = m_db->deletedObjects();
312     for (const DeletedObject& delObj : delObjList) {
313         writeDeletedObject(delObj);
314     }
315 
316     m_xml.writeEndElement();
317 }
318 
writeDeletedObject(const DeletedObject & delObj)319 void KeePass2XmlWriter::writeDeletedObject(const DeletedObject& delObj)
320 {
321     m_xml.writeStartElement("DeletedObject");
322 
323     writeUuid("UUID", delObj.uuid);
324     writeDateTime("DeletionTime", delObj.deletionTime);
325 
326     m_xml.writeEndElement();
327 }
328 
writeEntry(const Entry * entry)329 void KeePass2XmlWriter::writeEntry(const Entry* entry)
330 {
331     Q_ASSERT(!entry->uuid().isNull());
332 
333     m_xml.writeStartElement("Entry");
334 
335     writeUuid("UUID", entry->uuid());
336     writeNumber("IconID", entry->iconNumber());
337     if (!entry->iconUuid().isNull()) {
338         writeUuid("CustomIconUUID", entry->iconUuid());
339     }
340     writeColor("ForegroundColor", entry->foregroundColor());
341     writeColor("BackgroundColor", entry->backgroundColor());
342     writeString("OverrideURL", entry->overrideUrl());
343     writeString("Tags", entry->tags());
344     writeTimes(entry->timeInfo());
345 
346     const QList<QString> attributesKeyList = entry->attributes()->keys();
347     for (const QString& key : attributesKeyList) {
348         m_xml.writeStartElement("String");
349 
350         bool protect = ( ((key == "Title") && m_meta->protectTitle()) ||
351                          ((key == "UserName") && m_meta->protectUsername()) ||
352                          ((key == "Password") && m_meta->protectPassword()) ||
353                          ((key == "URL") && m_meta->protectUrl()) ||
354                          ((key == "Notes") && m_meta->protectNotes()) ||
355                          entry->attributes()->isProtected(key) );
356 
357         writeString("Key", key);
358 
359         m_xml.writeStartElement("Value");
360         QString value;
361 
362         if (protect) {
363             if (m_randomStream) {
364                 m_xml.writeAttribute("Protected", "True");
365                 bool ok;
366                 QByteArray rawData = m_randomStream->process(entry->attributes()->value(key).toUtf8(), &ok);
367                 if (!ok) {
368                     raiseError(m_randomStream->errorString());
369                 }
370                 value = QString::fromLatin1(rawData.toBase64());
371             }
372             else {
373                 m_xml.writeAttribute("ProtectInMemory", "True");
374                 value = entry->attributes()->value(key);
375             }
376         }
377         else {
378             value = entry->attributes()->value(key);
379         }
380 
381         if (!value.isEmpty()) {
382             m_xml.writeCharacters(stripInvalidXml10Chars(value));
383         }
384         m_xml.writeEndElement();
385 
386         m_xml.writeEndElement();
387     }
388 
389     const QList<QString> attachmentsKeyList = entry->attachments()->keys();
390     for (const QString& key : attachmentsKeyList) {
391         m_xml.writeStartElement("Binary");
392 
393         writeString("Key", key);
394 
395         m_xml.writeStartElement("Value");
396         m_xml.writeAttribute("Ref", QString::number(m_idMap[entry->attachments()->value(key)]));
397         m_xml.writeEndElement();
398 
399         m_xml.writeEndElement();
400     }
401 
402     writeAutoType(entry);
403     // write history only for entries that are not history items
404     if (entry->parent()) {
405         writeEntryHistory(entry);
406     }
407 
408     m_xml.writeEndElement();
409 }
410 
writeAutoType(const Entry * entry)411 void KeePass2XmlWriter::writeAutoType(const Entry* entry)
412 {
413     m_xml.writeStartElement("AutoType");
414 
415     writeBool("Enabled", entry->autoTypeEnabled());
416     writeNumber("DataTransferObfuscation", entry->autoTypeObfuscation());
417     writeString("DefaultSequence", entry->defaultAutoTypeSequence());
418 
419     const QList<AutoTypeAssociations::Association> autoTypeAssociations = entry->autoTypeAssociations()->getAll();
420     for (const AutoTypeAssociations::Association& assoc : autoTypeAssociations) {
421         writeAutoTypeAssoc(assoc);
422     }
423 
424     m_xml.writeEndElement();
425 }
426 
writeAutoTypeAssoc(const AutoTypeAssociations::Association & assoc)427 void KeePass2XmlWriter::writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc)
428 {
429     m_xml.writeStartElement("Association");
430 
431     writeString("Window", assoc.window);
432     writeString("KeystrokeSequence", assoc.sequence);
433 
434     m_xml.writeEndElement();
435 }
436 
writeEntryHistory(const Entry * entry)437 void KeePass2XmlWriter::writeEntryHistory(const Entry* entry)
438 {
439     m_xml.writeStartElement("History");
440 
441     const QList<Entry*>& historyItems = entry->historyItems();
442     for (const Entry* item : historyItems) {
443         writeEntry(item);
444     }
445 
446     m_xml.writeEndElement();
447 }
448 
writeString(const QString & qualifiedName,const QString & string)449 void KeePass2XmlWriter::writeString(const QString& qualifiedName, const QString& string)
450 {
451     if (string.isEmpty()) {
452         m_xml.writeEmptyElement(qualifiedName);
453     }
454     else {
455         m_xml.writeTextElement(qualifiedName, stripInvalidXml10Chars(string));
456     }
457 }
458 
writeNumber(const QString & qualifiedName,int number)459 void KeePass2XmlWriter::writeNumber(const QString& qualifiedName, int number)
460 {
461     writeString(qualifiedName, QString::number(number));
462 }
463 
writeBool(const QString & qualifiedName,bool b)464 void KeePass2XmlWriter::writeBool(const QString& qualifiedName, bool b)
465 {
466     if (b) {
467         writeString(qualifiedName, "True");
468     }
469     else {
470         writeString(qualifiedName, "False");
471     }
472 }
473 
writeDateTime(const QString & qualifiedName,const QDateTime & dateTime)474 void KeePass2XmlWriter::writeDateTime(const QString& qualifiedName, const QDateTime& dateTime)
475 {
476     Q_ASSERT(dateTime.isValid());
477     Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
478 
479     QString dateTimeStr = dateTime.toString(Qt::ISODate);
480 
481     // Qt < 4.8 doesn't append a 'Z' at the end
482     if (!dateTimeStr.isEmpty() && dateTimeStr[dateTimeStr.size() - 1] != 'Z') {
483         dateTimeStr.append('Z');
484     }
485 
486     writeString(qualifiedName, dateTimeStr);
487 }
488 
writeUuid(const QString & qualifiedName,const Uuid & uuid)489 void KeePass2XmlWriter::writeUuid(const QString& qualifiedName, const Uuid& uuid)
490 {
491     writeString(qualifiedName, uuid.toBase64());
492 }
493 
writeUuid(const QString & qualifiedName,const Group * group)494 void KeePass2XmlWriter::writeUuid(const QString& qualifiedName, const Group* group)
495 {
496     if (group) {
497         writeUuid(qualifiedName, group->uuid());
498     }
499     else {
500         writeUuid(qualifiedName, Uuid());
501     }
502 }
503 
writeUuid(const QString & qualifiedName,const Entry * entry)504 void KeePass2XmlWriter::writeUuid(const QString& qualifiedName, const Entry* entry)
505 {
506     if (entry) {
507         writeUuid(qualifiedName, entry->uuid());
508     }
509     else {
510         writeUuid(qualifiedName, Uuid());
511     }
512 }
513 
writeBinary(const QString & qualifiedName,const QByteArray & ba)514 void KeePass2XmlWriter::writeBinary(const QString& qualifiedName, const QByteArray& ba)
515 {
516     writeString(qualifiedName, QString::fromLatin1(ba.toBase64()));
517 }
518 
writeColor(const QString & qualifiedName,const QColor & color)519 void KeePass2XmlWriter::writeColor(const QString& qualifiedName, const QColor& color)
520 {
521     QString colorStr;
522 
523     if (color.isValid()) {
524       colorStr = QString("#%1%2%3").arg(colorPartToString(color.red()))
525               .arg(colorPartToString(color.green()))
526               .arg(colorPartToString(color.blue()));
527     }
528 
529     writeString(qualifiedName, colorStr);
530 }
531 
writeTriState(const QString & qualifiedName,Group::TriState triState)532 void KeePass2XmlWriter::writeTriState(const QString& qualifiedName, Group::TriState triState)
533 {
534     QString value;
535 
536     if (triState == Group::Inherit) {
537         value = "null";
538     }
539     else if (triState == Group::Enable) {
540         value = "true";
541     }
542     else {
543         value = "false";
544     }
545 
546     writeString(qualifiedName, value);
547 }
548 
colorPartToString(int value)549 QString KeePass2XmlWriter::colorPartToString(int value)
550 {
551     QString str = QString::number(value, 16).toUpper();
552     if (str.length() == 1) {
553         str.prepend("0");
554     }
555 
556     return str;
557 }
558 
stripInvalidXml10Chars(QString str)559 QString KeePass2XmlWriter::stripInvalidXml10Chars(QString str)
560 {
561     for (int i = str.size() - 1; i >= 0; i--) {
562         const QChar ch = str.at(i);
563         const ushort uc = ch.unicode();
564 
565         if (ch.isLowSurrogate() && i != 0 && str.at(i - 1).isHighSurrogate()) {
566             // keep valid surrogate pair
567             i--;
568         }
569         else if ((uc < 0x20 && uc != 0x09 && uc != 0x0A && uc != 0x0D)  // control chracters
570                  || (uc >= 0x7F && uc <= 0x84)  // control chracters, valid but discouraged by XML
571                  || (uc >= 0x86 && uc <= 0x9F)  // control chracters, valid but discouraged by XML
572                  || (uc > 0xFFFD)               // noncharacter
573                  || ch.isLowSurrogate()         // single low surrogate
574                  || ch.isHighSurrogate())       // single high surrogate
575         {
576             qWarning("Stripping invalid XML 1.0 codepoint %x", uc);
577             str.remove(i, 1);
578         }
579     }
580 
581     return str;
582 }
583 
raiseError(const QString & errorMessage)584 void KeePass2XmlWriter::raiseError(const QString& errorMessage)
585 {
586     m_error = true;
587     m_errorStr = errorMessage;
588 }
589