Add Freedesktop.org Secret Storage Spec Server Side API (Fix #1403)

This plugin implements the Secret Storage specification version 0.2.
While running KeePassXC, it acts as a Secret Service server, registered
on DBus, so clients like seahorse, python-secretstorage, or other
implementations can connect and access the exposed database in KeePassXC.

Squashed commits:

- Initial code
- Add SessionAdaptor and fix build
- The skeletons for all dbus objects are in place
- Implement collection creation and deletion
- Emit collectionChanged signal
- Implement app-wise settings page
- Implement error message on GUI
- Implement settings
- Fix uuid to dbus path
- Implement app level settings
- Add freedesktop logo
- Implement database settings page
- Change database settings to a treeview
- Move all settings read/write to one place
- Rename SecretServiceOptionsPage to SettingsWidgetFdoSecrets
- Fix selected group can not be saved if the user hasn't click on the item
- Show selected group per database in app settings
- Disable editing of various readonly widgets
- Remove unused warning about non exposed database
- Fix method signature on dbus adaptors
- Fix type derived from DBusObject not recognized as QDBusContext
- Resolve a few TODOs around error handling
- Remove const when passing DBus exposed objects
- Move dismiss to PromptBase
- Implement per collection locking/unlocking
- Fix const correctness on Item::setSecret
- Implement SecretService::getSecrets
- Rework the signal connections around collections.
- Remove generateId from DBusObject
- Per spec, use encoded label as DBus object path for collections
- Fix some corner cases around collection name changes
- Implement alias
- Fix wrong alias dbus path
- Implement encryption per spec
- Cleanup SessionCipher
- Implement searchItems for SecretService
- Use Tools::uuidToHex
- Implement Item attributes and delete
- Implement createItem
- Always check if the database is unlocked before perform any operation
- Add missing ReadAlias/SetAlias on service
- Reorganize and fix OpenSession always returning empty output
- Overhaul error handling
- Make sure default alias is always present
- Remove collection aliases early in doDelete
- Handles all content types, fix setProperties not working
- Fix sometimes there is an extraneous leading zero when converting from MPI
- Fix session encryption negotiation
- Do not expose recycle bin
- Protect against the methods not called from DBus
- Also emit collectionChanged signal when lock state changes
- Show notification when entry secret is requested
- Add a README file
- Actually close session when client disconnects
- Gracefully return alternative label when collection is locked
- Reorganize, rename secretservice to fdosecrets
- Fix issues reported by clazy
- Unify UI strings and fix icon
- Implement a setting to skip confirmation when deleting entries from DBus
- Remove some unused debugging log
- Simply ignore errors when DBus context is not available. QtDBus won't set QDBusContext when deliver property get/set, and there is no way to get a QDBusMessage in property getter/setter.
- Simplify GcryptMPI using std::unique_ptr and add unit test
- Format code in fdosecrets
- Move DBusReturnImpl to details namespace
- Fix crash when locking a database: don't modify exposedGroup setting in customData when database is deleted
- Make sure Collection::searchItems works, whether it's locked or not
- Fix FdoSecrets::Collection becomes empty after a database reload
- Fix crash when looping while modifying the list
This commit is contained in:
Aetf 2019-03-25 23:07:18 -04:00 committed by Jonathan White
parent d93f33f514
commit e121f4bc28
56 changed files with 6031 additions and 0 deletions

View File

@ -50,6 +50,9 @@ option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)" OFF)
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
if(UNIX AND NOT APPLE)
option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF)
endif()
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
@ -65,6 +68,9 @@ if(WITH_XC_ALL)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
if(UNIX AND NOT APPLE)
set(WITH_XC_FDOSECRETS ON)
endif()
endif()
if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE)

View File

@ -241,3 +241,7 @@ License: LGPL-2.1
Files: share/macosx/dmg-background.tiff
Copyright: 2008-2014, Andrey Tarantsov
License: MIT
Files: share/icons/application/scalable/apps/freedesktop.svg
Copyright: GPL-2+
Comment: from Freedesktop.org website

View File

