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