Merge pull request #2109 from hicknhack-software/feature/sharing_groups

Implement group synchronization feature
This commit is contained in:
Jonathan White 2019-01-19 10:03:21 -05:00 committed by GitHub
commit b59fd6d06a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 7053 additions and 1260 deletions

View File

@ -47,6 +47,8 @@ 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 "Sharing integration with KeeShare" OFF)
option(WITH_XC_KEESHARE_SECURE "Sharing integration with secured KeeShare containers" OFF)
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
@ -58,9 +60,20 @@ if(WITH_XC_ALL)
set(WITH_XC_BROWSER ON)
set(WITH_XC_YUBIKEY ON)
set(WITH_XC_SSHAGENT ON)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
set(WITH_XC_KEESHARE ON)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
endif()
if(WITH_XC_KEESHARE_SECURE)
set(WITH_XC_KEESHARE ON)
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")
@ -349,6 +362,19 @@ endif()
include_directories(SYSTEM ${ARGON2_INCLUDE_DIR})
# Optional
if(WITH_XC_KEESHARE)
set(WITH_XC_KEESHARE_INSECURE ON)
if(WITH_XC_KEESHARE_SECURE)
# ZLIB is needed and already required
find_package(QuaZip REQUIRED)
include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR})
endif()
else()
set(WITH_XC_KEESHARE_INSECURE OFF)
set(WITH_XC_KEESHARE_SECURE OFF)
endif()
# Optional
if(WITH_XC_YUBIKEY)
find_package(YubiKey REQUIRED)

View File

@ -54,6 +54,8 @@ RUN set -x \
zlib1g-dev \
libxi-dev \
libxtst-dev \
# ubuntu:14.04 has no quazip (it's optional)
# libquazip5-dev \
mesa-common-dev \
libyubikey-dev \
libykpers-1-dev \

View File