@ -102,6 +102,7 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF)
-DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
-DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group synchronization extension (default: OFF)
-DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare signed containers, requires libquazip5 (default: OFF)
-DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF)

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="108.7505"
height="91.166321"
viewBox="0 0 87.000389 72.933061"
id="svg2"
sodipodi:docname="freedesktop.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<defs
id="defs14" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2106"
id="namedview12"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.7980996"
inkscape:cx="-97.45169"
inkscape:cy="25.551539"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<metadata
id="metadata57">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0.01402783,0.01402783)"
id="g37"
style="fill:#ffffff;fill-rule:nonzero;stroke:#3b80ae;stroke-width:2.45880008;stroke-miterlimit:4">
<g
id="g39">
<path
d="M 85.277,40.796 C 87.335,48.68 82.61,56.738 74.726,58.795 L 27.143,71.21 C 19.259,73.267 11.2,68.543 9.143,60.658 L 1.695,32.108 C -0.362,24.224 4.362,16.166 12.246,14.109 L 59.83,1.695 c 7.884,-2.057 15.942,2.667 17.999,10.551 l 7.449,28.55 z"
id="path41"
style="stroke:#bababa"
inkscape:connector-curvature="0" />
<path
d="m 80.444,39.778 c 1.749,7.854 -1.816,13.621 -9.504,15.447 L 28.704,66.245 C 21.135,68.641 14.615,65.064 12.866,57.409 L 6.53,33.127 C 4.781,24.982 7.239,20.238 16.033,17.68 L 58.27,6.661 c 8.144,-1.826 14.089,1.363 15.838,8.835 z"
id="path43"
style="fill:#3b80ae;stroke:none"
inkscape:connector-curvature="0" />
</g>
<path
d="M 45.542,51.793 24.104,31.102 62.204,26.709 Z"
id="path45"
style="opacity:0.5;fill:none;stroke:#ffffff"
inkscape:connector-curvature="0" />
<path
d="m 72.325,28.769 c 0.405,1.55 -0.525,3.136 -2.075,3.541 l -12.331,3.217 c -1.551,0.404 -3.137,-0.525 -3.542,-2.076 L 52.082,24.65 c -0.405,-1.551 0.524,-3.137 2.076,-3.542 l 12.33,-3.217 c 1.551,-0.405 3.137,0.525 3.542,2.076 l 2.295,8.801 z"
id="path47"
inkscape:connector-curvature="0" />
<path
d="m 36.51,33.625 c 0.496,1.9 -0.645,3.844 -2.545,4.34 l -15.112,3.943 c -1.901,0.496 -3.845,-0.644 -4.34,-2.544 L 11.699,28.578 c -0.496,-1.901 0.644,-3.844 2.544,-4.34 l 15.113,-3.942 c 1.901,-0.496 3.845,0.643 4.34,2.544 l 2.814,10.786 z"
id="path49"
inkscape:connector-curvature="0" />
<path
d="m 52.493,53.208 c 0.278,1.065 -0.36,2.154 -1.425,2.432 L 42.6,57.848 c -1.064,0.277 -2.153,-0.36 -2.431,-1.426 l -1.577,-6.043 c -0.277,-1.064 0.36,-2.153 1.425,-2.432 l 8.468,-2.209 c 1.064,-0.277 2.154,0.361 2.431,1.426 l 1.577,6.043 z"
id="path51"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -194,6 +194,9 @@ add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible wit
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking")
if(UNIX AND NOT APPLE)
add_feature_info(FdoSecrets WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API.")
endif()
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
endif()
@ -226,6 +229,11 @@ if(WITH_XC_SSHAGENT)
set(sshagent_LIB sshagent)
endif()
add_subdirectory(fdosecrets)
if(WITH_XC_FDOSECRETS)
set(fdosecrets_LIB fdosecrets)
endif()
set(autotype_SOURCES
core/Tools.cpp
autotype/AutoType.cpp
@ -270,6 +278,7 @@ target_link_libraries(keepassx_core
autotype
${keepassxcbrowser_LIB}
${qrcode_LIB}
${fdosecrets_LIB}
Qt5::Core
Qt5::Concurrent
Qt5::Network

View File

@ -22,6 +22,7 @@
#cmakedefine WITH_XC_KEESHARE_SECURE
#cmakedefine WITH_XC_UPDATECHECK
#cmakedefine WITH_XC_TOUCHID
#cmakedefine WITH_XC_FDOSECRETS
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"
#cmakedefine KEEPASSXC_BUILD_TYPE_RELEASE

View File

@ -0,0 +1,36 @@
if(WITH_XC_FDOSECRETS)
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
add_library(fdosecrets STATIC
# app settings page
FdoSecretsPlugin.cpp
widgets/SettingsWidgetFdoSecrets.cpp
# per database settings page
DatabaseSettingsPageFdoSecrets.cpp
widgets/DatabaseSettingsWidgetFdoSecrets.cpp
# setting storage
FdoSecretsSettings.cpp
# gcrypt MPI wrapper
GcryptMPI.cpp
# dbus objects
objects/DBusObject.cpp
objects/Service.cpp
objects/Session.cpp
objects/SessionCipher.cpp
objects/Collection.cpp
objects/Item.cpp
objects/Prompt.cpp
objects/adaptors/ServiceAdaptor.cpp
objects/adaptors/SessionAdaptor.cpp
objects/adaptors/CollectionAdaptor.cpp
objects/adaptors/ItemAdaptor.cpp
objects/adaptors/PromptAdaptor.cpp
objects/DBusReturn.cpp
objects/DBusTypes.cpp
)
target_link_libraries(fdosecrets Qt5::Core Qt5::Widgets Qt5::DBus ${GCRYPT_LIBRARIES})
endif()

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 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 "DatabaseSettingsPageFdoSecrets.h"
#include "fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h"
#include "core/FilePath.h"
QString DatabaseSettingsPageFdoSecrets::name()
{
return QObject::tr("Secret Service Integration");
}
QIcon DatabaseSettingsPageFdoSecrets::icon()
{
return filePath()->icon(QStringLiteral("apps"), QStringLiteral("freedesktop"));
}
QWidget* DatabaseSettingsPageFdoSecrets::createWidget()
{
return new DatabaseSettingsWidgetFdoSecrets;
}
void DatabaseSettingsPageFdoSecrets::loadSettings(QWidget* widget, QSharedPointer<Database> db)
{
auto settingsWidget = qobject_cast<DatabaseSettingsWidgetFdoSecrets*>(widget);
settingsWidget->loadSettings(db);
}
void DatabaseSettingsPageFdoSecrets::saveSettings(QWidget* widget)
{
auto settingsWidget = qobject_cast<DatabaseSettingsWidgetFdoSecrets*>(widget);
settingsWidget->saveSettings();
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H
#define KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H
#include "gui/dbsettings/DatabaseSettingsDialog.h"
class DatabaseSettingsPageFdoSecrets : public IDatabaseSettingsPage
{
Q_DISABLE_COPY(DatabaseSettingsPageFdoSecrets)
public:
DatabaseSettingsPageFdoSecrets() = default;
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
void saveSettings(QWidget* widget) override;
};
#endif // KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H

View File

@ -0,0 +1,88 @@
/*
* 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 "FdoSecretsPlugin.h"
#include "fdosecrets/FdoSecretsSettings.h"
#include "fdosecrets/objects/DBusTypes.h"
#include "fdosecrets/objects/Service.h"
#include "fdosecrets/widgets/SettingsWidgetFdoSecrets.h"
#include "gui/DatabaseTabWidget.h"
#include <utility>
using FdoSecrets::Service;
FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget)
: m_dbTabs(tabWidget)
{
FdoSecrets::registerDBusTypes();
}
QWidget* FdoSecretsPlugin::createWidget()
{
return new SettingsWidgetFdoSecrets(this);
}
void FdoSecretsPlugin::loadSettings(QWidget* widget)
{
qobject_cast<SettingsWidgetFdoSecrets*>(widget)->loadSettings();
}
void FdoSecretsPlugin::saveSettings(QWidget* widget)
{
qobject_cast<SettingsWidgetFdoSecrets*>(widget)->saveSettings();
updateServiceState();
}
void FdoSecretsPlugin::updateServiceState()
{
if (FdoSecrets::settings()->isEnabled()) {
if (!m_secretService && m_dbTabs) {
m_secretService.reset(new Service(this, m_dbTabs));
connect(m_secretService.data(), &Service::error, this, [this](const QString& msg) {
emit error(tr("Fdo Secret Service: %1").arg(msg));
});
if (!m_secretService->initialize()) {
m_secretService.reset();
}
}
} else {
if (m_secretService) {
m_secretService.reset();
}
}
}
Service* FdoSecretsPlugin::serviceInstance() const
{
return m_secretService.data();
}
void FdoSecretsPlugin::emitRequestSwitchToDatabases()
{
emit requestSwitchToDatabases();
}
void FdoSecretsPlugin::emitRequestShowNotification(const QString& msg, const QString& title)
{
if (!FdoSecrets::settings()->showNotification()) {
return;
}
emit requestShowNotification(msg, title, 10000);
}

View File

@ -0,0 +1,76 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETSPLUGIN_H
#define KEEPASSXC_FDOSECRETSPLUGIN_H
#include "core/FilePath.h"
#include "gui/ApplicationSettingsWidget.h"
#include <QPointer>
#include <QScopedPointer>
class DatabaseTabWidget;
namespace FdoSecrets
{
class Service;
} // namespace FdoSecrets
class FdoSecretsPlugin : public QObject, public ISettingsPage
{
Q_OBJECT
public:
explicit FdoSecretsPlugin(DatabaseTabWidget* tabWidget);
~FdoSecretsPlugin() override = default;
QString name() override
{
return QObject::tr("Secret Service Integration");
}
QIcon icon() override
{
return FilePath::instance()->icon("apps", "freedesktop");
}
QWidget* createWidget() override;
void loadSettings(QWidget* widget) override;
void saveSettings(QWidget* widget) override;
void updateServiceState();
/**
* @return The service instance, can be nullptr if the service is disabled.
*/
FdoSecrets::Service* serviceInstance() const;
public slots:
void emitRequestSwitchToDatabases();
void emitRequestShowNotification(const QString& msg, const QString& title = {});
signals:
void error(const QString& msg);
void requestSwitchToDatabases();
void requestShowNotification(const QString& msg, const QString& title, int msTimeoutHint);
private:
QPointer<DatabaseTabWidget> m_dbTabs;
QScopedPointer<FdoSecrets::Service> m_secretService;
};
#endif // KEEPASSXC_FDOSECRETSPLUGIN_H

View File

@ -0,0 +1,103 @@
/*
* 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 "FdoSecretsSettings.h"
#include "core/Config.h"
#include "core/CustomData.h"
#include "core/Database.h"
#include "core/Metadata.h"
namespace Keys
{
constexpr auto FdoSecretsEnabled = "FdoSecrets/Enabled";
constexpr auto FdoSecretsShowNotification = "FdoSecrets/ShowNotification";
constexpr auto FdoSecretsNoConfirmDeleteItem = "FdoSecrets/NoConfirmDeleteItem";
namespace Db
{
constexpr auto FdoSecretsExposedGroup = "FDO_SECRETS_EXPOSED_GROUP";
} // namespace Db
} // namespace Keys
namespace FdoSecrets
{
FdoSecretsSettings* FdoSecretsSettings::m_instance = nullptr;
FdoSecretsSettings* FdoSecretsSettings::instance()
{
if (!m_instance) {
m_instance = new FdoSecretsSettings;
}
return m_instance;
}
bool FdoSecretsSettings::isEnabled() const
{
return config()->get(Keys::FdoSecretsEnabled, false).toBool();
}
void FdoSecretsSettings::setEnabled(bool enabled)
{
config()->set(Keys::FdoSecretsEnabled, enabled);
}
bool FdoSecretsSettings::showNotification() const
{
return config()->get(Keys::FdoSecretsShowNotification, true).toBool();
}
void FdoSecretsSettings::setShowNotification(bool show)
{
config()->set(Keys::FdoSecretsShowNotification, show);
}
bool FdoSecretsSettings::noConfirmDeleteItem() const
{
return config()->get(Keys::FdoSecretsNoConfirmDeleteItem, false).toBool();
}
void FdoSecretsSettings::setNoConfirmDeleteItem(bool noConfirm)
{
config()->set(Keys::FdoSecretsNoConfirmDeleteItem, noConfirm);
}
QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer<Database>& db) const
{
return exposedGroup(db.data());
}
void FdoSecretsSettings::setExposedGroup(const QSharedPointer<Database>& db,
const QUuid& group) // clazy:exclude=function-args-by-value
{
setExposedGroup(db.data(), group);
}
QUuid FdoSecretsSettings::exposedGroup(Database* db) const
{
return {db->metadata()->customData()->value(Keys::Db::FdoSecretsExposedGroup)};
}
void FdoSecretsSettings::setExposedGroup(Database* db, const QUuid& group) // clazy:exclude=function-args-by-value
{
db->metadata()->customData()->set(Keys::Db::FdoSecretsExposedGroup, group.toString());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,62 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETSSETTINGS_H
#define KEEPASSXC_FDOSECRETSSETTINGS_H
#include <QSharedPointer>
#include <QUuid>
class Database;
namespace FdoSecrets
{
class FdoSecretsSettings
{
public:
FdoSecretsSettings() = default;
static FdoSecretsSettings* instance();
bool isEnabled() const;
void setEnabled(bool enabled);
bool showNotification() const;
void setShowNotification(bool show);
bool noConfirmDeleteItem() const;
void setNoConfirmDeleteItem(bool noConfirm);
// Per db settings
QUuid exposedGroup(const QSharedPointer<Database>& db) const;
void setExposedGroup(const QSharedPointer<Database>& db, const QUuid& group);
QUuid exposedGroup(Database* db) const;
void setExposedGroup(Database* db, const QUuid& group);
private:
static FdoSecretsSettings* m_instance;
};
inline FdoSecretsSettings* settings()
{
return FdoSecretsSettings::instance();
}
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETSSETTINGS_H

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2019 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 "GcryptMPI.h"
GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure, gcry_mpi_format format)
{
auto bufLen = static_cast<size_t>(bytes.size());
const char* buf = nullptr;
// gcry_mpi_scan uses secure memory only if input buffer is secure memory, so we have to make a copy
GcryptMemPtr secureBuf;
if (secure) {
secureBuf.reset(static_cast<char*>(gcry_malloc_secure(bufLen)));
memcpy(secureBuf.get(), bytes.data(), bufLen);
buf = secureBuf.get();
} else {
buf = bytes.data();
}
gcry_mpi_t rawMpi;
auto err = gcry_mpi_scan(&rawMpi, format, buf, format == GCRYMPI_FMT_HEX ? 0 : bufLen, nullptr);
GcryptMPI mpi(rawMpi);
if (gcry_err_code(err) != GPG_ERR_NO_ERROR) {
mpi.reset();
}
return mpi;
}
GcryptMPI MpiFromHex(const char* hex, bool secure)
{
return MpiFromBytes(QByteArray::fromRawData(hex, static_cast<int>(strlen(hex) + 1)), secure, GCRYMPI_FMT_HEX);
}
QByteArray MpiToBytes(const GcryptMPI& mpi)
{
unsigned char* buf = nullptr;
size_t buflen = 0;
gcry_mpi_aprint(GCRYMPI_FMT_USG, &buf, &buflen, mpi.get());
QByteArray bytes(reinterpret_cast<char*>(buf), static_cast<int>(buflen));
gcry_free(buf);
return bytes;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_GCRYPTMPI_H
#define KEEPASSXC_GCRYPTMPI_H
#include <QByteArray>
#include <gcrypt.h>
#include <memory>
#include <type_traits>
template <typename D, D fn> using deleter_from_fn = std::integral_constant<D, fn>;
/**
* A simple RAII wrapper for gcry_mpi_t
*/
using GcryptMPI = std::unique_ptr<gcry_mpi, deleter_from_fn<decltype(&gcry_mpi_release), &gcry_mpi_release>>;
/**
* A simple RAII wrapper for libgcrypt allocated memory
*/
using GcryptMemPtr = std::unique_ptr<char, deleter_from_fn<decltype(&gcry_free), &gcry_free>>;
/**
* Parse a QByteArray as MPI
* @param bytes
* @param secure
* @param format
* @return
*/
GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure = true, gcry_mpi_format format = GCRYMPI_FMT_USG);
/**
* Parse MPI from hex data
* @param hex
* @param secure
* @return
*/
GcryptMPI MpiFromHex(const char* hex, bool secure = true);
/**
* Dump MPI to bytes in USG format
* @param mpi
* @return
*/
QByteArray MpiToBytes(const GcryptMPI& mpi);
#endif // KEEPASSXC_GCRYPTMPI_H

42
src/fdosecrets/README.md Normal file
View File

@ -0,0 +1,42 @@
# Freedesktop.org Secret Storage Spec Server Side API
This plugin implements the [Secret Storage specification][secrets] version 0.2. While running KeePassXC, it acts as a
Secret Service server, registered on DBus, so clients like seahorse, python-secretstorage, or other implementations
can connect and access the exposed database in KeePassXC.
[secrets]: (https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/)
## Configurable settings
* The user can specify if a database is exposed on DBus, and which group is exposed.
* Whether to show desktop notification is shown when an entry is retrived.
* Whether to skip confirmation for entries deleted from DBus
## Implemented Attributes on Item Object
The following attributes are exposed:
|Key|Value|
|:---:|:---:|
|Title|The entry title|
|UserName|The entry user name|
|URL|The entry URL|
|Notes|The entry notes|
In addition, all non-protected custom attributes are also exposed.
## Architecture
* `FdoSecrets::Service` is the top level DBus service
* There is one and only one `FdoSecrets::Collection` per opened database tab
* Each entry under the exposed database group has a corresponding `FdoSecrets::Item` DBus object.
### Signal connections
- Collections are created when a corresponding database tab opened
- If the database is locked, a collection is created
- When the database is unlocked, collection populates its children
- If the unlocked database's exposed group is none, collection deletes itself
- If the database's exposed group changes, collection repopulates
- If the database's exposed group changes to none, collection deletes itself
- If the database's exposed group changes from none, the service recreates a collection

View File

@ -0,0 +1,654 @@
/*
* 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 "Collection.h"
#include "fdosecrets/FdoSecretsSettings.h"
#include "fdosecrets/objects/Item.h"
#include "fdosecrets/objects/Prompt.h"
#include "fdosecrets/objects/Service.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/EntrySearcher.h"
#include "gui/DatabaseTabWidget.h"
#include "gui/DatabaseWidget.h"
#include <QFileInfo>
namespace FdoSecrets
{
Collection::Collection(Service* parent, DatabaseWidget* backend)
: DBusObject(parent)
, m_backend(backend)
, m_exposedGroup(nullptr)
, m_registered(false)
{
// whenever the file path or the database object itself change, we do a full reload.
connect(backend, &DatabaseWidget::databaseFilePathChanged, this, &Collection::reloadBackend);
connect(backend, &DatabaseWidget::databaseReplaced, this, &Collection::reloadBackend);
// also remember to clear/populate the database when lock state changes.
connect(backend, &DatabaseWidget::databaseUnlocked, this, &Collection::onDatabaseLockChanged);
connect(backend, &DatabaseWidget::databaseLocked, this, &Collection::onDatabaseLockChanged);
reloadBackend();
}
void Collection::reloadBackend()
{
if (m_registered) {
// delete all items
// this has to be done because the backend is actually still there, just we don't expose them
// NOTE: Do NOT use a for loop, because Item::doDelete will remove itself from m_items.
while (!m_items.isEmpty()) {
m_items.first()->doDelete();
}
cleanupConnections();
unregisterCurrentPath();
m_registered = false;
}
// make sure we have updated copy of the filepath, which is used to identify the database.
m_backendPath = m_backend->database()->filePath();
// the database may not have a name (derived from filePath) yet, which may happen if it's newly created.
// defer the registration to next time a file path change happens.
if (!name().isEmpty()) {
registerWithPath(
QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), encodePath(name())),
new CollectionAdaptor(this));
m_registered = true;
}
// populate contents after expose on dbus, because items rely on parent's dbus object path
if (!backendLocked()) {
populateContents();
} else {
cleanupConnections();
}
}
DBusReturn<void> Collection::ensureBackend() const
{
if (!m_backend) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
}
return {};
}
DBusReturn<void> Collection::ensureUnlocked() const
{
if (backendLocked()) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED));
}
return {};
}
DBusReturn<const QList<Item*>> Collection::items() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
return m_items;
}
DBusReturn<QString> Collection::label() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
if (backendLocked()) {
return name();
}
return m_backend->database()->metadata()->name();
}
DBusReturn<void> Collection::setLabel(const QString& label)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
m_backend->database()->metadata()->setName(label);
return {};
}
DBusReturn<bool> Collection::locked() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
return backendLocked();
}
DBusReturn<qulonglong> Collection::created() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
return static_cast<qulonglong>(
m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000);
}
DBusReturn<qulonglong> Collection::modified() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
// FIXME: there seems not to have a global modified time.
// Use a more accurate time, considering all metadata, group, entry.
return static_cast<qulonglong>(
m_backend->database()->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000);
}
DBusReturn<PromptBase*> Collection::deleteCollection()
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
// Delete means close database
auto prompt = new DeleteCollectionPrompt(service(), this);
if (backendLocked()) {
// this won't raise a dialog, immediate execute
auto pret = prompt->prompt({});
if (pret.isError()) {
return pret;
}
prompt = nullptr;
}
// defer the close to the prompt
return prompt;
}
DBusReturn<const QList<Item*>> Collection::searchItems(const StringStringMap& attributes)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
// searchItems should work, whether `this` is locked or not.
// however, we can't search items the same way as in gnome-keying,
// because there's no database at all when locked.
return QList<Item*>{};
}
// shortcut logic for Uuid/Path attributes, as they can uniquely identify an item.
if (attributes.contains(ItemAttributes::UuidKey)) {
auto uuid = QUuid::fromRfc4122(QByteArray::fromHex(attributes.value(ItemAttributes::UuidKey).toLatin1()));
auto entry = m_exposedGroup->findEntryByUuid(uuid);
if (entry) {
return QList<Item*>{m_entryToItem.value(entry)};
} else {
return QList<Item*>{};
}
}
if (attributes.contains(ItemAttributes::PathKey)) {
auto path = attributes.value(ItemAttributes::PathKey);
auto entry = m_exposedGroup->findEntryByPath(path);
if (entry) {
return QList<Item*>{m_entryToItem.value(entry)};
} else {
return QList<Item*>{};
}
}
static QMap<QString, QString> attrKeyToField{
{EntryAttributes::TitleKey, QStringLiteral("title")},
{EntryAttributes::UserNameKey, QStringLiteral("user")},
{EntryAttributes::URLKey, QStringLiteral("url")},
{EntryAttributes::NotesKey, QStringLiteral("notes")},
};
QStringList terms;
for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
if (it.key() == EntryAttributes::PasswordKey) {
continue;
}
auto field = attrKeyToField.value(it.key(), QStringLiteral("_") + Item::encodeAttributeKey(it.key()));
terms << QStringLiteral(R"raw(+%1:"%2")raw").arg(field, it.value());
}
QList<Item*> items;
const auto foundEntries = EntrySearcher().search(terms.join(' '), m_exposedGroup);
items.reserve(foundEntries.size());
for (const auto& entry : foundEntries) {
items << m_entryToItem.value(entry);
}
return items;
}
DBusReturn<Item*>
Collection::createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
prompt = nullptr;
Item* item = nullptr;
QString itemPath;
StringStringMap attributes;
// check existing item using attributes
auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes"));
if (iterAttr != properties.end()) {
attributes = qdbus_cast<StringStringMap>(iterAttr.value().value<QDBusArgument>());
itemPath = attributes.value(ItemAttributes::PathKey);
auto existings = searchItems(attributes);
if (existings.isError()) {
return existings;
}
if (!existings.value().isEmpty() && replace) {
item = existings.value().front();
}
}
if (!item) {
// normalize itemPath
itemPath = itemPath.startsWith('/') ? QString{} : QStringLiteral("/") + itemPath;
// split itemPath to groupPath and itemName
auto components = itemPath.split('/');
Q_ASSERT(components.size() >= 2);
auto itemName = components.takeLast();
Group* group = findCreateGroupByPath(components.join('/'));
// create new Entry in backend
auto* entry = new Entry();
entry->setUuid(QUuid::createUuid());
entry->setTitle(itemName);
entry->setUsername(m_backend->database()->metadata()->defaultUserName());
group->applyGroupIconTo(entry);
entry->setGroup(group);
// when creation finishes in backend, we will already have item
item = m_entryToItem.value(entry, nullptr);
Q_ASSERT(item);
}
ret = item->setProperties(properties);
if (ret.isError()) {
return ret;
}
ret = item->setSecret(secret);
if (ret.isError()) {
return ret;
}
return item;
}
DBusReturn<void> Collection::setProperties(const QVariantMap& properties)
{
auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION ".Label")).toString();
auto ret = setLabel(label);
if (ret.isError()) {
return ret;
}
return {};
}
const QSet<QString> Collection::aliases() const
{
return m_aliases;
}
DBusReturn<void> Collection::addAlias(QString alias)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
alias = encodePath(alias);
if (m_aliases.contains(alias)) {
return {};
}
emit aliasAboutToAdd(alias);
bool ok = QDBusConnection::sessionBus().registerObject(
QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias), this);
if (ok) {
m_aliases.insert(alias);
emit aliasAdded(alias);
}
return {};
}
DBusReturn<void> Collection::removeAlias(QString alias)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
alias = encodePath(alias);
if (!m_aliases.contains(alias)) {
return {};
}
QDBusConnection::sessionBus().unregisterObject(
QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias));
m_aliases.remove(alias);
emit aliasRemoved(alias);
return {};
}
QString Collection::name() const
{
return QFileInfo(m_backendPath).baseName();
}
DatabaseWidget* Collection::backend() const
{
return m_backend;
}
void Collection::onDatabaseLockChanged()
{
auto locked = backendLocked();
if (!locked) {
populateContents();
} else {
cleanupConnections();
}
emit collectionLockChanged(locked);
emit collectionChanged();
}
void Collection::populateContents()
{
if (!m_registered) {
return;
}
// we have an unlocked db
auto newUuid = FdoSecrets::settings()->exposedGroup(m_backend->database());
auto newGroup = m_backend->database()->rootGroup()->findGroupByUuid(newUuid);
if (!newGroup) {
// no exposed group, delete self
doDelete();
return;
}
// clean up old group connections
cleanupConnections();
m_exposedGroup = newGroup;
// Attach signal to update exposed group settings if the group was removed.
// The lifetime of the connection is bound to the database object, because
// in Database::~Database, groups are also deleted, but we don't want to
// trigger this.
// This rely on the fact that QObject disconnects signals BEFORE deleting
// children.
QPointer<Database> db = m_backend->database().data();
connect(m_exposedGroup.data(), &Group::groupAboutToRemove, db, [db](Group* toBeRemoved) {
if (!db) {
return;
}
auto uuid = FdoSecrets::settings()->exposedGroup(db);
auto exposedGroup = db->rootGroup()->findGroupByUuid(uuid);
if (toBeRemoved == exposedGroup) {
// reset the exposed group to none
FdoSecrets::settings()->setExposedGroup(db, {});
}
});
// Monitor exposed group settings
connect(m_backend->database()->metadata()->customData(), &CustomData::customDataModified, this, [this]() {
if (!m_exposedGroup || !m_backend) {
return;
}
if (m_exposedGroup->uuid() == FdoSecrets::settings()->exposedGroup(m_backend->database())) {
// no change
return;
}
onDatabaseExposedGroupChanged();
});
// Add items for existing entry
const auto entries = m_exposedGroup->entriesRecursive(false);
for (const auto& entry : entries) {
onEntryAdded(entry, false);
}
connectGroupSignalRecursive(m_exposedGroup);
}
void Collection::onDatabaseExposedGroupChanged()
{
// delete all items
// this has to be done because the backend is actually still there
// just we don't expose them
for (const auto& item : asConst(m_items)) {
item->doDelete();
}
// repopulate
Q_ASSERT(!backendLocked());
populateContents();
}
void Collection::onEntryAdded(Entry* entry, bool emitSignal)
{
if (inRecycleBin(entry)) {
return;
}
auto item = new Item(this, entry);
m_items << item;
m_entryToItem[entry] = item;
// forward delete signals
connect(entry->group(), &Group::entryAboutToRemove, item, [item](Entry* toBeRemoved) {
if (item->backend() == toBeRemoved) {
item->doDelete();
}
});
// relay signals
connect(item, &Item::itemChanged, this, [this, item]() { emit itemChanged(item); });
connect(item, &Item::itemAboutToDelete, this, [this, item]() {
m_items.removeAll(item);
m_entryToItem.remove(item->backend());
emit itemDeleted(item);
});
if (emitSignal) {
emit itemCreated(item);
}
}
void Collection::connectGroupSignalRecursive(Group* group)
{
if (inRecycleBin(group)) {
return;
}
connect(group, &Group::groupModified, this, &Collection::collectionChanged);
connect(group, &Group::entryAdded, this, [this](Entry* entry) { onEntryAdded(entry, true); });
const auto children = group->children();
for (const auto& cg : children) {
connectGroupSignalRecursive(cg);
}
}
Service* Collection::service() const
{
return qobject_cast<Service*>(parent());
}
void Collection::doLock()
{
Q_ASSERT(m_backend);
m_backend->lock();
}
void Collection::doUnlock()
{
Q_ASSERT(m_backend);
service()->doUnlockDatabaseInDialog(m_backend);
}
void Collection::doDelete()
{
emit collectionAboutToDelete();
unregisterCurrentPath();
// remove alias manually to trigger signal
for (const auto& a : aliases()) {
removeAlias(a).okOrDie();
}
cleanupConnections();
m_exposedGroup = nullptr;
// reset backend and delete self
m_backend = nullptr;
deleteLater();
}
void Collection::cleanupConnections()
{
if (m_exposedGroup) {
m_exposedGroup->disconnect(this);
}
m_items.clear();
}
QString Collection::backendFilePath() const
{
return m_backendPath;
}
Group* Collection::exposedRootGroup() const
{
return m_exposedGroup;
}
bool Collection::backendLocked() const
{
return !m_backend || !m_backend->database()->isInitialized() || m_backend->isLocked();
}
void Collection::doDeleteEntries(QList<Entry*> entries)
{
m_backend->deleteEntries(std::move(entries));
}
Group* Collection::findCreateGroupByPath(const QString& groupPath)
{
auto group = m_exposedGroup->findGroupByPath(groupPath);
if (group) {
return group;
}
// groupPath can't be empty here, because otherwise it will match m_exposedGroup and was returned above
Q_ASSERT(!groupPath.isEmpty());
auto groups = groupPath.split('/', QString::SkipEmptyParts);
auto groupName = groups.takeLast();
// create parent group
auto parentGroup = findCreateGroupByPath(groups.join('/'));
// create this group
group = new Group();
group->setUuid(QUuid::createUuid());
group->setName(groupName);
group->setIcon(Group::DefaultIconNumber);
group->setParent(parentGroup);
return group;
}
bool Collection::inRecycleBin(Group* group) const
{
Q_ASSERT(m_backend);
if (!m_backend->database()->metadata()->recycleBin()) {
return false;
}
while (group) {
if (group->uuid() == m_backend->database()->metadata()->recycleBin()->uuid()) {
return true;
}
group = group->parentGroup();
}
return false;
}
bool Collection::inRecycleBin(Entry* entry) const
{
Q_ASSERT(entry);
return inRecycleBin(entry->group());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,152 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_COLLECTION_H
#define KEEPASSXC_FDOSECRETS_COLLECTION_H
#include "DBusObject.h"
#include "adaptors/CollectionAdaptor.h"
#include <QPointer>
#include <QSet>
class Database;
class DatabaseWidget;
class Entry;
class Group;
namespace FdoSecrets
{
class Item;
class PromptBase;
class Service;
class Collection : public DBusObject
{
Q_OBJECT
public:
explicit Collection(Service* parent, DatabaseWidget* backend);
DBusReturn<const QList<Item*>> items() const;
DBusReturn<QString> label() const;
DBusReturn<void> setLabel(const QString& label);
DBusReturn<bool> locked() const;
DBusReturn<qulonglong> created() const;
DBusReturn<qulonglong> modified() const;
DBusReturn<PromptBase*> deleteCollection();
DBusReturn<const QList<Item*>> searchItems(const StringStringMap& attributes);
DBusReturn<Item*>
createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt);
signals:
void itemCreated(const Item* item);
void itemDeleted(const Item* item);
void itemChanged(const Item* item);
void collectionChanged();
void collectionAboutToDelete();
void collectionLockChanged(bool newLocked);
void aliasAboutToAdd(const QString& alias);
void aliasAdded(const QString& alias);
void aliasRemoved(const QString& alias);
public:
DBusReturn<void> setProperties(const QVariantMap& properties);
DBusReturn<void> removeAlias(QString alias);
DBusReturn<void> addAlias(QString alias);
const QSet<QString> aliases() const;
/**
* A human readable name of the collection, available even if the db is locked
* @return
*/
QString name() const;
Group* exposedRootGroup() const;
DatabaseWidget* backend() const;
QString backendFilePath() const;
Service* service() const;
bool inRecycleBin(Group* group) const;
bool inRecycleBin(Entry* entry) const;
public slots:
// expose some methods for Prmopt to use
void doLock();
void doUnlock();
// will remove self
void doDelete();
// delete the Entry in backend from this collection
void doDeleteEntries(QList<Entry*> entries);
private slots:
void onDatabaseLockChanged();
void onDatabaseExposedGroupChanged();
void reloadBackend();
private:
friend class DeleteCollectionPrompt;
friend class CreateCollectionPrompt;
void onEntryAdded(Entry* entry, bool emitSignal);
void populateContents();
void connectGroupSignalRecursive(Group* group);
void cleanupConnections();
bool backendLocked() const;
/**
* Check if the backend is a valid object, send error reply if not.
* @return true if the backend is valid.
*/
DBusReturn<void> ensureBackend() const;
/**
* Ensure the database is unlocked, send error reply if locked.
* @return true if the database is locked
*/
DBusReturn<void> ensureUnlocked() const;
/**
* Like mkdir -p, find or create the group by path, under m_exposedGroup
* @param groupPath
* @return created or found group
*/
Group* findCreateGroupByPath(const QString& groupPath);
private:
QPointer<DatabaseWidget> m_backend;
QString m_backendPath;
QPointer<Group> m_exposedGroup;
QSet<QString> m_aliases;
QList<Item*> m_items;
QMap<const Entry*, Item*> m_entryToItem;
bool m_registered;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_COLLECTION_H

View File

@ -0,0 +1,64 @@
/*
* 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 "DBusObject.h"
#include <QDBusAbstractAdaptor>
#include <QFile>
#include <QRegularExpression>
#include <QTextStream>
#include <QUrl>
#include <QUuid>
#include <utility>
namespace FdoSecrets
{
DBusObject::DBusObject(DBusObject* parent)
: QObject(parent)
{
}
void DBusObject::registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor)
{
m_objectPath.setPath(path);
m_dbusAdaptor = adaptor;
adaptor->setParent(this);
auto ok = QDBusConnection::sessionBus().registerObject(m_objectPath.path(), this);
Q_UNUSED(ok);
Q_ASSERT(ok);
}
QString DBusObject::callingPeerName() const
{
auto pid = callingPeerPid();
QFile proc(QStringLiteral("/proc/%1/comm").arg(pid));
if (!proc.open(QFile::ReadOnly)) {
return callingPeer();
}
QTextStream stream(&proc);
return stream.readAll().trimmed();
}
QString encodePath(const QString& value)
{
// force "-.~_" to be encoded
return QUrl::toPercentEncoding(value, "", "-.~_").replace('%', '_');
}
} // namespace FdoSecrets

View File

@ -0,0 +1,178 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_DBUSOBJECT_H
#define KEEPASSXC_FDOSECRETS_DBUSOBJECT_H
#include "fdosecrets/objects/DBusReturn.h"
#include "fdosecrets/objects/DBusTypes.h"
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusContext>
#include <QDBusObjectPath>
#include <QDebug>
#include <QList>
#include <QMetaProperty>
#include <QObject>
#include <QScopedPointer>
class QDBusAbstractAdaptor;
namespace FdoSecrets
{
class Service;
/**
* @brief A common base class for all dbus-exposed objects
*/
class DBusObject : public QObject, public QDBusContext
{
Q_OBJECT
public:
explicit DBusObject(DBusObject* parent = nullptr);
const QDBusObjectPath& objectPath() const
{
return m_objectPath;
}
protected:
void registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor);
void unregisterCurrentPath()
{
QDBusConnection::sessionBus().unregisterObject(m_objectPath.path());
m_dbusAdaptor = nullptr;
m_objectPath.setPath(QStringLiteral("/"));
}
QString callingPeer() const
{
Q_ASSERT(calledFromDBus());
return message().service();
}
uint callingPeerPid() const
{
return connection().interface()->servicePid(callingPeer());
}
QString callingPeerName() const;
template <typename Adaptor> Adaptor& dbusAdaptor() const
{
return *static_cast<Adaptor*>(m_dbusAdaptor);
}
DBusObject* p() const
{
return qobject_cast<DBusObject*>(parent());
}
private:
/**
* Derived class should not directly use sendErrorReply.
* Instead, use raiseError
*/
using QDBusContext::sendErrorReply;
template <typename U> friend class DBusReturn;
private:
QDBusAbstractAdaptor* m_dbusAdaptor;
QDBusObjectPath m_objectPath;
};
/**
* Return the object path of the pointed DBusObject, or "/" if the pointer is null
* @tparam T
* @param object
* @return
*/
template <typename T> QDBusObjectPath objectPathSafe(T* object)
{
if (object) {
return object->objectPath();
}
return QDBusObjectPath(QStringLiteral("/"));
}
/**
* Convert a list of DBusObjects to object path
* @tparam T
* @param objects
* @return
*/
template <typename T> QList<QDBusObjectPath> objectsToPath(QList<T*> objects)
{
QList<QDBusObjectPath> res;
res.reserve(objects.size());
for (auto object : objects) {
res.append(objectPathSafe(object));
}
return res;
}
/**
* Convert an object path to a pointer of the object
* @tparam T
* @param path
* @return the pointer of the object, or nullptr if path is "/"
*/
template <typename T> T* pathToObject(const QDBusObjectPath& path)
{
if (path.path() == QStringLiteral("/")) {
return nullptr;
}
return qobject_cast<T*>(QDBusConnection::sessionBus().objectRegisteredAt(path.path()));
}
/**
* Convert a list of object paths to a list of objects.
* "/" paths (i.e. nullptrs) will be skipped in the resulting list
* @tparam T
* @param paths
* @return
*/
template <typename T> QList<T*> pathsToObject(const QList<QDBusObjectPath>& paths)
{
QList<T*> res;
res.reserve(paths.size());
for (const auto& path : paths) {
auto object = pathToObject<T>(path);
if (object) {
res.append(object);
}
}
return res;
}
/**
* Encode the string value to a DBus object path safe representation,
* using a schema similar to URI encoding, but with percentage(%) replaced with
* underscore(_). All characters except [A-Za-z0-9] are encoded. For non-ascii
* characters, UTF-8 encoding is first applied and each of the resulting byte
* value is encoded.
* @param value
* @return encoded string
*/
QString encodePath(const QString& value);
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H

View File

@ -0,0 +1,18 @@
/*
* Copyright (C) 2019 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 "DBusReturn.h"

View File

@ -0,0 +1,246 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_DBUSRETURN_H
#define KEEPASSXC_FDOSECRETS_DBUSRETURN_H
#include <QDBusError>
#include <QDebug>
#include <QString>
#include <type_traits>
namespace FdoSecrets
{
namespace details
{
class DBusReturnImpl
{
public:
/**
* Check if this object contains an error
* @return true if it contains an error, false otherwise.
*/
bool isError() const
{
return !m_errorName.isEmpty();
}
/**
* Get the error name
* @return
*/
QString errorName() const
{
return m_errorName;
}
void okOrDie() const
{
Q_ASSERT(!isError());
}
protected:
struct WithErrorTag
{
};
/**
* Construct from an error
* @param errorName
* @param value
*/
DBusReturnImpl(QString errorName, WithErrorTag)
: m_errorName(std::move(errorName))
{
}
DBusReturnImpl() = default;
protected:
QString m_errorName;
};
} // namespace details
/**
* Either a return value or a DBus error
* @tparam T
*/
template <typename T = void> class DBusReturn : public details::DBusReturnImpl
{
protected:
using DBusReturnImpl::DBusReturnImpl;
public:
using value_type = T;
DBusReturn() = default;
/**
* Implicitly construct from a value
* @param value
*/
DBusReturn(T&& value) // NOLINT(google-explicit-constructor)
: m_value(std::move(value))
{
}
DBusReturn(const T& value) // NOLINT(google-explicit-constructor)
: m_value(std::move(value))
{
}
/**
* Implicitly convert from another error of different value type.
*
* @tparam U must not be the same as T
* @param other
*/
template <typename U, typename = typename std::enable_if<!std::is_same<T, U>::value>::type>
DBusReturn(const DBusReturn<U>& other) // NOLINT(google-explicit-constructor)
: DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{})
{
Q_ASSERT(other.isError());
}
/**
* Construct from error
* @param errorType
* @return a DBusReturn object containing the error
*/
static DBusReturn Error(QDBusError::ErrorType errorType)
{
return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}};
}
/**
* Overloaded version
* @param errorName
* @return a DBusReturnImpl object containing the error
*/
static DBusReturn Error(QString errorName)
{
return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}};
}
/**
* Get a reference to the enclosed value
* @return
*/
const T& value() const&
{
okOrDie();
return m_value;
}
/**
* Get a rvalue reference to the enclosed value if this object is rvalue
* @return a rvalue reference to the enclosed value
*/
T value() &&
{
okOrDie();
return std::move(m_value);
}
template <typename P> T valueOrHandle(P* p) const&
{
if (isError()) {
if (p->calledFromDBus()) {
p->sendErrorReply(errorName());
}
return {};
}
return m_value;
}
template <typename P> T&& valueOrHandle(P* p) &&
{
if (isError()) {
if (p->calledFromDBus()) {
p->sendErrorReply(errorName());
}
}
return std::move(m_value);
}
private:
T m_value{};
};
template <> class DBusReturn<void> : public details::DBusReturnImpl
{
protected:
using DBusReturnImpl::DBusReturnImpl;
public:
using value_type = void;
DBusReturn() = default;
/**
* Implicitly convert from another error of different value type.
*
* @tparam U must not be the same as T
* @param other
*/
template <typename U, typename = typename std::enable_if<!std::is_same<void, U>::value>::type>
DBusReturn(const DBusReturn<U>& other) // NOLINT(google-explicit-constructor)
: DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{})
{
Q_ASSERT(other.isError());
}
/**
* Construct from error
* @param errorType
* @return a DBusReturn object containing the error
*/
static DBusReturn Error(QDBusError::ErrorType errorType)
{
return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}};
}
/**
* Overloaded version
* @param errorName
* @return a DBusReturnImpl object containing the error
*/
static DBusReturn Error(QString errorName)
{
return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}};
}
/**
* If this is return contains an error, handle it if we were called from DBus
* @tparam P
* @param p
*/
template <typename P> void handle(P* p) const
{
if (isError()) {
if (p->calledFromDBus()) {
p->sendErrorReply(errorName());
}
}
}
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_DBUSRETURN_H

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
* Copyright 2010, Michael Leupold <lemma@confuego.org>
*
* 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 "DBusTypes.h"
#include <QDBusMetaType>
namespace FdoSecrets
{
void registerDBusTypes()
{
// register meta-types needed for this adaptor
qRegisterMetaType<SecretStruct>();
qDBusRegisterMetaType<SecretStruct>();
qRegisterMetaType<StringStringMap>();
qDBusRegisterMetaType<StringStringMap>();
qRegisterMetaType<ObjectPathSecretMap>();
qDBusRegisterMetaType<ObjectPathSecretMap>();
// NOTE: this is already registered by Qt in qtextratypes.h
// qRegisterMetaType<QList<QDBusObjectPath > >();
// qDBusRegisterMetaType<QList<QDBusObjectPath> >();
}
} // namespace FdoSecrets

