mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-04-26 10:29:09 -04:00

Fixes #3365 Add prompt to delete the recycle bin when disabling it. If the user chooses not to delete it, the recycle bin will be suffixed with "(old)" and the icon changed to the default group icon. Also moved recycle bin creation within the database class where it belongs.
856 lines
21 KiB
C++
856 lines
21 KiB
C++
/*
|
|
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
* version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "Database.h"
|
|
|
|
#include "core/Clock.h"
|
|
#include "core/Group.h"
|
|
#include "core/Merger.h"
|
|
#include "core/Metadata.h"
|
|
#include "format/KdbxXmlReader.h"
|
|
#include "format/KeePass2Reader.h"
|
|
#include "format/KeePass2Writer.h"
|
|
#include "keys/FileKey.h"
|
|
#include "keys/PasswordKey.h"
|
|
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QSaveFile>
|
|
#include <QTemporaryFile>
|
|
#include <QTimer>
|
|
#include <QXmlStreamReader>
|
|
|
|
QHash<QUuid, QPointer<Database>> Database::s_uuidMap;
|
|
QHash<QString, QPointer<Database>> Database::s_filePathMap;
|
|
|
|
Database::Database()
|
|
: m_metadata(new Metadata(this))
|
|
, m_data()
|
|
, m_rootGroup(nullptr)
|
|
, m_timer(new QTimer(this))
|
|
, m_emitModified(false)
|
|
, m_uuid(QUuid::createUuid())
|
|
{
|
|
setRootGroup(new Group());
|
|
rootGroup()->setUuid(QUuid::createUuid());
|
|
rootGroup()->setName(tr("Root", "Root group name"));
|
|
m_timer->setSingleShot(true);
|
|
|
|
s_uuidMap.insert(m_uuid, this);
|
|
|
|
connect(m_metadata, SIGNAL(metadataModified()), this, SLOT(markAsModified()));
|
|
connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified()));
|
|
connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames()));
|
|
|
|
m_modified = false;
|
|
m_emitModified = true;
|
|
}
|
|
|
|
Database::Database(const QString& filePath)
|
|
: Database()
|
|
{
|
|
setFilePath(filePath);
|
|
}
|
|
|
|
Database::~Database()
|
|
{
|
|
s_uuidMap.remove(m_uuid);
|
|
|
|
if (m_modified) {
|
|
emit databaseDiscarded();
|
|
}
|
|
}
|
|
|
|
QUuid Database::uuid() const
|
|
{
|
|
return m_uuid;
|
|
}
|
|
|
|
/**
|
|
* Open the database from a previously specified file.
|
|
* Unless `readOnly` is set to false, the database will be opened in
|
|
* read-write mode and fall back to read-only if that is not possible.
|
|
*
|
|
* @param key composite key for unlocking the database
|
|
* @param readOnly open in read-only mode
|
|
* @param error error message in case of failure
|
|
* @return true on success
|
|
*/
|
|
bool Database::open(QSharedPointer<const CompositeKey> key, QString* error, bool readOnly)
|
|
{
|
|
Q_ASSERT(!m_data.filePath.isEmpty());
|
|
if (m_data.filePath.isEmpty()) {
|
|
return false;
|
|
}
|
|
return open(m_data.filePath, std::move(key), error, readOnly);
|
|
}
|
|
|
|
/**
|
|
* Open the database from a file.
|
|
* Unless `readOnly` is set to false, the database will be opened in
|
|
* read-write mode and fall back to read-only if that is not possible.
|
|
*
|
|
* @param filePath path to the file
|
|
* @param key composite key for unlocking the database
|
|
* @param readOnly open in read-only mode
|
|
* @param error error message in case of failure
|
|
* @return true on success
|
|
*/
|
|
bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error, bool readOnly)
|
|
{
|
|
if (isInitialized() && m_modified) {
|
|
emit databaseDiscarded();
|
|
}
|
|
|
|
setEmitModified(false);
|
|
|
|
QFile dbFile(filePath);
|
|
if (!dbFile.exists()) {
|
|
if (error) {
|
|
*error = tr("File %1 does not exist.").arg(filePath);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) {
|
|
readOnly = true;
|
|
}
|
|
|
|
if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) {
|
|
if (error) {
|
|
*error = tr("Unable to open file %1.").arg(filePath);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
KeePass2Reader reader;
|
|
bool ok = reader.readDatabase(&dbFile, std::move(key), this);
|
|
if (reader.hasError()) {
|
|
if (error) {
|
|
*error = tr("Error while reading the database: %1").arg(reader.errorString());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
setReadOnly(readOnly);
|
|
setFilePath(filePath);
|
|
dbFile.close();
|
|
|
|
updateCommonUsernames();
|
|
|
|
setInitialized(ok);
|
|
markAsClean();
|
|
|
|
setEmitModified(true);
|
|
return ok;
|
|
}
|
|
|
|
/**
|
|
* Save the database back to the file is has been opened from.
|
|
* This method behaves the same as its overloads.
|
|
*
|
|
* @see Database::save(const QString&, bool, bool, QString*)
|
|
*
|
|
* @param atomic Use atomic file transactions
|
|
* @param backup Backup the existing database file, if exists
|
|
* @param error error message in case of failure
|
|
* @return true on success
|
|
*/
|
|
bool Database::save(QString* error, bool atomic, bool backup)
|
|
{
|
|
Q_ASSERT(!m_data.filePath.isEmpty());
|
|
if (m_data.filePath.isEmpty()) {
|
|
if (error) {
|
|
*error = tr("Could not save, database has no file name.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return save(m_data.filePath, error, atomic, backup);
|
|
}
|
|
|
|
/**
|
|
* Save the database to a file.
|
|
*
|
|
* This function uses QTemporaryFile instead of QSaveFile due to a bug
|
|
* in Qt (https://bugreports.qt.io/browse/QTBUG-57299) that may prevent
|
|
* the QSaveFile from renaming itself when using Dropbox, Drive, or OneDrive.
|
|
*
|
|
* The risk in using QTemporaryFile is that the rename function is not atomic
|
|
* and may result in loss of data if there is a crash or power loss at the
|
|
* wrong moment.
|
|
*
|
|
* @param filePath Absolute path of the file to save
|
|
* @param atomic Use atomic file transactions
|
|
* @param backup Backup the existing database file, if exists
|
|
* @param error error message in case of failure
|
|
* @return true on success
|
|
*/
|
|
bool Database::save(const QString& filePath, QString* error, bool atomic, bool backup)
|
|
{
|
|
// Disallow saving to the same file if read-only
|
|
if (m_data.isReadOnly && filePath == m_data.filePath) {
|
|
Q_ASSERT_X(false, "Database::save", "Could not save, database file is read-only.");
|
|
if (error) {
|
|
*error = tr("Could not save, database file is read-only.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath;
|
|
|
|
if (atomic) {
|
|
QSaveFile saveFile(filePath);
|
|
if (saveFile.open(QIODevice::WriteOnly)) {
|
|
// write the database to the file
|
|
if (!writeDatabase(&saveFile, error)) {
|
|
return false;
|
|
}
|
|
|
|
if (backup) {
|
|
backupDatabase(canonicalFilePath);
|
|
}
|
|
|
|
if (saveFile.commit()) {
|
|
// successfully saved database file
|
|
setFilePath(filePath);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
*error = saveFile.errorString();
|
|
}
|
|
} else {
|
|
QTemporaryFile tempFile;
|
|
if (tempFile.open()) {
|
|
// write the database to the file
|
|
if (!writeDatabase(&tempFile, error)) {
|
|
return false;
|
|
}
|
|
|
|
tempFile.close(); // flush to disk
|
|
|
|
if (backup) {
|
|
backupDatabase(canonicalFilePath);
|
|
}
|
|
|
|
// Delete the original db and move the temp file in place
|
|
QFile::remove(canonicalFilePath);
|
|
|
|
// Note: call into the QFile rename instead of QTemporaryFile
|
|
// due to an undocumented difference in how the function handles
|
|
// errors. This prevents errors when saving across file systems.
|
|
if (tempFile.QFile::rename(canonicalFilePath)) {
|
|
// successfully saved the database
|
|
tempFile.setAutoRemove(false);
|
|
setFilePath(filePath);
|
|
return true;
|
|
} else if (!backup || !restoreDatabase(canonicalFilePath)) {
|
|
// Failed to copy new database in place, and
|
|
// failed to restore from backup or backups disabled
|
|
tempFile.setAutoRemove(false);
|
|
if (error) {
|
|
*error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
|
|
}
|
|
markAsModified();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
*error = tempFile.errorString();
|
|
}
|
|
}
|
|
|
|
// Saving failed
|
|
markAsModified();
|
|
return false;
|
|
}
|
|
|
|
bool Database::writeDatabase(QIODevice* device, QString* error)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
if (m_data.isReadOnly) {
|
|
if (error) {
|
|
*error = tr("File cannot be written as it is opened in read-only mode.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QByteArray oldTransformedKey = m_data.transformedMasterKey;
|
|
KeePass2Writer writer;
|
|
setEmitModified(false);
|
|
writer.writeDatabase(device, this);
|
|
setEmitModified(true);
|
|
|
|
if (writer.hasError()) {
|
|
if (error) {
|
|
*error = writer.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Q_ASSERT(!m_data.transformedMasterKey.isEmpty());
|
|
Q_ASSERT(m_data.transformedMasterKey != oldTransformedKey);
|
|
if (m_data.transformedMasterKey.isEmpty() || m_data.transformedMasterKey == oldTransformedKey) {
|
|
if (error) {
|
|
*error = tr("Key not transformed. This is a bug, please report it to the developers!");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
markAsClean();
|
|
return true;
|
|
}
|
|
|
|
bool Database::extract(QByteArray& xmlOutput, QString* error)
|
|
{
|
|
KeePass2Writer writer;
|
|
writer.extractDatabase(this, xmlOutput);
|
|
if (writer.hasError()) {
|
|
if (error) {
|
|
*error = writer.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Database::import(const QString& xmlExportPath, QString* error)
|
|
{
|
|
KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
|
|
QFile file(xmlExportPath);
|
|
file.open(QIODevice::ReadOnly);
|
|
|
|
reader.readDatabase(&file, this);
|
|
|
|
if (reader.hasError()) {
|
|
if (error) {
|
|
*error = reader.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove the old backup and replace it with a new one
|
|
* backups are named <filename>.old.<extension>
|
|
*
|
|
* @param filePath Path to the file to backup
|
|
* @return true on success
|
|
*/
|
|
bool Database::backupDatabase(const QString& filePath)
|
|
{
|
|
static auto re = QRegularExpression("(\\.[^.]+)$");
|
|
|
|
auto match = re.match(filePath);
|
|
auto backupFilePath = filePath;
|
|
backupFilePath = backupFilePath.replace(re, "") + ".old" + match.captured(1);
|
|
QFile::remove(backupFilePath);
|
|
return QFile::copy(filePath, backupFilePath);
|
|
}
|
|
|
|
/**
|
|
* Restores the database file from the backup file with
|
|
* name <filename>.old.<extension> to filePath. This will
|
|
* overwrite the existing file!
|
|
*
|
|
* @param filePath Path to the file to restore
|
|
* @return true on success
|
|
*/
|
|
bool Database::restoreDatabase(const QString& filePath)
|
|
{
|
|
static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$");
|
|
|
|
auto match = re.match(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);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Database::isReadOnly() const
|
|
{
|
|
return m_data.isReadOnly;
|
|
}
|
|
|
|
void Database::setReadOnly(bool readOnly)
|
|
{
|
|
m_data.isReadOnly = readOnly;
|
|
}
|
|
|
|
/**
|
|
* Returns true if database has been fully decrypted and populated, i.e. if
|
|
* it's not just an empty default instance.
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
Group* Database::rootGroup()
|
|
{
|
|
return m_rootGroup;
|
|
}
|
|
|
|
const Group* Database::rootGroup() const
|
|
{
|
|
return m_rootGroup;
|
|
}
|
|
|
|
/**
|
|
* Sets group as the root group and takes ownership of it.
|
|
* Warning: Be careful when calling this method as it doesn't
|
|
* emit any notifications so e.g. models aren't updated.
|
|
* The caller is responsible for cleaning up the previous
|
|
root group.
|
|
*/
|
|
void Database::setRootGroup(Group* group)
|
|
{
|
|
Q_ASSERT(group);
|
|
|
|
if (isInitialized() && m_modified) {
|
|
emit databaseDiscarded();
|
|
}
|
|
|
|
m_rootGroup = group;
|
|
m_rootGroup->setParent(this);
|
|
}
|
|
|
|
Metadata* Database::metadata()
|
|
{
|
|
return m_metadata;
|
|
}
|
|
|
|
const Metadata* Database::metadata() const
|
|
{
|
|
return m_metadata;
|
|
}
|
|
|
|
/**
|
|
* Returns the original file path that was provided for
|
|
* this database. This path may not exist, may contain
|
|
* unresolved symlinks, or have malformed slashes.
|
|
*
|
|
* @return original file path
|
|
*/
|
|
QString Database::filePath() const
|
|
{
|
|
return m_data.filePath;
|
|
}
|
|
|
|
/**
|
|
* Returns the canonical file path of this databases'
|
|
* set file path. This returns an empty string if the
|
|
* file does not exist or cannot be resolved.
|
|
*
|
|
* @return canonical file path
|
|
*/
|
|
QString Database::canonicalFilePath() const
|
|
{
|
|
QFileInfo fileInfo(m_data.filePath);
|
|
return fileInfo.canonicalFilePath();
|
|
}
|
|
|
|
void Database::setFilePath(const QString& filePath)
|
|
{
|
|
if (filePath == m_data.filePath) {
|
|
return;
|
|
}
|
|
|
|
if (s_filePathMap.contains(m_data.filePath)) {
|
|
s_filePathMap.remove(m_data.filePath);
|
|
}
|
|
QString oldPath = m_data.filePath;
|
|
m_data.filePath = filePath;
|
|
s_filePathMap.insert(m_data.filePath, this);
|
|
|
|
emit filePathChanged(oldPath, filePath);
|
|
}
|
|
|
|
QList<DeletedObject> Database::deletedObjects()
|
|
{
|
|
return m_deletedObjects;
|
|
}
|
|
|
|
const QList<DeletedObject>& Database::deletedObjects() const
|
|
{
|
|
return m_deletedObjects;
|
|
}
|
|
|
|
bool Database::containsDeletedObject(const QUuid& uuid) const
|
|
{
|
|
for (const DeletedObject& currentObject : m_deletedObjects) {
|
|
if (currentObject.uuid == uuid) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Database::containsDeletedObject(const DeletedObject& object) const
|
|
{
|
|
for (const DeletedObject& currentObject : m_deletedObjects) {
|
|
if (currentObject.uuid == object.uuid) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
|
|
{
|
|
if (m_deletedObjects == delObjs) {
|
|
return;
|
|
}
|
|
m_deletedObjects = delObjs;
|
|
}
|
|
|
|
void Database::addDeletedObject(const DeletedObject& delObj)
|
|
{
|
|
Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
|
|
m_deletedObjects.append(delObj);
|
|
}
|
|
|
|
void Database::addDeletedObject(const QUuid& uuid)
|
|
{
|
|
DeletedObject delObj;
|
|
delObj.deletionTime = Clock::currentDateTimeUtc();
|
|
delObj.uuid = uuid;
|
|
|
|
addDeletedObject(delObj);
|
|
}
|
|
|
|
QList<QString> Database::commonUsernames()
|
|
{
|
|
return m_commonUsernames;
|
|
}
|
|
|
|
void Database::updateCommonUsernames(int topN)
|
|
{
|
|
m_commonUsernames.clear();
|
|
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
|
|
}
|
|
|
|
const QUuid& Database::cipher() const
|
|
{
|
|
return m_data.cipher;
|
|
}
|
|
|
|
Database::CompressionAlgorithm Database::compressionAlgorithm() const
|
|
{
|
|
return m_data.compressionAlgorithm;
|
|
}
|
|
|
|
QByteArray Database::transformedMasterKey() const
|
|
{
|
|
return m_data.transformedMasterKey;
|
|
}
|
|
|
|
QByteArray Database::challengeResponseKey() const
|
|
{
|
|
return m_data.challengeResponseKey;
|
|
}
|
|
|
|
bool Database::challengeMasterSeed(const QByteArray& masterSeed)
|
|
{
|
|
if (m_data.key) {
|
|
m_data.masterSeed = masterSeed;
|
|
return m_data.key->challenge(masterSeed, m_data.challengeResponseKey);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Database::setCipher(const QUuid& cipher)
|
|
{
|
|
Q_ASSERT(!cipher.isNull());
|
|
|
|
m_data.cipher = cipher;
|
|
}
|
|
|
|
void Database::setCompressionAlgorithm(Database::CompressionAlgorithm algo)
|
|
{
|
|
Q_ASSERT(static_cast<quint32>(algo) <= CompressionAlgorithmMax);
|
|
|
|
m_data.compressionAlgorithm = algo;
|
|
}
|
|
|
|
/**
|
|
* Set and transform a new encryption key.
|
|
*
|
|
* @param key key to set and transform or nullptr to reset the key
|
|
* @param updateChangedTime true to update database change time
|
|
* @param updateTransformSalt true to update the transform salt
|
|
* @param transformKey trigger the KDF after setting the key
|
|
* @return true on success
|
|
*/
|
|
bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
|
|
bool updateChangedTime,
|
|
bool updateTransformSalt,
|
|
bool transformKey)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
|
|
if (!key) {
|
|
m_data.key.reset();
|
|
m_data.transformedMasterKey = {};
|
|
m_data.hasKey = false;
|
|
return true;
|
|
}
|
|
|
|
if (updateTransformSalt) {
|
|
m_data.kdf->randomizeSeed();
|
|
Q_ASSERT(!m_data.kdf->seed().isEmpty());
|
|
}
|
|
|
|
QByteArray oldTransformedMasterKey = m_data.transformedMasterKey;
|
|
QByteArray transformedMasterKey;
|
|
if (!transformKey) {
|
|
transformedMasterKey = oldTransformedMasterKey;
|
|
} else if (!key->transform(*m_data.kdf, transformedMasterKey)) {
|
|
return false;
|
|
}
|
|
|
|
m_data.key = key;
|
|
m_data.transformedMasterKey = transformedMasterKey;
|
|
m_data.hasKey = true;
|
|
if (updateChangedTime) {
|
|
m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc());
|
|
}
|
|
|
|
if (oldTransformedMasterKey != m_data.transformedMasterKey) {
|
|
markAsModified();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Database::hasKey() const
|
|
{
|
|
return m_data.hasKey;
|
|
}
|
|
|
|
bool Database::verifyKey(const QSharedPointer<CompositeKey>& key) const
|
|
{
|
|
Q_ASSERT(hasKey());
|
|
|
|
if (!m_data.challengeResponseKey.isEmpty()) {
|
|
QByteArray result;
|
|
|
|
if (!key->challenge(m_data.masterSeed, result)) {
|
|
// challenge failed, (YubiKey?) removed?
|
|
return false;
|
|
}
|
|
|
|
if (m_data.challengeResponseKey != result) {
|
|
// wrong response from challenged device(s)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (m_data.key->rawKey() == key->rawKey());
|
|
}
|
|
|
|
QVariantMap& Database::publicCustomData()
|
|
{
|
|
return m_data.publicCustomData;
|
|
}
|
|
|
|
const QVariantMap& Database::publicCustomData() const
|
|
{
|
|
return m_data.publicCustomData;
|
|
}
|
|
|
|
void Database::setPublicCustomData(const QVariantMap& customData)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
m_data.publicCustomData = customData;
|
|
}
|
|
|
|
void Database::createRecycleBin()
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
|
|
auto recycleBin = new Group();
|
|
recycleBin->setUuid(QUuid::createUuid());
|
|
recycleBin->setParent(rootGroup());
|
|
recycleBin->setName(tr("Recycle Bin"));
|
|
recycleBin->setIcon(Group::RecycleBinIconNumber);
|
|
recycleBin->setSearchingEnabled(Group::Disable);
|
|
recycleBin->setAutoTypeEnabled(Group::Disable);
|
|
|
|
m_metadata->setRecycleBin(recycleBin);
|
|
}
|
|
|
|
void Database::recycleEntry(Entry* entry)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
if (m_metadata->recycleBinEnabled()) {
|
|
if (!m_metadata->recycleBin()) {
|
|
createRecycleBin();
|
|
}
|
|
entry->setGroup(metadata()->recycleBin());
|
|
} else {
|
|
delete entry;
|
|
}
|
|
}
|
|
|
|
void Database::recycleGroup(Group* group)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
if (m_metadata->recycleBinEnabled()) {
|
|
if (!m_metadata->recycleBin()) {
|
|
createRecycleBin();
|
|
}
|
|
group->setParent(metadata()->recycleBin());
|
|
} else {
|
|
delete group;
|
|
}
|
|
}
|
|
|
|
void Database::emptyRecycleBin()
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
if (m_metadata->recycleBinEnabled() && m_metadata->recycleBin()) {
|
|
// destroying direct entries of the recycle bin
|
|
QList<Entry*> subEntries = m_metadata->recycleBin()->entries();
|
|
for (Entry* entry : subEntries) {
|
|
delete entry;
|
|
}
|
|
// destroying direct subgroups of the recycle bin
|
|
QList<Group*> subGroups = m_metadata->recycleBin()->children();
|
|
for (Group* group : subGroups) {
|
|
delete group;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Database::setEmitModified(bool value)
|
|
{
|
|
if (m_emitModified && !value) {
|
|
m_timer->stop();
|
|
}
|
|
|
|
m_emitModified = value;
|
|
}
|
|
|
|
bool Database::isModified() const
|
|
{
|
|
return m_modified;
|
|
}
|
|
|
|
void Database::markAsModified()
|
|
{
|
|
m_modified = true;
|
|
if (m_emitModified) {
|
|
startModifiedTimer();
|
|
}
|
|
}
|
|
|
|
void Database::markAsClean()
|
|
{
|
|
bool emitSignal = m_modified;
|
|
m_modified = false;
|
|
if (emitSignal) {
|
|
emit databaseSaved();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param uuid UUID of the database
|
|
* @return pointer to the database or nullptr if no such database exists
|
|
*/
|
|
Database* Database::databaseByUuid(const QUuid& uuid)
|
|
{
|
|
return s_uuidMap.value(uuid, nullptr);
|
|
}
|
|
|
|
/**
|
|
* @param filePath file path of the database
|
|
* @return pointer to the database or nullptr if the database has not been opened
|
|
*/
|
|
Database* Database::databaseByFilePath(const QString& filePath)
|
|
{
|
|
return s_filePathMap.value(filePath, nullptr);
|
|
}
|
|
|
|
void Database::startModifiedTimer()
|
|
{
|
|
if (!m_emitModified) {
|
|
return;
|
|
}
|
|
|
|
if (m_timer->isActive()) {
|
|
m_timer->stop();
|
|
}
|
|
m_timer->start(150);
|
|
}
|
|
|
|
QSharedPointer<const CompositeKey> Database::key() const
|
|
{
|
|
return m_data.key;
|
|
}
|
|
|
|
QSharedPointer<Kdf> Database::kdf() const
|
|
{
|
|
return m_data.kdf;
|
|
}
|
|
|
|
void Database::setKdf(QSharedPointer<Kdf> kdf)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
m_data.kdf = std::move(kdf);
|
|
}
|
|
|
|
bool Database::changeKdf(const QSharedPointer<Kdf>& kdf)
|
|
{
|
|
Q_ASSERT(!m_data.isReadOnly);
|
|
|
|
kdf->randomizeSeed();
|
|
QByteArray transformedMasterKey;
|
|
if (!m_data.key) {
|
|
m_data.key = QSharedPointer<CompositeKey>::create();
|
|
}
|
|
if (!m_data.key->transform(*kdf, transformedMasterKey)) {
|
|
return false;
|
|
}
|
|
|
|
setKdf(kdf);
|
|
m_data.transformedMasterKey = transformedMasterKey;
|
|
markAsModified();
|
|
|
|
return true;
|
|
}
|