Add sharing of groups between databases

* Add source folder keeshare for sharing with corresponding define WITH_XC_KEESHARE
* Move common crypto parts to src/crypto/ssh
* Extended OpenSSHKey
* Move filewatching to own file (currently in two related classes DelayedFileWatcher and BulkFileWatcher)
* Small improvements for style and code in several classes
* Sharing is secured using RSA-Keys which are generated on demand
* Publisher signs the container using their private key
* Client can verify the signed container and choose to decline an import,
import only once or trust the publisher and automatically import all
data of this source henceforth
* Integration of settings into Group-Settings, Database-Settings and Application-Settings
* Introduced dependency QuaZip as dependency to allow combined export of
key container and the (custom format) certificate
This commit is contained in:
Christian Kieschnick 2018-10-01 10:26:24 -04:00 committed by Jonathan White
parent c1e9f45df9
commit eca9c658f4
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
106 changed files with 5828 additions and 503 deletions

View File

@ -47,6 +47,7 @@ option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website
option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF)
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Include sharing support with KeeShare." OFF)
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
@ -58,11 +59,18 @@ if(WITH_XC_ALL)
set(WITH_XC_BROWSER ON)
set(WITH_XC_YUBIKEY ON)
set(WITH_XC_SSHAGENT ON)
set(WITH_XC_KEESHARE ON)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
endif()
if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE)
set(WITH_XC_CRYPTO_SSH ON)
else()
set(WITH_XC_CRYPTO_SSH OFF)
endif()
set(KEEPASSXC_VERSION_MAJOR "2")
set(KEEPASSXC_VERSION_MINOR "4")
set(KEEPASSXC_VERSION_PATCH "0")
@ -347,6 +355,13 @@ endif()
include_directories(SYSTEM ${ARGON2_INCLUDE_DIR})
# Optional
if(WITH_XC_KEESHARE)
find_package(QuaZip REQUIRED)
include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR})
endif()
# Optional
if(WITH_XC_YUBIKEY)
find_package(YubiKey REQUIRED)

View File

@ -53,6 +53,8 @@ RUN set -x \
zlib1g-dev \
libxi-dev \
libxtst-dev \
libquazip5-headers \
libquazip5-dev \
mesa-common-dev \
libyubikey-dev \
libykpers-1-dev

View File

@ -98,8 +98,9 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct
-DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF)
-DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF)
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (favicon download) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Include SSH agent support. (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_SHARING=[ON|OFF] Enable/Disable Sharing extension (default: OFF)
-DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF)
-DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF)
-DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON)

View File