View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
* Copyright 2010, Michael Leupold <lemma@confuego.org>
* Copyright 2010-2011, Valentin Rusu <valir@kde.org>
*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_DBUSTYPES_H
#define KEEPASSXC_FDOSECRETS_DBUSTYPES_H
#include <QDBusArgument>
#include <QDBusObjectPath>
#include <QMap>
#include <QString>
#define DBUS_SERVICE_SECRET "org.freedesktop.secrets"
#define DBUS_INTERFACE_SECRET_SERVICE "org.freedesktop.Secret.Service"
#define DBUS_INTERFACE_SECRET_SESSION "org.freedesktop.Secret.Session"
#define DBUS_INTERFACE_SECRET_COLLECTION "org.freedesktop.Secret.Collection"
#define DBUS_INTERFACE_SECRET_ITEM "org.freedesktop.Secret.Item"
#define DBUS_INTERFACE_SECRET_PROMPT "org.freedesktop.Secret.Prompt"
#define DBUS_ERROR_SECRET_NO_SESSION "org.freedesktop.Secret.Error.NoSession"
#define DBUS_ERROR_SECRET_NO_SUCH_OBJECT "org.freedesktop.Secret.Error.NoSuchObject"
#define DBUS_ERROR_SECRET_IS_LOCKED "org.freedesktop.Secret.Error.IsLocked"
#define DBUS_PATH_SECRETS "/org/freedesktop/secrets"
#define DBUS_PATH_TEMPLATE_ALIAS "%1/aliases/%2"
#define DBUS_PATH_TEMPLATE_SESSION "%1/session/%2"
#define DBUS_PATH_TEMPLATE_COLLECTION "%1/collection/%2"
#define DBUS_PATH_TEMPLATE_ITEM "%1/%2"
namespace FdoSecrets
{
/**
* This is the basic Secret structure exchanged via the dbus API
* See the spec for more details
*/
struct SecretStruct
{
QDBusObjectPath session{};
QByteArray parameters{};
QByteArray value{};
QString contentType{};
};
inline QDBusArgument& operator<<(QDBusArgument& argument, const SecretStruct& secret)
{
argument.beginStructure();
argument << secret.session << secret.parameters << secret.value << secret.contentType;
argument.endStructure();
return argument;
}
inline const QDBusArgument& operator>>(const QDBusArgument& argument, SecretStruct& secret)
{
argument.beginStructure();
argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType;
argument.endStructure();
return argument;
}
/**
* Register the types needed for the fd.o Secrets D-Bus interface.
*/
void registerDBusTypes();
} // namespace FdoSecrets
typedef QMap<QString, QString> StringStringMap;
typedef QMap<QDBusObjectPath, FdoSecrets::SecretStruct> ObjectPathSecretMap;
Q_DECLARE_METATYPE(FdoSecrets::SecretStruct)
Q_DECLARE_METATYPE(StringStringMap);
Q_DECLARE_METATYPE(ObjectPathSecretMap);
#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H