@ -98,15 +98,19 @@ 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_KEESHARE=[ON|OFF] Enable/Disable KeeShare group syncronization 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_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare secure containers, requires libquazip5 (default: OFF)
-DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON)
-DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF)
-DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF)
-DWITH_ASAN=[ON|OFF] Enable/Disable address sanitizer checks (Linux / macOS only) (default: OFF)
-DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF)
-DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF)
-DWITH_APP_BUNDLE=[ON|OFF] Enable Application Bundle for macOS (default: ON)
```
* If you are on MacOS you must add this parameter to **Cmake**, with the Qt version you have installed<br/> `-DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake/`

View File

@ -1,7 +1,6 @@
# <img src="https://keepassxc.org/logo.png" width="40" height="40"/> KeePassXC
[![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(project:KeepassXC\)/statusIcon)](https://ci.keepassxc.org/?guest=1) [![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc)
## About KeePassXC
[KeePassXC](https://keepassxc.org) is a cross-platform community fork of
[KeePassX](https://www.keepassx.org/).
@ -35,6 +34,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)
- Synchronize 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

@ -54,6 +54,8 @@ RUN set -x \
zlib1g-dev \
libyubikey-dev \
libykpers-1-dev \
# ubuntu:14.04 has no quazip (it's optional)
# libquazip5-dev \
libxi-dev \
libxtst-dev \
libqrencode-dev \

View File

@ -28,9 +28,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

41
cmake/FindQuaZip.cmake Normal file
View File

@ -0,0 +1,41 @@
# QUAZIP_FOUND - QuaZip library was found
# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir
# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR)
# QUAZIP_LIBRARIES - List of QuaZip libraries
# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers
IF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES)
# in cache already
SET(QUAZIP_FOUND TRUE)
ELSE(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES)
IF(Qt5Core_FOUND)
set(QUAZIP_LIB_VERSION_SUFFIX 5)
ENDIF()
IF(WIN32)
FIND_PATH(QUAZIP_LIBRARY_DIR
WIN32_DEBUG_POSTFIX d
NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll
HINTS "C:/Programme/" "C:/Program Files"
PATH_SUFFIXES QuaZip/lib
)
FIND_LIBRARY(QUAZIP_LIBRARIES NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll HINTS ${QUAZIP_LIBRARY_DIR})
FIND_PATH(QUAZIP_INCLUDE_DIR NAMES quazip.h HINTS ${QUAZIP_LIBRARY_DIR}/../ PATH_SUFFIXES include/quazip5)
FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h)
ELSE(WIN32)
FIND_PACKAGE(PkgConfig)
pkg_check_modules(PC_QUAZIP quazip)
FIND_LIBRARY(QUAZIP_LIBRARIES
WIN32_DEBUG_POSTFIX d
NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX}
HINTS /usr/lib /usr/lib64
)
FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h
HINTS /usr/include /usr/local/include
PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX}
)
FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include)
ENDIF(WIN32)
INCLUDE(FindPackageHandleStandardArgs)
SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR})
find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS)
ENDIF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

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 or Tor Browser](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,87 @@ 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 for the application.
1. Go to Tools &rarr; Settings
2. Select the category KeeShare
3. Check _Allow import_ if you want to import shared credentials
4. Check _Allow export_ if you want to share credentials
To make sure that your data is valid when im imported by another client, please _generate_ (or _import_) a public/private key pair and enter your _signer_ name. This way your client may verify that the imported data is valid. When Importing, you'll see the known sources with names and fingerprint in the list at the bottom. This is the place to _trust_ or _untrust_ signers. It is only possible to trust someone on application level.
<img src="./KeeShare/AppSettings.png" height="600" width="800" alt="KeeShare Application 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 container
The export file will not be generated automatically. Instead, each time the database is saved, the file gets written (so please deactivate the autosafe feature). 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/GroupSettings_Export.png" height="600" width="800" alt="KeeShare 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 immediately 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 share container that is shared with you
1. Enter the password for the shared container
KeeShare observes the container for changes and merges them into your database when necessary. Importing merges in time order, so older data is moved to the history, which should have a sufficient size to prevent loss of needed data.
Please note, that the import currently is not restricted to the configured group. Every entry which was imported and moved outside the import group will be updated regardless of it's location!
<img src="./KeeShare/GroupSettings_Import.png" height="600" width="800" alt="KeeShare 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/GroupSettings_Sync.png" height="600" width="800" alt="KeeShare Group Synchronization Settings">
### Disable Sharing for Credentials
In case you don't want to share (import or export) some credentials, it is possible to you can
* use the application settings and uncheck the options or
* instead of selecting _Import from path_, _Export to path_ or _Synchronize with path_ you'll select _Inactive_ while leaving the path and the password untouched
### Sharing overview
There is a simple overview of shared groups to keep track of your data.
1. Open the Database Settings
1. Select the KeeShare category
<img src="./KeeShare/DatabaseSettings.png" height="600" width="800" alt="KeeShare Group Sharing Ovewview">
## Technical Details and Limitations of Sharing
Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC. Since the merge algorithm uses the history of entries to prevent data loss, this history must be enabled and have a sufficient size. Furthermore, the merge algorithm is location independend, therefore it does not matter if entries are moved outside of an import group. These entries will be updated none the less. Moving entries outside of export groups will prevent a further export of the entry, but it will not ensure that the already shared data will be removed from any client.
KeeShare uses a custom certification mechanism to ensure that the source of the data is the expected one. This ensures that the data was exported by the signer but it is not possible to detect if someone replaced the data with an older version from a valid signer. To prevent this, the container could be placed at a location which is only writeable for valid signers.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -50,6 +50,7 @@ set(keepassx_SOURCES
core/EntryAttributes.cpp
core/EntrySearcher.cpp
core/FilePath.cpp
core/FileWatcher.cpp
core/Bootstrap.cpp
core/Group.cpp
core/InactivityTimer.cpp
@ -201,6 +202,8 @@ 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(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
@ -219,6 +222,16 @@ add_subdirectory(cli)
add_subdirectory(qrcode)
set(qrcode_LIB qrcode)
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)
@ -257,21 +270,24 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL
target_link_libraries(keepassx_core
autotype
${keepassxcbrowser_LIB}
${sshagent_LIB}
${qrcode_LIB}
Qt5::Core
Qt5::Concurrent
Qt5::Network
Qt5::Widgets
${CURL_LIBRARIES}
${YUBIKEY_LIBRARIES}
${ZXCVBN_LIBRARIES}
${ARGON2_LIBRARIES}
${GCRYPT_LIBRARIES}
${GPGERROR_LIBRARIES}
${YUBIKEY_LIBRARIES}
${ZLIB_LIBRARIES}
${ZXCVBN_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 -framework AppKit")

View File

@ -17,6 +17,9 @@
#cmakedefine WITH_XC_BROWSER
#cmakedefine WITH_XC_YUBIKEY
#cmakedefine WITH_XC_SSHAGENT
#cmakedefine WITH_XC_KEESHARE
#cmakedefine WITH_XC_KEESHARE_INSECURE
#cmakedefine WITH_XC_KEESHARE_SECURE
#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

@ -65,7 +65,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

@ -472,8 +472,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 false;
}
void Database::setCipher(const QUuid& cipher)

View File

@ -24,6 +24,7 @@
#include <QObject>
#include <QPointer>
#include "config-keepassx.h"
#include "crypto/kdf/Kdf.h"
#include "format/KeePass2.h"
#include "keys/CompositeKey.h"

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

@ -1027,6 +1027,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()->rootGroup()->findEntryBySearchTerm(searchText, searchInType);
}
QString Entry::resolveMultiplePlaceholders(const QString& str) const
{
return resolveMultiplePlaceholdersRecursive(str, ResolveMaximumDepth);

View File

@ -201,6 +201,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;

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

@ -0,0 +1,295 @@
/*
* 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()), this, 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()));
connect(&m_pendingSignalsTimer, SIGNAL(timeout()), this, SLOT(emitSignals()));
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_watchedPaths.clear();
m_watchedFilesInDirectory.clear();
m_ignoreFilesChangess.clear();
}
void BulkFileWatcher::removePath(const QString& path)
{
const QFileInfo info(path);
const QString filePath = info.absoluteFilePath();
const QString directoryPath = info.absolutePath();
m_watchedFilesInDirectory[directoryPath].remove(filePath);
m_fileWatcher.removePath(filePath);
m_watchedPaths.remove(filePath);
if (m_watchedFilesInDirectory[directoryPath].isEmpty()) {
m_fileWatcher.removePath(directoryPath);
m_watchedPaths.remove(directoryPath);
m_watchedFilesInDirectory.remove(directoryPath);
}
}
void BulkFileWatcher::addPath(const QString& path)
{
const QFileInfo info(path);
const QString filePath = info.absoluteFilePath();
const QString directoryPath = info.absolutePath();
if (!m_watchedPaths.value(filePath)) {
const bool fileSuccess = m_fileWatcher.addPath(filePath);
m_watchedPaths[filePath] = fileSuccess;
}
if (!m_watchedPaths.value(directoryPath)) {
const bool directorySuccess = m_fileWatcher.addPath(directoryPath);
m_watchedPaths[directoryPath] = directorySuccess;
}
m_watchedFilesInDirectory[directoryPath][filePath] = info.exists();
}
void BulkFileWatcher::handleFileChanged(const QString& path)
{
const QFileInfo info(path);
const QString filePath = info.absoluteFilePath();
const QString directoryPath = info.absolutePath();
const QMap<QString, bool>& watchedFiles = m_watchedFilesInDirectory[directoryPath];
const bool created = !watchedFiles[filePath] && info.exists();
const bool deleted = watchedFiles[filePath] && !info.exists();
const bool changed = !created && !deleted;
addPath(path);
if (m_ignoreFilesChangess[info.canonicalFilePath()] > Clock::currentDateTimeUtc()) {
// changes are blocked
return;
}
if (created) {
qDebug("File created %s", qPrintable(path));
scheduleSignal(Created, filePath);
}
if (changed) {
qDebug("File changed %s", qPrintable(path));
scheduleSignal(Updated, filePath);
}
if (deleted) {
qDebug("File removed %s", qPrintable(path));
scheduleSignal(Removed, filePath);
}
}
void BulkFileWatcher::handleDirectoryChanged(const QString& path)
{
qDebug("Directory changed %s", qPrintable(path));
const QFileInfo directoryInfo(path);
const QString directoryPath = directoryInfo.absoluteFilePath();
QMap<QString, bool>& watchedFiles = m_watchedFilesInDirectory[directoryPath];
for (const QString& filename : watchedFiles.keys()) {
const QFileInfo fileInfo(filename);
const QString filePath = fileInfo.absoluteFilePath();
const bool existed = watchedFiles[filePath];
if (!fileInfo.exists() && existed) {
qDebug("Remove watch file %s", qPrintable(filePath));
m_fileWatcher.removePath(filePath);
m_watchedPaths.remove(filePath);
watchedFiles.remove(filePath);
scheduleSignal(Removed, filePath);
}
if (!existed && fileInfo.exists()) {
qDebug("Add watch file %s", qPrintable(filePath));
if (!m_watchedPaths.value(filePath)) {
const bool success = m_fileWatcher.addPath(filePath);
m_watchedPaths[filePath] = success;
watchedFiles[filePath] = fileInfo.exists();
}
scheduleSignal(Created, filePath);
}
if (existed && fileInfo.exists()) {
// this case is handled using
qDebug("Refresh watch file %s", qPrintable(fileInfo.absoluteFilePath()));
m_fileWatcher.removePath(fileInfo.absolutePath());
m_fileWatcher.addPath(fileInfo.absolutePath());
scheduleSignal(Updated, filePath);
}
m_watchedFilesInDirectory[directoryPath][filePath] = fileInfo.exists();
}
}
void BulkFileWatcher::emitSignals()
{
QMap<QString, QList<Signal>> queued;
m_pendingSignals.swap(queued);
for (const auto& path : queued.keys()){
const auto &signal = queued[path];
if (signal.last() == Removed) {
emit fileRemoved(path);
continue;
}
if (signal.first() == Created){
emit fileCreated(path);
continue;
}
emit fileChanged(path);
}
}
void BulkFileWatcher::scheduleSignal(Signal signal, const QString &path)
{
// we need to collect signals since the file watcher API may send multiple signals for a "single" change
// therefore we wait until the event loop finished before starting to import any changes
const QString filePath = QFileInfo(path).absoluteFilePath();
m_pendingSignals[filePath] << signal;
if (!m_pendingSignalsTimer.isActive()) {
m_pendingSignalsTimer.start();
}
}
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);
}
}

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

@ -0,0 +1,105 @@
/*
* 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
enum Signal {
Created,
Updated,
Removed
};
public:
explicit BulkFileWatcher(QObject* parent = nullptr);
void clear();
void removePath(const QString& path);
void addPath(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);
void emitSignals();
private:
void scheduleSignal(Signal event, const QString &path);
private:
QMap<QString, bool> m_watchedPaths;
QMap<QString, QDateTime> m_ignoreFilesChangess;
QFileSystemWatcher m_fileWatcher;
QMap<QString, QMap<QString, bool>> m_watchedFilesInDirectory;
// needed for Import/Export-References to prevent update after self-write
QTimer m_fileWatchUnblockTimer;
// needed to tolerate multiple signals for same event
QTimer m_pendingSignalsTimer;
QMap<QString, QList<Signal>> m_pendingSignals;
};
#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

@ -211,4 +211,30 @@ namespace Tools
QString uuidToHex(const QUuid& uuid) {
return QString::fromLatin1(uuid.toRfc4122().toHex());
}
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

@ -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

@ -87,7 +87,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;
}
@ -195,7 +194,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;
}
}
@ -204,7 +203,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;
}
}
@ -219,13 +218,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;
}
@ -256,7 +255,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);
} // namespace ASN1Key
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,45 +17,219 @@
*/
#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_comment(QString())
, m_error(QString())
: QObject(parent)
, m_type(QString())
, m_cipherName(QString("none"))
, m_kdfName(QString("none"))
, m_kdfOptions(QByteArray())
, m_rawType(QString())
, m_rawData(QByteArray())
, m_rawPublicData(QList<QByteArray>())
, m_rawPrivateData(QList<QByteArray>())
, m_comment(QString())
, m_error(QString())
{
}
OpenSSHKey::OpenSSHKey(const OpenSSHKey& other)
: QObject(nullptr)
, m_type(other.m_type)
, m_cipherName(other.m_cipherName)
, m_kdfName(other.m_kdfName)
, m_kdfOptions(other.m_kdfOptions)
, 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)
: QObject(nullptr)
, m_type(other.m_type)
, 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_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 (const 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 (const 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;
}
@ -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 (const 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 (const 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 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,
}
QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, 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 false;
}
@ -85,8 +84,7 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,
raiseError(tr("Unknown cipher"));
return false;
}
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 false;

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);
@ -86,8 +86,8 @@ private:
bool m_innerStreamProtectionDisabled = false;
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

@ -65,15 +65,15 @@ AboutDialog::AboutDialog(QWidget* parent)
#endif
debugInfo.append("\n").append(
QString("%1\n- Qt %2\n- %3\n\n")
.arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion()));
QString("%1\n- Qt %2\n- %3\n\n")
.arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion()));
#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4")
.arg(QSysInfo::prettyProductName(),
QSysInfo::currentCpuArchitecture(),
QSysInfo::kernelType(),
QSysInfo::kernelVersion()));
.arg(QSysInfo::prettyProductName(),
QSysInfo::currentCpuArchitecture(),
QSysInfo::kernelType(),
QSysInfo::kernelVersion()));
debugInfo.append("\n\n");
#endif
@ -88,6 +88,13 @@ AboutDialog::AboutDialog(QWidget* parent)
#ifdef WITH_XC_SSHAGENT
extensions += "\n- " + tr("SSH Agent");
#endif
#if defined(WITH_XC_KEESHARE_SECURE) && defined(WITH_XC_KEESHARE_INSECURE)
extensions += "\n- " + tr("KeeShare (signed and unsigned sharing)");
#elif defined(WITH_XC_KEESHARE_SECURE)
extensions += "\n- " + tr("KeeShare (only signed sharing)");
#elif defined(WITH_XC_KEESHARE_INSECURE)
extensions += "\n- " + tr("KeeShare (only unsigned sharing)");
#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,19 +78,18 @@ 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)));
// clang-format off
connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), SLOT(enableAutoSaveOnExit(bool)));
connect(m_generalUi->systrayShowCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableSystray(bool)));
connect(m_generalUi->toolbarHideCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableToolbarSettings(bool)));
connect(
m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool)));
connect(m_secUi->lockDatabaseIdleCheckBox,
SIGNAL(toggled(bool)),
m_secUi->lockDatabaseIdleSpinBox,
SLOT(setEnabled(bool)));
connect(m_secUi->touchIDResetCheckBox, SIGNAL(toggled(bool)), m_secUi->touchIDResetSpinBox, 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, SLOT(setEnabled(bool)));
connect(m_secUi->touchIDResetCheckBox, SIGNAL(toggled(bool)),
m_secUi->touchIDResetSpinBox, SLOT(setEnabled(bool)));
// clang-format on
#ifndef WITH_XC_NETWORKING
m_secUi->privacy->setVisible(false);
@ -121,7 +121,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

@ -36,6 +36,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"
@ -57,6 +58,7 @@
#include "gui/entry/EntryView.h"
#include "gui/group/EditGroupWidget.h"
#include "gui/group/GroupView.h"
#include "keeshare/KeeShare.h"
#include "touchid/TouchID.h"
#include "config-keepassx.h"
@ -72,7 +74,6 @@
DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
: QStackedWidget(parent)
, m_db(std::move(db))
, m_mainWidget(new QWidget(this))
, m_mainSplitter(new QSplitter(m_mainWidget))
, m_messageWidget(new MessageWidget(this))
@ -87,6 +88,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
, m_groupView(new GroupView(m_db.data(), m_mainSplitter))
, m_fileWatcher(new DelayingFileWatcher(this))
{
m_messageWidget->setHidden(true);
@ -129,7 +131,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_searchingLabel->setStyleSheet("color: rgb(0, 0, 0);"
"background-color: rgb(255, 253, 160);"
"border: 2px solid rgb(190, 190, 190);"
"border-radius: 2px;");
"border-radius: 4px;");
m_searchingLabel->setVisible(false);
m_previewView->hide();
@ -174,17 +176,11 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(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()));
// clang-format on
connectDatabaseSignals();
m_fileWatchTimer.setSingleShot(true);
m_fileWatchUnblockTimer.setSingleShot(true);
m_ignoreAutoReload = false;
connectDatabaseSignals();
m_EntrySearcher = new EntrySearcher(false);
m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
@ -377,6 +373,9 @@ void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
connectDatabaseSignals();
m_groupView->changeDatabase(m_db);
processAutoOpen();
#if defined(WITH_XC_KEESHARE)
KeeShare::instance()->connectDatabase(m_db, oldDb);
#endif
}
void DatabaseWidget::cloneEntry()
@ -831,10 +830,10 @@ void DatabaseWidget::loadDatabase(bool accepted)
if (accepted) {
replaceDatabase(openWidget->database());
switchToMainView();
m_fileWatcher.addPath(m_db->filePath());
m_fileWatcher->restart();
emit databaseUnlocked();
} else {
m_fileWatcher.removePath(m_db->filePath());
m_fileWatcher->stop();
if (m_databaseOpenWidget->database()) {
m_databaseOpenWidget->database().reset();
}
@ -1246,56 +1245,19 @@ bool DatabaseWidget::lock()
void DatabaseWidget::updateFilePath(const QString& filePath)
{
if (!m_db->filePath().isEmpty()) {
m_fileWatcher.removePath(m_db->filePath());
}
#ifdef 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_db->setFilePath(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_db->filePath());
}
void DatabaseWidget::onWatchedFileChanged()
{
if (m_ignoreAutoReload) {
return;
}
if (m_fileWatchTimer.isActive())
return;
m_fileWatchTimer.start(500);
}
void DatabaseWidget::reloadDatabaseFile()
{
if (!m_db || isLocked()) {
@ -1313,7 +1275,7 @@ void DatabaseWidget::reloadDatabaseFile()
// Notify everyone the database does not match the file
m_db->markAsModified();
// Rewatch the database file
m_fileWatcher.addPath(m_db->filePath());
m_fileWatcher->restart();
return;
}
}
@ -1361,7 +1323,7 @@ void DatabaseWidget::reloadDatabaseFile()
}
// Rewatch the database file
m_fileWatcher.addPath(m_db->filePath());
m_fileWatcher->restart();
}
int DatabaseWidget::numberOfSelectedEntries() const

View File

@ -34,6 +34,7 @@ class DatabaseOpenWidget;
class KeePass1OpenWidget;
class DatabaseSettingsDialog;
class Database;
class DelayingFileWatcher;
class EditEntryWidget;
class EditGroupWidget;
class Entry;
@ -47,7 +48,6 @@ class QSplitter;
class QLabel;
class MessageWidget;
class EntryPreviewWidget;
class QFileSystemWatcher;
namespace Ui
{
@ -215,10 +215,8 @@ private slots:
void mergeDatabase(bool accepted);
void emitCurrentModeChanged();
// Database autoreload slots
void onWatchedFileChanged();
void reloadDatabaseFile();
void restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& EntryUuid);
void unblockAutoReload();
private:
int addChildWidget(QWidget* w);
@ -258,10 +256,7 @@ private:
bool m_searchLimitGroup;
// Autoreload
QFileSystemWatcher m_fileWatcher;
QTimer m_fileWatchTimer;
QTimer m_fileWatchUnblockTimer;
bool m_ignoreAutoReload;
QPointer<DelayingFileWatcher> m_fileWatcher;
};
#endif // KEEPASSX_DATABASEWIDGET_H

View File

@ -18,6 +18,8 @@
#include "EditWidgetProperties.h"
#include "ui_EditWidgetProperties.h"
#include "core/CustomData.h"
#include "core/TimeInfo.h"
#include "MessageBox.h"
#include <QUuid>
@ -25,7 +27,6 @@
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);
@ -53,17 +54,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(customDataModified()), SLOT(update()));
}
update();
}
void EditWidgetProperties::removeSelectedPluginData()
@ -85,7 +88,7 @@ void EditWidgetProperties::removeSelectedPluginData()
const QString key = index.data().toString();
m_customData->remove(key);
}
updateModel();
update();
}
}
@ -94,16 +97,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

@ -26,10 +26,12 @@
#include "core/FilePath.h"
#include "entry/EntryAttachmentsModel.h"
#include "gui/Clipboard.h"
#if defined(WITH_XC_KEESHARE)
#include "keeshare/KeeShare.h"
#endif
namespace
{
constexpr int GeneralTabIndex = 0;
namespace {
constexpr int GeneralTabIndex = 0;
}
EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
@ -104,6 +106,10 @@ void EntryPreviewWidget::setGroup(Group* selectedGroup)
updateGroupGeneralTab();
updateGroupNotesTab();
#if defined(WITH_XC_KEESHARE)
updateGroupSharingTab();
#endif
setVisible(!config()->get("GUI/HidePreviewPanel").toBool());
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
@ -202,7 +208,7 @@ void EntryPreviewWidget::updateEntryGeneralTab()
const TimeInfo entryTime = m_currentEntry->timeInfo();
const QString expires =
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
m_ui->entryExpirationLabel->setText(expires);
}
@ -252,7 +258,7 @@ void EntryPreviewWidget::updateEntryAutotypeTab()
const auto associations = autotypeAssociations->getAll();
for (const auto& assoc : associations) {
const QString sequence =
assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence() : assoc.sequence;
assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence() : assoc.sequence;
items.append(new QTreeWidgetItem(m_ui->entryAutotypeTree, {assoc.window, sequence}));
}
@ -278,7 +284,7 @@ void EntryPreviewWidget::updateGroupGeneralTab()
const TimeInfo groupTime = m_currentGroup->timeInfo();
const QString expiresText =
groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
m_ui->groupExpirationLabel->setText(expiresText);
}
@ -290,6 +296,17 @@ void EntryPreviewWidget::updateGroupNotesTab()
m_ui->groupNotesEdit->setText(notes);
}
#if defined(WITH_XC_KEESHARE)
void EntryPreviewWidget::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 EntryPreviewWidget::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>
@ -56,6 +57,9 @@ private slots:
void updateGroupHeaderLine();
void updateGroupGeneralTab();
void updateGroupNotesTab();
#if defined(WITH_XC_KEESHARE)
void updateGroupSharingTab();
#endif
void updateTotpLabel();
void updateTabIndexes();

File diff suppressed because it is too large Load Diff

View File

@ -79,13 +79,72 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent,
}
}
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;
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);
#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);
if (!defaultExtension.isEmpty()) {
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& defaultExtension,
const QString& defaultName)
{
if (!m_nextFileName.isEmpty()) {
QString result = m_nextFileName;
@ -98,6 +157,7 @@ QString FileDialog::getSaveFileName(QWidget* parent,
QString result;
#if defined(Q_OS_MACOS) || 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 +168,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 +193,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

@ -21,6 +21,7 @@
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFileInfo>
#include <QMimeData>
#include <QShortcut>
#include <QTimer>
@ -32,18 +33,21 @@
#include "core/FilePath.h"
#include "core/InactivityTimer.h"
#include "core/Metadata.h"
#include "keys/CompositeKey.h"
#include "keys/PasswordKey.h"
#include "keys/FileKey.h"
#include "gui/AboutDialog.h"
#include "gui/DatabaseWidget.h"
#include "gui/SearchWidget.h"
#include "keys/CompositeKey.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#ifdef WITH_XC_SSHAGENT
#include "sshagent/AgentSettingsPage.h"
#include "sshagent/SSHAgent.h"
#endif
#if defined(WITH_XC_KEESHARE)
#include "keeshare/KeeShare.h"
#include "keeshare/SettingsPageKeeShare.h"
#endif
#ifdef WITH_XC_BROWSER
#include "browser/BrowserOptionDialog.h"
#include "browser/BrowserSettings.h"
@ -114,7 +118,10 @@ private:
const QString MainWindow::BaseWindowTitle = "KeePassXC";
MainWindow* g_MainWindow = nullptr;
MainWindow* getMainWindow() { return g_MainWindow; }
MainWindow* getMainWindow()
{
return g_MainWindow;
}
MainWindow::MainWindow()
: m_ui(new Ui::MainWindow())
@ -148,19 +155,27 @@ MainWindow::MainWindow()
#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
#if defined(WITH_XC_KEESHARE)
KeeShare::init(this);
m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget));
connect(KeeShare::instance(),
SIGNAL(sharingMessage(QString, MessageWidget::MessageType)),
SLOT(displayGlobalMessage(QString, MessageWidget::MessageType)));
#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()));
// clang-format off
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()));
// clang-format on
m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile);
m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases);
@ -349,10 +364,10 @@ MainWindow::MainWindow()
setUnifiedTitleAndToolBarOnMac(true);
#endif
// clang-format off
connect(m_ui->tabWidget,
SIGNAL(messageGlobal(QString,MessageWidget::MessageType)),
this,
SLOT(displayGlobalMessage(QString,MessageWidget::MessageType)));
connect(m_ui->tabWidget,
SIGNAL(messageGlobal(QString,MessageWidget::MessageType)),
this,
SLOT(displayGlobalMessage(QString,MessageWidget::MessageType)));
// clang-format on
connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage()));
@ -838,7 +853,8 @@ bool MainWindow::saveLastDatabases()
bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool();
if (openPreviousDatabasesOnStartup) {
connect(m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&)));
connect(
m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&)));
}
accept = m_ui->tabWidget->closeAllDatabaseTabs();

View File

@ -640,8 +640,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>
@ -666,12 +677,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

@ -25,12 +25,37 @@
#ifdef WITH_XC_BROWSER
#include "DatabaseSettingsWidgetBrowser.h"
#endif
#if defined(WITH_XC_KEESHARE)
#include "keeshare/DatabaseSettingsPageKeeShare.h"
#endif
#include "core/Global.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "touchid/TouchID.h"
class DatabaseSettingsDialog::ExtraPage
{
public:
ExtraPage(IDatabaseSettingsPage* page, QWidget* widget)
: settingsPage(page)
, widget(widget)
{
}
void loadSettings(QSharedPointer<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())
@ -55,6 +80,10 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
#if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
m_ui->stackedWidget->setCurrentIndex(0);
m_securityTabWidget->setCurrentIndex(0);
@ -84,10 +113,24 @@ void DatabaseSettingsDialog::load(QSharedPointer<Database> db)
#ifdef WITH_XC_BROWSER
m_browserWidget->load(db);
#endif
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.
*/
@ -111,6 +154,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

@ -39,6 +39,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, QSharedPointer<Database> db) = 0;
virtual void saveSettings(QWidget* widget) = 0;
};
class DatabaseSettingsDialog : public DialogyWidget
{
Q_OBJECT
@ -49,6 +62,7 @@ public:
Q_DISABLE_COPY(DatabaseSettingsDialog);
void load(QSharedPointer<Database> db);
void addSettingsPage(IDatabaseSettingsPage* page);
void showMasterKeySettings();
signals:
@ -76,6 +90,9 @@ private:
#ifdef WITH_XC_BROWSER
QPointer<DatabaseSettingsWidgetBrowser> m_browserWidget;
#endif
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();
@ -111,6 +116,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()
@ -543,13 +550,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;
}
@ -684,6 +691,8 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q
void EditEntryWidget::setForms(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);
@ -779,7 +788,6 @@ void EditEntryWidget::setForms(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());
@ -888,7 +896,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

@ -30,6 +30,7 @@
class AutoTypeAssociations;
class AutoTypeAssociationsModel;
class CustomData;
class Database;
class EditWidgetIcons;
class EditWidgetProperties;
@ -157,11 +158,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,18 +23,49 @@
#include "gui/EditWidgetIcons.h"
#include "gui/EditWidgetProperties.h"
#if defined(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)
: EditWidget(parent)
, m_mainUi(new Ui::EditGroupWidgetMain())
, m_editGroupWidgetMain(new QWidget())
, m_editGroupWidgetIcons(new EditWidgetIcons())
, m_editWidgetProperties(new EditWidgetProperties())
, m_group(nullptr)
{
m_mainUi->setupUi(m_editGroupWidgetMain);
addPage(tr("Group"), FilePath::instance()->icon("actions", "document-edit"), m_editGroupWidgetMain);
addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_editGroupWidgetIcons);
#if defined(WITH_XC_KEESHARE)
addEditPage(new EditGroupPageKeeShare(this));
#endif
addPage(tr("Properties"), FilePath::instance()->icon("actions", "document-properties"), m_editWidgetProperties);
connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool)));
@ -65,6 +96,8 @@ void EditGroupWidget::loadGroup(Group* group, bool create, QSharedPointer<Databa
m_group = group;
m_db = database;
m_temporaryGroup.reset(group->clone(Entry::CloneNoFlags, Group::CloneNoFlags));
if (create) {
setHeadline(tr("Add group"));
} else {
@ -93,12 +126,15 @@ void EditGroupWidget::loadGroup(Group* group, bool create, QSharedPointer<Databa
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_db, 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);
@ -114,31 +150,36 @@ 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()
@ -155,9 +196,19 @@ void EditGroupWidget::clear()
{
m_group = nullptr;
m_db.reset();
m_temporaryGroup.reset(nullptr);
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;
} // namespace Ui
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, QSharedPointer<Database> database);
void clear();
void addEditPage(IEditGroupPage* page);
signals:
void editFinished(bool accepted);
void messageEditEntry(QString, MessageWidget::MessageType);
@ -60,13 +76,18 @@ private:
Group::TriState triStateFromIndex(int index);
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
QPointer<QWidget> m_editGroupWidgetMain;
QPointer<EditWidgetIcons> m_editGroupWidgetIcons;
QPointer<EditWidgetProperties> m_editWidgetProperties;
QScopedPointer<Group> m_temporaryGroup;
QPointer<Group> m_group;
QSharedPointer<Database> m_db;
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)
@ -123,13 +124,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");
#if defined(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();
#if defined(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,23 @@
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})
if(WITH_XC_KEESHARE_SECURE)
target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB})
else()
target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB})
endif()
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, QSharedPointer<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, QSharedPointer<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(QSharedPointer<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(tr(" > ", "Breadcrumb separator")));
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(QSharedPointer<Database> db);
void saveSettings();
private:
QScopedPointer<Ui::DatabaseSettingsWidgetKeeShare> m_ui;
QScopedPointer<QStandardItemModel> m_referencesModel;
QSharedPointer<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>

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

@ -0,0 +1,218 @@
/*
* 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");
} // namespace
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;
}
KeeShare::KeeShare(QObject* parent)
: QObject(parent)
{
connect(config(), SIGNAL(changed(QString)), SLOT(handleSettingsChanged(QString)));
}
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);
}
bool KeeShare::isEnabled(const Group* group)
{
const auto reference = KeeShare::referenceOf(group);
#if !defined(WITH_XC_KEESHARE_SECURE)
if (reference.path.endsWith(signedContainerFileType(), Qt::CaseInsensitive)) {
return false;
}
#endif
#if !defined(WITH_XC_KEESHARE_INSECURE)
if (reference.path.endsWith(unsignedContainerFileType(), Qt::CaseInsensitive)) {
return false;
}
#endif
const auto active = KeeShare::active();
return (reference.isImporting() && active.in) || (reference.isExporting() && active.out);
}
QPixmap KeeShare::indicatorBadge(const Group* group, QPixmap pixmap)
{
if (!isShared(group)) {
return pixmap;
}
const QPixmap badge = isEnabled(group) ? 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(QSharedPointer<Database> newDb, QSharedPointer<Database> oldDb)
{
if (oldDb && m_observersByDatabase.contains(oldDb->uuid())) {
QPointer<ShareObserver> observer = m_observersByDatabase.take(oldDb->uuid());
if (observer) {
delete observer;
}
}
if (newDb && !m_observersByDatabase.contains(newDb->uuid())) {
QPointer<ShareObserver> observer(new ShareObserver(newDb, this));
m_observersByDatabase[newDb->uuid()] = observer;
connect(observer.data(),
SIGNAL(sharingMessage(QString, MessageWidget::MessageType)),
SIGNAL(sharingMessage(QString, MessageWidget::MessageType)));
}
}
const QString& KeeShare::signedContainerFileType()
{
static const QString filetype("kdbx.share");
return filetype;
}
const QString& KeeShare::unsignedContainerFileType()
{
static const QString filetype("kdbx");
return filetype;
}
void KeeShare::handleSettingsChanged(const QString& key)
{
if (key == KeeShare_Active) {
emit activeChanged();
}
}

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

@ -0,0 +1,77 @@
/*
* 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 <QUuid>
#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 bool isEnabled(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(QSharedPointer<Database> newDb, QSharedPointer<Database> oldDb);
static const QString& signedContainerFileType();
static const QString& unsignedContainerFileType();
signals:
void activeChanged();
void sharingMessage(QString, MessageWidget::MessageType);
private slots:
void handleSettingsChanged(const QString&);
private:
static KeeShare* m_instance;
explicit KeeShare(QObject* parent);
QMap<QUuid, QPointer<ShareObserver>> m_observersByDatabase;
};
#endif // KEEPASSXC_KEESHARE_H

View File

@ -0,0 +1,497 @@
/*
* 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, const QString& signer)
{
KeeShareSettings::Certificate extracted;
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);
}
} // namespace
void Certificate::serialize(QXmlStreamWriter& writer, const Certificate& certificate)
{
if (certificate.isNull()) {
return;
}
writer.writeStartElement("Signer");
writer.writeCharacters(certificate.signer);
writer.writeEndElement();
writer.writeStartElement("Key");
writer.writeCharacters(certificate.key.toBase64());
writer.writeEndElement();
}
bool Certificate::operator==(const Certificate& other) const
{
return key == other.key && signer == other.signer;
}
bool Certificate::operator!=(const Certificate& other) const
{
return !operator==(other);
}
bool Certificate::isNull() const
{
return 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() == "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, 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;
}
bool ScopedCertificate::operator==(const ScopedCertificate& other) const
{
return trust == other.trust && path == other.path && certificate == other.certificate;
}
bool ScopedCertificate::operator!=(const ScopedCertificate& other) const
{
return !operator==(other);
}
void ScopedCertificate::serialize(QXmlStreamWriter& writer, const ScopedCertificate& scopedCertificate)
{
writer.writeAttribute("Path", scopedCertificate.path);
QString trust = "Ask";
if (scopedCertificate.trust == KeeShareSettings::Trust::Trusted) {
trust = "Trusted";
}
if (scopedCertificate.trust == KeeShareSettings::Trust::Untrusted) {
trust = "Untrusted";
}
writer.writeAttribute("Trust", trust);
Certificate::serialize(writer, scopedCertificate.certificate);
}
ScopedCertificate ScopedCertificate::deserialize(QXmlStreamReader& reader)
{
ScopedCertificate scopedCertificate;
scopedCertificate.path = reader.attributes().value("Path").toString();
scopedCertificate.trust = KeeShareSettings::Trust::Ask;
auto trust = reader.attributes().value("Trust").toString();
if (trust.compare("Trusted", Qt::CaseInsensitive) == 0) {
scopedCertificate.trust = KeeShareSettings::Trust::Trusted;
}
if (trust.compare("Untrusted", Qt::CaseInsensitive) == 0) {
scopedCertificate.trust = KeeShareSettings::Trust::Untrusted;
}
scopedCertificate.certificate = Certificate::deserialize(reader);
return scopedCertificate;
}
QString Foreign::serialize(const Foreign& foreign)
{
return xmlSerialize([&](QXmlStreamWriter& writer) {
writer.writeStartElement("Foreign");
for (const ScopedCertificate& scopedCertificate : foreign.certificates) {
writer.writeStartElement("Certificate");
ScopedCertificate::serialize(writer, scopedCertificate);
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 << ScopedCertificate::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;
}
} // namespace KeeShareSettings

View File

@ -0,0 +1,181 @@
/*
* 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 operator==(const Certificate& other) const;
bool operator!=(const Certificate& other) const;
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();
};
enum class Trust
{
Ask,
Untrusted,
Trusted
};
struct ScopedCertificate
{
QString path;
Certificate certificate;
Trust trust;
bool operator==(const ScopedCertificate& other) const;
bool operator!=(const ScopedCertificate& other) const;
bool isUnknown() const
{
return certificate.isNull();
}
bool isKnown() const
{
return !certificate.isNull();
}
static void serialize(QXmlStreamWriter& writer, const ScopedCertificate& certificate);
static ScopedCertificate deserialize(QXmlStreamReader& reader);
};
struct Foreign
{
QList<ScopedCertificate> certificates;
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);
};
}; // namespace KeeShareSettings
#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,245 @@
/*
* 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);
#if !defined(WITH_XC_KEESHARE_SECURE)
// Setting does not help the user of Version without signed export
m_ui->ownCertificateGroupBox->setVisible(false);
#endif
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->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates()));
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()
{
auto headers = QStringList() << tr("Path") << tr("Status");
#if defined(WITH_XC_KEESHARE_SECURE)
headers << tr("Signer") << tr("Fingerprint") << tr("Certificate");
#endif
m_importedCertificateModel.reset(new QStandardItemModel());
m_importedCertificateModel->setHorizontalHeaderLabels(headers);
for (const auto& scopedCertificate : m_foreign.certificates) {
const auto items = QList<QStandardItem*>()
<< new QStandardItem(scopedCertificate.path)
<< new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask
? tr("Ask")
: (scopedCertificate.trust == KeeShareSettings::Trust::Trusted
? tr("Trusted")
: tr("Untrusted")))
#if defined(WITH_XC_KEESHARE_SECURE)
<< new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer
: tr("Unknown"))
<< new QStandardItem(scopedCertificate.certificate.fingerprint())
<< new QStandardItem(scopedCertificate.certificate.publicKey())
#endif
;
m_importedCertificateModel->appendRow(items);
}
m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data());
m_ui->importedCertificateTableView->resizeColumnsToContents();
}
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
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, QFileDialog::Options(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, QFileDialog::Options(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()].trust = KeeShareSettings::Trust::Trusted;
}
updateForeignCertificates();
}
void SettingsWidgetKeeShare::askSelectedCertificates()
{
const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel();
Q_ASSERT(selectionModel);
for (const auto& index : selectionModel->selectedRows()) {
m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask;
}
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()].trust = KeeShareSettings::Trust::Untrusted;
}
updateForeignCertificates();
}
void SettingsWidgetKeeShare::removeSelectedCertificates()
{
auto 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,73 @@
/*
* 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 askSelectedCertificates();
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,256 @@
<?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>434</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="askImportedCertificateButton">
<property name="text">
<string>Ask</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,806 @@
/*
* 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 "config-keepassx.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>
#if defined(WITH_XC_KEESHARE_SECURE)
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
#endif
namespace
{
static const QString KeeShare_Signature("container.share.signature");
static const QString KeeShare_Container("container.share.kdbx");
enum Trust
{
Invalid,
Own,
UntrustedForever,
UntrustedOnce,
TrustedOnce,
TrustedForever,
};
bool isOfExportType(const QFileInfo& fileInfo, const QString type)
{
return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive);
}
QPair<Trust, KeeShareSettings::Certificate>
check(QByteArray& data,
const KeeShareSettings::Reference& reference,
const KeeShareSettings::Certificate& ownCertificate,
const QList<KeeShareSettings::ScopedCertificate>& knownCertificates,
const KeeShareSettings::Sign& sign)
{
KeeShareSettings::Certificate certificate;
if (!sign.signature.isEmpty()) {
certificate = sign.certificate;
auto key = sign.certificate.sshKey();
key.openKey(QString());
const auto signer = Signature();
if (!signer.verify(data, sign.signature, key)) {
qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path));
return {Invalid, KeeShareSettings::Certificate()};
}
if (ownCertificate.key == sign.certificate.key) {
return {Own, ownCertificate};
}
}
enum Scope
{
Invalid,
Global,
Local
};
Scope scope = Invalid;
KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask;
for (const auto& scopedCertificate : knownCertificates) {
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
// Global scope is overwritten by local scope
scope = Global;
trusted = scopedCertificate.trust;
}
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
scope = Local;
trusted = scopedCertificate.trust;
break;
}
}
if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask) {
// we introduce now scopes if there is a global
return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate};
}
QMessageBox warning;
if (sign.signature.isEmpty()) {
warning.setIcon(QMessageBox::Warning);
warning.setWindowTitle(ShareObserver::tr("Import from container without signature"));
warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not "
"signed. Do you really want to import from %1?")
.arg(reference.path));
} else {
warning.setIcon(QMessageBox::Question);
warning.setWindowTitle(ShareObserver::tr("Import from container with certificate"));
warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3")
.arg(certificate.signer, certificate.fingerprint(), reference.path));
}
auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole);
auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole);
auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole);
auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole);
warning.setDefaultButton(untrustedOnce);
warning.exec();
if (warning.clickedButton() == trustedForever) {
return {TrustedForever, certificate};
}
if (warning.clickedButton() == trustedOnce) {
return {TrustedOnce, certificate};
}
if (warning.clickedButton() == untrustedOnce) {
return {UntrustedOnce, certificate};
}
if (warning.clickedButton() == untrustedForever) {
return {UntrustedForever, certificate};
}
return {UntrustedOnce, certificate};
}
} // End Namespace
ShareObserver::ShareObserver(QSharedPointer<Database> db, QObject* parent)
: QObject(parent)
, m_db(std::move(db))
, m_fileWatcher(new BulkFileWatcher(this))
{
connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged()));
connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged()));
connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved()));
connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileCreated(QString)));
connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString)));
connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileDeleted(QString)));
handleDatabaseChanged();
}
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 (const auto& 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 auto 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::handleFileCreated(const QString& path)
{
// there is currently no difference in handling an added share or updating from one
this->handleFileUpdated(path);
}
void ShareObserver::handleFileDeleted(const QString& path)
{
Q_UNUSED(path);
// There is nothing we can or should do for now, ignore deletion
}
void ShareObserver::handleFileUpdated(const QString& path)
{
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, result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
notifyAbout(success, warning, error);
}
ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSettings::Reference& reference,
Group* targetGroup)
{
#if !defined(WITH_XC_KEESHARE_SECURE)
Q_UNUSED(targetGroup);
return {reference.path, Result::Warning, tr("Signed share container are not supported - import prevented")};
#else
QuaZip zip(reference.path);
if (!zip.open(QuaZip::mdUnzip)) {
qCritical("Unable to open file %s.", qPrintable(reference.path));
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(reference.path));
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 = QSharedPointer<Database>::create();
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
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 trust = check(payload, reference, own.certificate, foreign.certificates, sign);
switch (trust.first) {
case Invalid:
qWarning("Prevent untrusted import");
return {reference.path, Result::Error, tr("Untrusted import prevented")};
case UntrustedForever:
case TrustedForever: {
bool found = false;
const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted
: KeeShareSettings::Trust::Untrusted;
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
scopedCertificate.certificate.signer = trust.second.signer;
scopedCertificate.path = reference.path;
scopedCertificate.trust = trusted;
found = true;
break;
}
}
if (!found) {
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
}
// update foreign certificates with new settings
KeeShare::setForeign(foreign);
if (trust.first == TrustedForever) {
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 signed import")};
}
}
// Silent ignore of untrusted import or unchanging import
return {};
}
case TrustedOnce:
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 signed import")};
}
return {};
}
default:
Q_ASSERT(false);
return {reference.path, Result::Error, tr("Unexpected error")};
}
#endif
}
ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareSettings::Reference& reference,
Group* targetGroup)
{
#if !defined(WITH_XC_KEESHARE_INSECURE)
Q_UNUSED(targetGroup);
return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")};
#else
QFile file(reference.path);
if (!file.open(QIODevice::ReadOnly)) {
qCritical("Unable to open file %s.", qPrintable(reference.path));
return {reference.path, Result::Error, tr("File is not readable")};
}
auto payload = file.readAll();
file.close();
QBuffer buffer(&payload);
buffer.open(QIODevice::ReadOnly);
KeePass2Reader reader;
auto key = QSharedPointer<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
auto sourceDb = QSharedPointer<Database>::create();
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
return {reference.path, Result::Error, reader.errorString()};
}
auto foreign = KeeShare::foreign();
const auto own = KeeShare::own();
const auto sign = KeeShareSettings::Sign(); // invalid sign
auto trust = check(payload, reference, own.certificate, foreign.certificates, sign);
switch (trust.first) {
case UntrustedForever:
case TrustedForever: {
bool found = false;
const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted
: KeeShareSettings::Trust::Untrusted;
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
scopedCertificate.certificate.signer = trust.second.signer;
scopedCertificate.path = reference.path;
scopedCertificate.trust = trusted;
found = true;
break;
}
}
if (!found) {
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
}
// update foreign certificates with new settings
KeeShare::setForeign(foreign);
if (trust.first == TrustedForever) {
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 signed import")};
}
}
return {};
}
case TrustedOnce: {
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 unsigned import")};
}
return {};
}
default:
qWarning("Prevent untrusted import");
return {reference.path, Result::Warning, tr("Untrusted import prevented")};
}
#endif
}
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")};
}
if (isOfExportType(info, KeeShare::signedContainerFileType())) {
return importSingedContainerInto(reference, targetGroup);
}
if (isOfExportType(info, KeeShare::unsignedContainerFileType())) {
return importUnsignedContainerInto(reference, targetGroup);
}
return {reference.path, Result::Error, tr("Unknown share container type")};
}
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) {
// changes of inactive references are ignored
return {};
}
if (reference.type == KeeShareSettings::ExportTo) {
// changes of export only references are ignored
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->rootGroup()->findEntryByUuid(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;
}
QSharedPointer<Database> ShareObserver::database()
{
return m_db;
}
ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const KeeShareSettings::Reference& reference,
Database* targetDb)
{
#if !defined(WITH_XC_KEESHARE_SECURE)
Q_UNUSED(targetDb);
return {
reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")};
#else
QByteArray bytes;
{
QBuffer buffer(&bytes);
buffer.open(QIODevice::WriteOnly);
KeePass2Writer writer;
writer.writeDatabase(&buffer, targetDb);
if (writer.hasError()) {
qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data());
return {reference.path, Result::Error, writer.errorString()};
}
}
const auto own = KeeShare::own();
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());
return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())};
}
{
QuaZipFile file(&zip);
const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature));
if (!signatureOpened) {
::qWarning("Embedding signature failed: %d", zip.getZipError());
return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())};
}
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());
return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())};
}
file.close();
}
{
QuaZipFile file(&zip);
const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container));
if (!dbOpened) {
::qWarning("Embedding database failed: %d", zip.getZipError());
return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())};
}
if (file.getZipError() != ZIP_OK) {
::qWarning("Embedding database failed: %d", zip.getZipError());
return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())};
}
file.write(bytes);
file.close();
}
zip.close();
return {reference.path};
#endif
}
ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference& reference,
Database* targetDb)
{
#if !defined(WITH_XC_KEESHARE_INSECURE)
Q_UNUSED(targetDb);
return {reference.path,
Result::Warning,
tr("Overwriting unsigned share container is not supported - export prevented")};
#else
QFile file(reference.path);
const bool fileOpened = file.open(QIODevice::WriteOnly);
if (!fileOpened) {
::qWarning("Opening export file failed");
return {reference.path, Result::Error, tr("Could not write export container")};
}
KeePass2Writer writer;
writer.writeDatabase(&file, targetDb);
if (writer.hasError()) {
qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data());
return {reference.path, Result::Error, writer.errorString()};
}
file.close();
#endif
return {reference.path};
}
QList<ShareObserver::Result> ShareObserver::exportIntoReferenceContainers()
{
QList<Result> results;
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));
QFileInfo info(reference.path);
if (isOfExportType(info, KeeShare::signedContainerFileType())) {
results << exportIntoReferenceSignedContainer(reference, targetDb.data());
m_fileWatcher->observeFileChanges(true);
continue;
}
if (isOfExportType(info, KeeShare::unsignedContainerFileType())) {
results << exportIntoReferenceUnsignedContainer(reference, targetDb.data());
m_fileWatcher->observeFileChanges(true);
continue;
}
Q_ASSERT(false);
results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")};
}
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,106 @@
/*
* 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(QSharedPointer<Database> db, QObject* parent = nullptr);
~ShareObserver();
QSharedPointer<Database> database();
signals:
void sharingMessage(QString, MessageWidget::MessageType);
private slots:
void handleDatabaseChanged();
void handleDatabaseSaved();
void handleFileCreated(const QString& path);
void handleFileUpdated(const QString& path);
void handleFileDeleted(const QString& path);
private:
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 exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference& reference,
Database* targetDb);
static Result exportIntoReferenceSignedContainer(const KeeShareSettings::Reference& reference, Database* targetDb);
static Result importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup);
static Result importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup);
static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup);
static Result importDatabaseInto();
Result importFromReferenceContainer(const QString& path);
QList<Result> exportIntoReferenceContainers();
void deinitialize();
void reinitialize();
void notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error);
private:
QSharedPointer<Database> 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,304 @@
/*
* 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();
m_ui->messageWidget->setCloseButtonVisible(false);
m_ui->messageWidget->setAutoHideTimeout(-1);
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(editingFinished()), SLOT(selectPath()));
connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog()));
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(groupModified()), SLOT(update()));
}
update();
}
void EditGroupWidgetKeeShare::showSharingState()
{
if (!m_temporaryGroup) {
return;
}
auto supportedExtensions = QStringList();
#if defined(WITH_XC_KEESHARE_INSECURE)
supportedExtensions << KeeShare::unsignedContainerFileType();
#endif
#if defined(WITH_XC_KEESHARE_SECURE)
supportedExtensions << KeeShare::signedContainerFileType();
#endif
const auto reference = KeeShare::referenceOf(m_temporaryGroup);
if (!reference.path.isEmpty()) {
bool supported = false;
for (const auto& extension : supportedExtensions) {
if (reference.path.endsWith(extension, Qt::CaseInsensitive)) {
supported = true;
break;
}
}
if (!supported) {
m_ui->messageWidget->showMessage(
tr("Your KeePassXC version does not support sharing your container type. Please use %1.")
.arg(supportedExtensions.join(", ")),
MessageWidget::Warning);
return;
} else {
m_ui->messageWidget->hide();
}
}
const auto active = KeeShare::active();
if (!active.in && !active.out) {
m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information);
return;
}
if (active.in && !active.out) {
m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information);
return;
}
if (!active.in && active.out) {
m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information);
return;
}
}
void EditGroupWidgetKeeShare::update()
{
if (!m_temporaryGroup) {
m_ui->passwordEdit->clear();
m_ui->pathEdit->clear();
} 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();
}
m_ui->passwordGenerator->hide();
m_ui->togglePasswordGeneratorButton->setChecked(false);
m_ui->togglePasswordButton->setChecked(false);
}
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::selectPath()
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.path = m_ui->pathEdit->text();
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}
void EditGroupWidgetKeeShare::launchPathSelectionDialog()
{
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);
QString defaultFiletype = "";
auto supportedExtensions = QStringList();
auto unsupportedExtensions = QStringList();
auto knownFilters = QStringList() << QString("%1 (*)").arg("All files");
#if defined(WITH_XC_KEESHARE_INSECURE)
defaultFiletype = KeeShare::unsignedContainerFileType();
supportedExtensions << KeeShare::unsignedContainerFileType();
knownFilters.prepend(
QString("%1 (*.%2)").arg(tr("KeeShare unsigned container"), KeeShare::unsignedContainerFileType()));
#else
unsupportedExtensions << KeeShare::unsignedContainerFileType();
#endif
#if defined(WITH_XC_KEESHARE_SECURE)
defaultFiletype = KeeShare::signedContainerFileType();
supportedExtensions << KeeShare::signedContainerFileType();
knownFilters.prepend(
QString("%1 (*.%2)").arg(tr("KeeShare signed container"), KeeShare::signedContainerFileType()));
#else
unsupportedExtensions << KeeShare::signedContainerFileType();
#endif
const auto filters = knownFilters.join(";;");
auto filename = reference.path;
if (filename.isEmpty()) {
filename = m_temporaryGroup->name();
}
switch (reference.type) {
case KeeShareSettings::ImportFrom:
filename = fileDialog()->getFileName(this,
tr("Select import source"),
defaultDirPath,
filters,
nullptr,
QFileDialog::DontConfirmOverwrite,
defaultFiletype,
filename);
break;
case KeeShareSettings::ExportTo:
filename = fileDialog()->getFileName(this,
tr("Select export target"),
defaultDirPath,
filters,
nullptr,
QFileDialog::Option(0),
defaultFiletype,
filename);
break;
case KeeShareSettings::SynchronizeWith:
case KeeShareSettings::Inactive:
filename = fileDialog()->getFileName(this,
tr("Select import/export file"),
defaultDirPath,
filters,
nullptr,
QFileDialog::Option(0),
defaultFiletype,
filename);
break;
}
if (filename.isEmpty()) {
return;
}
bool validFilename = false;
for (const auto& extension : supportedExtensions + unsupportedExtensions) {
if (filename.endsWith(extension, Qt::CaseInsensitive)) {
validFilename = true;
break;
}
}
if (!validFilename) {
filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype;
}
m_ui->pathEdit->setText(filename);
selectPath();
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 launchPathSelectionDialog();
void selectPath();
void setGeneratedPassword(const QString& password);
void togglePasswordGeneratorButton(bool checked);
private:
QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui;
QPointer<Group> m_temporaryGroup;
};
#endif // KEEPASSXC_EDITGROUPWIDGETKEESHARE_H

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