diff options
Diffstat (limited to 'src/core/Database.cpp')
-rw-r--r-- | src/core/Database.cpp | 198 |
1 files changed, 106 insertions, 92 deletions
diff --git a/src/core/Database.cpp b/src/core/Database.cpp index fcd48f1e2..a315adf96 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -18,6 +18,7 @@ #include "Database.h" +#include "core/AsyncTask.h" #include "core/Clock.h" #include "core/FileWatcher.h" #include "core/Group.h" @@ -48,7 +49,7 @@ Database::Database() { setRootGroup(new Group()); rootGroup()->setUuid(QUuid::createUuid()); - rootGroup()->setName(tr("Root", "Root group name")); + rootGroup()->setName(tr("Passwords", "Root group name")); m_modifiedTimer.setSingleShot(true); s_uuidMap.insert(m_uuid, this); @@ -57,7 +58,7 @@ Database::Database() connect(&m_modifiedTimer, SIGNAL(timeout()), SIGNAL(databaseModified())); connect(this, SIGNAL(databaseOpened()), SLOT(updateCommonUsernames())); connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames())); - connect(m_fileWatcher, SIGNAL(fileChanged()), SIGNAL(databaseFileChanged())); + connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged); m_modified = false; m_emitModified = true; @@ -111,13 +112,6 @@ bool Database::open(QSharedPointer<const CompositeKey> key, QString* error, bool */ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error, bool readOnly) { - if (isInitialized() && m_modified) { - emit databaseDiscarded(); - } - - m_initialized = false; - setEmitModified(false); - QFile dbFile(filePath); if (!dbFile.exists()) { if (error) { @@ -126,17 +120,22 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> return false; } - if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) { - readOnly = true; - } - - if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) { + // Don't autodetect read-only mode, as it triggers an upstream bug. + // See https://github.com/keepassxreboot/keepassxc/issues/803 + // if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) { + // readOnly = true; + // } + // + // if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) { + if (!dbFile.open(QIODevice::ReadOnly)) { if (error) { *error = tr("Unable to open file %1.").arg(filePath); } return false; } + setEmitModified(false); + KeePass2Reader reader; if (!reader.readDatabase(&dbFile, std::move(key), this)) { if (error) { @@ -151,7 +150,6 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> markAsClean(); - m_initialized = true; emit databaseOpened(); m_fileWatcher->start(canonicalFilePath(), 30, 1); setEmitModified(true); @@ -159,6 +157,15 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> return true; } +bool Database::isSaving() +{ + bool locked = m_saveMutex.tryLock(); + if (locked) { + m_saveMutex.unlock(); + } + return !locked; +} + /** * Save the database to the current file path. It is an error to call this function * if no file path has been defined. @@ -201,6 +208,25 @@ bool Database::save(QString* error, bool atomic, bool backup) */ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup) { + // Disallow overlapping save operations + if (isSaving()) { + if (error) { + *error = tr("Database save is already in progress."); + } + return false; + } + + // Never save an uninitialized database + if (!isInitialized()) { + if (error) { + *error = tr("Could not save, database has not been initialized!"); + } + return false; + } + + // Prevent destructive operations while saving + QMutexLocker locker(&m_saveMutex); + if (filePath == m_data.filePath) { // Disallow saving to the same file if read-only if (m_data.isReadOnly) { @@ -225,12 +251,17 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool setReadOnly(false); m_fileWatcher->stop(); - auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath; - bool ok = performSave(canonicalFilePath, error, atomic, backup); + QFileInfo fileInfo(filePath); + auto realFilePath = fileInfo.exists() ? fileInfo.canonicalFilePath() : fileInfo.absoluteFilePath(); + bool isNewFile = !QFile::exists(realFilePath); + bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, error, atomic, backup); }); if (ok) { markAsClean(); setFilePath(filePath); - m_fileWatcher->start(canonicalFilePath, 30, 1); + if (isNewFile) { + QFile::setPermissions(realFilePath, QFile::ReadUser | QFile::WriteUser); + } + m_fileWatcher->start(realFilePath, 30, 1); } else { // Saving failed, don't rewatch file since it does not represent our database markAsModified(); @@ -277,6 +308,7 @@ bool Database::performSave(const QString& filePath, QString* error, bool atomic, } // Delete the original db and move the temp file in place + auto perms = QFile::permissions(filePath); QFile::remove(filePath); // Note: call into the QFile rename instead of QTemporaryFile @@ -285,6 +317,7 @@ bool Database::performSave(const QString& filePath, QString* error, bool atomic, if (tempFile.QFile::rename(filePath)) { // successfully saved the database tempFile.setAutoRemove(false); + QFile::setPermissions(filePath, perms); return true; } else if (!backup || !restoreDatabase(filePath)) { // Failed to copy new database in place, and @@ -317,8 +350,8 @@ bool Database::writeDatabase(QIODevice* device, QString* error) } PasswordKey oldTransformedKey; - if (m_data.hasKey) { - oldTransformedKey.setHash(m_data.transformedMasterKey->rawKey()); + if (m_data.key->isEmpty()) { + oldTransformedKey.setHash(m_data.transformedDatabaseKey->rawKey()); } KeePass2Writer writer; @@ -333,7 +366,7 @@ bool Database::writeDatabase(QIODevice* device, QString* error) return false; } - QByteArray newKey = m_data.transformedMasterKey->rawKey(); + QByteArray newKey = m_data.transformedDatabaseKey->rawKey(); Q_ASSERT(!newKey.isEmpty()); Q_ASSERT(newKey != oldTransformedKey.rawKey()); if (newKey.isEmpty() || newKey == oldTransformedKey.rawKey()) { @@ -389,30 +422,29 @@ bool Database::import(const QString& xmlExportPath, QString* error) void Database::releaseData() { - s_uuidMap.remove(m_uuid); - m_uuid = QUuid(); + // Prevent data release while saving + QMutexLocker locker(&m_saveMutex); if (m_modified) { emit databaseDiscarded(); } + setEmitModified(false); + m_modified = false; + m_modifiedTimer.stop(); + + s_uuidMap.remove(m_uuid); + m_uuid = QUuid(); + m_data.clear(); + m_metadata->clear(); - if (m_rootGroup && m_rootGroup->parent() == this) { - delete m_rootGroup; - } - if (m_metadata) { - delete m_metadata; - } - if (m_fileWatcher) { - delete m_fileWatcher; - } + setRootGroup(new Group()); + + m_fileWatcher->stop(); m_deletedObjects.clear(); m_commonUsernames.clear(); - - m_initialized = false; - m_modified = false; } /** @@ -428,9 +460,12 @@ bool Database::backupDatabase(const QString& filePath) auto match = re.match(filePath); auto backupFilePath = filePath; + auto perms = QFile::permissions(filePath); backupFilePath = backupFilePath.replace(re, "") + ".old" + match.captured(1); QFile::remove(backupFilePath); - return QFile::copy(filePath, backupFilePath); + bool res = QFile::copy(filePath, backupFilePath); + QFile::setPermissions(backupFilePath, perms); + return res; } /** @@ -446,11 +481,13 @@ bool Database::restoreDatabase(const QString& filePath) static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$"); auto match = re.match(filePath); + auto perms = QFile::permissions(filePath); auto backupFilePath = match.captured(1) + ".old" + match.captured(2); // Only try to restore if the backup file actually exists if (QFile::exists(backupFilePath)) { QFile::remove(filePath); return QFile::copy(backupFilePath, filePath); + QFile::setPermissions(filePath, perms); } return false; } @@ -466,22 +503,14 @@ void Database::setReadOnly(bool readOnly) } /** - * Returns true if database has been fully decrypted and populated, i.e. if - * it's not just an empty default instance. + * Returns true if the database key exists, has subkeys, and the + * root group exists * * @return true if database has been fully initialized */ bool Database::isInitialized() const { - return m_initialized; -} - -/** - * @param initialized true to mark database as initialized - */ -void Database::setInitialized(bool initialized) -{ - m_initialized = initialized; + return m_data.key && !m_data.key->isEmpty() && m_rootGroup; } Group* Database::rootGroup() @@ -505,7 +534,7 @@ void Database::setRootGroup(Group* group) { Q_ASSERT(group); - if (isInitialized() && m_modified) { + if (isInitialized() && isModified()) { emit databaseDiscarded(); } @@ -633,9 +662,9 @@ Database::CompressionAlgorithm Database::compressionAlgorithm() const return m_data.compressionAlgorithm; } -QByteArray Database::transformedMasterKey() const +QByteArray Database::transformedDatabaseKey() const { - return m_data.transformedMasterKey->rawKey(); + return m_data.transformedDatabaseKey->rawKey(); } QByteArray Database::challengeResponseKey() const @@ -645,10 +674,11 @@ QByteArray Database::challengeResponseKey() const bool Database::challengeMasterSeed(const QByteArray& masterSeed) { + m_keyError.clear(); if (m_data.key) { m_data.masterSeed->setHash(masterSeed); QByteArray response; - bool ok = m_data.key->challenge(masterSeed, response); + bool ok = m_data.key->challenge(masterSeed, response, &m_keyError); if (ok && !response.isEmpty()) { m_data.challengeResponseKey->setHash(response); } else if (ok && response.isEmpty()) { @@ -689,11 +719,11 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, bool transformKey) { Q_ASSERT(!m_data.isReadOnly); + m_keyError.clear(); if (!key) { m_data.key.reset(); - m_data.transformedMasterKey.reset(new PasswordKey()); - m_data.hasKey = false; + m_data.transformedDatabaseKey.reset(new PasswordKey()); return true; } @@ -702,59 +732,37 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, Q_ASSERT(!m_data.kdf->seed().isEmpty()); } - PasswordKey oldTransformedMasterKey; - if (m_data.hasKey) { - oldTransformedMasterKey.setHash(m_data.transformedMasterKey->rawKey()); + PasswordKey oldTransformedDatabaseKey; + if (m_data.key && !m_data.key->isEmpty()) { + oldTransformedDatabaseKey.setHash(m_data.transformedDatabaseKey->rawKey()); } - QByteArray transformedMasterKey; + QByteArray transformedDatabaseKey; if (!transformKey) { - transformedMasterKey = QByteArray(oldTransformedMasterKey.rawKey()); - } else if (!key->transform(*m_data.kdf, transformedMasterKey)) { + transformedDatabaseKey = QByteArray(oldTransformedDatabaseKey.rawKey()); + } else if (!key->transform(*m_data.kdf, transformedDatabaseKey, &m_keyError)) { return false; } m_data.key = key; - if (!transformedMasterKey.isEmpty()) { - m_data.transformedMasterKey->setHash(transformedMasterKey); + if (!transformedDatabaseKey.isEmpty()) { + m_data.transformedDatabaseKey->setHash(transformedDatabaseKey); } - m_data.hasKey = true; if (updateChangedTime) { - m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc()); + m_metadata->setDatabaseKeyChanged(Clock::currentDateTimeUtc()); } - if (oldTransformedMasterKey.rawKey() != m_data.transformedMasterKey->rawKey()) { + if (oldTransformedDatabaseKey.rawKey() != m_data.transformedDatabaseKey->rawKey()) { markAsModified(); } return true; } -bool Database::hasKey() const +QString Database::keyError() { - return m_data.hasKey; -} - -bool Database::verifyKey(const QSharedPointer<CompositeKey>& key) const -{ - Q_ASSERT(hasKey()); - - if (!m_data.challengeResponseKey->rawKey().isEmpty()) { - QByteArray result; - - if (!key->challenge(m_data.masterSeed->rawKey(), result)) { - // challenge failed, (YubiKey?) removed? - return false; - } - - if (m_data.challengeResponseKey->rawKey() != result) { - // wrong response from challenged device(s) - return false; - } - } - - return (m_data.key->rawKey() == key->rawKey()); + return m_keyError; } QVariantMap& Database::publicCustomData() @@ -840,9 +848,9 @@ void Database::setEmitModified(bool value) m_emitModified = value; } -bool Database::isModified() const +bool Database::isModified(bool includeNonDataChanges) const { - return m_modified; + return m_modified || (includeNonDataChanges && m_hasNonDataChange); } void Database::markAsModified() @@ -859,11 +867,17 @@ void Database::markAsClean() bool emitSignal = m_modified; m_modified = false; m_modifiedTimer.stop(); + m_hasNonDataChange = false; if (emitSignal) { emit databaseSaved(); } } +void Database::markNonDataChange() +{ + m_hasNonDataChange = true; +} + /** * @param uuid UUID of the database * @return pointer to the database or nullptr if no such database exists @@ -894,16 +908,16 @@ bool Database::changeKdf(const QSharedPointer<Kdf>& kdf) Q_ASSERT(!m_data.isReadOnly); kdf->randomizeSeed(); - QByteArray transformedMasterKey; + QByteArray transformedDatabaseKey; if (!m_data.key) { m_data.key = QSharedPointer<CompositeKey>::create(); } - if (!m_data.key->transform(*kdf, transformedMasterKey)) { + if (!m_data.key->transform(*kdf, transformedDatabaseKey)) { return false; } setKdf(kdf); - m_data.transformedMasterKey->setHash(transformedMasterKey); + m_data.transformedDatabaseKey->setHash(transformedDatabaseKey); markAsModified(); return true; |