View File

@ -0,0 +1,420 @@
/*
* Copyright (C) 2019 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 "Item.h"
#include "fdosecrets/FdoSecretsPlugin.h"
#include "fdosecrets/objects/Collection.h"
#include "fdosecrets/objects/Prompt.h"
#include "fdosecrets/objects/Service.h"
#include "fdosecrets/objects/Session.h"
#include "core/Entry.h"
#include "core/EntryAttributes.h"
#include "core/Group.h"
#include "core/Tools.h"
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QSet>
#include <QTextCodec>
namespace FdoSecrets
{
const QSet<QString> Item::ReadOnlyAttributes(QSet<QString>() << ItemAttributes::UuidKey << ItemAttributes::PathKey);
static void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType);
static SecretStruct getEntrySecret(Entry* entry);
namespace
{
constexpr auto FDO_SECRETS_DATA = "FDO_SECRETS_DATA";
constexpr auto FDO_SECRETS_CONTENT_TYPE = "FDO_SECRETS_CONTENT_TYPE";
} // namespace
Item::Item(Collection* parent, Entry* backend)
: DBusObject(parent)
, m_backend(backend)
{
Q_ASSERT(!p()->objectPath().path().isEmpty());
registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_ITEM).arg(p()->objectPath().path(), m_backend->uuidToHex()),
new ItemAdaptor(this));
connect(m_backend.data(), &Entry::entryModified, this, &Item::itemChanged);
}
DBusReturn<bool> Item::locked() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
return collection()->locked();
}
DBusReturn<const StringStringMap> Item::attributes() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
StringStringMap attrs;
// add default attributes except password
auto entryAttrs = m_backend->attributes();
for (const auto& attr : EntryAttributes::DefaultAttributes) {
if (entryAttrs->isProtected(attr) || attr == EntryAttributes::PasswordKey) {
continue;
}
auto value = entryAttrs->value(attr);
if (entryAttrs->isReference(attr)) {
value = m_backend->maskPasswordPlaceholders(value);
value = m_backend->resolveMultiplePlaceholders(value);
}
attrs[attr] = value;
}
// add custom attributes
const auto customKeys = entryAttrs->customKeys();
for (const auto& attr : customKeys) {
// decode attr key
auto decoded = decodeAttributeKey(attr);
attrs[decoded] = entryAttrs->value(attr);
}
// add some informative and readonly attributes
attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex();
attrs[ItemAttributes::PathKey] = path();
return attrs;
}
DBusReturn<void> Item::setAttributes(const StringStringMap& attrs)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
m_backend->beginUpdate();
auto entryAttrs = m_backend->attributes();
for (auto it = attrs.constBegin(); it != attrs.constEnd(); ++it) {
if (entryAttrs->isProtected(it.key()) || it.key() == EntryAttributes::PasswordKey) {
continue;
}
if (ReadOnlyAttributes.contains(it.key())) {
continue;
}
auto encoded = encodeAttributeKey(it.key());
entryAttrs->set(encoded, it.value());
}
m_backend->endUpdate();
return {};
}
DBusReturn<QString> Item::label() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
return m_backend->title();
}
DBusReturn<void> Item::setLabel(const QString& label)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
m_backend->beginUpdate();
m_backend->setTitle(label);
m_backend->endUpdate();
return {};
}
DBusReturn<qulonglong> Item::created() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
return static_cast<qulonglong>(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000);
}
DBusReturn<qulonglong> Item::modified() const
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
return static_cast<qulonglong>(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000);
}
DBusReturn<PromptBase*> Item::deleteItem()
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
auto prompt = new DeleteItemPrompt(service(), this);
return prompt;
}
DBusReturn<SecretStruct> Item::getSecret(Session* session)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
if (!session) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION));
}
auto secret = getEntrySecret(m_backend);
// encode using session
secret = session->encode(secret);
// show notification is this was directly called from DBus
if (calledFromDBus()) {
service()->plugin()->emitRequestShowNotification(
tr(R"(Entry "%1" from database "%2" was used by %3)")
.arg(m_backend->title(), collection()->name(), callingPeerName()));
}
return secret;
}
DBusReturn<void> Item::setSecret(const SecretStruct& secret)
{
auto ret = ensureBackend();
if (ret.isError()) {
return ret;
}
ret = ensureUnlocked();
if (ret.isError()) {
return ret;
}
auto session = pathToObject<Session>(secret.session);
if (!session) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION));
}
// decode using session
auto decoded = session->decode(secret);
// set in backend
m_backend->beginUpdate();
setEntrySecret(m_backend, decoded.value, decoded.contentType);
m_backend->endUpdate();
return {};
}
DBusReturn<void> Item::setProperties(const QVariantMap& properties)
{
auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Label")).toString();
auto ret = setLabel(label);
if (ret.isError()) {
return ret;
}
auto attributes = qdbus_cast<StringStringMap>(
properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value<QDBusArgument>());
ret = setAttributes(attributes);
if (ret.isError()) {
return ret;
}
return {};
}
Collection* Item::collection() const
{
return qobject_cast<Collection*>(p());
}
DBusReturn<void> Item::ensureBackend() const
{
if (!m_backend) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
}
return {};
}
DBusReturn<void> Item::ensureUnlocked() const
{
auto locked = collection()->locked();
if (locked.isError()) {
return locked;
}
if (locked.value()) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED));
}
return {};
}
Entry* Item::backend() const
{
return m_backend;
}
void Item::doDelete()
{
emit itemAboutToDelete();
// Unregister current path early, do not rely on deleteLater's call to destructor
// as in case of Entry moving between groups, new Item will be created at the same DBus path
// before the current Item is deleted in the event loop.
unregisterCurrentPath();
m_backend = nullptr;
deleteLater();
}
Service* Item::service() const
{
return collection()->service();
}
QString Item::path() const
{
QStringList pathComponents{m_backend->title()};
Group* group = m_backend->group();
while (group && group != collection()->exposedRootGroup()) {
pathComponents.prepend(group->name());
group = group->parentGroup();
}
// we should always reach the exposed root group
Q_ASSERT(group);
// root group is represented by a single slash, thus adding an empty component.
pathComponents.prepend(QLatin1Literal(""));
return pathComponents.join('/');
}
QString Item::encodeAttributeKey(const QString &key)
{
return QUrl::toPercentEncoding(key, "", "_:").replace('%', '_');
}
QString Item::decodeAttributeKey(const QString &key)
{
return QString::fromUtf8(QByteArray::fromPercentEncoding(key.toLatin1(), '_'));
}
void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType)
{
auto mimeName = contentType.split(';').takeFirst().trimmed();
// find the mime type
QMimeDatabase db;
auto mimeType = db.mimeTypeForName(mimeName);
// find a suitable codec
QTextCodec* codec = nullptr;
static const QRegularExpression charsetPattern(QStringLiteral(R"re(charset=(?<encode>.+)$)re"));
auto match = charsetPattern.match(contentType);
if (match.hasMatch()) {
codec = QTextCodec::codecForName(match.captured(QStringLiteral("encode")).toLatin1());
} else {
codec = QTextCodec::codecForName(QByteArrayLiteral("utf-8"));
}
if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) {
// we can't handle this content type, save the data as attachment
entry->attachments()->set(FDO_SECRETS_DATA, data);
entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType);
return;
}
// save the data to password field
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
entry->attachments()->remove(FDO_SECRETS_DATA);
}
if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
entry->attributes()->remove(FDO_SECRETS_CONTENT_TYPE);
}
Q_ASSERT(codec);
entry->setPassword(codec->toUnicode(data));
}
SecretStruct getEntrySecret(Entry* entry)
{
SecretStruct ss;
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
ss.value = entry->attachments()->value(FDO_SECRETS_DATA);
Q_ASSERT(entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE));
ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE);
return ss;
}
ss.value = entry->resolveMultiplePlaceholders(entry->password()).toUtf8();
ss.contentType = QStringLiteral("text/plain");
return ss;
}
} // namespace FdoSecrets

View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_ITEM_H
#define KEEPASSXC_FDOSECRETS_ITEM_H
#include "fdosecrets/objects/DBusObject.h"
#include "fdosecrets/objects/adaptors/ItemAdaptor.h"
#include <QPointer>
class Entry;
namespace FdoSecrets
{
namespace ItemAttributes
{
constexpr const auto UuidKey = "Uuid";
constexpr const auto PathKey = "Path";
} // namespace ItemAttributes
class Session;
class Collection;
class PromptBase;
class Item : public DBusObject
{
Q_OBJECT
public:
explicit Item(Collection* parent, Entry* backend);
DBusReturn<bool> locked() const;
DBusReturn<const StringStringMap> attributes() const;
DBusReturn<void> setAttributes(const StringStringMap& attrs);
DBusReturn<QString> label() const;
DBusReturn<void> setLabel(const QString& label);
DBusReturn<qulonglong> created() const;
DBusReturn<qulonglong> modified() const;
DBusReturn<PromptBase*> deleteItem();
DBusReturn<SecretStruct> getSecret(Session* session);
DBusReturn<void> setSecret(const SecretStruct& secret);
signals:
void itemChanged();
void itemAboutToDelete();
public:
static const QSet<QString> ReadOnlyAttributes;
/**
* Due to the limitation in EntrySearcher, custom attr key cannot contain ':',
* Thus we encode the key when saving and decode it when returning.
* @param key
* @return
*/
static QString encodeAttributeKey(const QString& key);
static QString decodeAttributeKey(const QString& key);
DBusReturn<void> setProperties(const QVariantMap& properties);
Entry* backend() const;
Collection* collection() const;
Service* service() const;
/**
* Compute the entry path relative to the exposed group
* @return the entry path
*/
QString path() const;
public slots:
void doDelete();
private:
/**
* Check if the backend is a valid object, send error reply if not.
* @return No error if the backend is valid.
*/
DBusReturn<void> ensureBackend() const;
/**
* Ensure the database is unlocked, send error reply if locked.
* @return true if the database is locked
*/
DBusReturn<void> ensureUnlocked() const;
private:
QPointer<Entry> m_backend;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_ITEM_H

