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