1 /*
2  *  Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
3  *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 2 or (at your option)
8  *  version 3 of the License.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "Database.h"
20 
21 #include "core/AsyncTask.h"
22 #include "core/Clock.h"
23 #include "core/FileWatcher.h"
24 #include "core/Group.h"
25 #include "core/Merger.h"
26 #include "core/Metadata.h"
27 #include "format/KdbxXmlReader.h"
28 #include "format/KeePass2Reader.h"
29 #include "format/KeePass2Writer.h"
30 #include "keys/FileKey.h"
31 #include "keys/PasswordKey.h"
32 
33 #include <QFile>
34 #include <QFileInfo>
35 #include <QSaveFile>
36 #include <QTemporaryFile>
37 #include <QTimer>
38 #include <QXmlStreamReader>
39 
40 QHash<QUuid, QPointer<Database>> Database::s_uuidMap;
41 
Database()42 Database::Database()
43     : m_metadata(new Metadata(this))
44     , m_data()
45     , m_rootGroup(nullptr)
46     , m_fileWatcher(new FileWatcher(this))
47     , m_emitModified(false)
48     , m_uuid(QUuid::createUuid())
49 {
50     setRootGroup(new Group());
51     rootGroup()->setUuid(QUuid::createUuid());
52     rootGroup()->setName(tr("Passwords", "Root group name"));
53     m_modifiedTimer.setSingleShot(true);
54 
55     s_uuidMap.insert(m_uuid, this);
56 
57     connect(m_metadata, SIGNAL(metadataModified()), SLOT(markAsModified()));
58     connect(&m_modifiedTimer, SIGNAL(timeout()), SIGNAL(databaseModified()));
59     connect(this, SIGNAL(databaseOpened()), SLOT(updateCommonUsernames()));
60     connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames()));
61     connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
62 
63     m_modified = false;
64     m_emitModified = true;
65 }
66 
Database(const QString & filePath)67 Database::Database(const QString& filePath)
68     : Database()
69 {
70     setFilePath(filePath);
71 }
72 
~Database()73 Database::~Database()
74 {
75     releaseData();
76 }
77 
uuid() const78 QUuid Database::uuid() const
79 {
80     return m_uuid;
81 }
82 
83 /**
84  * Open the database from a previously specified file.
85  * Unless `readOnly` is set to false, the database will be opened in
86  * read-write mode and fall back to read-only if that is not possible.
87  *
88  * @param key composite key for unlocking the database
89  * @param readOnly open in read-only mode
90  * @param error error message in case of failure
91  * @return true on success
92  */
open(QSharedPointer<const CompositeKey> key,QString * error,bool readOnly)93 bool Database::open(QSharedPointer<const CompositeKey> key, QString* error, bool readOnly)
94 {
95     Q_ASSERT(!m_data.filePath.isEmpty());
96     if (m_data.filePath.isEmpty()) {
97         return false;
98     }
99     return open(m_data.filePath, std::move(key), error, readOnly);
100 }
101 
102 /**
103  * Open the database from a file.
104  * Unless `readOnly` is set to false, the database will be opened in
105  * read-write mode and fall back to read-only if that is not possible.
106  *
107  * @param filePath path to the file
108  * @param key composite key for unlocking the database
109  * @param readOnly open in read-only mode
110  * @param error error message in case of failure
111  * @return true on success
112  */
open(const QString & filePath,QSharedPointer<const CompositeKey> key,QString * error,bool readOnly)113 bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error, bool readOnly)
114 {
115     QFile dbFile(filePath);
116     if (!dbFile.exists()) {
117         if (error) {
118             *error = tr("File %1 does not exist.").arg(filePath);
119         }
120         return false;
121     }
122 
123     // Don't autodetect read-only mode, as it triggers an upstream bug.
124     // See https://github.com/keepassxreboot/keepassxc/issues/803
125     // if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) {
126     //     readOnly = true;
127     // }
128     //
129     // if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) {
130     if (!dbFile.open(QIODevice::ReadOnly)) {
131         if (error) {
132             *error = tr("Unable to open file %1.").arg(filePath);
133         }
134         return false;
135     }
136 
137     setEmitModified(false);
138 
139     KeePass2Reader reader;
140     if (!reader.readDatabase(&dbFile, std::move(key), this)) {
141         if (error) {
142             *error = tr("Error while reading the database: %1").arg(reader.errorString());
143         }
144         return false;
145     }
146 
147     setReadOnly(readOnly);
148     setFilePath(filePath);
149     dbFile.close();
150 
151     markAsClean();
152 
153     emit databaseOpened();
154     m_fileWatcher->start(canonicalFilePath(), 30, 1);
155     setEmitModified(true);
156 
157     return true;
158 }
159 
isSaving()160 bool Database::isSaving()
161 {
162     bool locked = m_saveMutex.tryLock();
163     if (locked) {
164         m_saveMutex.unlock();
165     }
166     return !locked;
167 }
168 
169 /**
170  * Save the database to the current file path. It is an error to call this function
171  * if no file path has been defined.
172  *
173  * @param error error message in case of failure
174  * @param atomic Use atomic file transactions
175  * @param backup Backup the existing database file, if exists
176  * @return true on success
177  */
save(QString * error,bool atomic,bool backup)178 bool Database::save(QString* error, bool atomic, bool backup)
179 {
180     Q_ASSERT(!m_data.filePath.isEmpty());
181     if (m_data.filePath.isEmpty()) {
182         if (error) {
183             *error = tr("Could not save, database does not point to a valid file.");
184         }
185         return false;
186     }
187 
188     return saveAs(m_data.filePath, error, atomic, backup);
189 }
190 
191 /**
192  * Save the database to a specific file.
193  *
194  * If atomic is false, this function uses QTemporaryFile instead of QSaveFile
195  * due to a bug in Qt (https://bugreports.qt.io/browse/QTBUG-57299) that may
196  * prevent the QSaveFile from renaming itself when using Dropbox, Google Drive,
197  * or OneDrive.
198  *
199  * The risk in using QTemporaryFile is that the rename function is not atomic
200  * and may result in loss of data if there is a crash or power loss at the
201  * wrong moment.
202  *
203  * @param filePath Absolute path of the file to save
204  * @param error error message in case of failure
205  * @param atomic Use atomic file transactions
206  * @param backup Backup the existing database file, if exists
207  * @return true on success
208  */
saveAs(const QString & filePath,QString * error,bool atomic,bool backup)209 bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup)
210 {
211     // Disallow overlapping save operations
212     if (isSaving()) {
213         if (error) {
214             *error = tr("Database save is already in progress.");
215         }
216         return false;
217     }
218 
219     // Never save an uninitialized database
220     if (!isInitialized()) {
221         if (error) {
222             *error = tr("Could not save, database has not been initialized!");
223         }
224         return false;
225     }
226 
227     // Prevent destructive operations while saving
228     QMutexLocker locker(&m_saveMutex);
229 
230     if (filePath == m_data.filePath) {
231         // Disallow saving to the same file if read-only
232         if (m_data.isReadOnly) {
233             Q_ASSERT_X(false, "Database::saveAs", "Could not save, database file is read-only.");
234             if (error) {
235                 *error = tr("Could not save, database file is read-only.");
236             }
237             return false;
238         }
239 
240         // Fail-safe check to make sure we don't overwrite underlying file changes
241         // that have not yet triggered a file reload/merge operation.
242         if (!m_fileWatcher->hasSameFileChecksum()) {
243             if (error) {
244                 *error = tr("Database file has unmerged changes.");
245             }
246             return false;
247         }
248     }
249 
250     // Clear read-only flag
251     setReadOnly(false);
252     m_fileWatcher->stop();
253 
254     QFileInfo fileInfo(filePath);
255     auto realFilePath = fileInfo.exists() ? fileInfo.canonicalFilePath() : fileInfo.absoluteFilePath();
256     bool isNewFile = !QFile::exists(realFilePath);
257     bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, error, atomic, backup); });
258     if (ok) {
259         markAsClean();
260         setFilePath(filePath);
261         if (isNewFile) {
262             QFile::setPermissions(realFilePath, QFile::ReadUser | QFile::WriteUser);
263         }
264         m_fileWatcher->start(realFilePath, 30, 1);
265     } else {
266         // Saving failed, don't rewatch file since it does not represent our database
267         markAsModified();
268     }
269 
270     return ok;
271 }
272 
performSave(const QString & filePath,QString * error,bool atomic,bool backup)273 bool Database::performSave(const QString& filePath, QString* error, bool atomic, bool backup)
274 {
275 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
276     QFileInfo info(filePath);
277     auto createTime = info.exists() ? info.birthTime() : QDateTime::currentDateTime();
278 #endif
279 
280     if (atomic) {
281         QSaveFile saveFile(filePath);
282         if (saveFile.open(QIODevice::WriteOnly)) {
283             // write the database to the file
284             if (!writeDatabase(&saveFile, error)) {
285                 return false;
286             }
287 
288             if (backup) {
289                 backupDatabase(filePath);
290             }
291 
292 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
293             // Retain orginal creation time
294             saveFile.setFileTime(createTime, QFile::FileBirthTime);
295 #endif
296 
297             if (saveFile.commit()) {
298                 // successfully saved database file
299                 return true;
300             }
301         }
302 
303         if (error) {
304             *error = saveFile.errorString();
305         }
306     } else {
307         QTemporaryFile tempFile;
308         if (tempFile.open()) {
309             // write the database to the file
310             if (!writeDatabase(&tempFile, error)) {
311                 return false;
312             }
313 
314             tempFile.close(); // flush to disk
315 
316             if (backup) {
317                 backupDatabase(filePath);
318             }
319 
320             // Delete the original db and move the temp file in place
321             auto perms = QFile::permissions(filePath);
322             QFile::remove(filePath);
323 
324             // Note: call into the QFile rename instead of QTemporaryFile
325             // due to an undocumented difference in how the function handles
326             // errors. This prevents errors when saving across file systems.
327             if (tempFile.QFile::rename(filePath)) {
328                 // successfully saved the database
329                 tempFile.setAutoRemove(false);
330                 QFile::setPermissions(filePath, perms);
331 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
332                 // Retain orginal creation time
333                 tempFile.setFileTime(createTime, QFile::FileBirthTime);
334 #endif
335                 return true;
336             } else if (!backup || !restoreDatabase(filePath)) {
337                 // Failed to copy new database in place, and
338                 // failed to restore from backup or backups disabled
339                 tempFile.setAutoRemove(false);
340                 if (error) {
341                     *error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
342                 }
343                 return false;
344             }
345         }
346 
347         if (error) {
348             *error = tempFile.errorString();
349         }
350     }
351 
352     // Saving failed
353     return false;
354 }
355 
writeDatabase(QIODevice * device,QString * error)356 bool Database::writeDatabase(QIODevice* device, QString* error)
357 {
358     Q_ASSERT(!m_data.isReadOnly);
359     if (m_data.isReadOnly) {
360         if (error) {
361             *error = tr("File cannot be written as it is opened in read-only mode.");
362         }
363         return false;
364     }
365 
366     PasswordKey oldTransformedKey;
367     if (m_data.key->isEmpty()) {
368         oldTransformedKey.setHash(m_data.transformedDatabaseKey->rawKey());
369     }
370 
371     KeePass2Writer writer;
372     setEmitModified(false);
373     writer.writeDatabase(device, this);
374     setEmitModified(true);
375 
376     if (writer.hasError()) {
377         if (error) {
378             *error = writer.errorString();
379         }
380         return false;
381     }
382 
383     QByteArray newKey = m_data.transformedDatabaseKey->rawKey();
384     Q_ASSERT(!newKey.isEmpty());
385     Q_ASSERT(newKey != oldTransformedKey.rawKey());
386     if (newKey.isEmpty() || newKey == oldTransformedKey.rawKey()) {
387         if (error) {
388             *error = tr("Key not transformed. This is a bug, please report it to the developers!");
389         }
390         return false;
391     }
392 
393     return true;
394 }
395 
extract(QByteArray & xmlOutput,QString * error)396 bool Database::extract(QByteArray& xmlOutput, QString* error)
397 {
398     KeePass2Writer writer;
399     writer.extractDatabase(this, xmlOutput);
400     if (writer.hasError()) {
401         if (error) {
402             *error = writer.errorString();
403         }
404         return false;
405     }
406 
407     return true;
408 }
409 
import(const QString & xmlExportPath,QString * error)410 bool Database::import(const QString& xmlExportPath, QString* error)
411 {
412     KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
413     QFile file(xmlExportPath);
414     file.open(QIODevice::ReadOnly);
415 
416     reader.readDatabase(&file, this);
417 
418     if (reader.hasError()) {
419         if (error) {
420             *error = reader.errorString();
421         }
422         return false;
423     }
424 
425     return true;
426 }
427 
428 /**
429  * Release all stored group, entry, and meta data of this database.
430  *
431  * Call this method to ensure all data is cleared even if valid
432  * pointers to this Database object are still being held.
433  *
434  * A previously reparented root group will not be freed.
435  */
436 
releaseData()437 void Database::releaseData()
438 {
439     // Prevent data release while saving
440     QMutexLocker locker(&m_saveMutex);
441 
442     if (m_modified) {
443         emit databaseDiscarded();
444     }
445 
446     setEmitModified(false);
447     m_modified = false;
448     m_modifiedTimer.stop();
449 
450     s_uuidMap.remove(m_uuid);
451     m_uuid = QUuid();
452 
453     m_data.clear();
454     m_metadata->clear();
455 
456     setRootGroup(new Group());
457 
458     m_fileWatcher->stop();
459 
460     m_deletedObjects.clear();
461     m_commonUsernames.clear();
462 }
463 
464 /**
465  * Remove the old backup and replace it with a new one
466  * backups are named <filename>.old.<extension>
467  *
468  * @param filePath Path to the file to backup
469  * @return true on success
470  */
backupDatabase(const QString & filePath)471 bool Database::backupDatabase(const QString& filePath)
472 {
473     static auto re = QRegularExpression("(\\.[^.]+)$");
474 
475     auto match = re.match(filePath);
476     auto backupFilePath = filePath;
477     auto perms = QFile::permissions(filePath);
478     backupFilePath = backupFilePath.replace(re, "") + ".old" + match.captured(1);
479     QFile::remove(backupFilePath);
480     bool res = QFile::copy(filePath, backupFilePath);
481     QFile::setPermissions(backupFilePath, perms);
482     return res;
483 }
484 
485 /**
486  * Restores the database file from the backup file with
487  * name <filename>.old.<extension> to filePath. This will
488  * overwrite the existing file!
489  *
490  * @param filePath Path to the file to restore
491  * @return true on success
492  */
restoreDatabase(const QString & filePath)493 bool Database::restoreDatabase(const QString& filePath)
494 {
495     static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$");
496 
497     auto match = re.match(filePath);
498     auto perms = QFile::permissions(filePath);
499     auto backupFilePath = match.captured(1) + ".old" + match.captured(2);
500     // Only try to restore if the backup file actually exists
501     if (QFile::exists(backupFilePath)) {
502         QFile::remove(filePath);
503         if (QFile::copy(backupFilePath, filePath)) {
504             return QFile::setPermissions(filePath, perms);
505         }
506     }
507     return false;
508 }
509 
isReadOnly() const510 bool Database::isReadOnly() const
511 {
512     return m_data.isReadOnly;
513 }
514 
setReadOnly(bool readOnly)515 void Database::setReadOnly(bool readOnly)
516 {
517     m_data.isReadOnly = readOnly;
518 }
519 
520 /**
521  * Returns true if the database key exists, has subkeys, and the
522  * root group exists
523  *
524  * @return true if database has been fully initialized
525  */
isInitialized() const526 bool Database::isInitialized() const
527 {
528     return m_data.key && !m_data.key->isEmpty() && m_rootGroup;
529 }
530 
rootGroup()531 Group* Database::rootGroup()
532 {
533     return m_rootGroup;
534 }
535 
rootGroup() const536 const Group* Database::rootGroup() const
537 {
538     return m_rootGroup;
539 }
540 
541 /**
542  * Sets group as the root group and takes ownership of it.
543  * Warning: Be careful when calling this method as it doesn't
544  *          emit any notifications so e.g. models aren't updated.
545  *          The caller is responsible for cleaning up the previous
546             root group.
547  */
setRootGroup(Group * group)548 void Database::setRootGroup(Group* group)
549 {
550     Q_ASSERT(group);
551 
552     if (isInitialized() && isModified()) {
553         emit databaseDiscarded();
554     }
555 
556     m_rootGroup = group;
557     m_rootGroup->setParent(this);
558 }
559 
metadata()560 Metadata* Database::metadata()
561 {
562     return m_metadata;
563 }
564 
metadata() const565 const Metadata* Database::metadata() const
566 {
567     return m_metadata;
568 }
569 
570 /**
571  * Returns the original file path that was provided for
572  * this database. This path may not exist, may contain
573  * unresolved symlinks, or have malformed slashes.
574  *
575  * @return original file path
576  */
filePath() const577 QString Database::filePath() const
578 {
579     return m_data.filePath;
580 }
581 
582 /**
583  * Returns the canonical file path of this databases'
584  * set file path. This returns an empty string if the
585  * file does not exist or cannot be resolved.
586  *
587  * @return canonical file path
588  */
canonicalFilePath() const589 QString Database::canonicalFilePath() const
590 {
591     QFileInfo fileInfo(m_data.filePath);
592     return fileInfo.canonicalFilePath();
593 }
594 
setFilePath(const QString & filePath)595 void Database::setFilePath(const QString& filePath)
596 {
597     if (filePath != m_data.filePath) {
598         QString oldPath = m_data.filePath;
599         m_data.filePath = filePath;
600         // Don't watch for changes until the next open or save operation
601         m_fileWatcher->stop();
602         emit filePathChanged(oldPath, filePath);
603     }
604 }
605 
deletedObjects()606 QList<DeletedObject> Database::deletedObjects()
607 {
608     return m_deletedObjects;
609 }
610 
deletedObjects() const611 const QList<DeletedObject>& Database::deletedObjects() const
612 {
613     return m_deletedObjects;
614 }
615 
containsDeletedObject(const QUuid & uuid) const616 bool Database::containsDeletedObject(const QUuid& uuid) const
617 {
618     for (const DeletedObject& currentObject : m_deletedObjects) {
619         if (currentObject.uuid == uuid) {
620             return true;
621         }
622     }
623     return false;
624 }
625 
containsDeletedObject(const DeletedObject & object) const626 bool Database::containsDeletedObject(const DeletedObject& object) const
627 {
628     for (const DeletedObject& currentObject : m_deletedObjects) {
629         if (currentObject.uuid == object.uuid) {
630             return true;
631         }
632     }
633     return false;
634 }
635 
setDeletedObjects(const QList<DeletedObject> & delObjs)636 void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
637 {
638     if (m_deletedObjects == delObjs) {
639         return;
640     }
641     m_deletedObjects = delObjs;
642 }
643 
addDeletedObject(const DeletedObject & delObj)644 void Database::addDeletedObject(const DeletedObject& delObj)
645 {
646     Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
647     m_deletedObjects.append(delObj);
648 }
649 
addDeletedObject(const QUuid & uuid)650 void Database::addDeletedObject(const QUuid& uuid)
651 {
652     DeletedObject delObj;
653     delObj.deletionTime = Clock::currentDateTimeUtc();
654     delObj.uuid = uuid;
655 
656     addDeletedObject(delObj);
657 }
658 
commonUsernames()659 QList<QString> Database::commonUsernames()
660 {
661     return m_commonUsernames;
662 }
663 
updateCommonUsernames(int topN)664 void Database::updateCommonUsernames(int topN)
665 {
666     m_commonUsernames.clear();
667     m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
668 }
669 
cipher() const670 const QUuid& Database::cipher() const
671 {
672     return m_data.cipher;
673 }
674 
compressionAlgorithm() const675 Database::CompressionAlgorithm Database::compressionAlgorithm() const
676 {
677     return m_data.compressionAlgorithm;
678 }
679 
transformedDatabaseKey() const680 QByteArray Database::transformedDatabaseKey() const
681 {
682     return m_data.transformedDatabaseKey->rawKey();
683 }
684 
challengeResponseKey() const685 QByteArray Database::challengeResponseKey() const
686 {
687     return m_data.challengeResponseKey->rawKey();
688 }
689 
challengeMasterSeed(const QByteArray & masterSeed)690 bool Database::challengeMasterSeed(const QByteArray& masterSeed)
691 {
692     m_keyError.clear();
693     if (m_data.key) {
694         m_data.masterSeed->setHash(masterSeed);
695         QByteArray response;
696         bool ok = m_data.key->challenge(masterSeed, response, &m_keyError);
697         if (ok && !response.isEmpty()) {
698             m_data.challengeResponseKey->setHash(response);
699         } else if (ok && response.isEmpty()) {
700             // no CR key present, make sure buffer is empty
701             m_data.challengeResponseKey.reset(new PasswordKey);
702         }
703         return ok;
704     }
705     return false;
706 }
707 
setCipher(const QUuid & cipher)708 void Database::setCipher(const QUuid& cipher)
709 {
710     Q_ASSERT(!cipher.isNull());
711 
712     m_data.cipher = cipher;
713 }
714 
setCompressionAlgorithm(Database::CompressionAlgorithm algo)715 void Database::setCompressionAlgorithm(Database::CompressionAlgorithm algo)
716 {
717     Q_ASSERT(static_cast<quint32>(algo) <= CompressionAlgorithmMax);
718 
719     m_data.compressionAlgorithm = algo;
720 }
721 
722 /**
723  * Set and transform a new encryption key.
724  *
725  * @param key key to set and transform or nullptr to reset the key
726  * @param updateChangedTime true to update database change time
727  * @param updateTransformSalt true to update the transform salt
728  * @param transformKey trigger the KDF after setting the key
729  * @return true on success
730  */
setKey(const QSharedPointer<const CompositeKey> & key,bool updateChangedTime,bool updateTransformSalt,bool transformKey)731 bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
732                       bool updateChangedTime,
733                       bool updateTransformSalt,
734                       bool transformKey)
735 {
736     Q_ASSERT(!m_data.isReadOnly);
737     m_keyError.clear();
738 
739     if (!key) {
740         m_data.key.reset();
741         m_data.transformedDatabaseKey.reset(new PasswordKey());
742         return true;
743     }
744 
745     if (updateTransformSalt) {
746         m_data.kdf->randomizeSeed();
747         Q_ASSERT(!m_data.kdf->seed().isEmpty());
748     }
749 
750     PasswordKey oldTransformedDatabaseKey;
751     if (m_data.key && !m_data.key->isEmpty()) {
752         oldTransformedDatabaseKey.setHash(m_data.transformedDatabaseKey->rawKey());
753     }
754 
755     QByteArray transformedDatabaseKey;
756 
757     if (!transformKey) {
758         transformedDatabaseKey = QByteArray(oldTransformedDatabaseKey.rawKey());
759     } else if (!key->transform(*m_data.kdf, transformedDatabaseKey, &m_keyError)) {
760         return false;
761     }
762 
763     m_data.key = key;
764     if (!transformedDatabaseKey.isEmpty()) {
765         m_data.transformedDatabaseKey->setHash(transformedDatabaseKey);
766     }
767     if (updateChangedTime) {
768         m_metadata->setDatabaseKeyChanged(Clock::currentDateTimeUtc());
769     }
770 
771     if (oldTransformedDatabaseKey.rawKey() != m_data.transformedDatabaseKey->rawKey()) {
772         markAsModified();
773     }
774 
775     return true;
776 }
777 
keyError()778 QString Database::keyError()
779 {
780     return m_keyError;
781 }
782 
publicCustomData()783 QVariantMap& Database::publicCustomData()
784 {
785     return m_data.publicCustomData;
786 }
787 
publicCustomData() const788 const QVariantMap& Database::publicCustomData() const
789 {
790     return m_data.publicCustomData;
791 }
792 
setPublicCustomData(const QVariantMap & customData)793 void Database::setPublicCustomData(const QVariantMap& customData)
794 {
795     Q_ASSERT(!m_data.isReadOnly);
796     m_data.publicCustomData = customData;
797 }
798 
createRecycleBin()799 void Database::createRecycleBin()
800 {
801     Q_ASSERT(!m_data.isReadOnly);
802 
803     auto recycleBin = new Group();
804     recycleBin->setUuid(QUuid::createUuid());
805     recycleBin->setParent(rootGroup());
806     recycleBin->setName(tr("Recycle Bin"));
807     recycleBin->setIcon(Group::RecycleBinIconNumber);
808     recycleBin->setSearchingEnabled(Group::Disable);
809     recycleBin->setAutoTypeEnabled(Group::Disable);
810 
811     m_metadata->setRecycleBin(recycleBin);
812 }
813 
recycleEntry(Entry * entry)814 void Database::recycleEntry(Entry* entry)
815 {
816     Q_ASSERT(!m_data.isReadOnly);
817     if (m_metadata->recycleBinEnabled()) {
818         if (!m_metadata->recycleBin()) {
819             createRecycleBin();
820         }
821         entry->setGroup(metadata()->recycleBin());
822     } else {
823         delete entry;
824     }
825 }
826 
recycleGroup(Group * group)827 void Database::recycleGroup(Group* group)
828 {
829     Q_ASSERT(!m_data.isReadOnly);
830     if (m_metadata->recycleBinEnabled()) {
831         if (!m_metadata->recycleBin()) {
832             createRecycleBin();
833         }
834         group->setParent(metadata()->recycleBin());
835     } else {
836         delete group;
837     }
838 }
839 
emptyRecycleBin()840 void Database::emptyRecycleBin()
841 {
842     Q_ASSERT(!m_data.isReadOnly);
843     if (m_metadata->recycleBinEnabled() && m_metadata->recycleBin()) {
844         // destroying direct entries of the recycle bin
845         QList<Entry*> subEntries = m_metadata->recycleBin()->entries();
846         for (Entry* entry : subEntries) {
847             delete entry;
848         }
849         // destroying direct subgroups of the recycle bin
850         QList<Group*> subGroups = m_metadata->recycleBin()->children();
851         for (Group* group : subGroups) {
852             delete group;
853         }
854     }
855 }
856 
setEmitModified(bool value)857 void Database::setEmitModified(bool value)
858 {
859     if (m_emitModified && !value) {
860         m_modifiedTimer.stop();
861     }
862 
863     m_emitModified = value;
864 }
865 
isModified() const866 bool Database::isModified() const
867 {
868     return m_modified;
869 }
870 
hasNonDataChanges() const871 bool Database::hasNonDataChanges() const
872 {
873     return m_hasNonDataChange;
874 }
875 
markAsModified()876 void Database::markAsModified()
877 {
878     m_modified = true;
879     if (m_emitModified && !m_modifiedTimer.isActive()) {
880         // Small time delay prevents numerous consecutive saves due to repeated signals
881         m_modifiedTimer.start(150);
882     }
883 }
884 
markAsClean()885 void Database::markAsClean()
886 {
887     bool emitSignal = m_modified;
888     m_modified = false;
889     m_modifiedTimer.stop();
890     m_hasNonDataChange = false;
891     if (emitSignal) {
892         emit databaseSaved();
893     }
894 }
895 
markNonDataChange()896 void Database::markNonDataChange()
897 {
898     m_hasNonDataChange = true;
899 }
900 
901 /**
902  * @param uuid UUID of the database
903  * @return pointer to the database or nullptr if no such database exists
904  */
databaseByUuid(const QUuid & uuid)905 Database* Database::databaseByUuid(const QUuid& uuid)
906 {
907     return s_uuidMap.value(uuid, nullptr);
908 }
909 
key() const910 QSharedPointer<const CompositeKey> Database::key() const
911 {
912     return m_data.key;
913 }
914 
kdf() const915 QSharedPointer<Kdf> Database::kdf() const
916 {
917     return m_data.kdf;
918 }
919 
setKdf(QSharedPointer<Kdf> kdf)920 void Database::setKdf(QSharedPointer<Kdf> kdf)
921 {
922     Q_ASSERT(!m_data.isReadOnly);
923     m_data.kdf = std::move(kdf);
924 }
925 
changeKdf(const QSharedPointer<Kdf> & kdf)926 bool Database::changeKdf(const QSharedPointer<Kdf>& kdf)
927 {
928     Q_ASSERT(!m_data.isReadOnly);
929 
930     kdf->randomizeSeed();
931     QByteArray transformedDatabaseKey;
932     if (!m_data.key) {
933         m_data.key = QSharedPointer<CompositeKey>::create();
934     }
935     if (!m_data.key->transform(*kdf, transformedDatabaseKey)) {
936         return false;
937     }
938 
939     setKdf(kdf);
940     m_data.transformedDatabaseKey->setHash(transformedDatabaseKey);
941     markAsModified();
942 
943     return true;
944 }
945