View File

@ -0,0 +1,238 @@
/*
* Copyright (C) 2019 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 "Prompt.h"
#include "fdosecrets/FdoSecretsSettings.h"
#include "fdosecrets/objects/Collection.h"
#include "fdosecrets/objects/Item.h"
#include "fdosecrets/objects/Service.h"
#include "core/Tools.h"
#include "gui/DatabaseWidget.h"
#include "gui/MessageBox.h"
#include <QThread>
#include <QWindow>
namespace FdoSecrets
{
PromptBase::PromptBase(Service* parent)
: DBusObject(parent)
{
registerWithPath(
QStringLiteral("%1/prompt/%2").arg(p()->objectPath().path(), Tools::uuidToHex(QUuid::createUuid())),
new PromptAdaptor(this));
connect(this, &PromptBase::completed, this, &PromptBase::deleteLater);
}
QWindow* PromptBase::findWindow(const QString& windowId)
{
// find parent window, or nullptr if not found
bool ok = false;
WId wid = windowId.toULongLong(&ok, 0);
QWindow* parent = nullptr;
if (ok) {
parent = QWindow::fromWinId(wid);
}
if (parent) {
// parent is not the child of any object, so make sure it gets deleted at some point
QObject::connect(this, &QObject::destroyed, parent, &QObject::deleteLater);
}
return parent;
}
Service* PromptBase::service() const
{
return qobject_cast<Service*>(parent());
}
DBusReturn<void> PromptBase::dismiss()
{
emit completed(true, {});
return {};
}
DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll)
: PromptBase(parent)
, m_collection(coll)
{
}
DBusReturn<void> DeleteCollectionPrompt::prompt(const QString& windowId)
{
if (thread() != QThread::currentThread()) {
DBusReturn<void> ret;
QMetaObject::invokeMethod(this,
"prompt",
Qt::BlockingQueuedConnection,
Q_ARG(QString, windowId),
Q_RETURN_ARG(DBusReturn<void>, ret));
return ret;
}
if (!m_collection) {
return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
}
MessageBox::OverrideParent override(findWindow(windowId));
// only need to delete in backend, collection will react itself.
service()->doCloseDatabase(m_collection->backend());
emit completed(false, {});
return {};
}
CreateCollectionPrompt::CreateCollectionPrompt(Service* parent)
: PromptBase(parent)
{
}
DBusReturn<void> CreateCollectionPrompt::prompt(const QString& windowId)
{
if (thread() != QThread::currentThread()) {
DBusReturn<void> ret;
QMetaObject::invokeMethod(this,
"prompt",
Qt::BlockingQueuedConnection,
Q_ARG(QString, windowId),
Q_RETURN_ARG(DBusReturn<void>, ret));
return ret;
}
MessageBox::OverrideParent override(findWindow(windowId));
auto coll = service()->doNewDatabase();
if (!coll) {
return dismiss();
}
emit collectionCreated(coll);
emit completed(false, coll->objectPath().path());
return {};
}
LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList<Collection*>& colls)
: PromptBase(parent)
{
m_collections.reserve(colls.size());
for (const auto& c : asConst(colls)) {
m_collections << c;
}
}
DBusReturn<void> LockCollectionsPrompt::prompt(const QString& windowId)
{
if (thread() != QThread::currentThread()) {
DBusReturn<void> ret;
QMetaObject::invokeMethod(this,
"prompt",
Qt::BlockingQueuedConnection,
Q_ARG(QString, windowId),
Q_RETURN_ARG(DBusReturn<void>, ret));
return ret;
}
MessageBox::OverrideParent override(findWindow(windowId));
QList<QDBusObjectPath> locked;
for (const auto& c : asConst(m_collections)) {
if (c) {
c->doLock();
locked << c->objectPath();
}
}
emit completed(false, QVariant::fromValue(locked));
return {};
}
UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList<Collection*>& colls)
: PromptBase(parent)
{
m_collections.reserve(colls.size());
for (const auto& c : asConst(colls)) {
m_collections << c;
}
}
DBusReturn<void> UnlockCollectionsPrompt::prompt(const QString& windowId)
{
if (thread() != QThread::currentThread()) {
DBusReturn<void> ret;
QMetaObject::invokeMethod(this,
"prompt",
Qt::BlockingQueuedConnection,
Q_ARG(QString, windowId),
Q_RETURN_ARG(DBusReturn<void>, ret));
return ret;
}
MessageBox::OverrideParent override(findWindow(windowId));
QList<QDBusObjectPath> unlocked;
for (const auto& c : asConst(m_collections)) {
if (c) {
c->doUnlock();
unlocked << c->objectPath();
}
}
emit completed(false, QVariant::fromValue(unlocked));
return {};
}
DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item)
: PromptBase(parent)
, m_item(item)
{
}
DBusReturn<void> DeleteItemPrompt::prompt(const QString& windowId)
{
if (thread() != QThread::currentThread()) {
DBusReturn<void> ret;
QMetaObject::invokeMethod(this,
"prompt",
Qt::BlockingQueuedConnection,
Q_ARG(QString, windowId),
Q_RETURN_ARG(DBusReturn<void>, ret));
return ret;
}
MessageBox::OverrideParent override(findWindow(windowId));
// delete item's backend. Item will be notified after the backend is deleted.
if (m_item) {
if (FdoSecrets::settings()->noConfirmDeleteItem()) {
MessageBox::setNextAnswer(MessageBox::Move);
}
m_item->collection()->doDeleteEntries({m_item->backend()});
}
emit completed(false, {});
return {};
}
} // namespace FdoSecrets

View File

@ -0,0 +1,121 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_PROMPT_H
#define KEEPASSXC_FDOSECRETS_PROMPT_H
#include "fdosecrets/objects/DBusObject.h"
#include "fdosecrets/objects/adaptors/PromptAdaptor.h"
#include <QPointer>
class QWindow;
class DatabaseWidget;
namespace FdoSecrets
{
class Service;
class PromptBase : public DBusObject
{
Q_OBJECT
public:
explicit PromptBase(Service* parent);
virtual DBusReturn<void> prompt(const QString& windowId) = 0;
virtual DBusReturn<void> dismiss();
signals:
void completed(bool dismissed, const QVariant& result);
protected:
QWindow* findWindow(const QString& windowId);
Service* service() const;
};
class Collection;
class DeleteCollectionPrompt : public PromptBase
{
Q_OBJECT
public:
explicit DeleteCollectionPrompt(Service* parent, Collection* coll);
DBusReturn<void> prompt(const QString& windowId) override;
private:
QPointer<Collection> m_collection;
};
class CreateCollectionPrompt : public PromptBase
{
Q_OBJECT
public:
explicit CreateCollectionPrompt(Service* parent);
DBusReturn<void> prompt(const QString& windowId) override;
signals:
void collectionCreated(Collection* coll);
};
class LockCollectionsPrompt : public PromptBase
{
Q_OBJECT
public:
explicit LockCollectionsPrompt(Service* parent, const QList<Collection*>& colls);
DBusReturn<void> prompt(const QString& windowId) override;
private:
QList<QPointer<Collection>> m_collections;
};
class UnlockCollectionsPrompt : public PromptBase
{
Q_OBJECT
public:
explicit UnlockCollectionsPrompt(Service* parent, const QList<Collection*>& coll);
DBusReturn<void> prompt(const QString& windowId) override;
private:
QList<QPointer<Collection>> m_collections;
};
class Item;
class DeleteItemPrompt : public PromptBase
{
Q_OBJECT
public:
explicit DeleteItemPrompt(Service* parent, Item* item);
DBusReturn<void> prompt(const QString& windowId) override;
private:
QPointer<Item> m_item;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_PROMPT_H

View File

@ -0,0 +1,477 @@
/*
* 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)
{
registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this));
}
Service::~Service()
{
QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET));
}
bool Service::initialize()
{
if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) {
qDebug() << "Another secret service is running";
emit error(tr("Failed to register DBus service at %1: another secret service is running.")
.arg(QLatin1Literal(DBUS_SERVICE_SECRET)));
return false;
}
// 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)
{
auto coll = new Collection(this, dbWidget);
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);
});
// a special case: the database changed from no expose to expose something.
// in this case, there is no collection out there monitoring it, so create a new collection
if (!dbWidget->isLocked()) {
monitorDatabaseExposedGroup(dbWidget);
}
connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() {
monitorDatabaseExposedGroup(dbWidget);
});
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;
}
}
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;
}
}
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;
}
void Service::doCloseDatabase(DatabaseWidget* dbWidget)
{
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::doSwitchToChangeDatabaseSettings(DatabaseWidget* dbWidget)
{
// switch selected to current
// unlock if needed
if (dbWidget->isLocked()) {
m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None);
}
m_databases->setCurrentWidget(dbWidget);
m_databases->changeDatabaseSettings();
// 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

View File

@ -0,0 +1,154 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_SERVICE_H
#define KEEPASSXC_FDOSECRETS_SERVICE_H
#include "fdosecrets/objects/DBusObject.h"
#include "fdosecrets/objects/adaptors/ServiceAdaptor.h"
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QScopedPointer>
#include <QVariant>
class QDBusServiceWatcher;
class DatabaseTabWidget;
class DatabaseWidget;
class Group;
class FdoSecretsPlugin;
namespace FdoSecrets
{
class Collection;
class Item;
class PromptBase;
class ServiceAdaptor;
class Session;
class Service : public DBusObject // clazy: exclude=ctor-missing-parent-argument
{
Q_OBJECT
public:
explicit Service(FdoSecretsPlugin* plugin, QPointer<DatabaseTabWidget> dbTabs);
~Service() override;
bool initialize();
DBusReturn<QVariant> openSession(const QString& algorithm, const QVariant& input, Session*& result);
DBusReturn<Collection*>
createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt);
DBusReturn<const QList<Item*>> searchItems(const StringStringMap& attributes, QList<Item*>& locked);
DBusReturn<const QList<DBusObject*>> unlock(const QList<DBusObject*>& objects, PromptBase*& prompt);
DBusReturn<const QList<DBusObject*>> lock(const QList<DBusObject*>& objects, PromptBase*& prompt);
DBusReturn<const QHash<Item*, SecretStruct>> getSecrets(const QList<Item*>& items, Session* session);
DBusReturn<Collection*> readAlias(const QString& name);
DBusReturn<void> setAlias(const QString& name, Collection* collection);
/**
* List of collections
* @return
*/
DBusReturn<const QList<Collection*>> collections() const;
signals:
void collectionCreated(Collection* collection);
void collectionDeleted(Collection* collection);
void collectionChanged(Collection* collection);
void sessionOpened(Session* sess);
void sessionClosed(Session* sess);
/**
* Report error message to the GUI
* @param msg
*/
void error(const QString& msg);
public:
/**
* List of sessions
* @return
*/
const QList<Session*> sessions() const;
FdoSecretsPlugin* plugin() const
{
return m_plugin;
}
public slots:
void doCloseDatabase(DatabaseWidget* dbWidget);
Collection* doNewDatabase();
void doSwitchToChangeDatabaseSettings(DatabaseWidget* dbWidget);
void doUnlockDatabaseInDialog(DatabaseWidget* dbWidget);
private slots:
void dbusServiceUnregistered(const QString& service);
void ensureDefaultAlias();
void onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal);
void monitorDatabaseExposedGroup(DatabaseWidget* dbWidget);
void onCollectionAliasAboutToAdd(const QString& alias);
void onCollectionAliasAdded(const QString& alias);
void onCollectionAliasRemoved(const QString& alias);
private:
/**
* Find collection by alias name
* @param alias
* @return the collection under alias
*/
Collection* findCollection(const QString& alias) const;
/**
* Find collection by dbWidget
* @param db
* @return the collection corresponding to the db
*/
Collection* findCollection(const DatabaseWidget* db) const;
private:
FdoSecretsPlugin* m_plugin;
QPointer<DatabaseTabWidget> m_databases;
QHash<QString, Collection*> m_aliases;
QList<Collection*> m_collections;
QHash<const DatabaseWidget*, Collection*> m_dbToCollection;
QList<Session*> m_sessions;
QHash<QString, Session*> m_peerToSession;
bool m_insdieEnsureDefaultAlias;
QScopedPointer<QDBusServiceWatcher> m_serviceWatcher;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_SERVICE_H

View File

@ -0,0 +1,105 @@
/*
* 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 "Session.h"
#include "fdosecrets/objects/SessionCipher.h"
#include "core/Tools.h"
namespace FdoSecrets
{
QHash<QString, QVariant> Session::negoniationState;
Session::Session(std::unique_ptr<CipherPair>&& cipher, const QString& peer, Service* parent)
: DBusObject(parent)
, m_cipher(std::move(cipher))
, m_peer(peer)
, m_id(QUuid::createUuid())
{
registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_SESSION).arg(p()->objectPath().path(), id()),
new SessionAdaptor(this));
}
void Session::CleanupNegotiation(const QString& peer)
{
negoniationState.remove(peer);
}
DBusReturn<void> Session::close()
{
emit aboutToClose();
deleteLater();
return {};
}
QString Session::peer() const
{
return m_peer;
}
QString Session::id() const
{
return Tools::uuidToHex(m_id);
}
std::unique_ptr<CipherPair> Session::CreateCiphers(const QString& peer,
const QString& algorithm,
const QVariant& input,
QVariant& output,
bool& incomplete)
{
Q_UNUSED(peer);
incomplete = false;
std::unique_ptr<CipherPair> cipher{};
if (algorithm == QLatin1Literal("plain")) {
cipher.reset(new PlainCipher);
} else if (algorithm == QLatin1Literal("dh-ietf1024-sha256-aes128-cbc-pkcs7")) {
QByteArray clientPublicKey = input.toByteArray();
cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey));
} else {
// error notSupported
}
if (!cipher) {
return {};
}
if (!cipher->isValid()) {
qWarning() << "FdoSecrets: Error creating cipher";
return {};
}
output = cipher->negotiationOutput();
return cipher;
}
SecretStruct Session::encode(const SecretStruct& input) const
{
auto output = m_cipher->encrypt(input);
output.session = objectPath();
return output;
}
SecretStruct Session::decode(const SecretStruct& input) const
{
return m_cipher->decrypt(input);
}
} // namespace FdoSecrets

View File

@ -0,0 +1,91 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_SESSION_H
#define KEEPASSXC_FDOSECRETS_SESSION_H
#include "fdosecrets/objects/DBusObject.h"
#include "fdosecrets/objects/Service.h"
#include "fdosecrets/objects/SessionCipher.h"
#include "fdosecrets/objects/adaptors/SessionAdaptor.h"
#include <QByteArray>
#include <QHash>
#include <QUuid>
#include <QVariant>
#include <memory>
namespace FdoSecrets
{
class CipherPair;
class Session : public DBusObject
{
Q_OBJECT
public:
static std::unique_ptr<CipherPair> CreateCiphers(const QString& peer,
const QString& algorithm,
const QVariant& intpu,
QVariant& output,
bool& incomplete);
static void CleanupNegotiation(const QString& peer);
explicit Session(std::unique_ptr<CipherPair>&& cipher, const QString& peer, Service* parent);
DBusReturn<void> close();
/**
* Encode the secret struct. Note only the value field is encoded.
* @param input
* @return
*/
SecretStruct encode(const SecretStruct& input) const;
/**
* Decode the secret struct.
* @param input
* @return
*/
SecretStruct decode(const SecretStruct& input) const;
/**
* The peer application that opened this session
* @return
*/
QString peer() const;
QString id() const;
signals:
/**
* The session is going to be closed
* @param sess
*/
void aboutToClose();
private:
std::unique_ptr<CipherPair> m_cipher;
QString m_peer;
QUuid m_id;
static QHash<QString, QVariant> negoniationState;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_SESSION_H