@ -35,6 +35,7 @@ so please check out your distribution's package list to see if KeePassXC is avai
[Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and
[passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#note-about-keepasshttp)
- Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk)
- Sharing of passwords using KeeShare. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details.
- Many bug fixes
For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document.

View File

@ -52,6 +52,8 @@ RUN set -x \
zlib1g-dev \
libyubikey-dev \
libykpers-1-dev \
libquazip5-headers \
libquazip5-dev \
libxi-dev \
libxtst-dev \
xvfb

View File

@ -29,9 +29,9 @@ set(EXCLUDED_FILES
gui/KMessageWidget.cpp
gui/MainWindowAdaptor.h
gui/MainWindowAdaptor.cpp
sshagent/bcrypt_pbkdf.cpp
sshagent/blf.h
sshagent/blowfish.c
crypto/ssh/bcrypt_pbkdf.cpp
crypto/ssh/blf.h
crypto/ssh/blowfish.c
tests/modeltest.cpp
tests/modeltest.h
# objective-c files

23
cmake/FindQuaZip.cmake Normal file
View File

@ -0,0 +1,23 @@
# Copyright (C) 2018 KeePassXC Team <team@keepassxc.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/>.
find_path(QUAZIP_INCLUDE_DIR quazip5/quazip.h)
find_library(QUAZIP_LIBRARIES quazip5)
mark_as_advanced(QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR)
include(FindPackageHandleStandardArgs)
include_directories(${QUAZIP_INCLUDE_DIR})
find_package_handle_standard_args(QuaZip DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR)

View File

@ -30,12 +30,12 @@ Leave the other options at their defaults.
* *In your default web browser,* install the KeePassXC Browser extension/add-on. Instructions for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) or [Chrome](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk)
* Click the KeePassXC icon in the upper-right corner. You'll see the dialog below.
* Click the blue Connect button to make the browser extension connect to the KeePassXC application.
<img src="./KeePassXC-Connect.png" height="200" alt="KeePassXC Connect dialog">
<img src="./KeePassHTTP/KeePassXC-Connect.png" height="200" alt="KeePassXC Connect dialog">
* *Switch back to KeePassXC.* You'll see a dialog (below) indicating that a request to connect has arrived.
* Give the connection a name (perhaps *Keepass-Browsername*, any unique name will suffice) and click OK to accept it.
* This one-time operation connects KeePassXC and your browser.
<img src="./KeePassXC-Accept-Button.png" height="200" alt="KeePassXC accept connection dialog">
<img src="./KeePassHTTP/KeePassXC-Accept-Button.png" height="200" alt="KeePassXC accept connection dialog">
## Using Browser Integration
@ -45,4 +45,60 @@ or select it and type Ctrl+U (Cmd+U on macOS).
* If there are username/password fields on that page, you will see the dialog below.
Click *Allow* to confirm that KeePassXC may access the credentials to auto-fill the fields.
* Check *Remember this decision* to allow this each time you visit the page.
<img src="./KeePassXC-Confirm.png" height="200" alt="KeePassCX Confirm Access dialog">
<img src="./KeePassHTTP/KeePassXC-Confirm.png" height="200" alt="KeePassCX Confirm Access dialog">
## Using Sharing
Sharing allows you to share a subset of your credentials with others and vice versa.
### Enable Sharing
To use sharing, you need to enable it on a database.
1. Go to Database &rarr; Database Settings
1. Check _Allow import_ if you want to import shared credentials
1. Check _Allow export_ if you want to share credentials
<img src="./KeeShare/Database-Settings.png" height="600" width="800" alt="KeePassXC Databse Sharing Settings">
### Sharing Credentials
If you checked _Allow export_ in the Sharing settings you now are good to go to share some passwords with others. Sharing always is defined on a group. If you enable sharing on a group, every entry under this group or it's children is shared. If you enable sharing on the root node, **every password** inside your database gets shared!
1. Open the edit sheet on a group you want to share
1. Select the sharing section
1. Choose _Export to path_ as the sharing method
1. Choose a path to store the shared credentials to
1. Generate a password for this share database
The export file will not be generated automatically. Instead, each time the database is saved, the file gets written. If an old file is present, the old file will be overwritten! The file should be written to a location that is accessible by others. An easy setup is a network share or storing the file inside the cloud.
<img src="./KeeShare/Share-Group.png" height="600" width="800" alt="KeePassXC Group Sharing Settings">
### Using Shared Credentials
Checking _Allow import_ in the Sharing settings of the database enables you to receive credentials from others. KeePass will watch sharing sources and import any changes immoderately into your database using the synchronization feature.
1. Create a group for import
1. Open the edit sheet on that group
1. Select the sharing section
1. Choose _Import from path_ as the sharing method
1. Choose a database that is shared with you
1. Enter the password for the shared database
<img src="./KeeShare/Import-Group.png" height="600" width="800" alt="KeePassXC Group Import Settings">
### Using Synchronized Credentials
Instead of using different groups for sharing and importing you can use a single group that acts as both. This way you can synchronize a number of credentials easily across many users without a lot of hassle.
1. Open the edit sheet on a group you want to synchronize
1. Select the sharing section
1. Choose _Synchronize with path_ as the sharing method
1. Choose a database that you want to use a synchronization file
1. Enter the password for the database
<img src="./KeeShare/Synchronize-Group.png" height="600" width="800" alt="KeePassXC Group Synchronization Settings">
## Technical Details of Sharing
Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC

View File

@ -51,6 +51,7 @@ set(keepassx_SOURCES
core/EntryAttributes.cpp
core/EntrySearcher.cpp
core/FilePath.cpp
core/FileWatcher.cpp
core/Global.h
core/Group.cpp
core/InactivityTimer.cpp
@ -211,6 +212,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing")
add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)")
add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser")
add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent")
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
@ -226,6 +228,17 @@ endif()
add_subdirectory(autotype)
add_subdirectory(cli)
add_subdirectory(crypto/ssh)
if(WITH_XC_CRYPTO_SSH)
set(crypto_ssh_LIB crypto_ssh)
endif()
add_subdirectory(keeshare)
if(WITH_XC_KEESHARE)
set(keeshare_LIB keeshare)
endif()
add_subdirectory(sshagent)
if(WITH_XC_SSHAGENT)
set(sshagent_LIB sshagent)
@ -269,7 +282,6 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL
target_link_libraries(keepassx_core
autotype
${keepassxcbrowser_LIB}
${sshagent_LIB}
Qt5::Core
Qt5::Network
Qt5::Concurrent
@ -280,7 +292,14 @@ target_link_libraries(keepassx_core
${ARGON2_LIBRARIES}
${GCRYPT_LIBRARIES}
${GPGERROR_LIBRARIES}
${ZLIB_LIBRARIES})
${ZLIB_LIBRARIES})
if(WITH_XC_SSHAGENT)
target_link_libraries(keepassx_core sshagent)
endif()
if(WITH_XC_KEESHARE)
target_link_libraries(keepassx_core keeshare)
endif()
if(APPLE)
target_link_libraries(keepassx_core "-framework Foundation")

View File

@ -17,6 +17,7 @@
#cmakedefine WITH_XC_BROWSER
#cmakedefine WITH_XC_YUBIKEY
#cmakedefine WITH_XC_SSHAGENT
#cmakedefine WITH_XC_KEESHARE
#cmakedefine WITH_XC_TOUCHID
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"

View File

@ -16,7 +16,7 @@
*/
#include "Clock.h"
QSharedPointer<Clock> Clock::m_instance = QSharedPointer<Clock>();
QSharedPointer<Clock> Clock::m_instance;
QDateTime Clock::currentDateTimeUtc()
{
@ -92,7 +92,7 @@ QDateTime Clock::currentDateTimeImpl() const
void Clock::resetInstance()
{
m_instance.clear();
m_instance.reset();
}
void Clock::setInstance(Clock* clock)

View File

@ -48,7 +48,16 @@ QString Config::getFileName()
void Config::set(const QString& key, const QVariant& value)
{
if (m_settings->contains(key) && m_settings->value(key) == value) {
return;
}
const bool surpressSignal = !m_settings->contains(key) && m_defaults.value(key) == value;
m_settings->setValue(key, value);
if (!surpressSignal) {
emit changed(key);
}
}
/**

View File

@ -43,6 +43,9 @@ public:
static void createConfigFromFile(const QString& file);
static void createTempFileInstance();
signals:
void changed(const QString& key);
private:
Config(const QString& fileName, QObject* parent);
explicit Config(QObject* parent);

View File

@ -111,7 +111,7 @@ void Database::setFilePath(const QString& filePath)
m_filePath = filePath;
}
Entry* Database::resolveEntry(const QUuid& uuid)
Entry* Database::resolveEntry(const QUuid& uuid) const
{
return findEntryRecursive(uuid, m_rootGroup);
}
@ -121,7 +121,7 @@ Entry* Database::resolveEntry(const QString& text, EntryReferenceType referenceT
return findEntryRecursive(text, referenceType, m_rootGroup);
}
Entry* Database::findEntryRecursive(const QUuid& uuid, Group* group)
Entry* Database::findEntryRecursive(const QUuid& uuid, Group* group) const
{
const QList<Entry*> entryList = group->entries();
for (Entry* entry : entryList) {
@ -289,8 +289,11 @@ QByteArray Database::challengeResponseKey() const
bool Database::challengeMasterSeed(const QByteArray& masterSeed)
{
m_data.masterSeed = masterSeed;
return m_data.key->challenge(masterSeed, m_data.challengeResponseKey);
if (m_data.key) {
m_data.masterSeed = masterSeed;
return m_data.key->challenge(masterSeed, m_data.challengeResponseKey);
}
return true;
}
void Database::setCipher(const QUuid& cipher)

View File

@ -23,6 +23,7 @@
#include <QHash>
#include <QObject>
#include "config-keepassx.h"
#include "crypto/kdf/Kdf.h"
#include "keys/CompositeKey.h"
@ -88,7 +89,7 @@ public:
const Metadata* metadata() const;
QString filePath() const;
void setFilePath(const QString& filePath);
Entry* resolveEntry(const QUuid& uuid);
Entry* resolveEntry(const QUuid& uuid) const;
Entry* resolveEntry(const QString& text, EntryReferenceType referenceType);
Group* resolveGroup(const QUuid& uuid);
QList<DeletedObject> deletedObjects();
@ -149,7 +150,7 @@ private slots:
void startModifiedTimer();
private:
Entry* findEntryRecursive(const QUuid& uuid, Group* group);
Entry* findEntryRecursive(const QUuid& uuid, Group* group) const;
Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group);
Group* findGroupRecursive(const QUuid& uuid, Group* group);

View File

@ -22,6 +22,8 @@
DatabaseIcons* DatabaseIcons::m_instance(nullptr);
const int DatabaseIcons::IconCount(69);
const int DatabaseIcons::ExpiredIconIndex(45);
const int DatabaseIcons::SharedIconIndex(1);
const int DatabaseIcons::UnsharedIconIndex(45);
// clang-format off
const char* const DatabaseIcons::m_indexToName[] = {

View File

@ -33,6 +33,8 @@ public:
static const int IconCount;
static const int ExpiredIconIndex;
static const int SharedIconIndex;
static const int UnsharedIconIndex;
private:
DatabaseIcons();

View File

@ -955,6 +955,20 @@ QString Entry::maskPasswordPlaceholders(const QString& str) const
return result;
}
Entry* Entry::resolveReference(const QString& str) const
{
QRegularExpressionMatch match = EntryAttributes::matchReference(str);
if (!match.hasMatch()) {
return nullptr;
}
const QString searchIn = match.captured(EntryAttributes::SearchInGroupName);
const QString searchText = match.captured(EntryAttributes::SearchTextGroupName);
const EntryReferenceType searchInType = Entry::referenceType(searchIn);
return m_group->database()->resolveEntry(searchText, searchInType);
}
QString Entry::resolveMultiplePlaceholders(const QString& str) const
{
return resolveMultiplePlaceholdersRecursive(str, ResolveMaximumDepth);

View File

@ -195,6 +195,7 @@ public:
Entry* clone(CloneFlags flags) const;
void copyDataFrom(const Entry* other);
QString maskPasswordPlaceholders(const QString& str) const;
Entry* resolveReference(const QString& str) const;
QString resolveMultiplePlaceholders(const QString& str) const;
QString resolvePlaceholder(const QString& str) const;
QString resolveUrlPlaceholder(const QString& str, PlaceholderType placeholderType) const;

238
src/core/FileWatcher.cpp Normal file
View File

@ -0,0 +1,238 @@
/*
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.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 "FileWatcher.h"
#include "core/Clock.h"
#include <QFileInfo>
#ifdef Q_OS_LINUX
#include <sys/vfs.h>
#endif
namespace
{
const int FileChangeDelay = 500;
const int TimerResolution = 100;
}
DelayingFileWatcher::DelayingFileWatcher(QObject* parent)
: QObject(parent)
, m_ignoreFileChange(false)
{
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged()));
connect(&m_fileUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges()));
connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged()));
m_fileChangeDelayTimer.setSingleShot(true);
m_fileUnblockTimer.setSingleShot(true);
}
void DelayingFileWatcher::restart()
{
m_fileWatcher.addPath(m_filePath);
}
void DelayingFileWatcher::stop()
{
m_fileWatcher.removePath(m_filePath);
}
void DelayingFileWatcher::start(const QString& filePath)
{
if (!m_filePath.isEmpty()) {
m_fileWatcher.removePath(m_filePath);
}
#if defined(Q_OS_LINUX)
struct statfs statfsBuf;
bool forcePolling = false;
const auto NFS_SUPER_MAGIC = 0x6969;
if (!statfs(filePath.toLocal8Bit().constData(), &statfsBuf)) {
forcePolling = (statfsBuf.f_type == NFS_SUPER_MAGIC);
} else {
// if we can't get the fs type let's fall back to polling
forcePolling = true;
}
auto objectName = forcePolling ? QLatin1String("_qt_autotest_force_engine_poller") : QLatin1String("");
m_fileWatcher.setObjectName(objectName);
#endif
m_fileWatcher.addPath(filePath);
if (!filePath.isEmpty()) {
m_filePath = filePath;
}
}
void DelayingFileWatcher::ignoreFileChanges()
{
m_ignoreFileChange = true;
m_fileChangeDelayTimer.stop();
}
void DelayingFileWatcher::observeFileChanges(bool delayed)
{
int timeout = 0;
if (delayed) {
timeout = FileChangeDelay;
} else {
m_ignoreFileChange = false;
start(m_filePath);
}
if (timeout > 0 && !m_fileUnblockTimer.isActive()) {
m_fileUnblockTimer.start(timeout);
}
}
void DelayingFileWatcher::onWatchedFileChanged()
{
if (m_ignoreFileChange) {
// the client forcefully silenced us
return;
}
if (m_fileChangeDelayTimer.isActive()) {
// we are waiting to fire the delayed fileChanged event, so nothing
// to do here
return;
}
m_fileChangeDelayTimer.start(FileChangeDelay);
}
BulkFileWatcher::BulkFileWatcher(QObject* parent)
: QObject(parent)
{
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileChanged(QString)));
connect(&m_fileWatcher, SIGNAL(directoryChanged(QString)), SLOT(handleDirectoryChanged(QString)));
connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges()));
m_fileWatchUnblockTimer.setSingleShot(true);
}
void BulkFileWatcher::clear()
{
for (const QString& path : m_fileWatcher.files() + m_fileWatcher.directories()) {
const QFileInfo info(path);
m_fileWatcher.removePath(info.absoluteFilePath());
m_fileWatcher.removePath(info.absolutePath());
}
m_filePaths.clear();
m_watchedFilesInDirectory.clear();
m_ignoreFilesChangess.clear();
}
void BulkFileWatcher::removePath(const QString& path)
{
const QFileInfo info(path);
m_fileWatcher.removePath(info.absoluteFilePath());
m_fileWatcher.removePath(info.absolutePath());
m_filePaths.remove(info.absoluteFilePath());
m_filePaths.remove(info.absolutePath());
m_watchedFilesInDirectory[info.absolutePath()].remove(info.absoluteFilePath());
}
void BulkFileWatcher::addPath(const QString& path)
{
const QFileInfo info(path);
m_fileWatcher.addPath(info.absoluteFilePath());
m_fileWatcher.addPath(info.absolutePath());
m_filePaths.insert(info.absoluteFilePath());
m_filePaths.insert(info.absolutePath());
m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists();
}
void BulkFileWatcher::restart(const QString& path)
{
const QFileInfo info(path);
Q_ASSERT(m_filePaths.contains(info.absoluteFilePath()));
Q_ASSERT(m_filePaths.contains(info.absolutePath()));
m_fileWatcher.addPath(info.absoluteFilePath());
m_fileWatcher.addPath(info.absolutePath());
}
void BulkFileWatcher::handleFileChanged(const QString& path)
{
addPath(path);
const QFileInfo info(path);
if (m_ignoreFilesChangess[info.canonicalFilePath()] > Clock::currentDateTimeUtc()) {
// changes are blocked
return;
}
emit fileChanged(path);
}
void BulkFileWatcher::handleDirectoryChanged(const QString& path)
{
qDebug("Directory changed %s", qPrintable(path));
const QFileInfo directory(path);
const QMap<QString, bool>& watchedFiles = m_watchedFilesInDirectory[directory.absolutePath()];
for (const QString& file : watchedFiles.keys()) {
const QFileInfo info(file);
const bool existed = watchedFiles[info.absoluteFilePath()];
if (!info.exists() && existed) {
qDebug("Remove watch file %s", qPrintable(info.absoluteFilePath()));
m_fileWatcher.removePath(info.absolutePath());
emit fileRemoved(info.absoluteFilePath());
}
if (!existed && info.exists()) {
qDebug("Add watch file %s", qPrintable(info.absoluteFilePath()));
m_fileWatcher.addPath(info.absolutePath());
emit fileCreated(info.absoluteFilePath());
}
if (existed && info.exists()) {
qDebug("Refresh watch file %s", qPrintable(info.absoluteFilePath()));
m_fileWatcher.removePath(info.absolutePath());
m_fileWatcher.addPath(info.absolutePath());
emit fileChanged(info.absoluteFilePath());
}
m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists();
}
}
void BulkFileWatcher::ignoreFileChanges(const QString& path)
{
const QFileInfo info(path);
m_ignoreFilesChangess[info.canonicalFilePath()] = Clock::currentDateTimeUtc().addMSecs(FileChangeDelay);
}
void BulkFileWatcher::observeFileChanges(bool delayed)
{
int timeout = 0;
if (delayed) {
timeout = TimerResolution;
} else {
const QDateTime current = Clock::currentDateTimeUtc();
for (const QString& key : m_ignoreFilesChangess.keys()) {
if (m_ignoreFilesChangess[key] < current) {
// We assume that there was no concurrent change of the database
// during our block - so no need to reimport
qDebug("Remove block from %s", qPrintable(key));
m_ignoreFilesChangess.remove(key);
continue;
}
qDebug("Keep block from %s", qPrintable(key));
timeout = static_cast<int>(current.msecsTo(m_ignoreFilesChangess[key]));
}
}
if (timeout > 0 && !m_fileWatchUnblockTimer.isActive()) {
m_fileWatchUnblockTimer.start(timeout);
}
}

92
src/core/FileWatcher.h Normal file
View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_FILEWATCHER_H
#define KEEPASSXC_FILEWATCHER_H
#include <QFileSystemWatcher>
#include <QSet>
#include <QTimer>
#include <QVariant>
class DelayingFileWatcher : public QObject
{
Q_OBJECT
public:
explicit DelayingFileWatcher(QObject* parent = nullptr);
void blockAutoReload(bool block);
void start(const QString& path);
void restart();
void stop();
void ignoreFileChanges();
signals:
void fileChanged();
public slots:
void observeFileChanges(bool delayed = false);
private slots:
void onWatchedFileChanged();
private:
QString m_filePath;
QFileSystemWatcher m_fileWatcher;
QTimer m_fileChangeDelayTimer;
QTimer m_fileUnblockTimer;
bool m_ignoreFileChange;
};
class BulkFileWatcher : public QObject
{
Q_OBJECT
public:
explicit BulkFileWatcher(QObject* parent = nullptr);
void clear();
void removePath(const QString& path);
void addPath(const QString& path);
void restart(const QString& path);
void ignoreFileChanges(const QString& path);
signals:
void fileCreated(QString);
void fileChanged(QString);
void fileRemoved(QString);
public slots:
void observeFileChanges(bool delayed = false);
private slots:
void handleFileChanged(const QString& path);
void handleDirectoryChanged(const QString& path);
private:
QSet<QString> m_filePaths;
QMap<QString, QDateTime> m_ignoreFilesChangess;
QFileSystemWatcher m_fileWatcher;
QMap<QString, QMap<QString, bool>> m_watchedFilesInDirectory;
QTimer m_fileWatchUnblockTimer; // needed for Import/Export-References
};
#endif // KEEPASSXC_FILEWATCHER_H

View File

@ -124,8 +124,7 @@ bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const
if (::compare(m_creationTime, other.m_creationTime, options) != 0) {
return false;
}
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options)
!= 0) {
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options) != 0) {
return false;
}
if (::compare(m_expires, m_expiryTime, other.m_expires, other.expiryTime(), options) != 0) {
@ -134,8 +133,7 @@ bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const
if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_usageCount, other.m_usageCount, options) != 0) {
return false;
}
if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options)
!= 0) {
if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options) != 0) {
return false;
}
return true;

View File

@ -346,4 +346,31 @@ namespace Tools
return bSuccess;
}
Buffer::Buffer()
: raw(nullptr)
, size(0)
{
}
Buffer::~Buffer()
{
clear();
}
void Buffer::clear()
{
if(size > 0){
free(raw);
}
raw = nullptr; size = 0;
}
QByteArray Buffer::content() const
{
return QByteArray(reinterpret_cast<char*>(raw), size );
}
} // namespace Tools

View File

@ -56,6 +56,33 @@ namespace Tools
}
}
template <typename Key, typename Value, void deleter(Value)> struct Map
{
QMap<Key, Value> values;
Value& operator[](const Key index)
{
return values[index];
}
~Map()
{
for (Value m : values) {
deleter(m);
}
}
};
struct Buffer
{
unsigned char* raw;
size_t size;
Buffer();
~Buffer();
void clear();
QByteArray content() const;
};
} // namespace Tools
#endif // KEEPASSX_TOOLS_H

View File

@ -58,8 +58,7 @@ CryptoHash::CryptoHash(Algorithm algo, bool hmac)
gcry_error_t error = gcry_md_open(&d->ctx, algoGcrypt, flagsGcrypt);
if (error != GPG_ERR_NO_ERROR) {
qWarning("Gcrypt error (ctor): %s", gcry_strerror(error));
qWarning("Gcrypt error (ctor): %s", gcry_strsource(error));
qWarning("Gcrypt error (ctor): %s\n %s", gcry_strerror(error), gcry_strsource(error));
}
Q_ASSERT(error == 0); // TODO: error handling
@ -92,8 +91,7 @@ void CryptoHash::setKey(const QByteArray& data)
gcry_error_t error = gcry_md_setkey(d->ctx, data.constData(), static_cast<size_t>(data.size()));
if (error) {
qWarning("Gcrypt error (setKey): %s", gcry_strerror(error));
qWarning("Gcrypt error (setKey): %s", gcry_strsource(error));
qWarning("Gcrypt error (setKey): %s\n %s", gcry_strerror(error), gcry_strsource(error));
}
Q_ASSERT(error == 0);
}

View File

@ -35,8 +35,8 @@ public:
~CryptoHash();
void addData(const QByteArray& data);
void reset();
QByteArray result() const;
void setKey(const QByteArray& data);
QByteArray result() const;
static QByteArray hash(const QByteArray& data, Algorithm algo);
static QByteArray hmac(const QByteArray& data, const QByteArray& key, Algorithm algo);

View File

@ -28,7 +28,7 @@ public:
void randomize(void* data, int len) override;
};
Random* Random::m_instance(nullptr);
QSharedPointer<Random> Random::m_instance;
void Random::randomize(QByteArray& ba)
{
@ -70,18 +70,20 @@ quint32 Random::randomUIntRange(quint32 min, quint32 max)
Random* Random::instance()
{
if (!m_instance) {
m_instance = new Random(new RandomBackendGcrypt());
m_instance.reset(new Random(new RandomBackendGcrypt()));
}
return m_instance;
return m_instance.data();
}
void Random::createWithBackend(RandomBackend* backend)
void Random::resetInstance()
{
Q_ASSERT(backend);
Q_ASSERT(!m_instance);
m_instance.reset();
}
m_instance = new Random(backend);
void Random::setInstance(RandomBackend* backend)
{
m_instance.reset(new Random(backend));
}
Random::Random(RandomBackend* backend)
@ -95,3 +97,7 @@ void RandomBackendGcrypt::randomize(void* data, int len)
gcry_randomize(data, len, GCRY_STRONG_RANDOM);
}
RandomBackend::~RandomBackend()
{
}

View File

@ -20,14 +20,13 @@
#include <QByteArray>
#include <QScopedPointer>
#include <QSharedPointer>
class RandomBackend
{
public:
virtual void randomize(void* data, int len) = 0;
virtual ~RandomBackend()
{
}
virtual ~RandomBackend();
};
class Random
@ -47,13 +46,17 @@ public:
quint32 randomUIntRange(quint32 min, quint32 max);
static Random* instance();
static void createWithBackend(RandomBackend* backend);
protected:
static void resetInstance();
static void setInstance(RandomBackend* backend);
private:
static QSharedPointer<Random> m_instance;
explicit Random(RandomBackend* backend);
QScopedPointer<RandomBackend> m_backend;
static Random* m_instance;
Q_DISABLE_COPY(Random)
};

View File

@ -89,7 +89,7 @@ int SymmetricCipher::blockSize() const
QString SymmetricCipher::errorString() const
{
return m_backend->errorString();
return m_backend->error();
}
SymmetricCipher::Algorithm SymmetricCipher::cipherToAlgorithm(const QUuid& cipher)

View File

@ -38,7 +38,7 @@ public:
virtual int keySize() const = 0;
virtual int blockSize() const = 0;
virtual QString errorString() const = 0;
virtual QString error() const = 0;
};
#endif // KEEPASSX_SYMMETRICCIPHERBACKEND_H

View File

@ -80,13 +80,12 @@ int SymmetricCipherGcrypt::gcryptMode(SymmetricCipher::Mode mode)
}
}
void SymmetricCipherGcrypt::setErrorString(gcry_error_t err)
void SymmetricCipherGcrypt::setError(const gcry_error_t& err)
{
const char* gcryptError = gcry_strerror(err);
const char* gcryptErrorSource = gcry_strsource(err);
m_errorString =
QString("%1/%2").arg(QString::fromLocal8Bit(gcryptErrorSource), QString::fromLocal8Bit(gcryptError));
m_error = QString("%1/%2").arg(QString::fromLocal8Bit(gcryptErrorSource), QString::fromLocal8Bit(gcryptError));
}
bool SymmetricCipherGcrypt::init()
@ -99,7 +98,7 @@ bool SymmetricCipherGcrypt::init()
gcry_cipher_close(m_ctx);
error = gcry_cipher_open(&m_ctx, m_algo, m_mode, 0);
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
@ -112,7 +111,7 @@ bool SymmetricCipherGcrypt::setKey(const QByteArray& key)
gcry_error_t error = gcry_cipher_setkey(m_ctx, m_key.constData(), m_key.size());
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
@ -131,7 +130,7 @@ bool SymmetricCipherGcrypt::setIv(const QByteArray& iv)
}
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
@ -154,7 +153,7 @@ QByteArray SymmetricCipherGcrypt::process(const QByteArray& data, bool* ok)
}
if (error != 0) {
setErrorString(error);
setError(error);
*ok = false;
} else {
*ok = true;
@ -176,7 +175,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data)
}
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
@ -197,7 +196,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data, quint64 rounds)
error = gcry_cipher_decrypt(m_ctx, rawData, size, nullptr, 0);
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
}
@ -206,7 +205,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data, quint64 rounds)
error = gcry_cipher_encrypt(m_ctx, rawData, size, nullptr, 0);
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
}
@ -221,13 +220,13 @@ bool SymmetricCipherGcrypt::reset()
error = gcry_cipher_reset(m_ctx);
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
error = gcry_cipher_setiv(m_ctx, m_iv.constData(), m_iv.size());
if (error != 0) {
setErrorString(error);
setError(error);
return false;
}
@ -258,7 +257,7 @@ int SymmetricCipherGcrypt::blockSize() const
return blockSizeT;
}
QString SymmetricCipherGcrypt::errorString() const
QString SymmetricCipherGcrypt::error() const
{
return m_errorString;
return m_error;
}

View File

@ -43,12 +43,12 @@ public:
int keySize() const;
int blockSize() const;
QString errorString() const;
QString error() const;
private:
static int gcryptAlgo(SymmetricCipher::Algorithm algo);
static int gcryptMode(SymmetricCipher::Mode mode);
void setErrorString(gcry_error_t err);
void setError(const gcry_error_t& err);
gcry_cipher_hd_t m_ctx;
const int m_algo;
@ -56,7 +56,7 @@ private:
const SymmetricCipher::Direction m_direction;
QByteArray m_key;
QByteArray m_iv;
QString m_errorString;
QString m_error;
};
#endif // KEEPASSX_SYMMETRICCIPHERGCRYPT_H

View File

@ -17,6 +17,8 @@
*/
#include "ASN1Key.h"
#include "crypto/ssh/BinaryStream.h"
#include <gcrypt.h>
namespace
@ -53,7 +55,17 @@ namespace
return true;
}
bool parseHeader(BinaryStream& stream, quint8 wantedType)
bool parsePublicHeader(BinaryStream& stream)
{
quint8 tag;
quint32 len;
nextTag(stream, tag, len);
return (tag == TAG_SEQUENCE);
}
bool parsePrivateHeader(BinaryStream& stream, quint8 wantedType)
{
quint8 tag;
quint32 len;
@ -118,7 +130,7 @@ bool ASN1Key::parseDSA(QByteArray& ba, OpenSSHKey& key)
{
BinaryStream stream(&ba);
if (!parseHeader(stream, KEY_ZERO)) {
if (!parsePrivateHeader(stream, KEY_ZERO)) {
return false;
}
@ -149,11 +161,38 @@ bool ASN1Key::parseDSA(QByteArray& ba, OpenSSHKey& key)
return true;
}
bool ASN1Key::parseRSA(QByteArray& ba, OpenSSHKey& key)
bool ASN1Key::parsePublicRSA(QByteArray& ba, OpenSSHKey& key)
{
BinaryStream stream(&ba);
if (!parseHeader(stream, KEY_ZERO)) {
if (!parsePublicHeader(stream)) {
return false;
}
QByteArray n, e;
readInt(stream, n);
readInt(stream, e);
QList<QByteArray> publicData;
publicData.append(e);
publicData.append(n);
QList<QByteArray> privateData;
privateData.append(n);
privateData.append(e);
key.setType("ssh-rsa");
key.setPublicData(publicData);
key.setPrivateData(privateData);
key.setComment("");
return true;
}
bool ASN1Key::parsePrivateRSA(QByteArray& ba, OpenSSHKey& key)
{
BinaryStream stream(&ba);
if (!parsePrivateHeader(stream, KEY_ZERO)) {
return false;
}

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ASN1KEY_H
#define ASN1KEY_H
#ifndef KEEPASSXC_ASN1KEY_H
#define KEEPASSXC_ASN1KEY_H
#include "OpenSSHKey.h"
#include <QtCore>
@ -25,7 +25,8 @@
namespace ASN1Key
{
bool parseDSA(QByteArray& ba, OpenSSHKey& key);
bool parseRSA(QByteArray& ba, OpenSSHKey& key);
bool parsePrivateRSA(QByteArray& ba, OpenSSHKey& key);
bool parsePublicRSA(QByteArray& ba, OpenSSHKey& key);
}
#endif // ASN1KEY_H
#endif // KEEPASSXC_ASN1KEY_H

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BINARYSTREAM_H
#define BINARYSTREAM_H
#ifndef KEEPASSXC_BINARYSTREAM_H
#define KEEPASSXC_BINARYSTREAM_H
#include <QBuffer>
#include <QIODevice>
@ -65,4 +65,4 @@ private:
QScopedPointer<QBuffer> m_buffer;
};
#endif // BINARYSTREAM_H
#endif // KEEPASSXC_BINARYSTREAM_H

View File

@ -0,0 +1,14 @@
if(WITH_XC_CRYPTO_SSH)
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(crypto_ssh_SOURCES
bcrypt_pbkdf.cpp
blowfish.c
ASN1Key.cpp
BinaryStream.cpp
OpenSSHKey.cpp
)
add_library(crypto_ssh STATIC ${crypto_ssh_SOURCES})
target_link_libraries(crypto_ssh Qt5::Core ${GCRYPT_LIBRARIES})
endif()

View File

@ -17,29 +17,202 @@
*/
#include "OpenSSHKey.h"
#include "ASN1Key.h"
#include "core/Tools.h"
#include "crypto/SymmetricCipher.h"
#include "crypto/ssh/ASN1Key.h"
#include "crypto/ssh/BinaryStream.h"
#include <QCryptographicHash>
#include <QRegularExpression>
#include <QStringList>
const QString OpenSSHKey::TYPE_DSA = "DSA PRIVATE KEY";
const QString OpenSSHKey::TYPE_RSA = "RSA PRIVATE KEY";
const QString OpenSSHKey::TYPE_OPENSSH = "OPENSSH PRIVATE KEY";
#include <gcrypt.h>
const QString OpenSSHKey::TYPE_DSA_PRIVATE = "DSA PRIVATE KEY";
const QString OpenSSHKey::TYPE_RSA_PRIVATE = "RSA PRIVATE KEY";
const QString OpenSSHKey::TYPE_RSA_PUBLIC = "RSA PUBLIC KEY";
const QString OpenSSHKey::TYPE_OPENSSH_PRIVATE = "OPENSSH PRIVATE KEY";
namespace
{
QPair<QString, QList<QByteArray>> binaryDeserialize(const QByteArray& serialized)
{
if (serialized.isEmpty()) {
return {};
}
QBuffer buffer;
buffer.setData(serialized);
buffer.open(QBuffer::ReadOnly);
BinaryStream stream(&buffer);
QString type;
stream.readString(type);
QByteArray temp;
QList<QByteArray> data;
while (stream.readString(temp)) {
data << temp;
}
return ::qMakePair(type, data);
}
QByteArray binarySerialize(const QString& type, const QList<QByteArray>& data)
{
if (type.isEmpty() && data.isEmpty()) {
return {};
}
QByteArray buffer;
BinaryStream stream(&buffer);
stream.writeString(type);
for (const QByteArray& part : data) {
stream.writeString(part);
}
return buffer;
}
}
// bcrypt_pbkdf.cpp
int bcrypt_pbkdf(const QByteArray& pass, const QByteArray& salt, QByteArray& key, quint32 rounds);
OpenSSHKey OpenSSHKey::generate(bool secure)
{
enum Index
{
Params,
CombinedKey,
PrivateKey,
PublicKey,
Private_N,
Private_E,
Private_D,
Private_P,
Private_Q,
Private_U, // private key
Public_N,
Public_E,
};
Tools::Map<Index, gcry_mpi_t, &gcry_mpi_release> mpi;
Tools::Map<Index, gcry_sexp_t, &gcry_sexp_release> sexp;
gcry_error_t rc = GPG_ERR_NO_ERROR;
rc = gcry_sexp_build(&sexp[Params], NULL, secure ? "(genkey (rsa (nbits 4:2048)))" : "(genkey (rsa (transient-key) (nbits 4:2048)))");
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not create ssh key" << gcry_err_code(rc);
return OpenSSHKey();
}
rc = gcry_pk_genkey(&sexp[CombinedKey], sexp[Params]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not create ssh key" << gcry_err_code(rc);
return OpenSSHKey();
}
sexp[PrivateKey] = gcry_sexp_find_token(sexp[CombinedKey], "private-key", 0);
sexp[PublicKey] = gcry_sexp_find_token(sexp[CombinedKey], "public-key", 0);
sexp[Private_N] = gcry_sexp_find_token(sexp[PrivateKey], "n", 1);
mpi[Private_N] = gcry_sexp_nth_mpi(sexp[Private_N], 1, GCRYMPI_FMT_USG);
sexp[Private_E] = gcry_sexp_find_token(sexp[PrivateKey], "e", 1);
mpi[Private_E] = gcry_sexp_nth_mpi(sexp[Private_E], 1, GCRYMPI_FMT_USG);
sexp[Private_D] = gcry_sexp_find_token(sexp[PrivateKey], "d", 1);
mpi[Private_D] = gcry_sexp_nth_mpi(sexp[Private_D], 1, GCRYMPI_FMT_USG);
sexp[Private_Q] = gcry_sexp_find_token(sexp[PrivateKey], "q", 1);
mpi[Private_Q] = gcry_sexp_nth_mpi(sexp[Private_Q], 1, GCRYMPI_FMT_USG);
sexp[Private_P] = gcry_sexp_find_token(sexp[PrivateKey], "p", 1);
mpi[Private_P] = gcry_sexp_nth_mpi(sexp[Private_P], 1, GCRYMPI_FMT_USG);
sexp[Private_U] = gcry_sexp_find_token(sexp[PrivateKey], "u", 1);
mpi[Private_U] = gcry_sexp_nth_mpi(sexp[Private_U], 1, GCRYMPI_FMT_USG);
sexp[Public_N] = gcry_sexp_find_token(sexp[PublicKey], "n", 1);
mpi[Public_N] = gcry_sexp_nth_mpi(sexp[Public_N], 1, GCRYMPI_FMT_USG);
sexp[Public_E] = gcry_sexp_find_token(sexp[PublicKey], "e", 1);
mpi[Public_E] = gcry_sexp_nth_mpi(sexp[Public_E], 1, GCRYMPI_FMT_USG);
QList<QByteArray> publicParts;
QList<QByteArray> privateParts;
Tools::Buffer buffer;
gcry_mpi_format format = GCRYMPI_FMT_USG;
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_N]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_E]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_D]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_U]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_P]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_Q]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract private key part" << gcry_err_code(rc);
return OpenSSHKey();
}
privateParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Public_E]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract public key part" << gcry_err_code(rc);
return OpenSSHKey();
}
publicParts << buffer.content();
buffer.clear();
rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Public_N]);
if (rc != GPG_ERR_NO_ERROR) {
qWarning() << "Could not extract public key part" << gcry_err_code(rc);
return OpenSSHKey();
}
publicParts << buffer.content();
OpenSSHKey key;
key.m_rawType = OpenSSHKey::TYPE_RSA_PRIVATE;
key.setType("ssh-rsa");
key.setPublicData(publicParts);
key.setPrivateData(privateParts);
key.setComment("");
return key;
}
OpenSSHKey::OpenSSHKey(QObject* parent)
: QObject(parent)
, m_type(QString())
, m_cipherName(QString("none"))
, m_kdfName(QString("none"))
, m_kdfOptions(QByteArray())
, m_rawPrivateData(QByteArray())
, m_publicData(QList<QByteArray>())
, m_privateData(QList<QByteArray>())
, m_privateType(QString())
, m_rawType(QString())
, m_rawData(QByteArray())
, m_rawPublicData(QList<QByteArray>())
, m_rawPrivateData(QList<QByteArray>())
, m_comment(QString())
, m_error(QString())
{
@ -51,9 +224,10 @@ OpenSSHKey::OpenSSHKey(const OpenSSHKey& other)
, m_cipherName(other.m_cipherName)
, m_kdfName(other.m_kdfName)
, m_kdfOptions(other.m_kdfOptions)
, m_rawType(other.m_rawType)
, m_rawData(other.m_rawData)
, m_rawPublicData(other.m_rawPublicData)
, m_rawPrivateData(other.m_rawPrivateData)
, m_publicData(other.m_publicData)
, m_privateData(other.m_privateData)
, m_comment(other.m_comment)
, m_error(other.m_error)
{
@ -77,22 +251,21 @@ const QString OpenSSHKey::type() const
int OpenSSHKey::keyLength() const
{
if (m_type == "ssh-dss" && m_publicData.length() == 4) {
return (m_publicData[0].length() - 1) * 8;
} else if (m_type == "ssh-rsa" && m_publicData.length() == 2) {
return (m_publicData[1].length() - 1) * 8;
} else if (m_type.startsWith("ecdsa-sha2-") && m_publicData.length() == 2) {
return (m_publicData[1].length() - 1) * 4;
} else if (m_type == "ssh-ed25519" && m_publicData.length() == 1) {
return m_publicData[0].length() * 8;
if (m_type == "ssh-dss" && m_rawPublicData.length() == 4) {
return (m_rawPublicData[0].length() - 1) * 8;
} else if (m_type == "ssh-rsa" && m_rawPublicData.length() == 2) {
return (m_rawPublicData[1].length() - 1) * 8;
} else if (m_type.startsWith("ecdsa-sha2-") && m_rawPublicData.length() == 2) {
return (m_rawPublicData[1].length() - 1) * 4;
} else if (m_type == "ssh-ed25519" && m_rawPublicData.length() == 1) {
return m_rawPublicData[0].length() * 8;
}
return 0;
}
const QString OpenSSHKey::fingerprint(QCryptographicHash::Algorithm algo) const
{
if (m_publicData.isEmpty()) {
if (m_rawPublicData.isEmpty()) {
return {};
}
@ -101,7 +274,7 @@ const QString OpenSSHKey::fingerprint(QCryptographicHash::Algorithm algo) const
stream.writeString(m_type);
for (QByteArray ba : m_publicData) {
for (const QByteArray& ba : m_rawPublicData) {
stream.writeString(ba);
}
@ -126,9 +299,27 @@ const QString OpenSSHKey::comment() const
return m_comment;
}
const QString OpenSSHKey::privateKey() const
{
if (m_rawPrivateData.isEmpty()) {
return {};
}
QByteArray privateKey;
BinaryStream stream(&privateKey);
stream.writeString(m_type);
for (QByteArray ba : m_rawPrivateData) {
stream.writeString(ba);
}
return m_type + " " + QString::fromLatin1(privateKey.toBase64()) + " " + m_comment;
}
const QString OpenSSHKey::publicKey() const
{
if (m_publicData.isEmpty()) {
if (m_rawPublicData.isEmpty()) {
return {};
}
@ -137,7 +328,7 @@ const QString OpenSSHKey::publicKey() const
stream.writeString(m_type);
for (QByteArray ba : m_publicData) {
for (QByteArray ba : m_rawPublicData) {
stream.writeString(ba);
}
@ -156,12 +347,12 @@ void OpenSSHKey::setType(const QString& type)
void OpenSSHKey::setPublicData(const QList<QByteArray>& data)
{
m_publicData = data;
m_rawPublicData = data;
}
void OpenSSHKey::setPrivateData(const QList<QByteArray>& data)
{
m_privateData = data;
m_rawPrivateData = data;
}
void OpenSSHKey::setComment(const QString& comment)
@ -171,11 +362,11 @@ void OpenSSHKey::setComment(const QString& comment)
void OpenSSHKey::clearPrivate()
{
m_rawData.clear();
m_rawPrivateData.clear();
m_privateData.clear();
}
bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out)
bool OpenSSHKey::extractPEM(const QByteArray& in, QByteArray& out)
{
QString pem = QString::fromLatin1(in);
QStringList rows = pem.split(QRegularExpression("(?:\r?\n|\r)"), QString::SkipEmptyParts);
@ -201,7 +392,7 @@ bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out)
return false;
}
m_privateType = beginMatch.captured(1);
m_rawType = beginMatch.captured(1);
rows.removeFirst();
rows.removeLast();
@ -237,17 +428,17 @@ bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out)
return true;
}
bool OpenSSHKey::parse(const QByteArray& in)
bool OpenSSHKey::parsePKCS1PEM(const QByteArray& in)
{
QByteArray data;
if (!parsePEM(in, data)) {
if (!extractPEM(in, data)) {
return false;
}
if (m_privateType == TYPE_DSA || m_privateType == TYPE_RSA) {
m_rawPrivateData = data;
} else if (m_privateType == TYPE_OPENSSH) {
if (m_rawType == TYPE_DSA_PRIVATE || m_rawType == TYPE_RSA_PRIVATE || m_rawType == TYPE_RSA_PUBLIC) {
m_rawData = data;
} else if (m_rawType == TYPE_OPENSSH_PRIVATE) {
BinaryStream stream(&data);
QByteArray magic;
@ -291,18 +482,18 @@ bool OpenSSHKey::parse(const QByteArray& in)
}
// padded list of keys
if (!stream.readString(m_rawPrivateData)) {
if (!stream.readString(m_rawData)) {
m_error = tr("Corrupted key file, reading private key failed");
return false;
}
} else {
m_error = tr("Unsupported key type: %1").arg(m_privateType);
m_error = tr("Unsupported key type: %1").arg(m_rawType);
return false;
}
// load private if no encryption
if (!encrypted()) {
return openPrivateKey();
return openKey();
}
return true;
@ -313,15 +504,15 @@ bool OpenSSHKey::encrypted() const
return (m_cipherName != "none");
}
bool OpenSSHKey::openPrivateKey(const QString& passphrase)
bool OpenSSHKey::openKey(const QString& passphrase)
{
QScopedPointer<SymmetricCipher> cipher;
if (!m_privateData.isEmpty()) {
if (!m_rawPrivateData.isEmpty()) {
return true;
}
if (m_rawPrivateData.isEmpty()) {
if (m_rawData.isEmpty()) {
m_error = tr("No private key payload to decrypt");
return false;
}
@ -390,7 +581,7 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase)
hash.addData(m_cipherIV.data(), 8);
mdBuf = hash.result();
keyData.append(mdBuf);
} while(keyData.size() < cipher->keySize());
} while (keyData.size() < cipher->keySize());
if (keyData.size() > cipher->keySize()) {
// If our key size isn't a multiple of 16 (e.g. AES-192 or something),
@ -407,33 +598,38 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase)
return false;
}
QByteArray rawPrivateData = m_rawPrivateData;
QByteArray rawData = m_rawData;
if (cipher && cipher->isInitalized()) {
bool ok = false;
rawPrivateData = cipher->process(rawPrivateData, &ok);
rawData = cipher->process(rawData, &ok);
if (!ok) {
m_error = tr("Decryption failed, wrong passphrase?");
return false;
}
}
if (m_privateType == TYPE_DSA) {
if (!ASN1Key::parseDSA(rawPrivateData, *this)) {
if (m_rawType == TYPE_DSA_PRIVATE) {
if (!ASN1Key::parseDSA(rawData, *this)) {
m_error = tr("Decryption failed, wrong passphrase?");
return false;
}
return true;
} else if (m_privateType == TYPE_RSA) {
if (!ASN1Key::parseRSA(rawPrivateData, *this)) {
} else if (m_rawType == TYPE_RSA_PRIVATE) {
if (!ASN1Key::parsePrivateRSA(rawData, *this)) {
m_error = tr("Decryption failed, wrong passphrase?");
return false;
}
return true;
} else if (m_privateType == TYPE_OPENSSH) {
BinaryStream keyStream(&rawPrivateData);
} else if (m_rawType == TYPE_RSA_PUBLIC) {
if (!ASN1Key::parsePublicRSA(rawData, *this)) {
m_error = tr("Decryption failed, wrong passphrase?");
return false;
}
return true;
} else if (m_rawType == TYPE_OPENSSH_PRIVATE) {
BinaryStream keyStream(&rawData);
quint32 checkInt1;
quint32 checkInt2;
@ -449,13 +645,13 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase)
return readPrivate(keyStream);
}
m_error = tr("Unsupported key type: %1").arg(m_privateType);
m_error = tr("Unsupported key type: %1").arg(m_rawType);
return false;
}
bool OpenSSHKey::readPublic(BinaryStream& stream)
{
m_publicData.clear();
m_rawPublicData.clear();
if (!stream.readString(m_type)) {
m_error = tr("Unexpected EOF while reading public key");
@ -484,7 +680,7 @@ bool OpenSSHKey::readPublic(BinaryStream& stream)
return false;
}
m_publicData.append(t);
m_rawPublicData.append(t);
}
return true;
@ -492,7 +688,7 @@ bool OpenSSHKey::readPublic(BinaryStream& stream)
bool OpenSSHKey::readPrivate(BinaryStream& stream)
{
m_privateData.clear();
m_rawPrivateData.clear();
if (!stream.readString(m_type)) {
m_error = tr("Unexpected EOF while reading private key");
@ -521,7 +717,7 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream)
return false;
}
m_privateData.append(t);
m_rawPrivateData.append(t);
}
if (!stream.readString(m_comment)) {
@ -534,7 +730,7 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream)
bool OpenSSHKey::writePublic(BinaryStream& stream)
{
if (m_publicData.isEmpty()) {
if (m_rawPublicData.isEmpty()) {
m_error = tr("Can't write public key as it is empty");
return false;
}
@ -544,7 +740,7 @@ bool OpenSSHKey::writePublic(BinaryStream& stream)
return false;
}
for (QByteArray t : m_publicData) {
for (QByteArray t : m_rawPublicData) {
if (!stream.writeString(t)) {
m_error = tr("Unexpected EOF when writing public key");
return false;
@ -556,7 +752,7 @@ bool OpenSSHKey::writePublic(BinaryStream& stream)
bool OpenSSHKey::writePrivate(BinaryStream& stream)
{
if (m_privateData.isEmpty()) {
if (m_rawPrivateData.isEmpty()) {
m_error = tr("Can't write private key as it is empty");
return false;
}
@ -566,7 +762,7 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream)
return false;
}
for (QByteArray t : m_privateData) {
for (QByteArray t : m_rawPrivateData) {
if (!stream.writeString(t)) {
m_error = tr("Unexpected EOF when writing private key");
return false;
@ -581,6 +777,49 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream)
return true;
}
QList<QByteArray> OpenSSHKey::publicParts() const
{
return m_rawPublicData;
}
QList<QByteArray> OpenSSHKey::privateParts() const
{
return m_rawPrivateData;
}
const QString& OpenSSHKey::privateType() const
{
return m_rawType;
}
OpenSSHKey OpenSSHKey::restoreFromBinary(Type type, const QByteArray& serialized)
{
OpenSSHKey key;
auto data = binaryDeserialize(serialized);
key.setType(data.first);
switch (type) {
case Public:
key.setPublicData(data.second);
break;
case Private:
key.setPrivateData(data.second);
break;
}
return key;
}
QByteArray OpenSSHKey::serializeToBinary(Type type, const OpenSSHKey& key)
{
Q_ASSERT(!key.encrypted());
switch (type) {
case Public:
return binarySerialize(key.type(), key.publicParts());
case Private:
return binarySerialize(key.type(), key.privateParts());
}
return {};
}
uint qHash(const OpenSSHKey& key)
{
return qHash(key.fingerprint());

View File

@ -16,23 +16,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef OPENSSHKEY_H
#define OPENSSHKEY_H
#ifndef KEEPASSXC_OPENSSHKEY_H
#define KEEPASSXC_OPENSSHKEY_H
#include "BinaryStream.h"
#include <QtCore>
class OpenSSHKey : QObject
class BinaryStream;
class OpenSSHKey : public QObject
{
Q_OBJECT
public:
static OpenSSHKey generate(bool secure = true);
explicit OpenSSHKey(QObject* parent = nullptr);
OpenSSHKey(const OpenSSHKey& other);
bool operator==(const OpenSSHKey& other) const;
bool parse(const QByteArray& in);
bool parsePKCS1PEM(const QByteArray& in);
bool encrypted() const;
bool openPrivateKey(const QString& passphrase = QString());
bool openKey(const QString& passphrase = QString());
const QString cipherName() const;
const QString type() const;
@ -40,6 +43,7 @@ public:
const QString fingerprint(QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256) const;
const QString comment() const;
const QString publicKey() const;
const QString privateKey() const;
const QString errorString() const;
void setType(const QString& type);
@ -54,26 +58,41 @@ public:
bool writePublic(BinaryStream& stream);
bool writePrivate(BinaryStream& stream);
private:
static const QString TYPE_DSA;
static const QString TYPE_RSA;
static const QString TYPE_OPENSSH;
QList<QByteArray> publicParts() const;
QList<QByteArray> privateParts() const;
const QString& privateType() const;
bool parsePEM(const QByteArray& in, QByteArray& out);
static const QString TYPE_DSA_PRIVATE;
static const QString TYPE_RSA_PRIVATE;
static const QString TYPE_RSA_PUBLIC;
static const QString TYPE_OPENSSH_PRIVATE;
enum Type
{
Public,
Private
};
static OpenSSHKey restoreFromBinary(Type eType, const QByteArray& serialized);
static QByteArray serializeToBinary(Type eType, const OpenSSHKey& key);
private:
bool extractPEM(const QByteArray& in, QByteArray& out);
QString m_type;
QString m_cipherName;
QByteArray m_cipherIV;
QString m_kdfName;
QByteArray m_kdfOptions;
QByteArray m_rawPrivateData;
QList<QByteArray> m_publicData;
QList<QByteArray> m_privateData;
QString m_privateType;
QString m_rawType;
QByteArray m_rawData;
QList<QByteArray> m_rawPublicData;
QList<QByteArray> m_rawPrivateData;
QString m_comment;
QString m_error;
};
uint qHash(const OpenSSHKey& key);
#endif // OPENSSHKEY_H
#endif // KEEPASSXC_OPENSSHKEY_H

View File

@ -69,8 +69,7 @@ Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device,
}
QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, m_db->transformedMasterKey());
if (headerHmac
!= CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) {
if (headerHmac != CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) {
raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)"));
return nullptr;
}
@ -85,8 +84,7 @@ Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device,
raiseError(tr("Unknown cipher"));
return nullptr;
}
SymmetricCipherStream cipherStream(
&hmacStream, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt);
SymmetricCipherStream cipherStream(&hmacStream, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt);
if (!cipherStream.init(finalKey, m_encryptionIV)) {
raiseError(cipherStream.errorString());
return nullptr;

View File

@ -34,7 +34,7 @@ KdbxXmlWriter::KdbxXmlWriter(quint32 version)
}
void KdbxXmlWriter::writeDatabase(QIODevice* device,
Database* db,
const Database* db,
KeePass2RandomStream* randomStream,
const QByteArray& headerHash)
{

View File

@ -37,7 +37,7 @@ public:
explicit KdbxXmlWriter(quint32 version);
void writeDatabase(QIODevice* device,
Database* db,
const Database *db,
KeePass2RandomStream* randomStream = nullptr,
const QByteArray& headerHash = QByteArray());
void writeDatabase(const QString& filename, Database* db);
@ -82,8 +82,8 @@ private:
const quint32 m_kdbxVersion;
QXmlStreamWriter m_xml;
QPointer<Database> m_db;
QPointer<Metadata> m_meta;
QPointer<const Database> m_db;
QPointer<const Metadata> m_meta;
KeePass2RandomStream* m_randomStream = nullptr;
QHash<QByteArray, int> m_idMap;
QByteArray m_headerHash;

View File

@ -89,6 +89,9 @@ AboutDialog::AboutDialog(QWidget* parent)
#ifdef WITH_XC_SSHAGENT
extensions += "\n- " + tr("SSH Agent");
#endif
#ifdef WITH_XC_KEESHARE
extensions += "\n- " + tr("KeeShare");
#endif
#ifdef WITH_XC_YUBIKEY
extensions += "\n- " + tr("YubiKey");
#endif

View File

@ -20,8 +20,9 @@
#include "ui_ApplicationSettingsWidgetGeneral.h"
#include "ui_ApplicationSettingsWidgetSecurity.h"
#include "autotype/AutoType.h"
#include "config-keepassx.h"
#include "autotype/AutoType.h"
#include "core/Config.h"
#include "core/FilePath.h"
#include "core/Global.h"
@ -77,12 +78,10 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
connect(this, SIGNAL(apply()), SLOT(saveSettings()));
connect(this, SIGNAL(rejected()), SLOT(reject()));
connect(
m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableAutoSaveOnExit(bool)));
connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableAutoSaveOnExit(bool)));
connect(m_generalUi->systrayShowCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableSystray(bool)));
connect(
m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool)));
connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool)));
connect(m_secUi->lockDatabaseIdleCheckBox,
SIGNAL(toggled(bool)),
m_secUi->lockDatabaseIdleSpinBox,
@ -120,7 +119,6 @@ void ApplicationSettingsWidget::addSettingsPage(ISettingsPage* page)
void ApplicationSettingsWidget::loadSettings()
{
if (config()->hasAccessError()) {
showMessage(tr("Access error for config file %1").arg(config()->getFileName()), MessageWidget::Error);
}

View File

@ -40,6 +40,9 @@
#include "gui/entry/EntryView.h"
#include "gui/group/GroupView.h"
#include "gui/wizard/NewDatabaseWizard.h"
#include "keeshare/KeeShare.h"
#include "config-keepassx.h"
DatabaseManagerStruct::DatabaseManagerStruct()
: dbWidget(nullptr)
@ -375,6 +378,11 @@ bool DatabaseTabWidget::saveDatabase(Database* db, QString filePath)
dbStruct.saveAttempts = 0;
dbStruct.fileInfo = QFileInfo(filePath);
dbStruct.dbWidget->databaseSaved();
#ifdef WITH_XC_KEESHARE
// TODO HNH: This is hacky - we need to remove the logic from the ui at this point to allow a proper
// architecture
KeeShare::instance()->handleDatabaseSaved(db);
#endif
updateTabName(db);
emit messageDismissTab();
return true;
@ -429,7 +437,14 @@ bool DatabaseTabWidget::saveDatabaseAs(Database* db)
// Failed to save, try again
continue;
}
#ifdef WITH_XC_KEESHARE
// Since we change to the saved database we should also export
// TODO HNH: This is hacky - we need to remove the logic from the ui at this point to allow a proper
// architecture
KeeShare::instance()->handleDatabaseSaved(db);
#endif
// changes of the current database
// SaveAs for non-existing datbase doesn't matter since one has to set the path while creation
dbStruct.dbWidget->updateFilePath(dbStruct.fileInfo.absoluteFilePath());
updateLastDatabases(dbStruct.fileInfo.absoluteFilePath());
return true;
@ -628,9 +643,9 @@ void DatabaseTabWidget::updateTabNameFromDbWidgetSender()
}
}
int DatabaseTabWidget::databaseIndex(Database* db)
int DatabaseTabWidget::databaseIndex(const Database* db)
{
QWidget* dbWidget = m_dbList.value(db).dbWidget;
QWidget* dbWidget = m_dbList.value(const_cast<Database*>(db)).dbWidget;
return indexOf(dbWidget);
}
@ -865,6 +880,35 @@ void DatabaseTabWidget::connectDatabase(Database* newDb, Database* oldDb)
connect(newDb, SIGNAL(nameTextChanged()), SLOT(updateTabNameFromDbSender()));
connect(newDb, SIGNAL(modified()), SLOT(modified()));
newDb->setEmitModified(true);
#ifdef WITH_XC_KEESHARE
KeeShare::instance()->connectDatabase(newDb, oldDb);
connect(KeeShare::instance(),
SIGNAL(sharingMessage(Database*, QString, MessageWidget::MessageType)),
this,
SLOT(handleDatabaseMessage(Database*, QString, MessageWidget::MessageType)),
Qt::UniqueConnection);
KeeShare::instance()->handleDatabaseOpened(newDb);
#endif
}
void DatabaseTabWidget::handleDatabaseMessage(Database* db, QString message, MessageWidget::MessageType type)
{
auto* databaseWidget = currentDatabaseWidget();
if (!databaseWidget) {
return;
}
auto* currentDb = currentDatabaseWidget()->database();
if (!currentDb) {
return;
}
if (currentDb != db) {
auto index = databaseIndex(db);
emit messageGlobal(tr("Update in background database %1:\n%2").arg(tabText(index)).arg(message), type);
} else {
emit messageTab(message, type);
}
}
void DatabaseTabWidget::performGlobalAutoType()

