2
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
3
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
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.
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.
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/>.
21
#include "core/AsyncTask.h"
22
#include "core/FileWatcher.h"
23
#include "core/Group.h"
24
#include "crypto/Random.h"
25
#include "format/KdbxXmlReader.h"
26
#include "format/KeePass2Reader.h"
27
#include "format/KeePass2Writer.h"
31
#include <QRegularExpression>
33
#include <QTemporaryFile>
40
QHash<QUuid, QPointer<Database>> Database::s_uuidMap;
43
: m_metadata(new Metadata(this))
45
, m_rootGroup(nullptr)
46
, m_fileWatcher(new FileWatcher(this))
47
, m_uuid(QUuid::createUuid())
49
// setup modified timer
50
m_modifiedTimer.setSingleShot(true);
51
connect(this, &Database::emitModifiedChanged, this, [this](bool value) {
56
connect(&m_modifiedTimer, &QTimer::timeout, this, &Database::emitModified);
59
connect(m_metadata, &Metadata::modified, this, &Database::markAsModified);
60
connect(this, &Database::databaseOpened, this, [this]() {
61
updateCommonUsernames();
64
connect(this, &Database::modified, this, [this] { updateTagList(); });
65
connect(this, &Database::databaseSaved, this, [this]() { updateCommonUsernames(); });
66
connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
69
s_uuidMap.insert(m_uuid, this);
71
// block modified signal and set root group
72
setEmitModified(false);
74
// Note: oldGroup is nullptr but need to respect return value capture
75
auto oldGroup = setRootGroup(new Group());
79
setEmitModified(true);
82
Database::Database(const QString& filePath)
85
setFilePath(filePath);
93
QUuid Database::uuid() const
99
* Open the database from a previously specified file.
100
* Unless `readOnly` is set to false, the database will be opened in
101
* read-write mode and fall back to read-only if that is not possible.
103
* @param key composite key for unlocking the database
104
* @param error error message in case of failure
105
* @return true on success
107
bool Database::open(QSharedPointer<const CompositeKey> key, QString* error)
109
Q_ASSERT(!m_data.filePath.isEmpty());
110
if (m_data.filePath.isEmpty()) {
113
return open(m_data.filePath, std::move(key), error);
117
* Open the database from a file.
118
* Unless `readOnly` is set to false, the database will be opened in
119
* read-write mode and fall back to read-only if that is not possible.
121
* If key is provided as null, only headers will be read.
123
* @param filePath path to the file
124
* @param key composite key for unlocking the database
125
* @param error error message in case of failure
126
* @return true on success
128
bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error)
130
QFile dbFile(filePath);
131
if (!dbFile.exists()) {
133
*error = tr("File %1 does not exist.").arg(filePath);
138
// Don't autodetect read-only mode, as it triggers an upstream bug.
139
// See https://github.com/keepassxreboot/keepassxc/issues/803
140
// if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) {
144
// if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) {
145
if (!dbFile.open(QIODevice::ReadOnly)) {
147
*error = tr("Unable to open file %1.").arg(filePath);
152
setEmitModified(false);
154
KeePass2Reader reader;
155
if (!reader.readDatabase(&dbFile, std::move(key), this)) {
157
*error = tr("Error while reading the database: %1").arg(reader.errorString());
162
setFilePath(filePath);
167
emit databaseOpened();
168
m_fileWatcher->start(canonicalFilePath(), 30, 1);
169
setEmitModified(true);
175
* KDBX format version.
177
quint32 Database::formatVersion() const
179
return m_data.formatVersion;
182
void Database::setFormatVersion(quint32 version)
184
m_data.formatVersion = version;
188
* Whether the KDBX minor version is greater than the newest supported.
190
bool Database::hasMinorVersionMismatch() const
192
return m_data.formatVersion > KeePass2::FILE_VERSION_MAX;
195
bool Database::isSaving()
197
bool locked = m_saveMutex.tryLock();
199
m_saveMutex.unlock();
205
* Save the database to the current file path. It is an error to call this function
206
* if no file path has been defined.
208
* @param error error message in case of failure
209
* @param atomic Use atomic file transactions
210
* @param backupFilePath Absolute file path to write the backup file to. Pass an empty QString to disable backup.
211
* @return true on success
213
bool Database::save(SaveAction action, const QString& backupFilePath, QString* error)
215
Q_ASSERT(!m_data.filePath.isEmpty());
216
if (m_data.filePath.isEmpty()) {
218
*error = tr("Could not save, database does not point to a valid file.");
223
return saveAs(m_data.filePath, action, backupFilePath, error);
227
* Save the database to a specific file.
229
* If atomic is false, this function uses QTemporaryFile instead of QSaveFile
230
* due to a bug in Qt (https://bugreports.qt.io/browse/QTBUG-57299) that may
231
* prevent the QSaveFile from renaming itself when using Dropbox, Google Drive,
234
* The risk in using QTemporaryFile is that the rename function is not atomic
235
* and may result in loss of data if there is a crash or power loss at the
238
* @param filePath Absolute path of the file to save
239
* @param error error message in case of failure
240
* @param atomic Use atomic file transactions
241
* @param backupFilePath Absolute path to the location where the backup should be stored. Passing an empty string
243
* @return true on success
245
bool Database::saveAs(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error)
247
// Disallow overlapping save operations
250
*error = tr("Database save is already in progress.");
255
// Never save an uninitialized database
256
if (!isInitialized()) {
258
*error = tr("Could not save, database has not been initialized!");
263
if (filePath == m_data.filePath) {
264
// Fail-safe check to make sure we don't overwrite underlying file changes
265
// that have not yet triggered a file reload/merge operation.
266
if (!m_fileWatcher->hasSameFileChecksum()) {
268
*error = tr("Database file has unmerged changes.");
274
// Clear read-only flag
275
m_fileWatcher->stop();
277
// Add random data to prevent side-channel data deduplication attacks
278
int length = Random::instance()->randomUIntRange(64, 512);
279
m_metadata->customData()->set(CustomData::RandomSlug, Random::instance()->randomArray(length).toHex());
281
// Prevent destructive operations while saving
282
QMutexLocker locker(&m_saveMutex);
284
QFileInfo fileInfo(filePath);
285
auto realFilePath = fileInfo.exists() ? fileInfo.canonicalFilePath() : fileInfo.absoluteFilePath();
286
bool isNewFile = !QFile::exists(realFilePath);
289
bool isHidden = fileInfo.isHidden();
292
bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, action, backupFilePath, error); });
294
setFilePath(filePath);
297
QFile::setPermissions(realFilePath, QFile::ReadUser | QFile::WriteUser);
302
SetFileAttributes(realFilePath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN);
306
m_fileWatcher->start(realFilePath, 30, 1);
308
// Saving failed, don't rewatch file since it does not represent our database
315
bool Database::performSave(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error)
317
if (!backupFilePath.isNull()) {
318
backupDatabase(filePath, backupFilePath);
321
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
322
QFileInfo info(filePath);
323
auto createTime = info.exists() ? info.birthTime() : QDateTime::currentDateTime();
328
QSaveFile saveFile(filePath);
329
if (saveFile.open(QIODevice::WriteOnly)) {
330
// write the database to the file
331
if (!writeDatabase(&saveFile, error)) {
335
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
336
// Retain original creation time
337
saveFile.setFileTime(createTime, QFile::FileBirthTime);
340
if (saveFile.commit()) {
341
// successfully saved database file
347
*error = saveFile.errorString();
352
QTemporaryFile tempFile;
353
if (tempFile.open()) {
354
// write the database to the file
355
if (!writeDatabase(&tempFile, error)) {
358
tempFile.close(); // flush to disk
360
// Delete the original db and move the temp file in place
361
auto perms = QFile::permissions(filePath);
362
QFile::remove(filePath);
364
// Note: call into the QFile rename instead of QTemporaryFile
365
// due to an undocumented difference in how the function handles
366
// errors. This prevents errors when saving across file systems.
367
if (tempFile.QFile::rename(filePath)) {
368
// successfully saved the database
369
tempFile.setAutoRemove(false);
370
QFile::setPermissions(filePath, perms);
371
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
372
// Retain original creation time
373
tempFile.setFileTime(createTime, QFile::FileBirthTime);
376
} else if (backupFilePath.isEmpty() || !restoreDatabase(filePath, backupFilePath)) {
377
// Failed to copy new database in place, and
378
// failed to restore from backup or backups disabled
379
tempFile.setAutoRemove(false);
381
*error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
388
*error = tempFile.errorString();
393
// Open the original database file for direct-write
394
QFile dbFile(filePath);
395
if (dbFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
396
if (!writeDatabase(&dbFile, error)) {
403
*error = dbFile.errorString();
413
bool Database::writeDatabase(QIODevice* device, QString* error)
415
Q_ASSERT(m_data.key);
416
Q_ASSERT(m_data.transformedDatabaseKey);
418
PasswordKey oldTransformedKey;
419
if (m_data.key->isEmpty()) {
420
oldTransformedKey.setRawKey(m_data.transformedDatabaseKey->rawKey());
423
KeePass2Writer writer;
424
setEmitModified(false);
425
writer.writeDatabase(device, this);
426
setEmitModified(true);
428
if (writer.hasError()) {
430
*error = writer.errorString();
435
QByteArray newKey = m_data.transformedDatabaseKey->rawKey();
436
Q_ASSERT(!newKey.isEmpty());
437
Q_ASSERT(newKey != oldTransformedKey.rawKey());
438
if (newKey.isEmpty() || newKey == oldTransformedKey.rawKey()) {
440
*error = tr("Key not transformed. This is a bug, please report it to the developers.");
448
bool Database::extract(QByteArray& xmlOutput, QString* error)
450
KeePass2Writer writer;
451
writer.extractDatabase(this, xmlOutput);
452
if (writer.hasError()) {
454
*error = writer.errorString();
462
bool Database::import(const QString& xmlExportPath, QString* error)
464
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
465
QFile file(xmlExportPath);
466
file.open(QIODevice::ReadOnly);
468
reader.readDatabase(&file, this);
470
if (reader.hasError()) {
472
*error = reader.errorString();
481
* Release all stored group, entry, and meta data of this database.
483
* Call this method to ensure all data is cleared even if valid
484
* pointers to this Database object are still being held.
486
* A previously reparented root group will not be freed.
489
void Database::releaseData()
491
// Prevent data release while saving
492
Q_ASSERT(!isSaving());
493
QMutexLocker locker(&m_saveMutex);
496
emit databaseDiscarded();
499
setEmitModified(false);
502
s_uuidMap.remove(m_uuid);
508
// Reset and delete the root group
509
auto oldGroup = setRootGroup(new Group());
512
m_fileWatcher->stop();
514
m_deletedObjects.clear();
515
m_commonUsernames.clear();
520
* Remove the old backup and replace it with a new one. Backup name is taken from destinationFilePath.
521
* Non-existing parent directories will be created automatically.
523
* @param filePath Path to the file to backup
524
* @param destinationFilePath Path to the backup destination file
525
* @return true on success
527
bool Database::backupDatabase(const QString& filePath, const QString& destinationFilePath)
529
// Ensure that the path to write to actually exists
530
auto parentDirectory = QFileInfo(destinationFilePath).absoluteDir();
531
if (!parentDirectory.exists()) {
532
if (!QDir().mkpath(parentDirectory.absolutePath())) {
536
auto perms = QFile::permissions(filePath);
537
QFile::remove(destinationFilePath);
538
bool res = QFile::copy(filePath, destinationFilePath);
539
QFile::setPermissions(destinationFilePath, perms);
544
* Restores the database file from the backup file with
545
* name <filename>.old.<extension> to filePath. This will
546
* overwrite the existing file!
548
* @param filePath Path to the file to restore
549
* @return true on success
551
bool Database::restoreDatabase(const QString& filePath, const QString& fromBackupFilePath)
553
auto perms = QFile::permissions(filePath);
554
// Only try to restore if the backup file actually exists
555
if (QFile::exists(fromBackupFilePath)) {
556
QFile::remove(filePath);
557
if (QFile::copy(fromBackupFilePath, filePath)) {
558
return QFile::setPermissions(filePath, perms);
565
* Returns true if the database key exists, has subkeys, and the
568
* @return true if database has been fully initialized
570
bool Database::isInitialized() const
572
return m_data.key && !m_data.key->isEmpty() && m_rootGroup;
575
Group* Database::rootGroup()
580
const Group* Database::rootGroup() const
585
/* Set the root group of the database and return
586
* the old root group. It is the responsibility
587
* of the calling function to dispose of the old
590
Group* Database::setRootGroup(Group* group)
594
if (isInitialized() && isModified()) {
595
emit databaseDiscarded();
598
auto oldRoot = m_rootGroup;
600
m_rootGroup->setParent(this);
602
// Initialize the root group if not done already
603
if (m_rootGroup->uuid().isNull()) {
604
m_rootGroup->setUuid(QUuid::createUuid());
605
m_rootGroup->setName(tr("Passwords", "Root group name"));
611
Metadata* Database::metadata()
616
const Metadata* Database::metadata() const
622
* Returns the original file path that was provided for
623
* this database. This path may not exist, may contain
624
* unresolved symlinks, or have malformed slashes.
626
* @return original file path
628
QString Database::filePath() const
630
return m_data.filePath;
634
* Returns the canonical file path of this databases'
635
* set file path. This returns an empty string if the
636
* file does not exist or cannot be resolved.
638
* @return canonical file path
640
QString Database::canonicalFilePath() const
642
QFileInfo fileInfo(m_data.filePath);
643
return fileInfo.canonicalFilePath();
646
void Database::setFilePath(const QString& filePath)
648
if (filePath != m_data.filePath) {
649
QString oldPath = m_data.filePath;
650
m_data.filePath = filePath;
651
// Don't watch for changes until the next open or save operation
652
m_fileWatcher->stop();
653
emit filePathChanged(oldPath, filePath);
657
QList<DeletedObject> Database::deletedObjects()
659
return m_deletedObjects;
662
const QList<DeletedObject>& Database::deletedObjects() const
664
return m_deletedObjects;
667
bool Database::containsDeletedObject(const QUuid& uuid) const
669
for (const DeletedObject& currentObject : m_deletedObjects) {
670
if (currentObject.uuid == uuid) {
677
bool Database::containsDeletedObject(const DeletedObject& object) const
679
for (const DeletedObject& currentObject : m_deletedObjects) {
680
if (currentObject.uuid == object.uuid) {
687
void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
689
if (m_deletedObjects == delObjs) {
692
m_deletedObjects = delObjs;
695
void Database::addDeletedObject(const DeletedObject& delObj)
697
Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
698
m_deletedObjects.append(delObj);
701
void Database::addDeletedObject(const QUuid& uuid)
703
DeletedObject delObj;
704
delObj.deletionTime = Clock::currentDateTimeUtc();
707
addDeletedObject(delObj);
710
const QStringList& Database::commonUsernames() const
712
return m_commonUsernames;
715
const QStringList& Database::tagList() const
720
void Database::updateCommonUsernames(int topN)
722
m_commonUsernames.clear();
723
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
726
void Database::updateTagList()
730
emit tagListUpdated();
734
// Search groups recursively looking for tags
735
// Use a set to prevent adding duplicates
736
QSet<QString> tagSet;
737
for (auto entry : m_rootGroup->entriesRecursive()) {
738
if (!entry->isRecycled()) {
739
for (auto tag : entry->tagList()) {
745
m_tagList = tagSet.toList();
747
emit tagListUpdated();
750
void Database::removeTag(const QString& tag)
756
for (auto entry : m_rootGroup->entriesRecursive()) {
757
entry->removeTag(tag);
761
const QUuid& Database::cipher() const
763
return m_data.cipher;
766
Database::CompressionAlgorithm Database::compressionAlgorithm() const
768
return m_data.compressionAlgorithm;
771
QByteArray Database::transformedDatabaseKey() const
773
Q_ASSERT(m_data.transformedDatabaseKey);
774
if (!m_data.transformedDatabaseKey) {
777
return m_data.transformedDatabaseKey->rawKey();
780
QByteArray Database::challengeResponseKey() const
782
Q_ASSERT(m_data.challengeResponseKey);
783
if (!m_data.challengeResponseKey) {
786
return m_data.challengeResponseKey->rawKey();
789
bool Database::challengeMasterSeed(const QByteArray& masterSeed)
791
Q_ASSERT(m_data.key);
792
Q_ASSERT(m_data.masterSeed);
795
if (m_data.key && m_data.masterSeed) {
796
m_data.masterSeed->setRawKey(masterSeed);
798
bool ok = m_data.key->challenge(masterSeed, response, &m_keyError);
799
if (ok && !response.isEmpty()) {
800
m_data.challengeResponseKey->setRawKey(response);
801
} else if (ok && response.isEmpty()) {
802
// no CR key present, make sure buffer is empty
803
m_data.challengeResponseKey.reset(new PasswordKey);
810
void Database::setCipher(const QUuid& cipher)
812
Q_ASSERT(!cipher.isNull());
814
m_data.cipher = cipher;
817
void Database::setCompressionAlgorithm(Database::CompressionAlgorithm algo)
819
Q_ASSERT(static_cast<quint32>(algo) <= CompressionAlgorithmMax);
821
m_data.compressionAlgorithm = algo;
825
* Set and transform a new encryption key.
827
* @param key key to set and transform or nullptr to reset the key
828
* @param updateChangedTime true to update database change time
829
* @param updateTransformSalt true to update the transform salt
830
* @param transformKey trigger the KDF after setting the key
831
* @return true on success
833
bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
834
bool updateChangedTime,
835
bool updateTransformSalt,
845
if (updateTransformSalt) {
846
m_data.kdf->randomizeSeed();
847
Q_ASSERT(!m_data.kdf->seed().isEmpty());
850
PasswordKey oldTransformedDatabaseKey;
851
if (m_data.key && !m_data.key->isEmpty()) {
852
oldTransformedDatabaseKey.setRawKey(m_data.transformedDatabaseKey->rawKey());
855
QByteArray transformedDatabaseKey;
858
transformedDatabaseKey = QByteArray(oldTransformedDatabaseKey.rawKey());
859
} else if (!key->transform(*m_data.kdf, transformedDatabaseKey, &m_keyError)) {
864
if (!transformedDatabaseKey.isEmpty()) {
865
m_data.transformedDatabaseKey->setRawKey(transformedDatabaseKey);
867
if (updateChangedTime) {
868
m_metadata->setDatabaseKeyChanged(Clock::currentDateTimeUtc());
871
if (oldTransformedDatabaseKey.rawKey() != m_data.transformedDatabaseKey->rawKey()) {
878
QString Database::keyError()
883
QVariantMap& Database::publicCustomData()
885
return m_data.publicCustomData;
888
const QVariantMap& Database::publicCustomData() const
890
return m_data.publicCustomData;
893
void Database::setPublicCustomData(const QVariantMap& customData)
895
m_data.publicCustomData = customData;
898
void Database::createRecycleBin()
900
auto recycleBin = new Group();
901
recycleBin->setUuid(QUuid::createUuid());
902
recycleBin->setParent(rootGroup());
903
recycleBin->setName(tr("Recycle Bin"));
904
recycleBin->setIcon(Group::RecycleBinIconNumber);
905
recycleBin->setSearchingEnabled(Group::Disable);
906
recycleBin->setAutoTypeEnabled(Group::Disable);
908
m_metadata->setRecycleBin(recycleBin);
911
void Database::recycleEntry(Entry* entry)
913
if (m_metadata->recycleBinEnabled()) {
914
if (!m_metadata->recycleBin()) {
917
entry->setGroup(metadata()->recycleBin());
923
void Database::recycleGroup(Group* group)
925
if (m_metadata->recycleBinEnabled()) {
926
if (!m_metadata->recycleBin()) {
929
group->setParent(metadata()->recycleBin());
935
void Database::emptyRecycleBin()
937
if (m_metadata->recycleBinEnabled() && m_metadata->recycleBin()) {
938
// destroying direct entries of the recycle bin
939
QList<Entry*> subEntries = m_metadata->recycleBin()->entries();
940
for (Entry* entry : subEntries) {
943
// destroying direct subgroups of the recycle bin
944
QList<Group*> subGroups = m_metadata->recycleBin()->children();
945
for (Group* group : subGroups) {
951
bool Database::isModified() const
956
bool Database::hasNonDataChanges() const
958
return m_hasNonDataChange;
961
void Database::markAsModified()
964
if (modifiedSignalEnabled() && !m_modifiedTimer.isActive()) {
965
// Small time delay prevents numerous consecutive saves due to repeated signals
966
startModifiedTimer();
970
void Database::markAsClean()
972
bool emitSignal = m_modified;
975
m_hasNonDataChange = false;
977
emit databaseSaved();
981
void Database::markNonDataChange()
983
m_hasNonDataChange = true;
984
emit databaseNonDataChanged();
988
* @param uuid UUID of the database
989
* @return pointer to the database or nullptr if no such database exists
991
Database* Database::databaseByUuid(const QUuid& uuid)
993
return s_uuidMap.value(uuid, nullptr);
996
QSharedPointer<const CompositeKey> Database::key() const
1001
QSharedPointer<Kdf> Database::kdf() const
1006
void Database::setKdf(QSharedPointer<Kdf> kdf)
1008
m_data.kdf = std::move(kdf);
1009
setFormatVersion(KeePass2Writer::kdbxVersionRequired(this, true, m_data.kdf.isNull()));
1012
bool Database::changeKdf(const QSharedPointer<Kdf>& kdf)
1014
kdf->randomizeSeed();
1015
QByteArray transformedDatabaseKey;
1017
m_data.key = QSharedPointer<CompositeKey>::create();
1019
if (!m_data.key->transform(*kdf, transformedDatabaseKey)) {
1024
m_data.transformedDatabaseKey->setRawKey(transformedDatabaseKey);
1030
// Prevent warning about QTimer not allowed to be started/stopped from other thread
1031
void Database::startModifiedTimer()
1033
QMetaObject::invokeMethod(&m_modifiedTimer, "start", Q_ARG(int, 150));
1036
// Prevent warning about QTimer not allowed to be started/stopped from other thread
1037
void Database::stopModifiedTimer()
1039
QMetaObject::invokeMethod(&m_modifiedTimer, "stop");
1042
QUuid Database::publicUuid()
1045
if (!publicCustomData().contains("KPXC_PUBLIC_UUID")) {
1046
publicCustomData().insert("KPXC_PUBLIC_UUID", QUuid::createUuid().toRfc4122());
1050
return QUuid::fromRfc4122(publicCustomData()["KPXC_PUBLIC_UUID"].toByteArray());