View File

@ -0,0 +1,224 @@
/*
* Copyright (C) 2019 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 "SessionCipher.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include <gcrypt.h>
#include <memory>
namespace
{
constexpr const auto IETF1024_SECOND_OAKLEY_GROUP_P_HEX = "FFFFFFFFFFFFFFFFC90FDAA22168C234"
"C4C6628B80DC1CD129024E088A67CC74"
"020BBEA63B139B22514A08798E3404DD"
"EF9519B3CD3A431B302B0A6DF25F1437"
"4FE1356D6D51C245E485B576625E7EC6"
"F44C42E9A637ED6B0BFF5CB6F406B7ED"
"EE386BFB5A899FA5AE9F24117C4B1FE6"
"49286651ECE65381FFFFFFFFFFFFFFFF";
constexpr const size_t KEY_SIZE_BYTES = 128;
constexpr const int AES_KEY_LEN = 16; // 128 bits
const auto IETF1024_SECOND_OAKLEY_GROUP_P = MpiFromHex(IETF1024_SECOND_OAKLEY_GROUP_P_HEX, false);
} // namespace
namespace FdoSecrets
{
QVariant CipherPair::negotiationOutput() const
{
return {};
}
DhIetf1024Sha256Aes128CbcPkcs7::DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes)
: m_valid(false)
{
// read client public key
auto clientPub = MpiFromBytes(clientPublicKeyBytes, false);
// generate server side private, 128 bytes
GcryptMPI serverPrivate(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
gcry_mpi_randomize(serverPrivate.get(), KEY_SIZE_BYTES * 8, GCRY_STRONG_RANDOM);
// generate server side public key
GcryptMPI serverPublic(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
// the generator of Second Oakley Group is 2
gcry_mpi_powm(serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
initialize(std::move(clientPub), std::move(serverPublic), std::move(serverPrivate));
}
bool
DhIetf1024Sha256Aes128CbcPkcs7::initialize(GcryptMPI clientPublic, GcryptMPI serverPublic, GcryptMPI serverPrivate)
{
QByteArray commonSecretBytes;
if (!diffieHullman(clientPublic, serverPrivate, commonSecretBytes)) {
return false;
}
m_privateKey = MpiToBytes(serverPrivate);
m_publicKey = MpiToBytes(serverPublic);
m_aesKey = hkdf(commonSecretBytes);
m_valid = true;
return true;
}
bool DhIetf1024Sha256Aes128CbcPkcs7::diffieHullman(const GcryptMPI& clientPub,
const GcryptMPI& serverPrivate,
QByteArray& commonSecretBytes)
{
if (!clientPub || !serverPrivate) {
return false;
}
// calc common secret
GcryptMPI commonSecret(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
gcry_mpi_powm(commonSecret.get(), clientPub.get(), serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
commonSecretBytes = MpiToBytes(commonSecret);
return true;
}
QByteArray DhIetf1024Sha256Aes128CbcPkcs7::hkdf(const QByteArray& IKM)
{
// HKDF-Extract(salt, IKM) -> PRK
// PRK = HMAC-Hash(salt, IKM)
// we use NULL salt as per spec
auto PRK = CryptoHash::hmac(IKM,
QByteArrayLiteral("\0\0\0\0\0\0\0\0"
"\0\0\0\0\0\0\0\0"
"\0\0\0\0\0\0\0\0"
"\0\0\0\0\0\0\0\0"),
CryptoHash::Sha256);
// HKDF-Expand(PRK, info, L) -> OKM
// N = ceil(L/HashLen)
// T = T(1) | T(2) | T(3) | ... | T(N)
// OKM = first L octets of T
// where:
// T(0) = empty string (zero length)
// T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
// T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
// T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
// ...
//
// (where the constant concatenated to the end of each T(n) is a
// single octet.)
// we use empty info as per spec
// HashLen = 32 (sha256)
// L = 16 (16 * 8 = 128 bits)
// N = ceil(16/32) = 1
auto T1 = CryptoHash::hmac(QByteArrayLiteral("\x01"), PRK, CryptoHash::Sha256);
// resulting AES key is first 128 bits
Q_ASSERT(T1.size() >= AES_KEY_LEN);
auto OKM = T1.left(AES_KEY_LEN);
return OKM;
}
SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const SecretStruct& input)
{
SecretStruct output = input;
output.value.clear();
output.parameters.clear();
SymmetricCipher encrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Encrypt);
auto IV = randomGen()->randomArray(SymmetricCipher::algorithmIvSize(SymmetricCipher::Aes128));
if (!encrypter.init(m_aesKey, IV)) {
qWarning() << "Error encrypt: " << encrypter.errorString();
return output;
}
output.parameters = IV;
bool ok;
output.value = input.value;
output.value = encrypter.process(padPkcs7(output.value, encrypter.blockSize()), &ok);
if (!ok) {
qWarning() << "Error encrypt: " << encrypter.errorString();
return output;
}
return output;
}
QByteArray& DhIetf1024Sha256Aes128CbcPkcs7::padPkcs7(QByteArray& input, int blockSize)
{
// blockSize must be a power of 2.
Q_ASSERT_X(blockSize > 0 && !(blockSize & (blockSize - 1)), "padPkcs7", "blockSize must be a power of 2");
int padLen = blockSize - (input.size() & (blockSize - 1));
input.append(QByteArray(padLen, static_cast<char>(padLen)));
return input;
}
SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const SecretStruct& input)
{
auto IV = input.parameters;
SymmetricCipher decrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
if (!decrypter.init(m_aesKey, IV)) {
qWarning() << "Error decoding: " << decrypter.errorString();
return input;
}
bool ok;
SecretStruct output = input;
output.parameters.clear();
output.value = decrypter.process(input.value, &ok);
if (!ok) {
qWarning() << "Error decoding: " << decrypter.errorString();
return input;
}
unpadPkcs7(output.value);
return output;
}
QByteArray& DhIetf1024Sha256Aes128CbcPkcs7::unpadPkcs7(QByteArray& input)
{
if (input.isEmpty()) {
return input;
}
int padLen = input[input.size() - 1];
input.chop(padLen);
return input;
}
bool DhIetf1024Sha256Aes128CbcPkcs7::isValid() const
{
return m_valid;
}
QVariant DhIetf1024Sha256Aes128CbcPkcs7::negotiationOutput() const
{
return m_publicKey;
}
} // namespace FdoSecrets

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H
#define KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H
#include "fdosecrets/GcryptMPI.h"
#include "fdosecrets/objects/Session.h"
class TestFdoSecrets;
namespace FdoSecrets
{
class CipherPair
{
Q_DISABLE_COPY(CipherPair)
public:
CipherPair() = default;
virtual ~CipherPair() = default;
virtual SecretStruct encrypt(const SecretStruct& input) = 0;
virtual SecretStruct decrypt(const SecretStruct& input) = 0;
virtual bool isValid() const = 0;
virtual QVariant negotiationOutput() const;
};
class PlainCipher : public CipherPair
{
Q_DISABLE_COPY(PlainCipher)
public:
PlainCipher() = default;
SecretStruct encrypt(const SecretStruct& input) override
{
return input;
}
SecretStruct decrypt(const SecretStruct& input) override
{
return input;
}
bool isValid() const override
{
return true;
}
};
class DhIetf1024Sha256Aes128CbcPkcs7 : public CipherPair
{
bool m_valid;
QByteArray m_privateKey;
QByteArray m_publicKey;
QByteArray m_aesKey;
/**
* Diffie Hullman Key Exchange
* Given client public key, generate server private/public key pair and common secret.
* This also sets m_publicKey to server's public key
* @param clientPublicKey client public key
* @param serverPrivate server private key
* @param commonSecretBytes output common secret
* @return true on success.
*/
bool
diffieHullman(const GcryptMPI& clientPublicKey, const GcryptMPI& serverPrivate, QByteArray& commonSecretBytes);
/**
* Perform HKDF defined in RFC5869, using sha256 as hash function
* @param IKM input keying material
* @return derived 128-bit key suitable for AES
*/
QByteArray hkdf(const QByteArray& IKM);
/**
* Add PKCS#7 style padding to input inplace
* @param input
* @param blockSize the block size to use, must be 2's power
* @return reference to input for chaining
*/
QByteArray& padPkcs7(QByteArray& input, int blockSize);
/**
* Remove PKCS#7 style padding from input inplace
* @param input
* @return reference to input for chaining
*/
QByteArray& unpadPkcs7(QByteArray& input);
bool initialize(GcryptMPI clientPublic, GcryptMPI serverPublic, GcryptMPI serverPrivate);
DhIetf1024Sha256Aes128CbcPkcs7()
: m_valid(false)
{
}
public:
explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes);
SecretStruct encrypt(const SecretStruct& input) override;
SecretStruct decrypt(const SecretStruct& input) override;
bool isValid() const override;
QVariant negotiationOutput() const override;
private:
Q_DISABLE_COPY(DhIetf1024Sha256Aes128CbcPkcs7);
friend class ::TestFdoSecrets;
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H

View File