View File

@ -101,6 +101,7 @@ private slots:
void changeDatabase(Database* newDb, bool unsavedChanges);
void emitActivateDatabaseChanged();
void emitDatabaseUnlockedFromDbWidgetSender();
void handleDatabaseMessage(Database* db, QString message, MessageWidget::MessageType type);
private:
Database* execNewDatabaseWizard();
@ -108,7 +109,7 @@ private:
bool saveDatabaseAs(Database* db);
bool closeDatabase(Database* db);
void deleteDatabase(Database* db);
int databaseIndex(Database* db);
int databaseIndex(const Database* db);
Database* indexDatabase(int index);
DatabaseManagerStruct indexDatabaseManagerStruct(int index);
Database* databaseFromDatabaseWidget(DatabaseWidget* dbWidget);

View File

@ -35,6 +35,7 @@
#include "core/Config.h"
#include "core/EntrySearcher.h"
#include "core/FilePath.h"
#include "core/FileWatcher.h"
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
@ -73,6 +74,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
, m_newGroup(nullptr)
, m_newEntry(nullptr)
, m_newParent(nullptr)
, m_importingCsv(false)
, m_fileWatcher(new DelayingFileWatcher(this))
{
m_mainWidget = new QWidget(this);
@ -198,9 +201,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool)));
connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool)));
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged()));
connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile()));
connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(unblockAutoReload()));
connect(m_fileWatcher.data(), SIGNAL(fileChanged()), this, SLOT(reloadDatabaseFile()));
connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged()));
connect(m_groupView, SIGNAL(groupPressed(Group*)), SLOT(emitPressedGroup(Group*)));
@ -211,10 +212,6 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
m_databaseModified = false;
m_fileWatchTimer.setSingleShot(true);
m_fileWatchUnblockTimer.setSingleShot(true);
m_ignoreAutoReload = false;
m_searchCaseSensitive = false;
m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
@ -817,9 +814,9 @@ void DatabaseWidget::openDatabase(bool accepted)
m_databaseOpenWidget = nullptr;
delete m_keepass1OpenWidget;
m_keepass1OpenWidget = nullptr;
m_fileWatcher.addPath(m_filePath);
m_fileWatcher->restart();
} else {
m_fileWatcher.removePath(m_filePath);
m_fileWatcher->stop();
if (m_databaseOpenWidget->database()) {
delete m_databaseOpenWidget->database();
}
@ -1175,26 +1172,7 @@ void DatabaseWidget::lock()
void DatabaseWidget::updateFilePath(const QString& filePath)
{
if (!m_filePath.isEmpty()) {
m_fileWatcher.removePath(m_filePath);
}
#if defined(Q_OS_LINUX)
struct statfs statfsBuf;
bool forcePolling = false;
const auto NFS_SUPER_MAGIC = 0x6969;
if (!statfs(filePath.toLocal8Bit().constData(), &statfsBuf)) {
forcePolling = (statfsBuf.f_type == NFS_SUPER_MAGIC);
} else {
// if we can't get the fs type let's fall back to polling
forcePolling = true;
}
auto objectName = forcePolling ? QLatin1String("_qt_autotest_force_engine_poller") : QLatin1String("");
m_fileWatcher.setObjectName(objectName);
#endif
m_fileWatcher.addPath(filePath);
m_fileWatcher->start(filePath);
m_filePath = filePath;
m_db->setFilePath(filePath);
}
@ -1202,30 +1180,12 @@ void DatabaseWidget::updateFilePath(const QString& filePath)
void DatabaseWidget::blockAutoReload(bool block)
{
if (block) {
m_ignoreAutoReload = true;
m_fileWatchTimer.stop();
m_fileWatcher->ignoreFileChanges();
} else {
m_fileWatchUnblockTimer.start(500);
m_fileWatcher->observeFileChanges(true);
}
}
void DatabaseWidget::unblockAutoReload()
{
m_ignoreAutoReload = false;
updateFilePath(m_filePath);
}
void DatabaseWidget::onWatchedFileChanged()
{
if (m_ignoreAutoReload) {
return;
}
if (m_fileWatchTimer.isActive())
return;
m_fileWatchTimer.start(500);
}
void DatabaseWidget::reloadDatabaseFile()
{
if (!m_db || currentMode() == DatabaseWidget::LockedMode) {
@ -1249,7 +1209,7 @@ void DatabaseWidget::reloadDatabaseFile()
m_db->markAsModified();
m_databaseModified = true;
// Rewatch the database file
m_fileWatcher.addPath(m_filePath);
m_fileWatcher->restart();
return;
}
}
@ -1307,7 +1267,7 @@ void DatabaseWidget::reloadDatabaseFile()
}
// Rewatch the database file
m_fileWatcher.addPath(m_filePath);
m_fileWatcher->restart();
}
int DatabaseWidget::numberOfSelectedEntries() const

