Definitions: * Database Key - Cryptographic hash used to perform encrypt/decrypt of the database. * Database Credentials - User facing term to refer to the collection of Password, Key File, and/or Hardware Key used to derive the Database Key. Changes: * Remove the term "master" and "key" from the user's lexicon and clarify the code base based on the definitions above. * Clean up wording in the UI to be clearer to the end user.
493 lines
16 KiB
C++
493 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2018 Aetf <aetf@unlimitedcodeworks.xyz>
|
|
*
|
|
* 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 "Service.h"
|
|
|
|
#include "fdosecrets/FdoSecretsPlugin.h"
|
|
#include "fdosecrets/FdoSecretsSettings.h"
|
|
#include "fdosecrets/objects/Collection.h"
|
|
#include "fdosecrets/objects/Item.h"
|
|
#include "fdosecrets/objects/Prompt.h"
|
|
#include "fdosecrets/objects/Session.h"
|
|
|
|
#include "gui/DatabaseTabWidget.h"
|
|
#include "gui/DatabaseWidget.h"
|
|
|
|
#include <QDBusConnection>
|
|
#include <QDBusServiceWatcher>
|
|
#include <QDebug>
|
|
|
|
namespace
|
|
{
|
|
constexpr auto DEFAULT_ALIAS = "default";
|
|
}
|
|
|
|
namespace FdoSecrets
|
|
{
|
|
|
|
Service::Service(FdoSecretsPlugin* plugin,
|
|
QPointer<DatabaseTabWidget> dbTabs) // clazy: exclude=ctor-missing-parent-argument
|
|
: DBusObject(nullptr)
|
|
, m_plugin(plugin)
|
|
, m_databases(std::move(dbTabs))
|
|
, m_insdieEnsureDefaultAlias(false)
|
|
, m_serviceWatcher(nullptr)
|
|
{
|
|
connect(
|
|
m_databases, &DatabaseTabWidget::databaseUnlockDialogFinished, this, &Service::doneUnlockDatabaseInDialog);
|
|
}
|
|
|
|
Service::~Service()
|
|
{
|
|
QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET));
|
|
}
|
|
|
|
bool Service::initialize()
|
|
{
|
|
if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) {
|
|
emit error(tr("Failed to register DBus service at %1.<br/>").arg(QLatin1String(DBUS_SERVICE_SECRET))
|
|
+ m_plugin->reportExistingService());
|
|
return false;
|
|
}
|
|
|
|
registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this));
|
|
|
|
// Connect to service unregistered signal
|
|
m_serviceWatcher.reset(new QDBusServiceWatcher());
|
|
connect(m_serviceWatcher.data(),
|
|
&QDBusServiceWatcher::serviceUnregistered,
|
|
this,
|
|
&Service::dbusServiceUnregistered);
|
|
|
|
m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
|
|
|
|
// Add existing database tabs
|
|
for (int idx = 0; idx != m_databases->count(); ++idx) {
|
|
auto dbWidget = m_databases->databaseWidgetFromIndex(idx);
|
|
onDatabaseTabOpened(dbWidget, false);
|
|
}
|
|
|
|
// Connect to new database signal
|
|
// No need to connect to close signal, as collection will remove itself when backend delete/close database tab.
|
|
connect(m_databases.data(), &DatabaseTabWidget::databaseOpened, this, [this](DatabaseWidget* dbWidget) {
|
|
onDatabaseTabOpened(dbWidget, true);
|
|
});
|
|
|
|
// make default alias track current activated database
|
|
connect(m_databases.data(), &DatabaseTabWidget::activateDatabaseChanged, this, &Service::ensureDefaultAlias);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Service::onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal)
|
|
{
|
|
// The Collection will monitor the database's exposed group.
|
|
// When the Collection finds that no exposed group, it will delete itself.
|
|
// Thus the service also needs to monitor it and recreate the collection if the user changes
|
|
// from no exposed to exposed something.
|
|
if (!dbWidget->isLocked()) {
|
|
monitorDatabaseExposedGroup(dbWidget);
|
|
}
|
|
connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() {
|
|
monitorDatabaseExposedGroup(dbWidget);
|
|
});
|
|
|
|
auto coll = new Collection(this, dbWidget);
|
|
// Creation may fail if the database is not exposed.
|
|
// This is okay, because we monitor the expose settings above
|
|
if (!coll->isValid()) {
|
|
coll->deleteLater();
|
|
return;
|
|
}
|
|
|
|
m_collections << coll;
|
|
m_dbToCollection[dbWidget] = coll;
|
|
|
|
// handle alias
|
|
connect(coll, &Collection::aliasAboutToAdd, this, &Service::onCollectionAliasAboutToAdd);
|
|
connect(coll, &Collection::aliasAdded, this, &Service::onCollectionAliasAdded);
|
|
connect(coll, &Collection::aliasRemoved, this, &Service::onCollectionAliasRemoved);
|
|
|
|
ensureDefaultAlias();
|
|
|
|
// Forward delete signal, we have to rely on filepath to identify the database being closed,
|
|
// but we can not access m_backend safely because during the databaseClosed signal,
|
|
// m_backend may already be reset to nullptr
|
|
// We want to remove the collection object from dbus as early as possible, to avoid
|
|
// race conditions when deleteLater was called on the m_backend, but not delivered yet,
|
|
// and new method calls from dbus occurred. Therefore we can't rely on the destroyed
|
|
// signal on m_backend.
|
|
// bind to coll lifespan
|
|
connect(m_databases.data(), &DatabaseTabWidget::databaseClosed, coll, [coll](const QString& filePath) {
|
|
if (filePath == coll->backendFilePath()) {
|
|
coll->doDelete();
|
|
}
|
|
});
|
|
|
|
// relay signals
|
|
connect(coll, &Collection::collectionChanged, this, [this, coll]() { emit collectionChanged(coll); });
|
|
connect(coll, &Collection::collectionAboutToDelete, this, [this, coll]() {
|
|
m_collections.removeAll(coll);
|
|
m_dbToCollection.remove(coll->backend());
|
|
emit collectionDeleted(coll);
|
|
});
|
|
|
|
if (emitSignal) {
|
|
emit collectionCreated(coll);
|
|
}
|
|
}
|
|
|
|
void Service::monitorDatabaseExposedGroup(DatabaseWidget* dbWidget)
|
|
{
|
|
Q_ASSERT(dbWidget);
|
|
connect(
|
|
dbWidget->database()->metadata()->customData(), &CustomData::customDataModified, this, [this, dbWidget]() {
|
|
if (!FdoSecrets::settings()->exposedGroup(dbWidget->database()).isNull() && !findCollection(dbWidget)) {
|
|
onDatabaseTabOpened(dbWidget, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
void Service::ensureDefaultAlias()
|
|
{
|
|
if (m_insdieEnsureDefaultAlias) {
|
|
return;
|
|
}
|
|
|
|
m_insdieEnsureDefaultAlias = true;
|
|
|
|
auto coll = findCollection(m_databases->currentDatabaseWidget());
|
|
if (coll) {
|
|
// adding alias will automatically remove the association with previous collection.
|
|
coll->addAlias(DEFAULT_ALIAS).okOrDie();
|
|
}
|
|
|
|
m_insdieEnsureDefaultAlias = false;
|
|
}
|
|
|
|
void Service::dbusServiceUnregistered(const QString& service)
|
|
{
|
|
Q_ASSERT(m_serviceWatcher);
|
|
|
|
auto removed = m_serviceWatcher->removeWatchedService(service);
|
|
Q_UNUSED(removed);
|
|
Q_ASSERT(removed);
|
|
|
|
Session::CleanupNegotiation(service);
|
|
auto sess = m_peerToSession.value(service, nullptr);
|
|
if (sess) {
|
|
sess->close().okOrDie();
|
|
}
|
|
}
|
|
|
|
DBusReturn<const QList<Collection*>> Service::collections() const
|
|
{
|
|
return m_collections;
|
|
}
|
|
|
|
DBusReturn<QVariant> Service::openSession(const QString& algorithm, const QVariant& input, Session*& result)
|
|
{
|
|
QVariant output;
|
|
bool incomplete = false;
|
|
auto peer = callingPeer();
|
|
|
|
// watch for service unregister to cleanup
|
|
Q_ASSERT(m_serviceWatcher);
|
|
m_serviceWatcher->addWatchedService(peer);
|
|
|
|
// negotiate cipher
|
|
auto ciphers = Session::CreateCiphers(peer, algorithm, input, output, incomplete);
|
|
if (incomplete) {
|
|
result = nullptr;
|
|
return output;
|
|
}
|
|
if (!ciphers) {
|
|
return DBusReturn<>::Error(QDBusError::NotSupported);
|
|
}
|
|
result = new Session(std::move(ciphers), callingPeerName(), this);
|
|
|
|
m_sessions.append(result);
|
|
m_peerToSession[peer] = result;
|
|
connect(result, &Session::aboutToClose, this, [this, peer, result]() {
|
|
emit sessionClosed(result);
|
|
m_sessions.removeAll(result);
|
|
m_peerToSession.remove(peer);
|
|
});
|
|
emit sessionOpened(result);
|
|
|
|
return output;
|
|
}
|
|
|
|
DBusReturn<Collection*>
|
|
Service::createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt)
|
|
{
|
|
prompt = nullptr;
|
|
|
|
// return existing collection if alias is non-empty and exists.
|
|
auto collection = findCollection(alias);
|
|
if (!collection) {
|
|
auto cp = new CreateCollectionPrompt(this);
|
|
prompt = cp;
|
|
|
|
// collection will be created when the prompt complets.
|
|
// once it's done, we set additional properties on the collection
|
|
connect(cp, &CreateCollectionPrompt::collectionCreated, cp, [alias, properties](Collection* coll) {
|
|
coll->setProperties(properties).okOrDie();
|
|
if (!alias.isEmpty()) {
|
|
coll->addAlias(alias).okOrDie();
|
|
}
|
|
});
|
|
}
|
|
return collection;
|
|
}
|
|
|
|
DBusReturn<const QList<Item*>> Service::searchItems(const StringStringMap& attributes, QList<Item*>& locked)
|
|
{
|
|
auto ret = collections();
|
|
if (ret.isError()) {
|
|
return ret;
|
|
}
|
|
|
|
QList<Item*> unlocked;
|
|
for (const auto& coll : ret.value()) {
|
|
auto items = coll->searchItems(attributes);
|
|
if (items.isError()) {
|
|
return items;
|
|
}
|
|
auto l = coll->locked();
|
|
if (l.isError()) {
|
|
return l;
|
|
}
|
|
if (l.value()) {
|
|
locked.append(items.value());
|
|
} else {
|
|
unlocked.append(items.value());
|
|
}
|
|
}
|
|
return unlocked;
|
|
}
|
|
|
|
DBusReturn<const QList<DBusObject*>> Service::unlock(const QList<DBusObject*>& objects, PromptBase*& prompt)
|
|
{
|
|
QSet<Collection*> needUnlock;
|
|
needUnlock.reserve(objects.size());
|
|
for (const auto& obj : asConst(objects)) {
|
|
auto coll = qobject_cast<Collection*>(obj);
|
|
if (coll) {
|
|
needUnlock << coll;
|
|
} else {
|
|
auto item = qobject_cast<Item*>(obj);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
// we lock the whole collection for item
|
|
needUnlock << item->collection();
|
|
}
|
|
}
|
|
|
|
// return anything already unlocked
|
|
QList<DBusObject*> unlocked;
|
|
QList<Collection*> toUnlock;
|
|
for (const auto& coll : asConst(needUnlock)) {
|
|
auto l = coll->locked();
|
|
if (l.isError()) {
|
|
return l;
|
|
}
|
|
if (!l.value()) {
|
|
unlocked << coll;
|
|
} else {
|
|
toUnlock << coll;
|
|
}
|
|
}
|
|
if (!toUnlock.isEmpty()) {
|
|
prompt = new UnlockCollectionsPrompt(this, toUnlock);
|
|
}
|
|
return unlocked;
|
|
}
|
|
|
|
DBusReturn<const QList<DBusObject*>> Service::lock(const QList<DBusObject*>& objects, PromptBase*& prompt)
|
|
{
|
|
QSet<Collection*> needLock;
|
|
needLock.reserve(objects.size());
|
|
for (const auto& obj : asConst(objects)) {
|
|
auto coll = qobject_cast<Collection*>(obj);
|
|
if (coll) {
|
|
needLock << coll;
|
|
} else {
|
|
auto item = qobject_cast<Item*>(obj);
|
|
if (!item) {
|
|
continue;
|
|
}
|
|
// we lock the whole collection for item
|
|
needLock << item->collection();
|
|
}
|
|
}
|
|
|
|
// return anything already locked
|
|
QList<DBusObject*> locked;
|
|
QList<Collection*> toLock;
|
|
for (const auto& coll : asConst(needLock)) {
|
|
auto l = coll->locked();
|
|
if (l.isError()) {
|
|
return l;
|
|
}
|
|
if (l.value()) {
|
|
locked << coll;
|
|
} else {
|
|
toLock << coll;
|
|
}
|
|
}
|
|
if (!toLock.isEmpty()) {
|
|
prompt = new LockCollectionsPrompt(this, toLock);
|
|
}
|
|
return locked;
|
|
}
|
|
|
|
DBusReturn<const QHash<Item*, SecretStruct>> Service::getSecrets(const QList<Item*>& items, Session* session)
|
|
{
|
|
if (!session) {
|
|
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION));
|
|
}
|
|
|
|
QHash<Item*, SecretStruct> res;
|
|
|
|
for (const auto& item : asConst(items)) {
|
|
auto ret = item->getSecret(session);
|
|
if (ret.isError()) {
|
|
return ret;
|
|
}
|
|
res[item] = std::move(ret).value();
|
|
}
|
|
if (calledFromDBus()) {
|
|
plugin()->emitRequestShowNotification(
|
|
tr(R"(%n Entry(s) was used by %1)", "%1 is the name of an application", res.size())
|
|
.arg(callingPeerName()));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
DBusReturn<Collection*> Service::readAlias(const QString& name)
|
|
{
|
|
return findCollection(name);
|
|
}
|
|
|
|
DBusReturn<void> Service::setAlias(const QString& name, Collection* collection)
|
|
{
|
|
if (!collection) {
|
|
// remove alias name from its collection
|
|
collection = findCollection(name);
|
|
if (!collection) {
|
|
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
|
|
}
|
|
return collection->removeAlias(name);
|
|
}
|
|
return collection->addAlias(name);
|
|
}
|
|
|
|
Collection* Service::findCollection(const QString& alias) const
|
|
{
|
|
if (alias.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto it = m_aliases.find(alias);
|
|
if (it != m_aliases.end()) {
|
|
return it.value();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Service::onCollectionAliasAboutToAdd(const QString& alias)
|
|
{
|
|
auto coll = qobject_cast<Collection*>(sender());
|
|
|
|
auto it = m_aliases.constFind(alias);
|
|
if (it != m_aliases.constEnd() && it.value() != coll) {
|
|
// another collection holds the alias
|
|
// remove it first
|
|
it.value()->removeAlias(alias).okOrDie();
|
|
|
|
// onCollectionAliasRemoved called through signal
|
|
// `it` becomes invalidated now
|
|
}
|
|
}
|
|
|
|
void Service::onCollectionAliasAdded(const QString& alias)
|
|
{
|
|
auto coll = qobject_cast<Collection*>(sender());
|
|
m_aliases[alias] = coll;
|
|
}
|
|
|
|
void Service::onCollectionAliasRemoved(const QString& alias)
|
|
{
|
|
m_aliases.remove(alias);
|
|
ensureDefaultAlias();
|
|
}
|
|
|
|
Collection* Service::findCollection(const DatabaseWidget* db) const
|
|
{
|
|
return m_dbToCollection.value(db, nullptr);
|
|
}
|
|
|
|
const QList<Session*> Service::sessions() const
|
|
{
|
|
return m_sessions;
|
|
}
|
|
|
|
bool Service::doCloseDatabase(DatabaseWidget* dbWidget)
|
|
{
|
|
return m_databases->closeDatabaseTab(dbWidget);
|
|
}
|
|
|
|
Collection* Service::doNewDatabase()
|
|
{
|
|
auto dbWidget = m_databases->newDatabase();
|
|
if (!dbWidget) {
|
|
return nullptr;
|
|
}
|
|
|
|
// database created through dbus will be exposed to dbus by default
|
|
auto db = dbWidget->database();
|
|
FdoSecrets::settings()->setExposedGroup(db, db->rootGroup()->uuid());
|
|
|
|
auto collection = findCollection(dbWidget);
|
|
|
|
Q_ASSERT(collection);
|
|
|
|
return collection;
|
|
}
|
|
|
|
void Service::doSwitchToDatabaseSettings(DatabaseWidget* dbWidget)
|
|
{
|
|
if (dbWidget->isLocked()) {
|
|
return;
|
|
}
|
|
// switch selected to current
|
|
m_databases->setCurrentWidget(dbWidget);
|
|
m_databases->showDatabaseSettings();
|
|
|
|
// open settings (switch from app settings to m_dbTabs)
|
|
m_plugin->emitRequestSwitchToDatabases();
|
|
}
|
|
|
|
void Service::doUnlockDatabaseInDialog(DatabaseWidget* dbWidget)
|
|
{
|
|
m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None);
|
|
}
|
|
|
|
} // namespace FdoSecrets
|