@ -0,0 +1,89 @@
/*
* 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 "CollectionAdaptor.h"
#include "fdosecrets/objects/Collection.h"
#include "fdosecrets/objects/Item.h"
#include "fdosecrets/objects/Prompt.h"
namespace FdoSecrets
{
CollectionAdaptor::CollectionAdaptor(Collection* parent)
: DBusAdaptor(parent)
{
connect(
p(), &Collection::itemCreated, this, [this](const Item* item) { emit ItemCreated(objectPathSafe(item)); });
connect(
p(), &Collection::itemDeleted, this, [this](const Item* item) { emit ItemDeleted(objectPathSafe(item)); });
connect(
p(), &Collection::itemChanged, this, [this](const Item* item) { emit ItemChanged(objectPathSafe(item)); });
}
const QList<QDBusObjectPath> CollectionAdaptor::items() const
{
return objectsToPath(p()->items().valueOrHandle(p()));
}
QString CollectionAdaptor::label() const
{
return p()->label().valueOrHandle(p());
}
void CollectionAdaptor::setLabel(const QString& label)
{
p()->setLabel(label).handle(p());
}
bool CollectionAdaptor::locked() const
{
return p()->locked().valueOrHandle(p());
}
qulonglong CollectionAdaptor::created() const
{
return p()->created().valueOrHandle(p());
}
qulonglong CollectionAdaptor::modified() const
{
return p()->modified().valueOrHandle(p());
}
QDBusObjectPath CollectionAdaptor::Delete()
{
return objectPathSafe(p()->deleteCollection().valueOrHandle(p()));
}
QList<QDBusObjectPath> CollectionAdaptor::SearchItems(const StringStringMap& attributes)
{
return objectsToPath(p()->searchItems(attributes).valueOrHandle(p()));
}
QDBusObjectPath CollectionAdaptor::CreateItem(const QVariantMap& properties,
const SecretStruct& secret,
bool replace,
QDBusObjectPath& prompt)
{
PromptBase* pp = nullptr;
auto item = p()->createItem(properties, secret, replace, pp).valueOrHandle(p());
prompt = objectPathSafe(pp);
return objectPathSafe(item);
}
} // namespace FdoSecrets

View File

@ -0,0 +1,71 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H
#define KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H
#include "fdosecrets/objects/adaptors/DBusAdaptor.h"
#include <QList>
namespace FdoSecrets
{
class Collection;
class CollectionAdaptor : public DBusAdaptor<Collection>
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_COLLECTION)
Q_PROPERTY(QList<QDBusObjectPath> Items READ items)
Q_PROPERTY(QString Label READ label WRITE setLabel)
Q_PROPERTY(bool Locked READ locked)
Q_PROPERTY(qulonglong Created READ created)
Q_PROPERTY(qulonglong Modified READ modified)
public:
explicit CollectionAdaptor(Collection* parent);
~CollectionAdaptor() override = default;
const QList<QDBusObjectPath> items() const;
QString label() const;
void setLabel(const QString& label);
bool locked() const;
qulonglong created() const;
qulonglong modified() const;
public slots:
QDBusObjectPath Delete();
QList<QDBusObjectPath> SearchItems(const StringStringMap& attributes);
QDBusObjectPath CreateItem(const QVariantMap& properties,
const FdoSecrets::SecretStruct& secret,
bool replace,
QDBusObjectPath& prompt);
signals:
void ItemCreated(const QDBusObjectPath& item);
void ItemDeleted(const QDBusObjectPath& item);
void ItemChanged(const QDBusObjectPath& item);
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H

View File

@ -0,0 +1,51 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H
#define KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H
#include "fdosecrets/objects/DBusReturn.h"
#include "fdosecrets/objects/DBusTypes.h"
#include <QDBusAbstractAdaptor>
namespace FdoSecrets
{
/**
* @brief A common adapter class
*/
template <typename Parent> class DBusAdaptor : public QDBusAbstractAdaptor
{
public:
explicit DBusAdaptor(QObject* parent = nullptr)
: QDBusAbstractAdaptor(parent)
{
}
~DBusAdaptor() override = default;
protected:
Parent* p() const
{
return qobject_cast<Parent*>(parent());
}
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H

View File

@ -0,0 +1,83 @@
/*
* Copyright (C) 2019 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 "ItemAdaptor.h"
#include "fdosecrets/objects/Item.h"
#include "fdosecrets/objects/Prompt.h"
#include "fdosecrets/objects/Session.h"
namespace FdoSecrets
{
ItemAdaptor::ItemAdaptor(Item* parent)
: DBusAdaptor(parent)
{
}
bool ItemAdaptor::locked() const
{
return p()->locked().valueOrHandle(p());
}
const StringStringMap ItemAdaptor::attributes() const
{
return p()->attributes().valueOrHandle(p());
}
void ItemAdaptor::setAttributes(const StringStringMap& attrs)
{
p()->setAttributes(attrs).handle(p());
}
QString ItemAdaptor::label() const
{
return p()->label().valueOrHandle(p());
}
void ItemAdaptor::setLabel(const QString& label)
{
p()->setLabel(label).handle(p());
}
qulonglong ItemAdaptor::created() const
{
return p()->created().valueOrHandle(p());
}
qulonglong ItemAdaptor::modified() const
{
return p()->modified().valueOrHandle(p());
}
QDBusObjectPath ItemAdaptor::Delete()
{
auto prompt = p()->deleteItem().valueOrHandle(p());
return objectPathSafe(prompt);
}
SecretStruct ItemAdaptor::GetSecret(const QDBusObjectPath& session)
{
return p()->getSecret(pathToObject<Session>(session)).valueOrHandle(p());
}
void ItemAdaptor::SetSecret(const SecretStruct& secret)
{
p()->setSecret(secret).handle(p());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H
#define KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H
#include "fdosecrets/objects/adaptors/DBusAdaptor.h"
namespace FdoSecrets
{
class Item;
class ItemAdaptor : public DBusAdaptor<Item>
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_ITEM)
Q_PROPERTY(bool Locked READ locked)
Q_PROPERTY(StringStringMap Attributes READ attributes WRITE setAttributes)
Q_PROPERTY(QString Label READ label WRITE setLabel)
Q_PROPERTY(qulonglong Created READ created)
Q_PROPERTY(qulonglong Modified READ modified)
public:
explicit ItemAdaptor(Item* parent);
~ItemAdaptor() override = default;
bool locked() const;
const StringStringMap attributes() const;
void setAttributes(const StringStringMap& attrs);
QString label() const;
void setLabel(const QString& label);
qulonglong created() const;
qulonglong modified() const;
public slots:
QDBusObjectPath Delete();
FdoSecrets::SecretStruct GetSecret(const QDBusObjectPath& session);
void SetSecret(const FdoSecrets::SecretStruct& secret);
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2019 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 "PromptAdaptor.h"
#include "fdosecrets/objects/Prompt.h"
namespace FdoSecrets
{
PromptAdaptor::PromptAdaptor(PromptBase* parent)
: DBusAdaptor(parent)
{
connect(p(), &PromptBase::completed, this, [this](bool dismissed, QVariant result) {
// make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it.
if (!result.isValid()) {
result = QString{};
}
emit Completed(dismissed, QDBusVariant(std::move(result)));
});
}
void PromptAdaptor::Prompt(const QString& windowId)
{
p()->prompt(windowId).handle(p());
}
void PromptAdaptor::Dismiss()
{
p()->dismiss().handle(p());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H
#define KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H
#include "fdosecrets/objects/adaptors/DBusAdaptor.h"
namespace FdoSecrets
{
class PromptBase;
class PromptAdaptor : public DBusAdaptor<PromptBase>
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT)
public:
explicit PromptAdaptor(PromptBase* parent);
~PromptAdaptor() override = default;
public slots:
void Prompt(const QString& windowId);
void Dismiss();
signals:
void Completed(bool dismissed, const QDBusVariant& result);
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H

View File

@ -0,0 +1,137 @@
/*
* 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 "ServiceAdaptor.h"
#include "fdosecrets/objects/Collection.h"
#include "fdosecrets/objects/Item.h"
#include "fdosecrets/objects/Prompt.h"
#include "fdosecrets/objects/Service.h"
#include "fdosecrets/objects/Session.h"
namespace FdoSecrets
{
ServiceAdaptor::ServiceAdaptor(Service* parent)
: DBusAdaptor(parent)
{
connect(p(), &Service::collectionCreated, this, [this](Collection* coll) {
emit CollectionCreated(objectPathSafe(coll));
});
connect(p(), &Service::collectionDeleted, this, [this](Collection* coll) {
emit CollectionDeleted(objectPathSafe(coll));
});
connect(p(), &Service::collectionChanged, this, [this](Collection* coll) {
emit CollectionChanged(objectPathSafe(coll));
});
}
const QList<QDBusObjectPath> ServiceAdaptor::collections() const
{
auto colls = p()->collections().valueOrHandle(p());
return objectsToPath(std::move(colls));
}
QDBusVariant
ServiceAdaptor::OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result)
{
Session* session = nullptr;
auto output = p()->openSession(algorithm, input.variant(), session).valueOrHandle(p());
result = objectPathSafe(session);
return QDBusVariant(std::move(output));
}
QDBusObjectPath
ServiceAdaptor::CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt)
{
PromptBase* pp;
auto coll = p()->createCollection(properties, alias, pp).valueOrHandle(p());
prompt = objectPathSafe(pp);
return objectPathSafe(coll);
}
const QList<QDBusObjectPath> ServiceAdaptor::SearchItems(const StringStringMap& attributes,
QList<QDBusObjectPath>& locked)
{
QList<Item*> lockedItems, unlockedItems;
unlockedItems = p()->searchItems(attributes, lockedItems).valueOrHandle(p());
locked = objectsToPath(lockedItems);
return objectsToPath(unlockedItems);
}
const QList<QDBusObjectPath> ServiceAdaptor::Unlock(const QList<QDBusObjectPath>& paths, QDBusObjectPath& prompt)
{
auto objects = pathsToObject<DBusObject>(paths);
if (!paths.isEmpty() && objects.isEmpty()) {
DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p());
return {};
}
PromptBase* pp = nullptr;
auto unlocked = p()->unlock(objects, pp).valueOrHandle(p());
prompt = objectPathSafe(pp);
return objectsToPath(unlocked);
}
const QList<QDBusObjectPath> ServiceAdaptor::Lock(const QList<QDBusObjectPath>& paths, QDBusObjectPath& prompt)
{
auto objects = pathsToObject<DBusObject>(paths);
if (!paths.isEmpty() && objects.isEmpty()) {
DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p());
return {};
}
PromptBase* pp = nullptr;
auto locked = p()->lock(objects, pp).valueOrHandle(p());
prompt = objectPathSafe(pp);
return objectsToPath(locked);
}
const ObjectPathSecretMap ServiceAdaptor::GetSecrets(const QList<QDBusObjectPath>& items,
const QDBusObjectPath& session)
{
auto itemObjects = pathsToObject<Item>(items);
if (!items.isEmpty() && itemObjects.isEmpty()) {
DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p());
return {};
}
auto secrets = p()->getSecrets(pathsToObject<Item>(items), pathToObject<Session>(session)).valueOrHandle(p());
ObjectPathSecretMap res;
auto iter = secrets.begin();
while (iter != secrets.end()) {
res[objectPathSafe(iter.key())] = std::move(iter.value());
++iter;
}
return res;
}
QDBusObjectPath ServiceAdaptor::ReadAlias(const QString& name)
{
auto coll = p()->readAlias(name).valueOrHandle(p());
return objectPathSafe(coll);
}
void ServiceAdaptor::SetAlias(const QString& name, const QDBusObjectPath& collection)
{
p()->setAlias(name, pathToObject<Collection>(collection)).handle(p());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,71 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H
#define KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H
#include "DBusAdaptor.h"
#include <QDBusObjectPath>
namespace FdoSecrets
{
/**
* @brief Adapter class for interface org.freedesktop.Secret.Service
*/
class Service;
class ServiceAdaptor : public DBusAdaptor<Service>
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SERVICE)
Q_PROPERTY(QList<QDBusObjectPath> Collections READ collections)
public:
explicit ServiceAdaptor(Service* parent);
~ServiceAdaptor() override = default;
const QList<QDBusObjectPath> collections() const;
public slots:
QDBusVariant OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result);
QDBusObjectPath CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt);
const QList<QDBusObjectPath> SearchItems(const StringStringMap& attributes, QList<QDBusObjectPath>& locked);
const QList<QDBusObjectPath> Unlock(const QList<QDBusObjectPath>& paths, QDBusObjectPath& prompt);
const QList<QDBusObjectPath> Lock(const QList<QDBusObjectPath>& paths, QDBusObjectPath& prompt);
const ObjectPathSecretMap GetSecrets(const QList<QDBusObjectPath>& items, const QDBusObjectPath& session);
QDBusObjectPath ReadAlias(const QString& name);
void SetAlias(const QString& name, const QDBusObjectPath& collection);
signals:
void CollectionCreated(const QDBusObjectPath& collection);
void CollectionDeleted(const QDBusObjectPath& collection);
void CollectionChanged(const QDBusObjectPath& collection);
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 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 "SessionAdaptor.h"
#include "fdosecrets/objects/Session.h"
namespace FdoSecrets
{
SessionAdaptor::SessionAdaptor(Session* parent)
: DBusAdaptor(parent)
{
}
void SessionAdaptor::Close()
{
p()->close().handle(p());
}
} // namespace FdoSecrets

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H
#define KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H
#include "fdosecrets/objects/adaptors/DBusAdaptor.h"
namespace FdoSecrets
{
class Session;
class SessionAdaptor : public DBusAdaptor<Session>
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SESSION)
public:
explicit SessionAdaptor(Session* parent);
~SessionAdaptor() override = default;
public slots:
void Close();
};
} // namespace FdoSecrets
#endif // KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2019 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 "DatabaseSettingsWidgetFdoSecrets.h"
#include "ui_DatabaseSettingsWidgetFdoSecrets.h"
#include "fdosecrets/FdoSecretsSettings.h"
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "gui/group/GroupModel.h"
#include <QSortFilterProxyModel>
namespace
{
enum class ExposedGroup
{
None,
Expose
};
} // namespace
class DatabaseSettingsWidgetFdoSecrets::GroupModelNoRecycle : public QSortFilterProxyModel
{
Q_OBJECT
Database* m_db;
public:
explicit GroupModelNoRecycle(Database* db)
: m_db(db)
{
Q_ASSERT(db);
setSourceModel(new GroupModel(m_db, this));
}
Group* groupFromIndex(const QModelIndex& index) const
{
return groupFromSourceIndex(mapToSource(index));
}
Group* groupFromSourceIndex(const QModelIndex& index) const
{
auto groupModel = qobject_cast<GroupModel*>(sourceModel());
Q_ASSERT(groupModel);
return groupModel->groupFromIndex(index);
}
QModelIndex indexFromGroup(Group* group) const
{
auto groupModel = qobject_cast<GroupModel*>(sourceModel());
Q_ASSERT(groupModel);
return mapFromSource(groupModel->index(group));
}
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
{
auto source_idx = sourceModel()->index(source_row, 0, source_parent);
if (!source_idx.isValid()) {
return false;
}
auto recycleBin = m_db->metadata()->recycleBin();
if (!recycleBin) {
return true;
}
// can not call mapFromSource, which internally calls filterAcceptsRow
auto group = groupFromSourceIndex(source_idx);
return group->uuid() != recycleBin->uuid();
}
};
DatabaseSettingsWidgetFdoSecrets::DatabaseSettingsWidgetFdoSecrets(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::DatabaseSettingsWidgetFdoSecrets)
{
m_ui->setupUi(this);
m_ui->buttonGroup->setId(m_ui->radioDonotExpose, static_cast<int>(ExposedGroup::None));
m_ui->buttonGroup->setId(m_ui->radioExpose, static_cast<int>(ExposedGroup::Expose));
// make sure there is at least a selection
connect(m_ui->radioExpose, &QRadioButton::toggled, this, [this](bool checked) {
if (checked && !m_ui->selectGroup->selectionModel()->hasSelection()) {
auto model = m_ui->selectGroup->model();
if (model) {
auto idx = model->index(0, 0);
m_ui->selectGroup->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent);
}
}
});
}
DatabaseSettingsWidgetFdoSecrets::~DatabaseSettingsWidgetFdoSecrets() = default;
void DatabaseSettingsWidgetFdoSecrets::loadSettings(QSharedPointer<Database> db)
{
m_db = std::move(db);
m_model.reset(new GroupModelNoRecycle(m_db.data()));
m_ui->selectGroup->setModel(m_model.data());
auto group = m_db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(m_db));
if (!group) {
m_ui->radioDonotExpose->setChecked(true);
} else {
auto idx = m_model->indexFromGroup(group);
m_ui->selectGroup->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent);
// expand all its parents
idx = idx.parent();
while (idx.isValid()) {
m_ui->selectGroup->expand(idx);
idx = idx.parent();
}
m_ui->radioExpose->setChecked(true);
}
settingsWarning();
}
void DatabaseSettingsWidgetFdoSecrets::saveSettings()
{
Q_ASSERT(m_db);
Q_ASSERT(m_model);
QUuid exposedGroup;
switch (static_cast<ExposedGroup>(m_ui->buttonGroup->checkedId())) {
case ExposedGroup::None:
break;
case ExposedGroup::Expose: {
auto idx = m_ui->selectGroup->selectionModel()->selectedIndexes().takeFirst();
Q_ASSERT(idx.isValid());
exposedGroup = m_model->groupFromIndex(idx)->uuid();
break;
}
}
FdoSecrets::settings()->setExposedGroup(m_db, exposedGroup);
}
void DatabaseSettingsWidgetFdoSecrets::settingsWarning()
{
if (FdoSecrets::settings()->isEnabled()) {
m_ui->groupBox->setEnabled(true);
m_ui->warningWidget->hideMessage();
} else {
m_ui->groupBox->setEnabled(false);
m_ui->warningWidget->showMessage(tr("Enable fd.o Secret Service to access these settings."),
MessageWidget::Warning);
m_ui->warningWidget->setCloseButtonVisible(false);
m_ui->warningWidget->setAutoHideTimeout(-1);
}
}
#include "DatabaseSettingsWidgetFdoSecrets.moc"

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H
#define KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H
#include <QScopedPointer>
#include <QSharedPointer>
#include <QWidget>
namespace Ui
{
class DatabaseSettingsWidgetFdoSecrets;
}
class Database;
class DatabaseSettingsWidgetFdoSecrets : public QWidget
{
Q_OBJECT
public:
explicit DatabaseSettingsWidgetFdoSecrets(QWidget* parent = nullptr);
~DatabaseSettingsWidgetFdoSecrets() override;
void loadSettings(QSharedPointer<Database> db);
void saveSettings();
private:
void settingsWarning();
private:
QScopedPointer<Ui::DatabaseSettingsWidgetFdoSecrets> m_ui;
QSharedPointer<Database> m_db;
class GroupModelNoRecycle;
QScopedPointer<GroupModelNoRecycle> m_model;
};
#endif // KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetFdoSecrets</class>
<widget class="QWidget" name="DatabaseSettingsWidgetFdoSecrets">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="MessageWidget" name="warningWidget" native="true"/>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Exposed Entries</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="radioDonotExpose">
<property name="text">
<string>Don't e&amp;xpose this database</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioExpose">
<property name="text">
<string>Expose entries &amp;under this group:</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QTreeView" name="selectGroup">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>radioExpose</sender>
<signal>toggled(bool)</signal>
<receiver>selectGroup</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>92</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>189</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>

View File