View File

@ -37,6 +37,7 @@ class EditEntryWidget;
class EditGroupWidget;
class Entry;
class EntryView;
class DelayingFileWatcher;
class Group;
class GroupView;
class KeePass1OpenWidget;
@ -48,7 +49,6 @@ class UnlockDatabaseWidget;
class MessageWidget;
class DetailsWidget;
class UnlockDatabaseDialog;
class QFileSystemWatcher;
namespace Ui
{
@ -200,10 +200,8 @@ private slots:
void unlockDatabase(bool accepted);
void emitCurrentModeChanged();
// Database autoreload slots
void onWatchedFileChanged();
void reloadDatabaseFile();
void restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& EntryUuid);
void unblockAutoReload();
private:
void setClipboardTextAndMinimize(const QString& text);
@ -227,11 +225,12 @@ private:
QSplitter* m_detailSplitter;
GroupView* m_groupView;
EntryView* m_entryView;
QString m_filePath;
QLabel* m_searchingLabel;
Group* m_newGroup;
Entry* m_newEntry;
Group* m_newParent;
QString m_filePath;
QUuid m_groupBeforeLock;
QUuid m_entryBeforeLock;
MessageWidget* m_messageWidget;
@ -246,10 +245,7 @@ private:
bool m_importingCsv;
// Autoreload
QFileSystemWatcher m_fileWatcher;
QTimer m_fileWatchTimer;
QTimer m_fileWatchUnblockTimer;
bool m_ignoreAutoReload;
QPointer<DelayingFileWatcher> m_fileWatcher;
bool m_databaseModified;
};

View File

@ -27,6 +27,9 @@
#include "core/FilePath.h"
#include "entry/EntryAttachmentsModel.h"
#include "gui/Clipboard.h"
#ifdef WITH_XC_KEESHARE
#include "keeshare/KeeShare.h"
#endif
namespace
{
@ -102,7 +105,9 @@ void DetailsWidget::setGroup(Group* selectedGroup)
updateGroupHeaderLine();
updateGroupGeneralTab();
updateGroupNotesTab();
#ifdef WITH_XC_KEESHARE
updateGroupSharingTab();
#endif
setVisible(!config()->get("GUI/HideDetailsView").toBool());
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
@ -267,6 +272,17 @@ void DetailsWidget::updateGroupNotesTab()
m_ui->groupNotesEdit->setText(notes);
}
#ifdef WITH_XC_KEESHARE
void DetailsWidget::updateGroupSharingTab()
{
Q_ASSERT(m_currentGroup);
setTabEnabled(m_ui->groupTabWidget, m_ui->groupShareTab, KeeShare::isShared(m_currentGroup));
auto reference = KeeShare::referenceOf(m_currentGroup);
m_ui->groupShareTypeLabel->setText(KeeShare::referenceTypeLabel(reference));
m_ui->groupSharePathLabel->setText(reference.path);
}
#endif
void DetailsWidget::updateTotpLabel()
{
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {

View File

@ -18,6 +18,7 @@
#ifndef KEEPASSX_DETAILSWIDGET_H
#define KEEPASSX_DETAILSWIDGET_H
#include "config-keepassx.h"
#include "gui/DatabaseWidget.h"
#include <QWidget>
@ -55,6 +56,9 @@ private slots:
void updateGroupHeaderLine();
void updateGroupGeneralTab();
void updateGroupNotesTab();
#ifdef WITH_XC_KEESHARE
void updateGroupSharingTab();
#endif
void updateTotpLabel();
void updateTabIndexes();

View File

@ -2,6 +2,14 @@
<ui version="4.0">
<class>DetailsWidget</class>
<widget class="QWidget" name="DetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>652</width>
<height>274</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="spacing">
<number>0</number>
@ -38,7 +46,7 @@
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,0,0,0,0">
<layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,0,0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
@ -76,6 +84,19 @@
</property>
</widget>
</item>
<item>
<spacer name="entryHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="entryTotpWidget" native="true">
<layout class="QGridLayout" name="gridLayout_3">
@ -167,47 +188,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="2">
<widget class="ElidedLabel" name="entryPasswordLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="ElidedLabel" name="entryUrlLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<string/>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="entryUsernameLabel">
<property name="sizePolicy">
@ -252,6 +232,54 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="ElidedLabel" name="entryPasswordLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="entryUrlTitleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>URL</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="ElidedLabel" name="entryUrlLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="entryExpirationTitleLabel">
<property name="sizePolicy">
@ -284,75 +312,15 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="entryUrlTitleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>URL</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="0">
<spacer name="entryLeftHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QLabel" name="entryPasswordTitleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Password</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="2">
<spacer name="verticalSpacer">
<spacer name="entryBottomVerticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
@ -473,7 +441,7 @@
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0,0">
<layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
@ -511,6 +479,19 @@
</property>
</widget>
</item>
<item>
<spacer name="groupHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="groupCloseButton">
<property name="toolTip">
@ -673,10 +654,16 @@
</widget>
</item>
<item row="3" column="2">
<spacer name="verticalSpacer_2">
<spacer name="groupBottomVerticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
@ -701,6 +688,89 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="groupShareTab">
<attribute name="title">
<string>Share</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<widget class="QWidget" name="groupShareWidget" native="true">
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="1" column="2">
<widget class="QLabel" name="groupSharePathLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">&lt;path&gt;</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="groupShareTypeLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">&lt;type&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>147</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

@ -22,10 +22,12 @@
#include "MessageBox.h"
#include "ui_EditWidgetProperties.h"
#include "core/CustomData.h"
#include "core/TimeInfo.h"
EditWidgetProperties::EditWidgetProperties(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EditWidgetProperties())
, m_customData(new CustomData(this))
, m_customDataModel(new QStandardItemModel(this))
{
m_ui->setupUi(this);
@ -51,17 +53,19 @@ void EditWidgetProperties::setFields(const TimeInfo& timeInfo, const QUuid& uuid
m_ui->uuidEdit->setText(uuid.toRfc4122().toHex());
}
void EditWidgetProperties::setCustomData(const CustomData* customData)
void EditWidgetProperties::setCustomData(CustomData* customData)
{
Q_ASSERT(customData);
m_customData->copyDataFrom(customData);
if (m_customData) {
m_customData->disconnect(this);
}
updateModel();
}
m_customData = customData;
const CustomData* EditWidgetProperties::customData() const
{
return m_customData;
if (m_customData) {
connect(m_customData, SIGNAL(modified()), SLOT(update()));
}
update();
}
void EditWidgetProperties::removeSelectedPluginData()
@ -81,7 +85,7 @@ void EditWidgetProperties::removeSelectedPluginData()
const QString key = index.data().toString();
m_customData->remove(key);
}
updateModel();
update();
}
}
@ -90,16 +94,17 @@ void EditWidgetProperties::toggleRemoveButton(const QItemSelection& selected)
m_ui->removeCustomDataButton->setEnabled(!selected.isEmpty());
}
void EditWidgetProperties::updateModel()
void EditWidgetProperties::update()
{
m_customDataModel->clear();
m_customDataModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")});
for (const QString& key : m_customData->keys()) {
m_customDataModel->appendRow(QList<QStandardItem*>() << new QStandardItem(key)
<< new QStandardItem(m_customData->value(key)));
if (!m_customData) {
m_ui->removeCustomDataButton->setEnabled(false);
} else {
for (const QString& key : m_customData->keys()) {
m_customDataModel->appendRow(QList<QStandardItem*>() << new QStandardItem(key)
<< new QStandardItem(m_customData->value(key)));
}
m_ui->removeCustomDataButton->setEnabled(!m_customData->isEmpty());
}
m_ui->removeCustomDataButton->setEnabled(false);
}

View File

@ -23,8 +23,9 @@
#include <QStandardItemModel>
#include <QWidget>
#include "core/CustomData.h"
#include "core/TimeInfo.h"
class CustomData;
class TimeInfo;
class QUuid;
namespace Ui
{
@ -40,21 +41,19 @@ public:
~EditWidgetProperties();
void setFields(const TimeInfo& timeInfo, const QUuid& uuid);
void setCustomData(const CustomData* customData);
const CustomData* customData() const;
void setCustomData(CustomData* customData);
private slots:
void update();
void removeSelectedPluginData();
void toggleRemoveButton(const QItemSelection& selected);
private:
void updateModel();
const QScopedPointer<Ui::EditWidgetProperties> m_ui;
QPointer<CustomData> m_customData;
QPointer<QStandardItemModel> m_customDataModel;
Q_DISABLE_COPY(EditWidgetProperties)
};

View File

@ -79,13 +79,14 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent,
}
}
QString FileDialog::getSaveFileName(QWidget* parent,
const QString& caption,
QString dir,
const QString& filter,
QString* selectedFilter,
QFileDialog::Options options,
const QString& defaultExtension)
QString FileDialog::getFileName(QWidget* parent,
const QString& caption,
QString dir,
const QString& filter,
QString* selectedFilter,
QFileDialog::Options options,
const QString& defaultExtension,
const QString& defaultName)
{
if (!m_nextFileName.isEmpty()) {
QString result = m_nextFileName;
@ -98,6 +99,63 @@ QString FileDialog::getSaveFileName(QWidget* parent,
QString result;
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
Q_UNUSED(defaultName);
Q_UNUSED(defaultExtension);
// the native dialogs on these platforms already append the file extension
result = QFileDialog::getSaveFileName(parent, caption, dir, filter, selectedFilter, options);
#else
QFileDialog dialog(parent, caption, dir, filter);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
if (selectedFilter) {
dialog.selectNameFilter(*selectedFilter);
}
if (!defaultName.isEmpty()) {
dialog.selectFile(defaultName);
}
dialog.setOptions(options);
dialog.setDefaultSuffix(defaultExtension);
dialog.setLabelText(QFileDialog::Accept, QFileDialog::tr("Select"));
QStringList results;
if (dialog.exec()) {
results = dialog.selectedFiles();
if (!results.isEmpty()) {
result = results[0];
}
}
#endif
// on Mac OS X the focus is lost after closing the native dialog
if (parent) {
parent->activateWindow();
}
saveLastDir(result);
return result;
}
}
QString FileDialog::getSaveFileName(QWidget* parent,
const QString& caption,
QString dir,
const QString& filter,
QString* selectedFilter,
QFileDialog::Options options,
const QString& defaultExtension,
const QString& defaultName)
{
if (!m_nextFileName.isEmpty()) {
QString result = m_nextFileName;
m_nextFileName.clear();
return result;
} else {
if (dir.isEmpty()) {
dir = config()->get("LastDir").toString();
}
QString result;
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
Q_UNUSED(defaultName);
Q_UNUSED(defaultExtension);
// the native dialogs on these platforms already append the file extension
result = QFileDialog::getSaveFileName(parent, caption, dir, filter, selectedFilter, options);
@ -108,6 +166,9 @@ QString FileDialog::getSaveFileName(QWidget* parent,
if (selectedFilter) {
dialog.selectNameFilter(*selectedFilter);
}
if (!defaultName.isEmpty()) {
dialog.selectFile(defaultName);
}
dialog.setOptions(options);
dialog.setDefaultSuffix(defaultExtension);
@ -130,8 +191,7 @@ QString FileDialog::getSaveFileName(QWidget* parent,
}
}
QString
FileDialog::getExistingDirectory(QWidget* parent, const QString& caption, QString dir, QFileDialog::Options options)
QString FileDialog::getExistingDirectory(QWidget* parent, const QString& caption, QString dir, QFileDialog::Options options)
{
if (!m_nextDirName.isEmpty()) {
QString result = m_nextDirName;

View File

@ -35,13 +35,22 @@ public:
const QString& filter = QString(),
QString* selectedFilter = nullptr,
QFileDialog::Options options = 0);
QString getFileName(QWidget* parent = nullptr,
const QString& caption = QString(),
QString dir = QString(),
const QString& filter = QString(),
QString* selectedFilter = nullptr,
QFileDialog::Options options = 0,
const QString& defaultExtension = QString(),
const QString& defaultName = QString());
QString getSaveFileName(QWidget* parent = nullptr,
const QString& caption = QString(),
QString dir = QString(),
const QString& filter = QString(),
QString* selectedFilter = nullptr,
QFileDialog::Options options = 0,
const QString& defaultExtension = QString());
const QString& defaultExtension = QString(),
const QString& defaultName = QString());
QString getExistingDirectory(QWidget* parent = nullptr,
const QString& caption = QString(),
QString dir = QString(),

View File

@ -40,7 +40,10 @@
#include "sshagent/AgentSettingsPage.h"
#include "sshagent/SSHAgent.h"
#endif
#ifdef WITH_XC_KEESHARE
#include "keeshare/KeeShare.h"
#include "keeshare/SettingsPageKeeShare.h"
#endif
#ifdef WITH_XC_BROWSER
#include "browser/BrowserOptionDialog.h"
#include "browser/BrowserSettings.h"
@ -141,22 +144,26 @@ MainWindow::MainWindow()
m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size();
restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray());
#ifdef WITH_XC_BROWSER
m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget));
#endif
#ifdef WITH_XC_SSHAGENT
SSHAgent::init(this);
connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString)));
m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget));
#endif
#ifdef WITH_XC_KEESHARE
KeeShare::init(this);
m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget));
#endif
setWindowIcon(filePath()->applicationIcon());
m_ui->globalMessageWidget->setHidden(true);
connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl);
connect(
m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show()));
connect(
m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide()));
connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show()));
connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide()));
m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile);
m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases);

View File

@ -608,8 +608,19 @@
<string>Report a &amp;bug</string>
</property>
</action>
<action name="actionShare_entry">
<property name="text">
<string>Share entry</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>PasswordGeneratorWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordGeneratorWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
@ -634,12 +645,6 @@
<header>gui/WelcomeWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PasswordGeneratorWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordGeneratorWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -21,11 +21,36 @@
#include "DatabaseSettingsWidgetGeneral.h"
#include "DatabaseSettingsWidgetEncryption.h"
#include "DatabaseSettingsWidgetMasterKey.h"
#ifdef WITH_XC_KEESHARE
#include "keeshare/DatabaseSettingsPageKeeShare.h"
#endif
#include "core/Global.h"
#include "core/Config.h"
#include "core/FilePath.h"
#include "core/Database.h"
class DatabaseSettingsDialog::ExtraPage
{
public:
ExtraPage(IDatabaseSettingsPage* page, QWidget* widget)
: settingsPage(page)
, widget(widget)
{
}
void loadSettings(Database* db) const
{
settingsPage->loadSettings(widget, db);
}
void saveSettings() const
{
settingsPage->saveSettings(widget);
}
private:
QSharedPointer<IDatabaseSettingsPage> settingsPage;
QWidget* widget;
};
DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
: DialogyWidget(parent)
, m_ui(new Ui::DatabaseSettingsDialog())
@ -47,6 +72,10 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
#ifdef WITH_XC_KEESHARE
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
m_ui->stackedWidget->setCurrentIndex(0);
m_securityTabWidget->setCurrentIndex(0);
@ -67,10 +96,24 @@ void DatabaseSettingsDialog::load(Database* db)
m_generalWidget->load(db);
m_masterKeyWidget->load(db);
m_encryptionWidget->load(db);
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(db);
}
m_ui->advancedSettingsToggle->setChecked(config()->get("GUI/AdvancedSettings", false).toBool());
m_db = db;
}
void DatabaseSettingsDialog::addSettingsPage(IDatabaseSettingsPage* page)
{
const int category = m_ui->categoryList->currentCategory();
QWidget* widget = page->createWidget();
widget->setParent(this);
m_extraPages.append(ExtraPage(page, widget));
m_ui->stackedWidget->addWidget(widget);
m_ui->categoryList->addCategory(page->name(), page->icon());
m_ui->categoryList->setCurrentCategory(category);
}
/**
* Show page and tab with database master key settings.
*/
@ -94,6 +137,10 @@ void DatabaseSettingsDialog::save()
return;
}
for (const ExtraPage& extraPage : asConst(m_extraPages)) {
extraPage.saveSettings();
}
#ifdef WITH_XC_TOUCHID
TouchID::getInstance().reset(m_db ? m_db->filePath() : "");
#endif

View File

@ -34,6 +34,19 @@ namespace Ui
class DatabaseSettingsDialog;
}
class IDatabaseSettingsPage
{
public:
virtual ~IDatabaseSettingsPage()
{
}
virtual QString name() = 0;
virtual QIcon icon() = 0;
virtual QWidget* createWidget() = 0;
virtual void loadSettings(QWidget* widget, Database* db) = 0;
virtual void saveSettings(QWidget* widget) = 0;
};
class DatabaseSettingsDialog : public DialogyWidget
{
Q_OBJECT
@ -44,6 +57,7 @@ public:
Q_DISABLE_COPY(DatabaseSettingsDialog);
void load(Database* db);
void addSettingsPage(IDatabaseSettingsPage* page);
void showMasterKeySettings();
signals:
@ -68,6 +82,9 @@ private:
QPointer<QTabWidget> m_securityTabWidget;
QPointer<DatabaseSettingsWidgetMasterKey> m_masterKeyWidget;
QPointer<DatabaseSettingsWidgetEncryption> m_encryptionWidget;
class ExtraPage;
QList<ExtraPage> m_extraPages;
};
#endif // KEEPASSX_DATABASESETTINGSWIDGET_H

View File

@ -44,8 +44,8 @@
#include "core/TimeDelta.h"
#include "core/Tools.h"
#ifdef WITH_XC_SSHAGENT
#include "crypto/ssh/OpenSSHKey.h"
#include "sshagent/KeeAgentSettings.h"
#include "sshagent/OpenSSHKey.h"
#include "sshagent/SSHAgent.h"
#endif
#include "gui/Clipboard.h"
@ -67,11 +67,14 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
, m_autoTypeUi(new Ui::EditEntryWidgetAutoType())
, m_sshAgentUi(new Ui::EditEntryWidgetSSHAgent())
, m_historyUi(new Ui::EditEntryWidgetHistory())
, m_customData(new CustomData())
, m_mainWidget(new QWidget())
, m_advancedWidget(new QWidget())
, m_iconsWidget(new EditWidgetIcons())
, m_autoTypeWidget(new QWidget())
#ifdef WITH_XC_SSHAGENT
, m_sshAgentWidget(new QWidget())
#endif
, m_editWidgetProperties(new EditWidgetProperties())
, m_historyWidget(new QWidget())
, m_entryAttributes(new EntryAttributes(this))
@ -87,6 +90,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
setupAdvanced();
setupIcon();
setupAutoType();
#ifdef WITH_XC_SSHAGENT
if (config()->get("SSHAgent", false).toBool()) {
setupSSHAgent();
@ -95,6 +99,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
m_sshAgentEnabled = false;
}
#endif
setupProperties();
setupHistory();
setupEntryUpdate();
@ -108,6 +113,8 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
connect(m_iconsWidget, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage()));
m_mainUi->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0);
m_editWidgetProperties->setCustomData(m_customData.data());
}
EditEntryWidget::~EditEntryWidget()
@ -522,13 +529,13 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt)
return false;
}
if (!key.parse(privateKeyData)) {
if (!key.parsePKCS1PEM(privateKeyData)) {
showMessage(key.errorString(), MessageWidget::Error);
return false;
}
if (key.encrypted() && (decrypt || key.publicKey().isEmpty())) {
if (!key.openPrivateKey(m_entry->password())) {
if (!key.openKey(m_entry->password())) {
showMessage(key.errorString(), MessageWidget::Error);
return false;
}
@ -669,6 +676,8 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q
void EditEntryWidget::setForms(const Entry* entry, bool restore)
{
m_customData->copyDataFrom(entry->customData());
m_mainUi->titleEdit->setReadOnly(m_history);
m_mainUi->usernameEdit->setReadOnly(m_history);
m_mainUi->urlEdit->setReadOnly(m_history);
@ -764,7 +773,6 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore)
#endif
m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid());
m_editWidgetProperties->setCustomData(entry->customData());
if (!m_history && !restore) {
m_historyModel->setEntries(entry->historyItems());
@ -873,7 +881,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
entry->attributes()->copyCustomKeysFrom(m_entryAttributes);
entry->attachments()->copyDataFrom(m_advancedUi->attachmentsWidget->entryAttachments());
entry->customData()->copyDataFrom(m_editWidgetProperties->customData());
entry->customData()->copyDataFrom(m_customData.data());
entry->setTitle(m_mainUi->titleEdit->text().replace(newLineRegex, " "));
entry->setUsername(m_mainUi->usernameEdit->text().replace(newLineRegex, " "));
entry->setUrl(m_mainUi->urlEdit->text().replace(newLineRegex, " "));

View File

@ -28,6 +28,7 @@
class AutoTypeAssociations;
class AutoTypeAssociationsModel;
class CustomData;
class Database;
class EditWidgetIcons;
class EditWidgetProperties;
@ -153,11 +154,15 @@ private:
const QScopedPointer<Ui::EditEntryWidgetAutoType> m_autoTypeUi;
const QScopedPointer<Ui::EditEntryWidgetSSHAgent> m_sshAgentUi;
const QScopedPointer<Ui::EditEntryWidgetHistory> m_historyUi;
const QScopedPointer<CustomData> m_customData;
QWidget* const m_mainWidget;
QWidget* const m_advancedWidget;
EditWidgetIcons* const m_iconsWidget;
QWidget* const m_autoTypeWidget;
#ifdef WITH_XC_SSHAGENT
QWidget* const m_sshAgentWidget;
#endif
EditWidgetProperties* const m_editWidgetProperties;
QWidget* const m_historyWidget;
EntryAttributes* const m_entryAttributes;

View File

@ -23,14 +23,40 @@
#include "gui/EditWidgetIcons.h"
#include "gui/EditWidgetProperties.h"
#ifdef WITH_XC_KEESHARE
#include "keeshare/group/EditGroupPageKeeShare.h"
#endif
class EditGroupWidget::ExtraPage
{
public:
ExtraPage(IEditGroupPage* page, QWidget* widget)
: editPage(page)
, widget(widget)
{
}
void set(Group* temporaryGroup) const
{
editPage->set(widget, temporaryGroup);
}
void assign() const
{
editPage->assign(widget);
}
private:
QSharedPointer<IEditGroupPage> editPage;
QWidget* widget;
};
EditGroupWidget::EditGroupWidget(QWidget* parent)
: EditWidget(parent)
, m_mainUi(new Ui::EditGroupWidgetMain())
, m_editGroupWidgetMain(new QWidget())
, m_editGroupWidgetIcons(new EditWidgetIcons())
, m_editWidgetProperties(new EditWidgetProperties())
, m_group(nullptr)
, m_database(nullptr)
{
m_mainUi->setupUi(m_editGroupWidgetMain);
@ -52,6 +78,10 @@ EditGroupWidget::EditGroupWidget(QWidget* parent)
SIGNAL(messageEditEntry(QString, MessageWidget::MessageType)),
SLOT(showMessage(QString, MessageWidget::MessageType)));
connect(m_editGroupWidgetIcons, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage()));
#ifdef WITH_XC_KEESHARE
addEditPage(new EditGroupPageKeeShare(this));
#endif
}
EditGroupWidget::~EditGroupWidget()
@ -63,6 +93,8 @@ void EditGroupWidget::loadGroup(Group* group, bool create, Database* database)
m_group = group;
m_database = database;
m_temporaryGroup.reset(group->clone(Entry::CloneNoFlags, Group::CloneNoFlags));
if (create) {
setHeadline(tr("Add group"));
} else {
@ -91,12 +123,15 @@ void EditGroupWidget::loadGroup(Group* group, bool create, Database* database)
m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence());
IconStruct iconStruct;
iconStruct.uuid = group->iconUuid();
iconStruct.number = group->iconNumber();
m_editGroupWidgetIcons->load(group->uuid(), database, iconStruct);
iconStruct.uuid = m_temporaryGroup->iconUuid();
iconStruct.number = m_temporaryGroup->iconNumber();
m_editGroupWidgetIcons->load(m_temporaryGroup->uuid(), m_database, iconStruct);
m_editWidgetProperties->setFields(m_temporaryGroup->timeInfo(), m_temporaryGroup->uuid());
m_editWidgetProperties->setCustomData(m_temporaryGroup->customData());
m_editWidgetProperties->setFields(group->timeInfo(), group->uuid());
m_editWidgetProperties->setCustomData(group->customData());
for (const ExtraPage& page : asConst(m_extraPages)) {
page.set(m_temporaryGroup.data());
}
setCurrentPage(0);
@ -112,50 +147,61 @@ void EditGroupWidget::save()
void EditGroupWidget::apply()
{
m_group->setName(m_mainUi->editName->text());
m_group->setNotes(m_mainUi->editNotes->toPlainText());
m_group->setExpires(m_mainUi->expireCheck->isChecked());
m_group->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC());
m_temporaryGroup->setName(m_mainUi->editName->text());
m_temporaryGroup->setNotes(m_mainUi->editNotes->toPlainText());
m_temporaryGroup->setExpires(m_mainUi->expireCheck->isChecked());
m_temporaryGroup->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC());
m_group->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex()));
m_group->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex()));
m_group->customData()->copyDataFrom(m_editWidgetProperties->customData());
m_temporaryGroup->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex()));
m_temporaryGroup->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex()));
if (m_mainUi->autoTypeSequenceInherit->isChecked()) {
m_group->setDefaultAutoTypeSequence(QString());
m_temporaryGroup->setDefaultAutoTypeSequence(QString());
} else {
m_group->setDefaultAutoTypeSequence(m_mainUi->autoTypeSequenceCustomEdit->text());
m_temporaryGroup->setDefaultAutoTypeSequence(m_mainUi->autoTypeSequenceCustomEdit->text());
}
IconStruct iconStruct = m_editGroupWidgetIcons->state();
if (iconStruct.number < 0) {
m_group->setIcon(Group::DefaultIconNumber);
m_temporaryGroup->setIcon(Group::DefaultIconNumber);
} else if (iconStruct.uuid.isNull()) {
m_group->setIcon(iconStruct.number);
m_temporaryGroup->setIcon(iconStruct.number);
} else {
m_group->setIcon(iconStruct.uuid);
m_temporaryGroup->setIcon(iconStruct.uuid);
}
for (const ExtraPage& page : asConst(m_extraPages)) {
page.assign();
}
// Icons add/remove are applied globally outside the transaction!
m_group->copyDataFrom(m_temporaryGroup.data());
}
void EditGroupWidget::cancel()
{
if (!m_group->iconUuid().isNull() && !m_database->metadata()->containsCustomIcon(m_group->iconUuid())) {
m_group->setIcon(Entry::DefaultIconNumber);
}
clear();
emit editFinished(false);
}
void EditGroupWidget::clear()
{
m_group = nullptr;
m_database = nullptr;
m_temporaryGroup.reset(nullptr);
m_database.clear();
m_group.clear();
m_editGroupWidgetIcons->reset();
}
void EditGroupWidget::addEditPage(IEditGroupPage* page)
{
QWidget* widget = page->createWidget();
widget->setParent(this);
m_extraPages.append(ExtraPage(page, widget));
addPage(page->name(), page->icon(), widget);
}
void EditGroupWidget::addTriStateItems(QComboBox* comboBox, bool inheritDefault)
{
QString inheritDefaultString;

View File

@ -24,6 +24,7 @@
#include "core/Group.h"
#include "gui/EditWidget.h"
class CustomData;
class EditWidgetIcons;
class EditWidgetProperties;
@ -33,6 +34,19 @@ namespace Ui
class EditWidget;
}
class IEditGroupPage
{
public:
virtual ~IEditGroupPage()
{
}
virtual QString name() = 0;
virtual QIcon icon() = 0;
virtual QWidget* createWidget() = 0;
virtual void set(QWidget* widget, Group* tempoaryGroup) = 0;
virtual void assign(QWidget* widget) = 0;
};
class EditGroupWidget : public EditWidget
{
Q_OBJECT
@ -44,6 +58,8 @@ public:
void loadGroup(Group* group, bool create, Database* database);
void clear();
void addEditPage(IEditGroupPage* page);
signals:
void editFinished(bool accepted);
void messageEditEntry(QString, MessageWidget::MessageType);
@ -60,12 +76,17 @@ private:
Group::TriState triStateFromIndex(int index);
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
QPointer<QWidget> m_editGroupWidgetMain;
QPointer<EditWidgetIcons> m_editGroupWidgetIcons;
QPointer<EditWidgetProperties> m_editWidgetProperties;
QPointer<Group> m_group;
QScopedPointer<Group> m_temporaryGroup;
QPointer<Database> m_database;
QPointer<Group> m_group;
class ExtraPage;
QList<ExtraPage> m_extraPages;
Q_DISABLE_COPY(EditGroupWidget)
};

View File