@ -0,0 +1,320 @@
/*
* 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 "SettingsWidgetFdoSecrets.h"
#include "ui_SettingsWidgetFdoSecrets.h"
#include "fdosecrets/FdoSecretsPlugin.h"
#include "fdosecrets/FdoSecretsSettings.h"
#include "fdosecrets/objects/Collection.h"
#include "fdosecrets/objects/Prompt.h"
#include "fdosecrets/objects/Session.h"
#include "core/DatabaseIcons.h"
#include "core/FilePath.h"
#include "gui/DatabaseWidget.h"
#include <QAction>
#include <QDebug>
#include <QFileInfo>
#include <QHeaderView>
#include <QPushButton>
#include <QTableWidget>
#include <QToolBar>
#include <QVariant>
using FdoSecrets::Collection;
using FdoSecrets::Service;
using FdoSecrets::Session;
SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::SettingsWidgetFdoSecrets())
, m_plugin(plugin)
{
m_ui->setupUi(this);
auto sessHeader = m_ui->tableSessions->horizontalHeader();
sessHeader->setSelectionMode(QAbstractItemView::NoSelection);
sessHeader->setSectionsClickable(false);
sessHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application
sessHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button
auto dbHeader = m_ui->tableDatabases->horizontalHeader();
dbHeader->setSelectionMode(QAbstractItemView::NoSelection);
dbHeader->setSectionsClickable(false);
dbHeader->setSectionResizeMode(0, QHeaderView::Stretch); // file name
dbHeader->setSectionResizeMode(1, QHeaderView::Stretch); // group
dbHeader->setSectionResizeMode(2, QHeaderView::ResizeToContents); // manage button
m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked());
connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled);
}
SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default;
void SettingsWidgetFdoSecrets::populateSessions(bool enabled)
{
m_ui->tableSessions->setRowCount(0);
auto service = m_plugin->serviceInstance();
if (!service || !enabled) {
return;
}
for (const auto& sess : service->sessions()) {
addSessionRow(sess);
}
}
void SettingsWidgetFdoSecrets::addSessionRow(Session* sess)
{
auto row = m_ui->tableSessions->rowCount();
m_ui->tableSessions->insertRow(row);
// column 0: application name
auto item = new QTableWidgetItem(sess->peer());
item->setData(Qt::UserRole, QVariant::fromValue(sess));
m_ui->tableSessions->setItem(row, 0, item);
// column 1: disconnect button
auto btn = new QPushButton(tr("Disconnect"));
connect(btn, &QPushButton::clicked, sess, &Session::close);
m_ui->tableSessions->setCellWidget(row, 1, btn);
// column 2: hidden uuid
m_ui->tableSessions->setItem(row, 2, new QTableWidgetItem(sess->id()));
}
void SettingsWidgetFdoSecrets::removeSessionRow(Session* sess)
{
int row = 0;
while (row != m_ui->tableSessions->rowCount()) {
auto item = m_ui->tableSessions->item(row, 0);
const auto itemSess = item->data(Qt::UserRole).value<Session*>();
if (itemSess == sess) {
break;
}
++row;
}
if (row == m_ui->tableSessions->rowCount()) {
qWarning() << "Unknown Fdo Secret Service session" << sess->id() << "while removing collection from table";
return;
}
m_ui->tableSessions->removeRow(row);
}
void SettingsWidgetFdoSecrets::populateDatabases(bool enabled)
{
m_ui->tableDatabases->setRowCount(0);
auto service = m_plugin->serviceInstance();
if (!service || !enabled) {
return;
}
auto ret = service->collections();
if (ret.isError()) {
return;
}
for (const auto& coll : ret.value()) {
addDatabaseRow(coll);
}
}
void SettingsWidgetFdoSecrets::addDatabaseRow(Collection* coll)
{
auto row = m_ui->tableDatabases->rowCount();
m_ui->tableDatabases->insertRow(row);
// column 0: File name
QFileInfo fi(coll->backend()->database()->filePath());
auto item = new QTableWidgetItem(fi.fileName());
item->setData(Qt::UserRole, QVariant::fromValue(coll));
m_ui->tableDatabases->setItem(row, 0, item);
// column 2: manage button: hboxlayout: unlock/lock settings
// create this first so we have a widget to bind connection to,
// which can then be auto deleted when the row is deleted.
auto widget = createManageButtons(coll);
m_ui->tableDatabases->setCellWidget(row, 2, widget);
// column 1: Group name
auto itemGroupName = new QTableWidgetItem();
updateExposedGroupItem(itemGroupName, coll);
connect(coll, &Collection::collectionLockChanged, widget, [this, itemGroupName, coll](bool) {
updateExposedGroupItem(itemGroupName, coll);
});
m_ui->tableDatabases->setItem(row, 1, itemGroupName);
}
QWidget* SettingsWidgetFdoSecrets::createManageButtons(Collection* coll)
{
auto toolbar = new QToolBar;
toolbar->setFloatable(false);
toolbar->setMovable(false);
// db settings
auto dbSettingsAct = new QAction(tr("Database settings"), toolbar);
dbSettingsAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("document-edit")));
dbSettingsAct->setToolTip(tr("Edit database settings"));
dbSettingsAct->setEnabled(!coll->locked().value());
connect(dbSettingsAct, &QAction::triggered, this, [this, coll]() {
auto db = coll->backend();
m_plugin->serviceInstance()->doSwitchToChangeDatabaseSettings(db);
});
toolbar->addAction(dbSettingsAct);
// unlock/lock
auto lockAct = new QAction(tr("Unlock database"), toolbar);
lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true));
lockAct->setToolTip(tr("Unlock database to show more information"));
connect(coll, &Collection::collectionLockChanged, lockAct, [lockAct, dbSettingsAct](bool locked) {
if (locked) {
lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true));
lockAct->setToolTip(tr("Unlock database to show more information"));
} else {
lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"), true));
lockAct->setToolTip(tr("Lock database"));
}
dbSettingsAct->setEnabled(!locked);
});
connect(lockAct, &QAction::triggered, this, [coll]() {
if (coll->locked().value()) {
coll->doUnlock();
} else {
coll->doLock();
}
});
toolbar->addAction(lockAct);
return toolbar;
}
void SettingsWidgetFdoSecrets::updateExposedGroupItem(QTableWidgetItem* item, Collection* coll)
{
if (coll->locked().value()) {
item->setText(tr("Unlock to show"));
item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked"), true));
QFont font;
font.setItalic(true);
item->setFont(font);
return;
}
auto db = coll->backend()->database();
auto group = db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(db));
if (group) {
item->setText(group->name());
item->setIcon(group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex)
: group->iconScaledPixmap());
if (group->isExpired()) {
QFont font;
font.setStrikeOut(true);
item->setFont(font);
}
} else {
item->setText(tr("None"));
item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none"), true));
}
}
void SettingsWidgetFdoSecrets::removeDatabaseRow(Collection* coll)
{
int row = 0;
while (row != m_ui->tableDatabases->rowCount()) {
auto item = m_ui->tableDatabases->item(row, 0);
const auto itemColl = item->data(Qt::UserRole).value<Collection*>();
if (itemColl == coll) {
break;
}
++row;
}
if (row == m_ui->tableDatabases->rowCount()) {
qWarning() << "Unknown Fdo Secret Service collection" << coll->name() << "while removing collection from table";
return;
}
m_ui->tableDatabases->removeRow(row);
}
void SettingsWidgetFdoSecrets::loadSettings()
{
m_ui->enableFdoSecretService->setChecked(FdoSecrets::settings()->isEnabled());
m_ui->showNotification->setChecked(FdoSecrets::settings()->showNotification());
m_ui->noConfirmDeleteItem->setChecked(FdoSecrets::settings()->noConfirmDeleteItem());
}
void SettingsWidgetFdoSecrets::saveSettings()
{
FdoSecrets::settings()->setEnabled(m_ui->enableFdoSecretService->isChecked());
FdoSecrets::settings()->setShowNotification(m_ui->showNotification->isChecked());
FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked());
}
void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, true));
}
void SettingsWidgetFdoSecrets::hideEvent(QHideEvent* event)
{
QWidget::hideEvent(event);
QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, false));
}
void SettingsWidgetFdoSecrets::updateTables(bool enabled)
{
if (enabled) {
// update the table
populateDatabases(m_ui->enableFdoSecretService->isChecked());
populateSessions(m_ui->enableFdoSecretService->isChecked());
// re-layout the widget to adjust the table cell size
adjustSize();
connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions);
connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases);
auto service = m_plugin->serviceInstance();
if (service) {
connect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow);
connect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow);
connect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow);
connect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow);
}
} else {
disconnect(
m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions);
disconnect(
m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases);
auto service = m_plugin->serviceInstance();
if (service) {
disconnect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow);
disconnect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow);
disconnect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow);
disconnect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* 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/>.
*/
#ifndef KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H
#define KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H
#include <QScopedPointer>
#include <QWidget>
class QTableWidgetItem;
namespace FdoSecrets
{
class Session;
class Collection;
} // namespace FdoSecrets
class FdoSecretsPlugin;
namespace Ui
{
class SettingsWidgetFdoSecrets;
}
class SettingsWidgetFdoSecrets : public QWidget
{
Q_OBJECT
public:
explicit SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent = nullptr);
~SettingsWidgetFdoSecrets() override;
public slots:
void loadSettings();
void saveSettings();
private slots:
void populateSessions(bool enabled);
void populateDatabases(bool enabled);
void addSessionRow(FdoSecrets::Session* sess);
void removeSessionRow(FdoSecrets::Session* sess);
void addDatabaseRow(FdoSecrets::Collection* coll);
void removeDatabaseRow(FdoSecrets::Collection* coll);
void updateTables(bool enabled);
protected:
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
private:
QWidget* createManageButtons(FdoSecrets::Collection* coll);
void updateExposedGroupItem(QTableWidgetItem* item, FdoSecrets::Collection* coll);
private:
QScopedPointer<Ui::SettingsWidgetFdoSecrets> m_ui;
FdoSecretsPlugin* m_plugin;
};
#endif // KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsWidgetFdoSecrets</class>
<widget class="QWidget" name="SettingsWidgetFdoSecrets">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>525</width>
<height>457</height>
</rect>
</property>
<property name="windowTitle">
<string>Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="warningMsg" native="true"/>
</item>
<item>
<widget class="QCheckBox" name="enableFdoSecretService">
<property name="text">
<string>Enable KeepassXC Freedesktop.org Secret Service integration</string>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="showNotification">
<property name="text">
<string>Show notification when credentials are requested</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="noConfirmDeleteItem">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If recycle bin is enabled for the database, entries will be moved to recycle bin directly. Otherwise, they will be deleted without confirmation.&lt;/p&gt;&lt;p&gt;You will still be prompted if any entries are referenced by others.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Don't confirm when entries are deleted by clients.</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Exposed database groups:</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableDatabases">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>File Name</string>
</property>
</column>
<column>
<property name="text">
<string>Group</string>
</property>
</column>
<column>
<property name="text">
<string>Manage</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Authorization</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>These applications are currently connected:</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableSessions">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Application</string>
</property>
</column>
<column>
<property name="text">
<string>Manage</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -59,6 +59,11 @@
#include "keeshare/KeeShare.h"
#include "keeshare/SettingsPageKeeShare.h"
#endif
#ifdef WITH_XC_FDOSECRETS
#include "fdosecrets/FdoSecretsPlugin.h"
#endif
#ifdef WITH_XC_BROWSER
#include "browser/BrowserOptionDialog.h"
#include "browser/BrowserSettings.h"
@ -181,6 +186,16 @@ MainWindow::MainWindow()
SIGNAL(sharingMessage(QString, MessageWidget::MessageType)),
SLOT(displayGlobalMessage(QString, MessageWidget::MessageType)));
#endif
#ifdef WITH_XC_FDOSECRETS
auto fdoSS = new FdoSecretsPlugin(m_ui->tabWidget);
connect(fdoSS, &FdoSecretsPlugin::error, this, &MainWindow::showErrorMessage);
connect(fdoSS, &FdoSecretsPlugin::requestSwitchToDatabases, this, &MainWindow::switchToDatabases);
connect(fdoSS, &FdoSecretsPlugin::requestShowNotification, this, &MainWindow::displayDesktopNotification);
fdoSS->updateServiceState();
m_ui->settingsWidget->addSettingsPage(fdoSS);
#endif
setWindowIcon(filePath()->applicationIcon());
m_ui->globalMessageWidget->setHidden(true);
// clang-format off

View File

@ -28,6 +28,9 @@
#if defined(WITH_XC_KEESHARE)
#include "keeshare/DatabaseSettingsPageKeeShare.h"
#endif
#if defined(WITH_XC_FDOSECRETS)
#include "fdosecrets/DatabaseSettingsPageFdoSecrets.h"
#endif
#include "core/Config.h"
#include "core/Database.h"
@ -85,6 +88,10 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
#if defined(WITH_XC_FDOSECRETS)
addSettingsPage(new DatabaseSettingsPageFdoSecrets());
#endif
m_ui->stackedWidget->setCurrentIndex(0);
m_securityTabWidget->setCurrentIndex(0);

View File

@ -220,6 +220,11 @@ add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
add_unit_test(NAME testtools SOURCES TestTools.cpp
LIBS ${TEST_LIBRARIES})
if(WITH_XC_FDOSECRETS)
add_unit_test(NAME testfdosecrets SOURCES TestFdoSecrets.cpp
LIBS testsupport ${TEST_LIBRARIES})
endif()
if(WITH_GUI_TESTS)
# CLI clip tests need X environment on Linux

92
tests/TestFdoSecrets.cpp Normal file
View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2019 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 "TestFdoSecrets.h"
#include "TestGlobal.h"
#include "fdosecrets/GcryptMPI.h"
#include "fdosecrets/objects/SessionCipher.h"
#include "crypto/Crypto.h"
QTEST_GUILESS_MAIN(TestFdoSecrets)
void TestFdoSecrets::initTestCase()
{
QVERIFY(Crypto::init());
}
void TestFdoSecrets::cleanupTestCase()
{
}
void TestFdoSecrets::testGcryptMPI()
{
auto bytes = QByteArray::fromHex(QByteArrayLiteral("DEADBEEF"));
auto mpi = MpiFromBytes(bytes);
auto another = MpiFromHex("DEADBEEF");
// verify it can parse the bytes in USG mode
QVERIFY(mpi.get());
QVERIFY(another.get());
// verify the number is of the correct value
QCOMPARE(gcry_mpi_cmp_ui(mpi.get(), 0xdeadbeef), 0);
QCOMPARE(gcry_mpi_cmp_ui(another.get(), 0xdeadbeef), 0);
// verify it can convert back
QCOMPARE(MpiToBytes(mpi), bytes);
QCOMPARE(MpiToBytes(another), bytes);
}
void TestFdoSecrets::testDhIetf1024Sha256Aes128CbcPkcs7()
{
auto clientPublic = MpiFromHex("40a0c8d27012c651bf270ebd96890a538"
"396fae3852aef69c0c19bae420d667577"
"ed471cd8ba5a49ef0ec91b568b95f87f0"
"9ec31d271f1699ed140c5b38644c42f60"
"ef84b5a6c406e17c07cd3208e5a605626"
"a5266153b447529946be2394dd43e5638"
"5ffbc4322902c2942391d1a36e8d125dc"
"809e3e406a2f5c2dcf39d3da2");
auto serverPublic = MpiFromHex("e407997e8b918419cf851cf3345358fdf"
"ffb9564a220ac9c3934efd277cea20d17"
"467ecdc56e817f75ac39501f38a4a04ff"
"64d627e16c09981c7ad876da255b61c8e"
"6a8408236c2a4523cfe6961c26dbdfc77"
"c1a27a5b425ca71a019e829fae32c0b42"
"0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
"b263cee30bdb1a57af9bfa93f");
auto serverPrivate = MpiFromHex("013f4f3381ef0ca11c4c7363079577b56"
"99b238644e0aba47e24bdba6173590216"
"4f1e12dd0944800a373e090e63192f53b"
"93583e9a9e50bb9d792aafaa3a0f5ae77"
"de0c3423f5820848d88ee3bdd01c889f2"
"7af58a02f5b6693d422b9d189b300d7b1"
"be5076b5795cf8808c31e2e2898368d18"
"ab5c26b0ea3480c9aba8154cf");
std::unique_ptr<FdoSecrets::DhIetf1024Sha256Aes128CbcPkcs7> cipher{new FdoSecrets::DhIetf1024Sha256Aes128CbcPkcs7};
cipher->initialize(std::move(clientPublic), std::move(serverPublic), std::move(serverPrivate));
QVERIFY(cipher->isValid());
QCOMPARE(cipher->m_aesKey.toHex(), QByteArrayLiteral("6b8f5ee55138eac37118508be21e7834"));
}

35
tests/TestFdoSecrets.h Normal file
View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 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/>.
*/
#ifndef KEEPASSXC_TESTFDOSECRETS_H
#define KEEPASSXC_TESTFDOSECRETS_H
#include <QObject>
class TestFdoSecrets : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testGcryptMPI();
void testDhIetf1024Sha256Aes128CbcPkcs7();
};
#endif // KEEPASSXC_TESTFDOSECRETS_H