@ -25,6 +25,7 @@
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Tools.h"
#include "keeshare/KeeShare.h"
GroupModel::GroupModel(Database* db, QObject* parent)
: QAbstractItemModel(parent)
@ -125,13 +126,18 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const
Group* group = groupFromIndex(index);
if (role == Qt::DisplayRole) {
return group->name();
QString nameTemplate = tr("%1", "Template for name without annotation");
#ifdef WITH_XC_KEESHARE
nameTemplate = KeeShare::indicatorSuffix(group, nameTemplate);
#endif
return nameTemplate.arg(group->name());
} else if (role == Qt::DecorationRole) {
if (group->isExpired()) {
return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex);
} else {
return group->iconScaledPixmap();
}
QPixmap pixmap = group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex)
: group->iconScaledPixmap();
#ifdef WITH_XC_KEESHARE
pixmap = KeeShare::indicatorBadge(group, pixmap);
#endif
return pixmap;
} else if (role == Qt::FontRole) {
QFont font;
if (group->isExpired()) {

View File

@ -0,0 +1,19 @@
if(WITH_XC_KEESHARE)
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(keeshare_SOURCES
SettingsPageKeeShare.cpp
SettingsWidgetKeeShare.cpp
DatabaseSettingsPageKeeShare.cpp
DatabaseSettingsWidgetKeeShare.cpp
group/EditGroupWidgetKeeShare.cpp
group/EditGroupPageKeeShare.cpp
KeeShare.cpp
KeeShareSettings.cpp
ShareObserver.cpp
Signature.cpp
)
add_library(keeshare STATIC ${keeshare_SOURCES})
target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB})
endif()

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "DatabaseSettingsPageKeeShare.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include "keeshare/DatabaseSettingsWidgetKeeShare.h"
#include "keeshare/KeeShare.h"
#include <QApplication>
QString DatabaseSettingsPageKeeShare::name()
{
return QApplication::tr("KeeShare");
}
QIcon DatabaseSettingsPageKeeShare::icon()
{
return FilePath::instance()->icon("apps", "preferences-system-network-sharing");
}
QWidget* DatabaseSettingsPageKeeShare::createWidget()
{
return new DatabaseSettingsWidgetKeeShare();
}
void DatabaseSettingsPageKeeShare::loadSettings(QWidget* widget, Database* db)
{
DatabaseSettingsWidgetKeeShare* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetKeeShare*>(widget);
settingsWidget->loadSettings(db);
}
void DatabaseSettingsPageKeeShare::saveSettings(QWidget* widget)
{
DatabaseSettingsWidgetKeeShare* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetKeeShare*>(widget);
settingsWidget->saveSettings();
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_DATABASESETTINGSPAGEKEESHARE_H
#define KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H
#include <QObject>
#include <QPointer>
#include <QWidget>
#include "gui/dbsettings/DatabaseSettingsDialog.h"
class DatabaseSettingsPageKeeShare : public IDatabaseSettingsPage
{
public:
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void loadSettings(QWidget* widget, Database* db) override;
void saveSettings(QWidget* widget) override;
};
#endif // KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "DatabaseSettingsWidgetKeeShare.h"
#include "ui_DatabaseSettingsWidgetKeeShare.h"
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "keeshare/KeeShare.h"
#include "keeshare/KeeShareSettings.h"
#include <QMessageBox>
#include <QStandardItemModel>
DatabaseSettingsWidgetKeeShare::DatabaseSettingsWidgetKeeShare(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::DatabaseSettingsWidgetKeeShare())
{
m_ui->setupUi(this);
}
DatabaseSettingsWidgetKeeShare::~DatabaseSettingsWidgetKeeShare()
{
}
void DatabaseSettingsWidgetKeeShare::loadSettings(Database* db)
{
m_db = db;
m_referencesModel.reset(new QStandardItemModel());
m_referencesModel->setHorizontalHeaderLabels(
QStringList() << tr("Breadcrumb") << tr("Type") << tr("Path") << tr("Last Signer") << tr("Certificates"));
const QList<Group*> groups = db->rootGroup()->groupsRecursive(true);
for (const Group* group : groups) {
if (!KeeShare::isShared(group)) {
continue;
}
const KeeShareSettings::Reference reference = KeeShare::referenceOf(group);
QStringList hierarchy = group->hierarchy();
hierarchy.removeFirst();
QList<QStandardItem*> row = QList<QStandardItem*>();
row << new QStandardItem(hierarchy.join(" > "));
row << new QStandardItem(KeeShare::referenceTypeLabel(reference));
row << new QStandardItem(reference.path);
m_referencesModel->appendRow(row);
}
m_ui->sharedGroupsView->setModel(m_referencesModel.data());
}
void DatabaseSettingsWidgetKeeShare::saveSettings()
{
// nothing to do - the tab is passive
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_DATABASESETTINGSWIDGETKEESHARE_H
#define KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
class Database;
class QStandardItemModel;
namespace Ui
{
class DatabaseSettingsWidgetKeeShare;
}
class DatabaseSettingsWidgetKeeShare : public QWidget
{
Q_OBJECT
public:
explicit DatabaseSettingsWidgetKeeShare(QWidget* parent = nullptr);
~DatabaseSettingsWidgetKeeShare();
void loadSettings(Database* db);
void saveSettings();
private:
QScopedPointer<Ui::DatabaseSettingsWidgetKeeShare> m_ui;
QScopedPointer<QStandardItemModel> m_referencesModel;
QPointer<Database> m_db;
};
#endif // KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetKeeShare</class>
<widget class="QWidget" name="DatabaseSettingsWidgetKeeShare">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<height>379</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="enableGroupBox">
<property name="title">
<string>Sharing</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" rowspan="2">
<widget class="QTableView" name="sharedGroupsView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<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>
<resources/>
<connections/>
</ui>

234
src/keeshare/KeeShare.cpp Normal file
View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "KeeShare.h"
#include "core/Config.h"
#include "core/CustomData.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "crypto/ssh/OpenSSHKey.h"
#include "keeshare/ShareObserver.h"
#include "keeshare/Signature.h"
#include <QMessageBox>
#include <QPainter>
#include <QPushButton>
namespace
{
static const QString KeeShare_Reference("KeeShare/Reference");
static const QString KeeShare_Own("KeeShare/Settings.own");
static const QString KeeShare_Foreign("KeeShare/Settings.foreign");
static const QString KeeShare_Active("KeeShare/Settings.active");
}
KeeShare* KeeShare::m_instance = nullptr;
KeeShare* KeeShare::instance()
{
if (!m_instance) {
qFatal("Race condition: instance wanted before it was initialized, this is a bug.");
}
return m_instance;
}
void KeeShare::init(QObject* parent)
{
Q_ASSERT(!m_instance);
m_instance = new KeeShare(parent);
}
KeeShareSettings::Own KeeShare::own()
{
return KeeShareSettings::Own::deserialize(config()->get(KeeShare_Own).toString());
}
KeeShareSettings::Active KeeShare::active()
{
return KeeShareSettings::Active::deserialize(config()->get(KeeShare_Active).toString());
}
KeeShareSettings::Foreign KeeShare::foreign()
{
return KeeShareSettings::Foreign::deserialize(config()->get(KeeShare_Foreign).toString());
}
void KeeShare::setForeign(const KeeShareSettings::Foreign& foreign)
{
config()->set(KeeShare_Foreign, KeeShareSettings::Foreign::serialize(foreign));
}
void KeeShare::setActive(const KeeShareSettings::Active& active)
{
config()->set(KeeShare_Active, KeeShareSettings::Active::serialize(active));
}
void KeeShare::setOwn(const KeeShareSettings::Own& own)
{
config()->set(KeeShare_Own, KeeShareSettings::Own::serialize(own));
}
bool KeeShare::isShared(const Group* group)
{
return group->customData()->contains(KeeShare_Reference);
}
KeeShareSettings::Reference KeeShare::referenceOf(const Group* group)
{
static const KeeShareSettings::Reference s_emptyReference;
const CustomData* customData = group->customData();
if (!customData->contains(KeeShare_Reference)) {
return s_emptyReference;
}
const auto encoded = customData->value(KeeShare_Reference);
const auto serialized = QString::fromUtf8(QByteArray::fromBase64(encoded.toLatin1()));
KeeShareSettings::Reference reference = KeeShareSettings::Reference::deserialize(serialized);
if (reference.isNull()) {
qWarning("Invalid sharing reference detected - sharing disabled");
return s_emptyReference;
}
return reference;
}
void KeeShare::setReferenceTo(Group* group, const KeeShareSettings::Reference& reference)
{
CustomData* customData = group->customData();
if (reference.isNull()) {
customData->remove(KeeShare_Reference);
return;
}
const auto serialized = KeeShareSettings::Reference::serialize(reference);
const auto encoded = serialized.toUtf8().toBase64();
customData->set(KeeShare_Reference, encoded);
}
QPixmap KeeShare::indicatorBadge(const Group* group, QPixmap pixmap)
{
if (!isShared(group)) {
return pixmap;
}
const auto reference = KeeShare::referenceOf(group);
const auto active = KeeShare::active();
const bool enabled = (reference.isImporting() && active.in) || (reference.isExporting() && active.out);
const QPixmap badge = enabled ? databaseIcons()->iconPixmap(DatabaseIcons::SharedIconIndex)
: databaseIcons()->iconPixmap(DatabaseIcons::UnsharedIconIndex);
QImage canvas = pixmap.toImage();
const QRectF target(canvas.width() * 0.4, canvas.height() * 0.4, canvas.width() * 0.6, canvas.height() * 0.6);
QPainter painter(&canvas);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawPixmap(target, badge, badge.rect());
pixmap.convertFromImage(canvas);
return pixmap;
}
QString KeeShare::referenceTypeLabel(const KeeShareSettings::Reference& reference)
{
switch (reference.type) {
case KeeShareSettings::Inactive:
return tr("Disabled share");
case KeeShareSettings::ImportFrom:
return tr("Import from");
case KeeShareSettings::ExportTo:
return tr("Export to");
case KeeShareSettings::SynchronizeWith:
return tr("Synchronize with");
}
return "";
}
QString KeeShare::indicatorSuffix(const Group* group, const QString& text)
{
// we not adjust the display name for now - it's just an alternative to the icon
Q_UNUSED(group);
return text;
}
void KeeShare::connectDatabase(Database* newDb, Database* oldDb)
{
if (oldDb && m_observersByDatabase.contains(oldDb)) {
QPointer<ShareObserver> observer = m_observersByDatabase.take(oldDb);
if (observer) {
delete observer;
}
}
if (newDb && !m_observersByDatabase.contains(newDb)) {
QPointer<ShareObserver> observer(new ShareObserver(newDb, newDb));
m_observersByDatabase[newDb] = observer;
connect(observer.data(),
SIGNAL(sharingMessage(QString, MessageWidget::MessageType)),
this,
SLOT(emitSharingMessage(QString, MessageWidget::MessageType)));
}
}
void KeeShare::handleDatabaseOpened(Database* db)
{
QPointer<ShareObserver> observer = m_observersByDatabase.value(db);
if (observer) {
observer->handleDatabaseOpened();
}
}
void KeeShare::handleDatabaseSaved(Database* db)
{
QPointer<ShareObserver> observer = m_observersByDatabase.value(db);
if (observer) {
observer->handleDatabaseSaved();
}
}
void KeeShare::emitSharingMessage(const QString& message, KMessageWidget::MessageType type)
{
QObject* observer = sender();
Database* db = m_databasesByObserver.value(observer);
if (db) {
emit sharingMessage(db, message, type);
}
}
void KeeShare::handleDatabaseDeleted(QObject* db)
{
auto observer = m_observersByDatabase.take(db);
if (observer) {
m_databasesByObserver.remove(observer);
}
}
void KeeShare::handleObserverDeleted(QObject* observer)
{
auto database = m_databasesByObserver.take(observer);
if (database) {
m_observersByDatabase.remove(database);
}
}
void KeeShare::handleSettingsChanged(const QString& key)
{
if (key == KeeShare_Active) {
emit activeChanged();
}
}
KeeShare::KeeShare(QObject* parent)
: QObject(parent)
{
connect(config(), SIGNAL(changed(QString)), this, SLOT(handleSettingsChanged(QString)));
}

79
src/keeshare/KeeShare.h Normal file
View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_KEESHARE_H
#define KEEPASSXC_KEESHARE_H
#include <QMap>
#include <QObject>
#include "gui/MessageWidget.h"
#include "keeshare/KeeShareSettings.h"
class Group;
class Database;
class ShareObserver;
class QXmlStreamWriter;
class QXmlStreamReader;
class KeeShare : public QObject
{
Q_OBJECT
public:
static KeeShare* instance();
static void init(QObject* parent);
static QString indicatorSuffix(const Group* group, const QString& text);
static QPixmap indicatorBadge(const Group* group, QPixmap pixmap);
static bool isShared(const Group* group);
static KeeShareSettings::Own own();
static KeeShareSettings::Active active();
static KeeShareSettings::Foreign foreign();
static void setForeign(const KeeShareSettings::Foreign& foreign);
static void setActive(const KeeShareSettings::Active& active);
static void setOwn(const KeeShareSettings::Own& own);
static KeeShareSettings::Reference referenceOf(const Group* group);
static void setReferenceTo(Group* group, const KeeShareSettings::Reference& reference);
static QString referenceTypeLabel(const KeeShareSettings::Reference& reference);
void connectDatabase(Database* newDb, Database* oldDb);
void handleDatabaseOpened(Database* db);
void handleDatabaseSaved(Database* db);
signals:
void activeChanged();
void sharingMessage(Database*, QString, MessageWidget::MessageType);
private slots:
void emitSharingMessage(const QString&, MessageWidget::MessageType);
void handleDatabaseDeleted(QObject*);
void handleObserverDeleted(QObject*);
void handleSettingsChanged(const QString&);
private:
static KeeShare* m_instance;
explicit KeeShare(QObject* parent);
QMap<QObject*, QPointer<ShareObserver>> m_observersByDatabase;
QMap<QObject*, QPointer<Database>> m_databasesByObserver;
};
#endif // KEEPASSXC_KEESHARE_H

View File

@ -0,0 +1,463 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "KeeShareSettings.h"
#include "core/CustomData.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "crypto/ssh/OpenSSHKey.h"
#include "keeshare/Signature.h"
#include <QMessageBox>
#include <QPainter>
#include <QPushButton>
namespace KeeShareSettings
{
namespace
{
Certificate packCertificate(const OpenSSHKey& key, bool verified, const QString& signer)
{
KeeShareSettings::Certificate extracted;
extracted.trusted = verified;
extracted.signer = signer;
Q_ASSERT(key.type() == "ssh-rsa");
extracted.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key);
return extracted;
}
Key packKey(const OpenSSHKey& key)
{
KeeShareSettings::Key extracted;
Q_ASSERT(key.type() == "ssh-rsa");
extracted.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, key);
return extracted;
}
OpenSSHKey unpackKey(const Key& sign)
{
if (sign.key.isEmpty()) {
return OpenSSHKey();
}
OpenSSHKey key = OpenSSHKey::restoreFromBinary(OpenSSHKey::Private, sign.key);
Q_ASSERT(key.type() == "ssh-rsa");
return key;
}
OpenSSHKey unpackCertificate(const Certificate& certificate)
{
if (certificate.key.isEmpty()) {
return OpenSSHKey();
}
OpenSSHKey key = OpenSSHKey::restoreFromBinary(OpenSSHKey::Public, certificate.key);
Q_ASSERT(key.type() == "ssh-rsa");
return key;
}
QString xmlSerialize(std::function<void(QXmlStreamWriter& writer)> specific)
{
QString buffer;
QXmlStreamWriter writer(&buffer);
writer.setCodec(QTextCodec::codecForName("UTF-8"));
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
writer.writeStartElement("KeeShare");
writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
writer.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
specific(writer);
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndDocument();
return buffer;
}
void xmlDeserialize(const QString& raw, std::function<void(QXmlStreamReader& reader)> specific)
{
QXmlStreamReader reader(raw);
if (!reader.readNextStartElement() || reader.qualifiedName() != "KeeShare") {
return;
}
specific(reader);
}
}
void Certificate::serialize(QXmlStreamWriter& writer, const Certificate& certificate)
{
if (certificate.isNull()) {
return;
}
writer.writeStartElement("Signer");
writer.writeCharacters(certificate.signer);
writer.writeEndElement();
writer.writeStartElement("Trusted");
writer.writeCharacters(certificate.trusted ? "True" : "False");
writer.writeEndElement();
writer.writeStartElement("Key");
writer.writeCharacters(certificate.key.toBase64());
writer.writeEndElement();
}
bool Certificate::operator==(const Certificate& other) const
{
return trusted == other.trusted && key == other.key && signer == other.signer;
}
bool Certificate::operator!=(const Certificate& other) const
{
return !operator==(other);
}
bool Certificate::isNull() const
{
return !trusted && key.isEmpty() && signer.isEmpty();
}
QString Certificate::fingerprint() const
{
if (isNull()) {
return {};
}
return sshKey().fingerprint();
}
OpenSSHKey Certificate::sshKey() const
{
return unpackCertificate(*this);
}
QString Certificate::publicKey() const
{
if (isNull()) {
return {};
}
return sshKey().publicKey();
}
Certificate Certificate::deserialize(QXmlStreamReader& reader)
{
Certificate certificate;
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Signer") {
certificate.signer = reader.readElementText();
} else if (reader.name() == "Trusted") {
certificate.trusted = reader.readElementText() == "True";
} else if (reader.name() == "Key") {
certificate.key = QByteArray::fromBase64(reader.readElementText().toLatin1());
}
}
return certificate;
}
bool Key::operator==(const Key& other) const
{
return key == other.key;
}
bool Key::operator!=(const Key& other) const
{
return !operator==(other);
}
bool Key::isNull() const
{
return key.isEmpty();
}
QString Key::privateKey() const
{
if (isNull()) {
return {};
}
return sshKey().privateKey();
}
OpenSSHKey Key::sshKey() const
{
return unpackKey(*this);
}
void Key::serialize(QXmlStreamWriter& writer, const Key& key)
{
if (key.isNull()) {
return;
}
writer.writeCharacters(key.key.toBase64());
}
Key Key::deserialize(QXmlStreamReader& reader)
{
Key key;
key.key = QByteArray::fromBase64(reader.readElementText().toLatin1());
return key;
}
Own Own::generate()
{
OpenSSHKey key = OpenSSHKey::generate(false);
key.openKey(QString());
Own own;
own.key = packKey(key);
const QString name = qgetenv("USER"); // + "@" + QHostInfo::localHostName();
own.certificate = packCertificate(key, true, name);
return own;
}
QString Active::serialize(const Active& active)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("Active");
if (active.in) {
writer.writeEmptyElement("Import");
}
if (active.out) {
writer.writeEmptyElement("Export");
}
writer.writeEndElement();
});
}
Active Active::deserialize(const QString& raw)
{
Active active;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Active") {
while (reader.readNextStartElement()) {
if (reader.name() == "Import") {
active.in = true;
reader.skipCurrentElement();
} else if (reader.name() == "Export") {
active.out = true;
reader.skipCurrentElement();
} else {
break;
}
}
} else {
::qWarning() << "Unknown KeeShareSettings element" << reader.name();
reader.skipCurrentElement();
}
}
});
return active;
}
bool Own::operator==(const Own& other) const
{
return key == other.key && certificate == other.certificate;
}
bool Own::operator!=(const Own& other) const
{
return !operator==(other);
}
QString Own::serialize(const Own& own)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("PrivateKey");
Key::serialize(writer, own.key);
writer.writeEndElement();
writer.writeStartElement("PublicKey");
Certificate::serialize(writer, own.certificate);
writer.writeEndElement();
});
}
Own Own::deserialize(const QString& raw)
{
Own own;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "PrivateKey") {
own.key = Key::deserialize(reader);
} else if (reader.name() == "PublicKey") {
own.certificate = Certificate::deserialize(reader);
} else {
::qWarning() << "Unknown KeeShareSettings element" << reader.name();
reader.skipCurrentElement();
}
}
});
return own;
}
QString Foreign::serialize(const Foreign& foreign)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("Foreign");
for (const Certificate& certificate : foreign.certificates) {
writer.writeStartElement("Certificate");
Certificate::serialize(writer, certificate);
writer.writeEndElement();
}
writer.writeEndElement();
});
}
Foreign Foreign::deserialize(const QString& raw)
{
Foreign foreign;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Foreign") {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Certificate") {
foreign.certificates << Certificate::deserialize(reader);
} else {
::qWarning() << "Unknown Cerificates element" << reader.name();
reader.skipCurrentElement();
}
}
} else {
::qWarning() << "Unknown KeeShareSettings element" << reader.name();
reader.skipCurrentElement();
}
}
});
return foreign;
}
Reference::Reference()
: type(Inactive)
, uuid(QUuid::createUuid())
{
}
bool Reference::isNull() const
{
return type == Inactive && path.isEmpty() && password.isEmpty();
}
bool Reference::isValid() const
{
return type != Inactive && !path.isEmpty();
}
bool Reference::isExporting() const
{
return (type & ExportTo) != 0 && !path.isEmpty();
}
bool Reference::isImporting() const
{
return (type & ImportFrom) != 0 && !path.isEmpty();
}
bool Reference::operator<(const Reference& other) const
{
if (type != other.type) {
return type < other.type;
}
return path < other.path;
}
bool Reference::operator==(const Reference& other) const
{
return path == other.path && uuid == other.uuid && password == other.password && type == other.type;
}
QString Reference::serialize(const Reference& reference)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("Type");
if ((reference.type & ImportFrom) == ImportFrom) {
writer.writeEmptyElement("Import");
}
if ((reference.type & ExportTo) == ExportTo) {
writer.writeEmptyElement("Export");
}
writer.writeEndElement();
writer.writeStartElement("Group");
writer.writeCharacters(reference.uuid.toRfc4122().toBase64());
writer.writeEndElement();
writer.writeStartElement("Path");
writer.writeCharacters(reference.path.toUtf8().toBase64());
writer.writeEndElement();
writer.writeStartElement("Password");
writer.writeCharacters(reference.password.toUtf8().toBase64());
writer.writeEndElement();
});
}
Reference Reference::deserialize(const QString& raw)
{
Reference reference;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Type") {
while (reader.readNextStartElement()) {
if (reader.name() == "Import") {
reference.type |= ImportFrom;
reader.skipCurrentElement();
} else if (reader.name() == "Export") {
reference.type |= ExportTo;
reader.skipCurrentElement();
} else {
break;
}
}
} else if (reader.name() == "Group") {
reference.uuid = QUuid::fromRfc4122(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "Path") {
reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "Password") {
reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else {
::qWarning() << "Unknown Reference element" << reader.name();
reader.skipCurrentElement();
}
}
});
return reference;
}
QString Sign::serialize(const Sign& sign)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("Signature");
writer.writeCharacters(sign.signature);
writer.writeEndElement();
writer.writeStartElement("Certificate");
Certificate::serialize(writer, sign.certificate);
writer.writeEndElement();
});
}
Sign Sign::deserialize(const QString& raw)
{
Sign sign;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Signature") {
sign.signature = reader.readElementText();
} else if (reader.name() == "Certificate") {
sign.certificate = KeeShareSettings::Certificate::deserialize(reader);
} else {
::qWarning() << "Unknown Sign element" << reader.name();
reader.skipCurrentElement();
}
}
});
return sign;
}
}

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_KEESHARESETTINGS_H
#define KEEPASSXC_KEESHARESETTINGS_H
#include <QMap>
#include <QObject>
#include "crypto/ssh/OpenSSHKey.h"
class CustomData;
class QXmlStreamWriter;
class QXmlStreamReader;
namespace KeeShareSettings
{
struct Certificate
{
QByteArray key;
QString signer;
bool trusted;
bool operator==(const Certificate& other) const;
bool operator!=(const Certificate& other) const;
Certificate()
: trusted(false)
{
}
bool isNull() const;
QString fingerprint() const;
QString publicKey() const;
OpenSSHKey sshKey() const;
static void serialize(QXmlStreamWriter& writer, const Certificate& certificate);
static Certificate deserialize(QXmlStreamReader& reader);
};
struct Key
{
QByteArray key;
bool operator==(const Key& other) const;
bool operator!=(const Key& other) const;
bool isNull() const;
QString privateKey() const;
OpenSSHKey sshKey() const;
static void serialize(QXmlStreamWriter& writer, const Key& key);
static Key deserialize(QXmlStreamReader& reader);
};
struct Active
{
bool in;
bool out;
Active()
: in(false)
, out(false)
{
}
bool isNull() const
{
return !in && !out;
}
static QString serialize(const Active& active);
static Active deserialize(const QString& raw);
};
struct Own
{
Key key;
Certificate certificate;
bool operator==(const Own& other) const;
bool operator!=(const Own& other) const;
bool isNull() const
{
return key.isNull() && certificate.isNull();
}
static QString serialize(const Own& own);
static Own deserialize(const QString& raw);
static Own generate();
};
struct Foreign
{
QList<Certificate> certificates;
bool isNull() const
{
return certificates.isEmpty();
}
static QString serialize(const Foreign& foreign);
static Foreign deserialize(const QString& raw);
};
struct Sign
{
QString signature;
Certificate certificate;
bool isNull() const
{
return signature.isEmpty() && certificate.isNull();
}
static QString serialize(const Sign& sign);
static Sign deserialize(const QString& raw);
};
enum TypeFlag
{
Inactive = 0,
ImportFrom = 1 << 0,
ExportTo = 1 << 1,
SynchronizeWith = ImportFrom | ExportTo
};
Q_DECLARE_FLAGS(Type, TypeFlag)
struct Reference
{
Type type;
QUuid uuid;
QString path;
QString password;
Reference();
bool isNull() const;
bool isValid() const;
bool isExporting() const;
bool isImporting() const;
bool operator<(const Reference& other) const;
bool operator==(const Reference& other) const;
static QString serialize(const Reference& reference);
static Reference deserialize(const QString& raw);
};
};
#endif // KEEPASSXC_KEESHARESETTINGS_H

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "SettingsPageKeeShare.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include "gui/DatabaseTabWidget.h"
#include "gui/MessageWidget.h"
#include "keeshare/KeeShare.h"
#include "keeshare/SettingsWidgetKeeShare.h"
#include <QApplication>
#include <QObject>
SettingsPageKeeShare::SettingsPageKeeShare(DatabaseTabWidget* tabWidget)
: m_tabWidget(tabWidget)
{
}
QString SettingsPageKeeShare::name()
{
return QApplication::tr("KeeShare");
}
QIcon SettingsPageKeeShare::icon()
{
return FilePath::instance()->icon("apps", "preferences-system-network-sharing");
}
QWidget* SettingsPageKeeShare::createWidget()
{
auto* widget = new SettingsWidgetKeeShare();
QObject::connect(widget,
SIGNAL(settingsMessage(QString, MessageWidget::MessageType)),
m_tabWidget,
SIGNAL(messageGlobal(QString, MessageWidget::MessageType)));
return widget;
}
void SettingsPageKeeShare::loadSettings(QWidget* widget)
{
Q_UNUSED(widget);
SettingsWidgetKeeShare* settingsWidget = reinterpret_cast<SettingsWidgetKeeShare*>(widget);
settingsWidget->loadSettings();
}
void SettingsPageKeeShare::saveSettings(QWidget* widget)
{
Q_UNUSED(widget);
SettingsWidgetKeeShare* settingsWidget = reinterpret_cast<SettingsWidgetKeeShare*>(widget);
return settingsWidget->saveSettings();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_SETTINGSPAGEKEESHARE_H
#define KEEPASSXC_SETTINGSPAGEKEESHARE_H
#include <QObject>
#include <QPointer>
#include <QWidget>
#include "gui/ApplicationSettingsWidget.h"
class DatabaseTabWidget;
class SettingsPageKeeShare : public ISettingsPage
{
public:
SettingsPageKeeShare(DatabaseTabWidget* tabWidget);
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void loadSettings(QWidget* widget) override;
void saveSettings(QWidget* widget) override;
private:
QPointer<DatabaseTabWidget> m_tabWidget;
};
#endif // KEEPASSXC_SETTINGSPAGEKEESHARE_H

View File

@ -0,0 +1,214 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "SettingsWidgetKeeShare.h"
#include "ui_SettingsWidgetKeeShare.h"
#include "core/Config.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "gui/FileDialog.h"
#include "keeshare/KeeShare.h"
#include "keeshare/KeeShareSettings.h"
#include <QMessageBox>
#include <QStandardItemModel>
SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::SettingsWidgetKeeShare())
{
m_ui->setupUi(this);
connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString)));
connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate()));
connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate()));
connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate()));
connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates()));
connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates()));
connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates()));
}
SettingsWidgetKeeShare::~SettingsWidgetKeeShare()
{
}
void SettingsWidgetKeeShare::loadSettings()
{
const auto active = KeeShare::active();
m_ui->enableExportCheckBox->setChecked(active.out);
m_ui->enableImportCheckBox->setChecked(active.in);
m_own = KeeShare::own();
updateOwnCertificate();
m_foreign = KeeShare::foreign();
updateForeignCertificates();
}
void SettingsWidgetKeeShare::updateForeignCertificates()
{
m_importedCertificateModel.reset(new QStandardItemModel());
m_importedCertificateModel->setHorizontalHeaderLabels(
QStringList() << tr("Signer") << tr("Status") << tr("Fingerprint") << tr("Certificate"));
for (const KeeShareSettings::Certificate& certificate : m_foreign.certificates) {
QStandardItem* signer = new QStandardItem(certificate.signer);
QStandardItem* verified = new QStandardItem(certificate.trusted ? tr("trusted") : tr("untrusted"));
QStandardItem* fingerprint = new QStandardItem(certificate.fingerprint());
QStandardItem* key = new QStandardItem(certificate.publicKey());
m_importedCertificateModel->appendRow(QList<QStandardItem*>() << signer << verified << fingerprint << key);
}
m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data());
}
void SettingsWidgetKeeShare::updateOwnCertificate()
{
m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer);
m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey());
m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey());
m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint());
}
void SettingsWidgetKeeShare::saveSettings()
{
KeeShareSettings::Active active;
active.out = m_ui->enableExportCheckBox->isChecked();
active.in = m_ui->enableImportCheckBox->isChecked();
// TODO HNH: This depends on the order of saving new data - a better model would be to
// store changes to the settings in a temporary object and check on the final values
// of this object (similar scheme to Entry) - this way we could validate the settings before save
if (active.in) {
emit settingsMessage(tr("Make sure to have a history size greater than 2 to prevent data loss when importing!"), MessageWidget::Warning);
}
KeeShare::setOwn(m_own);
KeeShare::setForeign(m_foreign);
KeeShare::setActive(active);
}
void SettingsWidgetKeeShare::setVerificationExporter(const QString& signer)
{
m_own.certificate.signer = signer;
m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer);
}
void SettingsWidgetKeeShare::generateCertificate()
{
m_own = KeeShareSettings::Own::generate();
m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer);
m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey());
m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey());
m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint());
}
void SettingsWidgetKeeShare::importCertificate()
{
QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
}
const auto filetype = tr("key.share", "Filetype for KeeShare key");
const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files"));
QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0);
if (filename.isEmpty()) {
return;
}
QFile file(filename);
file.open(QIODevice::ReadOnly);
QTextStream stream(&file);
m_own = KeeShareSettings::Own::deserialize(stream.readAll());
file.close();
config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath());
updateOwnCertificate();
}
void SettingsWidgetKeeShare::exportCertificate()
{
if (KeeShare::own() != m_own) {
QMessageBox warning;
warning.setIcon(QMessageBox::Warning);
warning.setWindowTitle(tr("Exporting changed certificate"));
warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the current certificate?"));
auto yes = warning.addButton(QMessageBox::StandardButton::Yes);
auto no = warning.addButton(QMessageBox::StandardButton::No);
warning.setDefaultButton(no);
warning.exec();
if (warning.clickedButton() != yes) {
return;
}
}
QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
}
const auto filetype = tr("key.share", "Filetype for KeeShare key");
const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files"));
QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype);
filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0, filetype, filename);
if (filename.isEmpty()) {
return;
}
QFile file(filename);
file.open(QIODevice::Truncate | QIODevice::WriteOnly);
QTextStream stream(&file);
stream << KeeShareSettings::Own::serialize(m_own);
stream.flush();
file.close();
config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath());
}
void SettingsWidgetKeeShare::trustSelectedCertificates()
{
const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel();
Q_ASSERT(selectionModel);
for (const auto& index : selectionModel->selectedRows()) {
m_foreign.certificates[index.row()].trusted = true;
}
updateForeignCertificates();
}
void SettingsWidgetKeeShare::untrustSelectedCertificates()
{
const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel();
Q_ASSERT(selectionModel);
for (const auto& index : selectionModel->selectedRows()) {
m_foreign.certificates[index.row()].trusted = false;
}
updateForeignCertificates();
}
void SettingsWidgetKeeShare::removeSelectedCertificates()
{
QList<KeeShareSettings::Certificate> certificates = m_foreign.certificates;
const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel();
Q_ASSERT(selectionModel);
for (const auto& index : selectionModel->selectedRows()) {
certificates.removeOne(m_foreign.certificates[index.row()]);
}
m_foreign.certificates = certificates;
updateForeignCertificates();
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_SETTINGSWIDGETKEESHARE_H
#define KEEPASSXC_SETTINGSWIDGETKEESHARE_H
#include <QPointer>
#include <QScopedPointer>
#include <QWidget>
#include "gui/MessageWidget.h"
#include "keeshare/KeeShareSettings.h"
class Database;
class QStandardItemModel;
namespace Ui
{
class SettingsWidgetKeeShare;
}
class SettingsWidgetKeeShare : public QWidget
{
Q_OBJECT
public:
explicit SettingsWidgetKeeShare(QWidget* parent = nullptr);
~SettingsWidgetKeeShare();
void loadSettings();
void saveSettings();
signals:
void settingsMessage(const QString&, MessageWidget::MessageType type);
private slots:
void setVerificationExporter(const QString& signer);
void generateCertificate();
void importCertificate();
void exportCertificate();
void trustSelectedCertificates();
void untrustSelectedCertificates();
void removeSelectedCertificates();
private:
void updateOwnCertificate();
void updateForeignCertificates();
QScopedPointer<Ui::SettingsWidgetKeeShare> m_ui;
KeeShareSettings::Own m_own;
KeeShareSettings::Foreign m_foreign;
QScopedPointer<QStandardItemModel> m_importedCertificateModel;
};
#endif // KEEPASSXC_SETTINGSWIDGETKEESHARE_H

View File

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsWidgetKeeShare</class>
<widget class="QWidget" name="SettingsWidgetKeeShare">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<height>423</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="activeGroupBox">
<property name="title">
<string>Active</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QCheckBox" name="enableExportCheckBox">
<property name="text">
<string>Allow export</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="enableImportCheckBox">
<property name="text">
<string>Allow import</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="ownCertificateGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Own certificate</string>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1,1">
<item row="5" column="0">
<widget class="QLabel" name="ownCertificateFingerprintLabel">
<property name="text">
<string>Fingerprint:</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QLineEdit" name="ownCertificatePrivateKeyEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="ownCertificatePublicKeyLabel">
<property name="text">
<string>Certificate:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="ownCertificateSignerLabel">
<property name="text">
<string>Signer</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="ownCertificatePrivateKeyLabel">
<property name="text">
<string>Key:</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QLineEdit" name="ownCertificatePublicKeyEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QLineEdit" name="ownCertificateSignerEdit"/>
</item>
<item row="5" column="1" colspan="2">
<widget class="QLineEdit" name="ownCertificateFingerprintEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="generateOwnCerticateButton">
<property name="text">
<string>Generate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="importOwnCertificateButton">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="exportOwnCertificateButton">
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="importedCertificatesGroupBox">
<property name="title">
<string>Imported certificates</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0" rowspan="2">
<widget class="QTableView" name="importedCertificateTableView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="certificatesActionLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="trustImportedCertificateButton">
<property name="text">
<string>Trust</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="untrustImportedCertificateButton">
<property name="text">
<string>Untrust</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeImportedCertificateButton">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</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>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,637 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "ShareObserver.h"
#include "core/Clock.h"
#include "core/Config.h"
#include "core/CustomData.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Entry.h"
#include "core/FilePath.h"
#include "core/FileWatcher.h"
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
#include "format/KeePass2Reader.h"
#include "format/KeePass2Writer.h"
#include "keeshare/KeeShare.h"
#include "keeshare/KeeShareSettings.h"
#include "keeshare/Signature.h"
#include "keys/PasswordKey.h"
#include <QBuffer>
#include <QDebug>
#include <QFileInfo>
#include <QIcon>
#include <QMessageBox>
#include <QPainter>
#include <QPushButton>
#include <QStringBuilder>
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
namespace
{
static const QString KeeShare_Signature("container.share.signature");
static const QString KeeShare_Container("container.share.kdbx");
enum Trust
{
None,
Invalid,
Single,
Lasting,
Known,
Own
};
QPair<Trust, KeeShareSettings::Certificate> check(QByteArray& data,
const KeeShareSettings::Reference& reference,
const KeeShareSettings::Certificate& ownCertificate,
const QList<KeeShareSettings::Certificate>& knownCertificates,
const KeeShareSettings::Sign& sign)
{
if (sign.signature.isEmpty()) {
QMessageBox warning;
warning.setIcon(QMessageBox::Warning);
warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature"));
warning.setText(ShareObserver::tr("Do you want to import from unsigned container %1").arg(reference.path));
auto yes = warning.addButton(ShareObserver::tr("Import once"), QMessageBox::ButtonRole::YesRole);
auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole);
warning.setDefaultButton(no);
warning.exec();
const auto trust = warning.clickedButton() == yes ? Single : None;
return qMakePair(trust, KeeShareSettings::Certificate());
}
auto key = sign.certificate.sshKey();
key.openKey(QString());
const Signature signer;
if (!signer.verify(data, sign.signature, key)) {
const QFileInfo info(reference.path);
qCritical("Invalid signature for sharing container %s.", qPrintable(info.absoluteFilePath()));
return qMakePair(Invalid, KeeShareSettings::Certificate());
}
if (ownCertificate.key == sign.certificate.key) {
return qMakePair(Own, ownCertificate);
}
for (const auto& certificate : knownCertificates) {
if (certificate.key == certificate.key && certificate.trusted) {
return qMakePair(Known, certificate);
}
}
QMessageBox warning;
warning.setIcon(QMessageBox::Question);
warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container"));
warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2")
.arg(sign.certificate.signer)
.arg(sign.certificate.fingerprint()));
auto yes = warning.addButton(ShareObserver::tr("Import and trust"), QMessageBox::ButtonRole::YesRole);
auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole);
warning.setDefaultButton(no);
warning.exec();
if (warning.clickedButton() != yes) {
qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer));
return qMakePair(None, sign.certificate);
}
return qMakePair(Lasting, sign.certificate);
}
}
ShareObserver::ShareObserver(Database* db, QObject* parent)
: QObject(parent)
, m_db(db)
, m_fileWatcher(new BulkFileWatcher(this))
{
connect(KeeShare::instance(), SIGNAL(activeChanged()), this, SLOT(handleDatabaseChanged()));
connect(m_db, SIGNAL(modified()), this, SLOT(handleDatabaseChanged()));
connect(m_fileWatcher, SIGNAL(fileCreated(QString)), this, SLOT(handleFileCreated(QString)));
connect(m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(handleFileChanged(QString)));
connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), this, SLOT(handleFileRemoved(QString)));
}
ShareObserver::~ShareObserver()
{
}
void ShareObserver::deinitialize()
{
m_fileWatcher->clear();
m_groupToReference.clear();
m_referenceToGroup.clear();
}
void ShareObserver::reinitialize()
{
struct Update
{
Group* group;
KeeShareSettings::Reference oldReference;
KeeShareSettings::Reference newReference;
};
const auto active = KeeShare::active();
QList<Update> updated;
QList<Group*> groups = m_db->rootGroup()->groupsRecursive(true);
for (Group* group : groups) {
Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)};
if (couple.oldReference == couple.newReference) {
continue;
}
m_groupToReference.remove(couple.group);
m_referenceToGroup.remove(couple.oldReference);
m_shareToGroup.remove(couple.oldReference.path);
if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting())
|| (active.out && couple.newReference.isExporting()))) {
m_groupToReference[couple.group] = couple.newReference;
m_referenceToGroup[couple.newReference] = couple.group;
m_shareToGroup[couple.newReference.path] = couple.group;
}
updated << couple;
}
QStringList success;
QStringList warning;
QStringList error;
for (Update update : updated) {
if (!update.oldReference.path.isEmpty()) {
m_fileWatcher->removePath(update.oldReference.path);
}
if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) {
m_fileWatcher->addPath(update.newReference.path);
}
if (update.newReference.isImporting()) {
const Result result = this->importFromReferenceContainer(update.newReference.path);
if (!result.isValid()) {
// tolerable result - blocked import or missing source
continue;
}
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
}
}
notifyAbout(success, warning, error);
}
void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error)
{
if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) {
return;
}
MessageWidget::MessageType type = MessageWidget::Positive;
if (!warning.isEmpty()) {
type = MessageWidget::Warning;
}
if (!error.isEmpty()) {
type = MessageWidget::Error;
}
emit sharingMessage((success + warning + error).join("\n"), type);
}
void ShareObserver::handleDatabaseChanged()
{
if (!m_db) {
Q_ASSERT(m_db);
return;
}
const auto active = KeeShare::active();
if (!active.out && !active.in) {
deinitialize();
} else {
reinitialize();
}
}
void ShareObserver::handleFileUpdated(const QString& path, Change change)
{
switch (change) {
case Creation:
qDebug("File created %s", qPrintable(path));
break;
case Update:
qDebug("File changed %s", qPrintable(path));
break;
case Deletion:
qDebug("File deleted %s", qPrintable(path));
break;
}
const Result result = this->importFromReferenceContainer(path);
if (!result.isValid()) {
return;
}
QStringList success;
QStringList warning;
QStringList error;
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
notifyAbout(success, warning, error);
}
void ShareObserver::handleFileCreated(const QString& path)
{
handleFileUpdated(path, Creation);
}
void ShareObserver::handleFileChanged(const QString& path)
{
handleFileUpdated(path, Update);
}
void ShareObserver::handleFileRemoved(const QString& path)
{
handleFileUpdated(path, Deletion);
}
ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup)
{
const QFileInfo info(reference.path);
if (!info.exists()) {
qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath()));
return {reference.path, Result::Warning, tr("File does not exist")};
}
QuaZip zip(info.absoluteFilePath());
if (!zip.open(QuaZip::mdUnzip)) {
qCritical("Unable to open file %s.", qPrintable(info.absoluteFilePath()));
return {reference.path, Result::Error, tr("File is not readable")};
}
const auto expected = QSet<QString>() << KeeShare_Signature << KeeShare_Container;
const auto files = zip.getFileInfoList();
QSet<QString> actual;
for (const auto& file : files) {
actual << file.name;
}
if (expected != actual) {
qCritical("Invalid sharing container %s.", qPrintable(info.absoluteFilePath()));
return {reference.path, Result::Error, tr("Invalid sharing container")};
}
zip.setCurrentFile(KeeShare_Signature);
QuaZipFile signatureFile(&zip);
signatureFile.open(QuaZipFile::ReadOnly);
QTextStream stream(&signatureFile);
const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll());
signatureFile.close();
zip.setCurrentFile(KeeShare_Container);
QuaZipFile databaseFile(&zip);
databaseFile.open(QuaZipFile::ReadOnly);
auto payload = databaseFile.readAll();
databaseFile.close();
QBuffer buffer(&payload);
buffer.open(QIODevice::ReadOnly);
KeePass2Reader reader;
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
auto* sourceDb = reader.readDatabase(&buffer, key);
if (reader.hasError()) {
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
return {reference.path, Result::Error, reader.errorString()};
}
auto foreign = KeeShare::foreign();
auto own = KeeShare::own();
auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign);
switch (trusted.first) {
case None:
qWarning("Prevent untrusted import");
return {reference.path, Result::Warning, tr("Untrusted import prevented")};
case Invalid:
qCritical("Prevent untrusted import");
return {reference.path, Result::Error, tr("Untrusted import prevented")};
case Known:
case Lasting: {
bool found = false;
for (KeeShareSettings::Certificate& knownCertificate : foreign.certificates) {
if (knownCertificate.key == trusted.second.key) {
knownCertificate.signer = trusted.second.signer;
knownCertificate.trusted = true;
found = true;
}
}
if (!found) {
foreign.certificates << trusted.second;
// we need to update with the new signer
KeeShare::setForeign(foreign);
}
}
[[gnu::fallthrough]];
case Single:
case Own: {
qDebug("Synchronize %s %s with %s",
qPrintable(reference.path),
qPrintable(targetGroup->name()),
qPrintable(sourceDb->rootGroup()->name()));
Merger merger(sourceDb->rootGroup(), targetGroup);
merger.setForcedMergeMode(Group::Synchronize);
const bool changed = merger.merge();
if (changed) {
return {reference.path, Result::Success, tr("Successful import")};
}
return {};
}
default:
Q_ASSERT(false);
return {};
}
}
ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path)
{
if (!KeeShare::active().in) {
return {};
}
auto shareGroup = m_shareToGroup.value(path);
if (!shareGroup) {
qWarning("Source for %s does not exist", qPrintable(path));
Q_ASSERT(shareGroup);
return {};
}
const auto reference = KeeShare::referenceOf(shareGroup);
if (reference.type == KeeShareSettings::Inactive) {
qDebug("Ignore change of inactive reference %s", qPrintable(reference.path));
return {};
}
if (reference.type == KeeShareSettings::ExportTo) {
qDebug("Ignore change of export reference %s", qPrintable(reference.path));
return {};
}
Q_ASSERT(shareGroup->database() == m_db);
Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid()));
return importContainerInto(reference, shareGroup);
}
void ShareObserver::resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb)
{
for (const auto& attribute : EntryAttributes::DefaultAttributes) {
const auto standardValue = targetEntry->attributes()->value(attribute);
const auto type = targetEntry->placeholderType(standardValue);
if (type != Entry::PlaceholderType::Reference) {
// No reference to resolve
continue;
}
const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue);
if (referencedTargetEntry) {
// References is within scope, no resolving needed
continue;
}
// We could do more sophisticated **** trying to point the reference to the next in-scope reference
// but those cases with high propability constructed examples and very rare in real usage
const auto* sourceReference = sourceDb->resolveEntry(targetEntry->uuid());
const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue);
targetEntry->setUpdateTimeinfo(false);
targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute));
targetEntry->setUpdateTimeinfo(true);
}
}
Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
{
const auto* sourceDb = sourceRoot->database();
auto* targetDb = new Database();
targetDb->metadata()->setRecycleBinEnabled(false);
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
// Copy the source root as the root of the export database, memory manage the old root node
auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
const bool updateTimeinfo = targetRoot->canUpdateTimeinfo();
targetRoot->setUpdateTimeinfo(false);
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
targetRoot->setUpdateTimeinfo(updateTimeinfo);
const auto sourceEntries = sourceRoot->entriesRecursive(false);
for (const Entry* sourceEntry : sourceEntries) {
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
const bool updateTimeinfo = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->setGroup(targetRoot);
targetEntry->setUpdateTimeinfo(updateTimeinfo);
const auto iconUuid = targetEntry->iconUuid();
if (!iconUuid.isNull()) {
targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon());
}
}
targetDb->setKey(key);
auto* obsoleteRoot = targetDb->rootGroup();
targetDb->setRootGroup(targetRoot);
delete obsoleteRoot;
targetDb->metadata()->setName(sourceRoot->name());
// Push all deletions of the source database to the target
// simple moving out of a share group will not trigger a deletion in the
// target - a more elaborate mechanism may need the use of another custom
// attribute to share unshared entries from the target db
for (const auto& object : sourceDb->deletedObjects()) {
targetDb->addDeletedObject(object);
}
for (auto* targetEntry : targetRoot->entriesRecursive(false)) {
if (targetEntry->hasReferences()) {
resolveReferenceAttributes(targetEntry, sourceDb);
}
}
return targetDb;
}
const Database* ShareObserver::database() const
{
return m_db;
}
Database* ShareObserver::database()
{
return m_db;
}
void ShareObserver::handleDatabaseOpened()
{
if (!m_db) {
Q_ASSERT(m_db);
return;
}
const auto active = KeeShare::active();
if (!active.in && !active.out) {
deinitialize();
} else {
reinitialize();
}
}
QList<ShareObserver::Result> ShareObserver::exportIntoReferenceContainers()
{
QList<Result> results;
const auto own = KeeShare::own();
const auto groups = m_db->rootGroup()->groupsRecursive(true);
for (const auto* group : groups) {
const auto reference = KeeShare::referenceOf(group);
if (!reference.isExporting()) {
continue;
}
m_fileWatcher->ignoreFileChanges(reference.path);
QScopedPointer<Database> targetDb(exportIntoContainer(reference, group));
QByteArray bytes;
{
QBuffer buffer(&bytes);
buffer.open(QIODevice::WriteOnly);
KeePass2Writer writer;
writer.writeDatabase(&buffer, targetDb.data());
if (writer.hasError()) {
qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data());
results << Result{reference.path, Result::Error, writer.errorString()};
m_fileWatcher->observeFileChanges(true);
continue;
}
}
QuaZip zip(reference.path);
zip.setFileNameCodec("UTF-8");
const bool zipOpened = zip.open(QuaZip::mdCreate);
if (!zipOpened) {
::qWarning("Opening export file failed: %d", zip.getZipError());
results << Result{reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())};
m_fileWatcher->observeFileChanges(true);
continue;
}
{
QuaZipFile file(&zip);
const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature));
if (!signatureOpened) {
::qWarning("Embedding signature failed: %d", zip.getZipError());
results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())};
m_fileWatcher->observeFileChanges(true);
continue;
}
QTextStream stream(&file);
KeeShareSettings::Sign sign;
auto sshKey = own.key.sshKey();
sshKey.openKey(QString());
const Signature signer;
sign.signature = signer.create(bytes, sshKey);
sign.certificate = own.certificate;
stream << KeeShareSettings::Sign::serialize(sign);
stream.flush();
if (file.getZipError() != ZIP_OK) {
::qWarning("Embedding signature failed: %d", zip.getZipError());
results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())};
m_fileWatcher->observeFileChanges(true);
continue;
}
file.close();
}
{
QuaZipFile file(&zip);
const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container));
if (!dbOpened) {
::qWarning("Embedding database failed: %d", zip.getZipError());
results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())};
m_fileWatcher->observeFileChanges(true);
continue;
}
if (file.getZipError() != ZIP_OK) {
::qWarning("Embedding database failed: %d", zip.getZipError());
results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())};
m_fileWatcher->observeFileChanges(true);
continue;
}
file.write(bytes);
file.close();
}
zip.close();
m_fileWatcher->observeFileChanges(true);
results << Result{reference.path};
}
return results;
}
void ShareObserver::handleDatabaseSaved()
{
if (!KeeShare::active().out) {
return;
}
QStringList error;
QStringList warning;
QStringList success;
const auto results = exportIntoReferenceContainers();
for (const Result& result : results) {
if (!result.isValid()) {
Q_ASSERT(result.isValid());
continue;
}
if (result.isError()) {
error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isWarning()) {
warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isInfo()) {
success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message);
} else {
success << tr("Export to %1").arg(result.path);
}
}
notifyAbout(success, warning, error);
}
ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message)
: path(path)
, type(type)
, message(message)
{
}
bool ShareObserver::Result::isValid() const
{
return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty();
}
bool ShareObserver::Result::isError() const
{
return !message.isEmpty() && type == Error;
}
bool ShareObserver::Result::isInfo() const
{
return !message.isEmpty() && type == Info;
}
bool ShareObserver::Result::isWarning() const
{
return !message.isEmpty() && type == Warning;
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_SHAREOBSERVER_H
#define KEEPASSXC_SHAREOBSERVER_H
#include <QMap>
#include <QObject>
#include <QSet>
#include <QStringList>
#include <QTimer>
#include "gui/MessageWidget.h"
#include "keeshare/KeeShareSettings.h"
class BulkFileWatcher;
class Entry;
class Group;
class CustomData;
class Database;
class ShareObserver : public QObject
{
Q_OBJECT
public:
explicit ShareObserver(Database* db, QObject* parent = nullptr);
~ShareObserver();
void handleDatabaseSaved();
void handleDatabaseOpened();
const Database* database() const;
Database* database();
signals:
void sharingMessage(QString, MessageWidget::MessageType);
public slots:
void handleDatabaseChanged();
private slots:
void handleFileCreated(const QString& path);
void handleFileChanged(const QString& path);
void handleFileRemoved(const QString& path);
private:
enum Change
{
Creation,
Update,
Deletion
};
struct Result
{
enum Type
{
Success,
Info,
Warning,
Error
};
QString path;
Type type;
QString message;
Result(const QString& path = QString(), Type type = Success, const QString& message = QString());
bool isValid() const;
bool isError() const;
bool isWarning() const;
bool isInfo() const;
};
static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb);
static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot);
static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup);
Result importFromReferenceContainer(const QString& path);
QList<ShareObserver::Result> exportIntoReferenceContainers();
void deinitialize();
void reinitialize();
void handleFileUpdated(const QString& path, Change change);
void notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error);
private:
Database* const m_db;
QMap<KeeShareSettings::Reference, QPointer<Group>> m_referenceToGroup;
QMap<QPointer<Group>, KeeShareSettings::Reference> m_groupToReference;
QMap<QString, QPointer<Group>> m_shareToGroup;
BulkFileWatcher* m_fileWatcher;
};
#endif // KEEPASSXC_SHAREOBSERVER_H

260
src/keeshare/Signature.cpp Normal file
View File

@ -0,0 +1,260 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "Signature.h"
#include "core/Tools.h"
#include "crypto/Crypto.h"
#include "crypto/CryptoHash.h"
#include "crypto/ssh/OpenSSHKey.h"
#include <QByteArray>
#include <gcrypt.h>
struct RSASigner
{
gcry_error_t rc;
QString error;
void raiseError(const QString& message = QString())
{
if (message.isEmpty()) {
error = QString("%1/%2").arg(QString::fromLocal8Bit(gcry_strsource(rc)),
QString::fromLocal8Bit(gcry_strerror(rc)));
} else {
error = message;
}
}
RSASigner()
: rc(GPG_ERR_NO_ERROR)
{
}
QString sign(const QByteArray& data, const OpenSSHKey& key)
{
enum Index
{
N,
E,
D,
P,
Q,
U, // private key
R,
S, // signature
Data,
Key,
Sig
};
const QList<QByteArray> parts = key.privateParts();
if (parts.count() != 6) {
raiseError("Unsupported signing key");
return QString();
}
const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256);
Tools::Map<Index, gcry_mpi_t, &gcry_mpi_release> mpi;
Tools::Map<Index, gcry_sexp_t, &gcry_sexp_release> sexp;
const gcry_mpi_format format = GCRYMPI_FMT_USG;
rc = gcry_mpi_scan(&mpi[N], format, parts[0].data(), parts[0].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_mpi_scan(&mpi[E], format, parts[1].data(), parts[1].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_mpi_scan(&mpi[D], format, parts[2].data(), parts[2].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_mpi_scan(&mpi[U], format, parts[3].data(), parts[3].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_mpi_scan(&mpi[P], format, parts[4].data(), parts[4].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_mpi_scan(&mpi[Q], format, parts[5].data(), parts[5].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
if (gcry_mpi_cmp(mpi[P], mpi[Q]) > 0) {
// see https://www.gnupg.org/documentation/manuals/gcrypt/RSA-key-parameters.html#RSA-key-parameters
gcry_mpi_swap(mpi[P], mpi[Q]);
gcry_mpi_invm(mpi[U], mpi[P], mpi[Q]);
}
rc = gcry_sexp_build(&sexp[Key],
NULL,
"(private-key (rsa (n %m) (e %m) (d %m) (p %m) (q %m) (u %m)))",
mpi[N],
mpi[E],
mpi[D],
mpi[P],
mpi[Q],
mpi[U]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_pk_testkey(sexp[Key]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data());
// rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data());
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
rc = gcry_pk_sign(&sexp[Sig], sexp[Data], sexp[Key]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
sexp[S] = gcry_sexp_find_token(sexp[Sig], "s", 1);
mpi[S] = gcry_sexp_nth_mpi(sexp[S], 1, GCRYMPI_FMT_USG);
Tools::Buffer buffer;
rc = gcry_mpi_aprint(GCRYMPI_FMT_STD, &buffer.raw, &buffer.size, mpi[S]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return QString();
}
return QString("rsa|%1").arg(QString::fromLatin1(buffer.content().toHex()));
}
bool verify(const QByteArray& data, const OpenSSHKey& key, const QString& signature)
{
const gcry_mpi_format format = GCRYMPI_FMT_USG;
enum MPI
{
N,
E, // public key
R,
S // signature
};
enum SEXP
{
Data,
Key,
Sig
};
const QList<QByteArray> parts = key.publicParts();
if (parts.count() != 2) {
raiseError("Unsupported verification key");
return false;
}
const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256);
Tools::Map<MPI, gcry_mpi_t, &gcry_mpi_release> mpi;
Tools::Map<SEXP, gcry_sexp_t, &gcry_sexp_release> sexp;
rc = gcry_mpi_scan(&mpi[E], format, parts[0].data(), parts[0].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
rc = gcry_mpi_scan(&mpi[N], format, parts[1].data(), parts[1].size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
rc = gcry_sexp_build(&sexp[Key], NULL, "(public-key (rsa (n %m) (e %m)))", mpi[N], mpi[E]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
QRegExp extractor("rsa\\|([a-f0-9]+)", Qt::CaseInsensitive);
if (!extractor.exactMatch(signature) || extractor.captureCount() != 1) {
raiseError("Could not unpack signature parts");
return false;
}
const QByteArray sig_s = QByteArray::fromHex(extractor.cap(1).toLatin1());
rc = gcry_mpi_scan(&mpi[S], GCRYMPI_FMT_STD, sig_s.data(), sig_s.size(), nullptr);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
rc = gcry_sexp_build(&sexp[Sig], NULL, "(sig-val (rsa (s %m)))", mpi[S]);
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data());
// rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data());
if (rc != GPG_ERR_NO_ERROR) {
raiseError();
return false;
}
rc = gcry_pk_verify(sexp[Sig], sexp[Data], sexp[Key]);
if (rc != GPG_ERR_NO_ERROR && rc != GPG_ERR_BAD_SIGNATURE) {
raiseError();
return false;
}
return rc != GPG_ERR_BAD_SIGNATURE;
}
};
QString Signature::create(const QByteArray& data, const OpenSSHKey& key)
{
// TODO HNH: currently we publish the signature in our own non-standard format - it would
// be better to use a standard format (like ASN1 - but this would be more easy
// when we integrate a proper library)
// Even more, we could publish standard self signed certificates with the container
// instead of the custom certificates
if (key.type() == "ssh-rsa") {
RSASigner signer;
QString result = signer.sign(data, key);
if (signer.rc != GPG_ERR_NO_ERROR) {
::qWarning() << signer.error;
}
return result;
}
::qWarning() << "Unsupported Public/Private key format";
return QString();
}
bool Signature::verify(const QByteArray& data, const QString& signature, const OpenSSHKey& key)
{
if (key.type() == "ssh-rsa") {
RSASigner signer;
bool result = signer.verify(data, key, signature);
if (signer.rc != GPG_ERR_NO_ERROR) {
::qWarning() << signer.error;
}
return result;
}
::qWarning() << "Unsupported Public/Private key format";
return false;
}

34
src/keeshare/Signature.h Normal file
View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_SIGNATURE_H
#define KEEPASSXC_SIGNATURE_H
#include <QString>
#include <gcrypt.h>
class QByteArray;
class OpenSSHKey;
class Signature
{
public:
static QString create(const QByteArray& data, const OpenSSHKey& key);
static bool verify(const QByteArray& data, const QString& signature, const OpenSSHKey& key);
};
#endif // KEEPASSXC_SIGNATURE_H

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "EditGroupPageKeeShare.h"
#include "core/FilePath.h"
#include "keeshare/group/EditGroupWidgetKeeShare.h"
#include <QApplication>
EditGroupPageKeeShare::EditGroupPageKeeShare(EditGroupWidget* widget)
{
Q_UNUSED(widget);
}
QString EditGroupPageKeeShare::name()
{
return QApplication::tr("KeeShare");
}
QIcon EditGroupPageKeeShare::icon()
{
return FilePath::instance()->icon("apps", "preferences-system-network-sharing");
}
QWidget* EditGroupPageKeeShare::createWidget()
{
return new EditGroupWidgetKeeShare();
}
void EditGroupPageKeeShare::set(QWidget* widget, Group* temporaryGroup)
{
EditGroupWidgetKeeShare* settingsWidget = reinterpret_cast<EditGroupWidgetKeeShare*>(widget);
settingsWidget->setGroup(temporaryGroup);
}
void EditGroupPageKeeShare::assign(QWidget* widget)
{
Q_UNUSED(widget);
// everything is saved directly
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_EDITGROUPPAGEKEESHARE_H
#define KEEPASSXC_EDITGROUPPAGEKEESHARE_H
#include "gui/group/EditGroupWidget.h"
class Group;
class Database;
class EditGroupPageKeeShare : public IEditGroupPage
{
public:
EditGroupPageKeeShare(EditGroupWidget* widget);
QString name() override;
QIcon icon() override;
QWidget* createWidget() override;
void set(QWidget* widget, Group* temporaryGroup) override;
void assign(QWidget* widget) override;
};
#endif // KEEPASSXC_EDITGROUPPAGEKEESHARE_H

View File

@ -0,0 +1,228 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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 "EditGroupWidgetKeeShare.h"
#include "ui_EditGroupWidgetKeeShare.h"
#include "core/Config.h"
#include "core/CustomData.h"
#include "core/FilePath.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "crypto/ssh/OpenSSHKey.h"
#include "gui/FileDialog.h"
#include "keeshare/KeeShare.h"
#include <QDir>
#include <QStandardPaths>
EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EditGroupWidgetKeeShare())
{
m_ui->setupUi(this);
m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false));
m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0);
m_ui->passwordGenerator->hide();
m_ui->passwordGenerator->reset();
m_ui->messageWidget->hide();
connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool)));
connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool)));
connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword()));
connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString)));
connect(m_ui->pathEdit, SIGNAL(textChanged(QString)), SLOT(setPath(QString)));
connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(selectPath()));
connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType()));
connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState()));
const auto types = QList<KeeShareSettings::Type>() << KeeShareSettings::Inactive
<< KeeShareSettings::ImportFrom
<< KeeShareSettings::ExportTo
<< KeeShareSettings::SynchronizeWith;
for (const auto& type : types) {
QString name;
switch (type) {
case KeeShareSettings::Inactive:
name = tr("Inactive");
break;
case KeeShareSettings::ImportFrom:
name = tr("Import from path");
break;
case KeeShareSettings::ExportTo:
name = tr("Export to path");
break;
case KeeShareSettings::SynchronizeWith:
name = tr("Synchronize with path");
break;
}
m_ui->typeComboBox->insertItem(type, name, static_cast<int>(type));
}
}
EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare()
{
}
void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup)
{
if (m_temporaryGroup) {
m_temporaryGroup->disconnect(this);
}
m_temporaryGroup = temporaryGroup;
if (m_temporaryGroup) {
connect(m_temporaryGroup, SIGNAL(modified()), SLOT(update()));
}
update();
}
void EditGroupWidgetKeeShare::showSharingState()
{
if (!m_temporaryGroup) {
return;
}
const auto active = KeeShare::active();
if (!active.in && !active.out) {
m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information);
}
if (active.in && !active.out) {
m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information);
}
if (!active.in && active.out) {
m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information);
}
}
void EditGroupWidgetKeeShare::update()
{
if (!m_temporaryGroup) {
m_ui->passwordEdit->clear();
m_ui->pathEdit->clear();
m_ui->passwordGenerator->hide();
m_ui->togglePasswordGeneratorButton->setChecked(false);
} else {
const auto reference = KeeShare::referenceOf(m_temporaryGroup);
m_ui->typeComboBox->setCurrentIndex(reference.type);
m_ui->passwordEdit->setText(reference.password);
m_ui->pathEdit->setText(reference.path);
showSharingState();
}
}
void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked)
{
m_ui->passwordGenerator->regeneratePassword();
m_ui->passwordGenerator->setVisible(checked);
}
void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password)
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.password = password;
KeeShare::setReferenceTo(m_temporaryGroup, reference);
m_ui->togglePasswordGeneratorButton->setChecked(false);
}
void EditGroupWidgetKeeShare::setPath(const QString& path)
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.path = path;
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}
void EditGroupWidgetKeeShare::selectPath()
{
if (!m_temporaryGroup) {
return;
}
QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
const auto filetype = tr("kdbx.share", "Filetype for KeeShare container");
const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare Container"), tr("All files"));
auto filename = reference.path;
if (filename.isEmpty()) {
filename = tr("%1.%2", "Template for KeeShare container").arg(m_temporaryGroup->name()).arg(filetype);
}
switch (reference.type) {
case KeeShareSettings::ImportFrom:
filename = fileDialog()->getFileName(this,
tr("Select import source"),
defaultDirPath,
filters,
nullptr,
QFileDialog::DontConfirmOverwrite,
filetype,
filename);
break;
case KeeShareSettings::ExportTo:
filename = fileDialog()->getFileName(
this, tr("Select export target"), defaultDirPath, filters, nullptr, 0, filetype, filename);
break;
case KeeShareSettings::SynchronizeWith:
case KeeShareSettings::Inactive:
filename = fileDialog()->getFileName(
this, tr("Select import/export file"), defaultDirPath, filters, nullptr, 0, filetype, filename);
break;
}
if (filename.isEmpty()) {
return;
}
setPath(filename);
config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath());
}
void EditGroupWidgetKeeShare::selectPassword()
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.password = m_ui->passwordEdit->text();
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}
void EditGroupWidgetKeeShare::selectType()
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.type = static_cast<KeeShareSettings::Type>(m_ui->typeComboBox->currentData().toInt());
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.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_EDITGROUPWIDGETKEESHARE_H
#define KEEPASSXC_EDITGROUPWIDGETKEESHARE_H
#include <QPointer>
#include <QStandardItemModel>
#include <QWidget>
class Group;
class Database;
namespace Ui
{
class EditGroupWidgetKeeShare;
}
class EditGroupWidgetKeeShare : public QWidget
{
Q_OBJECT
public:
explicit EditGroupWidgetKeeShare(QWidget* parent = nullptr);
~EditGroupWidgetKeeShare();
void setGroup(Group* temporaryGroup);
private slots:
void showSharingState();
private slots:
void update();
void selectType();
void selectPassword();
void selectPath();
void setPath(const QString& path);
void setGeneratedPassword(const QString& password);
void togglePasswordGeneratorButton(bool checked);
private:
QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui;
QPointer<Group> m_temporaryGroup;
};
#endif // KEEPASSXC_EDITGROUPWIDGETKEESHARE_H

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditGroupWidgetKeeShare</class>
<widget class="QWidget" name="EditGroupWidgetKeeShare">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>342</width>
<height>378</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="2" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="typeComboBox"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="pathLabel">
<property name="text">
<string>Path:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="pathLayout">
<item>
<widget class="QLineEdit" name="pathEdit"/>
</item>
<item>
<widget class="QToolButton" name="pathSelectionButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="passwordLayout">
<item>
<widget class="PasswordEdit" name="passwordEdit">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="togglePasswordButton">
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="togglePasswordGeneratorButton">
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="1">
<widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/>
</item>
</layout>
</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>PasswordGeneratorWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordGeneratorWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PasswordEdit</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -2,17 +2,12 @@ if(WITH_XC_SSHAGENT)
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
set(sshagent_SOURCES
bcrypt_pbkdf.cpp
blowfish.c
AgentSettingsPage.cpp
AgentSettingsWidget.cpp
BinaryStream.cpp
KeeAgentSettings.cpp
OpenSSHKey.cpp
ASN1Key.cpp
SSHAgent.cpp
)
add_library(sshagent STATIC ${sshagent_SOURCES})
target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES})
target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB})
endif()

View File

@ -17,8 +17,10 @@
*/
#include "SSHAgent.h"
#include "BinaryStream.h"
#include "KeeAgentSettings.h"
#include "crypto/ssh/OpenSSHKey.h"
#include "crypto/ssh/BinaryStream.h"
#include "sshagent/KeeAgentSettings.h"
#ifndef Q_OS_WIN
#include <QtNetwork>
@ -306,11 +308,11 @@ void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode)
OpenSSHKey key;
if (!key.parse(keyData)) {
if (!key.parsePKCS1PEM(keyData)) {
continue;
}
if (!key.openPrivateKey(e->password())) {
if (!key.openKey(e->password())) {
continue;
}

View File

@ -16,14 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AGENTCLIENT_H
#define AGENTCLIENT_H
#ifndef KEEPASSXC_SSHAGENT_H
#define KEEPASSXC_SSHAGENT_H
#include "OpenSSHKey.h"
#include <QList>
#include <QtCore>
#include "gui/DatabaseWidget.h"
#include "crypto/ssh/OpenSSHKey.h"
class SSHAgent : public QObject
{
@ -75,4 +75,4 @@ private:
QString m_error;
};
#endif // AGENTCLIENT_H
#endif // KEEPASSXC_SSHAGENT_H

View File

@ -95,7 +95,7 @@ set(TEST_LIBRARIES
${ZLIB_LIBRARIES}
)
set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp)
set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp stub/TestRandom.cpp)
add_library(testsupport STATIC ${testsupport_SOURCES})
target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test)
@ -130,6 +130,11 @@ add_unit_test(NAME testcryptohash SOURCES TestCryptoHash.cpp
add_unit_test(NAME testsymmetriccipher SOURCES TestSymmetricCipher.cpp
LIBS ${TEST_LIBRARIES})
if(WITH_XC_KEESHARE)
add_unit_test(NAME testsignature SOURCES TestSignature.cpp
LIBS ${TEST_LIBRARIES})
endif()
add_unit_test(NAME testhashedblockstream SOURCES TestHashedBlockStream.cpp
LIBS testsupport ${TEST_LIBRARIES})
@ -154,9 +159,9 @@ if(WITH_XC_AUTOTYPE)
set_target_properties(testautotype PROPERTIES ENABLE_EXPORTS ON)
endif()
if(WITH_XC_SSHAGENT)
add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp
LIBS sshagent ${TEST_LIBRARIES})
if(WITH_XC_CRYPTO_SSH)
add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp
LIBS ${TEST_LIBRARIES})
endif()
add_unit_test(NAME testentry SOURCES TestEntry.cpp
@ -174,8 +179,8 @@ add_unit_test(NAME testbase32 SOURCES TestBase32.cpp
add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp
LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testrandom SOURCES TestRandom.cpp
LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testrandomgenerator SOURCES TestRandomGenerator.cpp
LIBS testsupport ${TEST_LIBRARIES})
add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp
LIBS ${TEST_LIBRARIES})
@ -187,6 +192,11 @@ add_unit_test(NAME testykchallengeresponsekey
SOURCES TestYkChallengeResponseKey.cpp
LIBS ${TEST_LIBRARIES})
if(WITH_XC_KEESHARE)
add_unit_test(NAME testsharing SOURCES TestSharing.cpp
LIBS testsupport ${TEST_LIBRARIES})
endif()
add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
LIBS ${TEST_LIBRARIES})

View File

@ -18,7 +18,8 @@
#include "TestOpenSSHKey.h"
#include "TestGlobal.h"
#include "crypto/Crypto.h"
#include "sshagent/OpenSSHKey.h"
#include "crypto/ssh/BinaryStream.h"
#include "crypto/ssh/OpenSSHKey.h"
QTEST_GUILESS_MAIN(TestOpenSSHKey)
@ -43,7 +44,7 @@ void TestOpenSSHKey::testParse()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(!key.encrypted());
QCOMPARE(key.cipherName(), QString("none"));
QCOMPARE(key.type(), QString("ssh-ed25519"));
@ -79,7 +80,7 @@ void TestOpenSSHKey::testParseDSA()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(!key.encrypted());
QCOMPARE(key.cipherName(), QString("none"));
QCOMPARE(key.type(), QString("ssh-dss"));
@ -125,11 +126,11 @@ void TestOpenSSHKey::testDecryptRSAAES128CBC()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("AES-128-CBC"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("correctpassphrase"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("correctpassphrase"));
QCOMPARE(key.type(), QString("ssh-rsa"));
QCOMPARE(key.comment(), QString(""));
QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U"));
@ -168,7 +169,7 @@ void TestOpenSSHKey::testParseRSA()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(!key.encrypted());
QCOMPARE(key.cipherName(), QString("none"));
QCOMPARE(key.type(), QString("ssh-rsa"));
@ -246,8 +247,8 @@ void TestOpenSSHKey::testParseRSACompare()
QByteArray oldPrivateKey, newPrivateKey;
BinaryStream oldPrivateStream(&oldPrivateKey), newPrivateStream(&newPrivateKey);
QVERIFY(oldKey.parse(oldKeyData));
QVERIFY(newKey.parse(newKeyData));
QVERIFY(oldKey.parsePKCS1PEM(oldKeyData));
QVERIFY(newKey.parsePKCS1PEM(newKeyData));
// comment is not part of the old format and writePrivate() includes it
oldKey.setComment("id_rsa");
@ -274,11 +275,11 @@ void TestOpenSSHKey::testDecryptOpenSSHAES256CBC()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("aes256-cbc"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("correctpassphrase"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("correctpassphrase"));
QCOMPARE(key.type(), QString("ssh-ed25519"));
QCOMPARE(key.comment(), QString("opensshkey-test-aes256cbc@keepassxc"));
@ -330,11 +331,11 @@ void TestOpenSSHKey::testDecryptRSAAES256CBC()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("AES-256-CBC"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("correctpassphrase"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("correctpassphrase"));
QCOMPARE(key.type(), QString("ssh-rsa"));
QCOMPARE(key.comment(), QString(""));
QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U"));
@ -354,11 +355,11 @@ void TestOpenSSHKey::testDecryptOpenSSHAES256CTR()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("aes256-ctr"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("correctpassphrase"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("correctpassphrase"));
QCOMPARE(key.type(), QString("ssh-ed25519"));
QCOMPARE(key.comment(), QString("opensshkey-test-aes256ctr@keepassxc"));
@ -410,11 +411,11 @@ void TestOpenSSHKey::testDecryptRSAAES256CTR()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("AES-256-CTR"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("correctpassphrase"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("correctpassphrase"));
QCOMPARE(key.type(), QString("ssh-rsa"));
QCOMPARE(key.comment(), QString(""));
QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U"));
@ -436,12 +437,21 @@ void TestOpenSSHKey::testDecryptUTF8()
const QByteArray keyData = keyString.toLatin1();
OpenSSHKey key;
QVERIFY(key.parse(keyData));
QVERIFY(key.parsePKCS1PEM(keyData));
QVERIFY(key.encrypted());
QCOMPARE(key.cipherName(), QString("aes256-ctr"));
QVERIFY(!key.openPrivateKey("incorrectpassphrase"));
QVERIFY(key.openPrivateKey("äåéëþüúíóö"));
QVERIFY(!key.openKey("incorrectpassphrase"));
QVERIFY(key.openKey("äåéëþüúíóö"));
QCOMPARE(key.fingerprint(), QString("SHA256:EfUXwvH4rOoys+AlbznCqjMwzIVW8KuhoWu9uT03FYA"));
QCOMPARE(key.type(), QString("ssh-ed25519"));
QCOMPARE(key.comment(), QString("opensshkey-test-utf8@keepassxc"));
}
void TestOpenSSHKey::testGenerateRSA()
{
OpenSSHKey key = OpenSSHKey::generate(false);
QVERIFY(!key.encrypted());
QCOMPARE(key.cipherName(), QString("none"));
QCOMPARE(key.type(), QString("ssh-rsa"));
QCOMPARE(key.comment(), QString(""));
}

View File

@ -38,6 +38,7 @@ private slots:
void testDecryptOpenSSHAES256CTR();
void testDecryptRSAAES256CTR();
void testDecryptUTF8();
void testGenerateRSA();
};
#endif // TESTOPENSSHKEY_H

View File

@ -15,21 +15,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TestRandom.h"
#include "TestRandomGenerator.h"
#include "TestGlobal.h"
#include "core/Endian.h"
#include "core/Global.h"
#include "stub/TestRandom.h"
QTEST_GUILESS_MAIN(TestRandom)
#include <QTest>
void TestRandom::initTestCase()
QTEST_GUILESS_MAIN(TestRandomGenerator)
void TestRandomGenerator::initTestCase()
{
m_backend = new RandomBackendTest();
m_backend = new RandomBackendPreset();
Random::createWithBackend(m_backend);
TestRandom::setup(m_backend);
}
void TestRandom::testUInt()
void TestRandomGenerator::cleanupTestCase()
{
TestRandom::teardown();
m_backend = nullptr;
}
void TestRandomGenerator::testUInt()
{
QByteArray nextBytes;
@ -60,7 +70,7 @@ void TestRandom::testUInt()
QCOMPARE(randomGen()->randomUInt((QUINT32_MAX / 2U) + 1U), QUINT32_MAX / 2U);
}
void TestRandom::testUIntRange()
void TestRandomGenerator::testUIntRange()
{
QByteArray nextBytes;
@ -68,27 +78,3 @@ void TestRandom::testUIntRange()
m_backend->setNextBytes(nextBytes);
QCOMPARE(randomGen()->randomUIntRange(100, 200), 142U);
}
RandomBackendTest::RandomBackendTest()
: m_bytesIndex(0)
{
}
void RandomBackendTest::randomize(void* data, int len)
{
QVERIFY(len <= (m_nextBytes.size() - m_bytesIndex));
char* charData = reinterpret_cast<char*>(data);
for (int i = 0; i < len; i++) {
charData[i] = m_nextBytes[m_bytesIndex + i];
}
m_bytesIndex += len;
}
void RandomBackendTest::setNextBytes(const QByteArray& nextBytes)
{
m_nextBytes = nextBytes;
m_bytesIndex = 0;
}

View File

@ -15,36 +15,27 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_TESTRANDOM_H
#define KEEPASSX_TESTRANDOM_H
#ifndef KEEPASSX_TESTRANDOMGENERATOR_H
#define KEEPASSX_TESTRANDOMGENERATOR_H
#include "crypto/Random.h"
#include <QObject>
class RandomBackendTest : public RandomBackend
{
public:
RandomBackendTest();
void randomize(void* data, int len) override;
void setNextBytes(const QByteArray& nextBytes);
class RandomBackendPreset;
private:
QByteArray m_nextBytes;
int m_bytesIndex;
};
class TestRandom : public QObject
class TestRandomGenerator : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testUInt();
void testUIntRange();
private:
RandomBackendTest* m_backend;
RandomBackendPreset* m_backend;
};
#endif // KEEPASSX_TESTRANDOM_H
#endif // KEEPASSX_TESTRANDOMGENERATOR_H

Some files were not shown because too many files have changed in this diff Show More