Merge branch 'master' into develop

This commit is contained in:
Jonathan White 2021-01-31 16:47:35 -05:00
parent 4e90cb5818
commit 61b85183f9
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
13 changed files with 158 additions and 74 deletions

View File

@ -1,5 +1,27 @@
# Changelog
## 2.6.4 (2021-01-31)
### Added
- Automatically adapt to light/dark system theme changes (Windows/macOS only) [#6034]
### Changed
- Show window title as tooltip on system tray [#5948]
- Compress Snap release as LZO for faster initial startup [#5877]
- Password generator: Set maximum selectable password length to 999 [#5937]
### Fixed
- Fix crash on app close when using SSH agent [#5935]
- Fix KDF selection showing wrong item when using Argon2id [#5923]
- Automatically close About dialog on database lock if it is still open [#5947]
- Linux: Fix automatic launch at system startup with AppImages [#5901]
- Linux: Fix click-to-move on empty area activating when using menus [#5971]
- Linux: Try multiple times to show tray icon if tray is not ready yet [#5948]
- macOS: Fix KeePassXC blocking clean shutdown [#6002]
## 2.6.3 (2021-01-12)
### Added

View File

@ -101,7 +101,7 @@ NOTE: You can disable the recycle bin within the Database Settings. If the recyc
Creating a clone of an entry provides you a ready-to-use template for creating new entries with similar details of a master entry.
To create a clone of an existing entry, perform the following steps:
1. Right-click on the entry for which you want to create a clone and select _Clone Entry_. Alternatively, select the desired entry and press `Ctrl+K`.
+
.Clone entry from context menu
@ -127,7 +127,7 @@ image::clone_entry_references.png[]
Where `<UUID>` is the Unique Identifier of the entry to pull data from and `<ShortCode>` is from the following:
+
* T - Title
* U - Username
* U - Username
* P - Password
* A - URL
* N - Notes
@ -288,6 +288,8 @@ image::database_security.png[]
.Database credentials
image::database_security_credentials.png[]
WARNING: Consider creating a backup of your YubiKey. Please refer to <<Creating a YubiKey backup>>
5. Encryption settings allows you to change the average time it takes to encrypt and decrypt the database. The longer time that is chosen, the harder it will be to brute force attack your database. *We recommend a setting of one second.*
+
.Database encryption
@ -305,6 +307,23 @@ The following key derivation functions are supported:
* AES-KDF (KDBX 4 and KDBX 3.1): This key derivation function is based on iterating AES. Users can change the number of iterations. The more iterations, the harder are dictionary and guessing attacks, but also database loading/saving takes more time (linearly). KDBX 3.1 only supports AES-KDF; any other key derivation function, like for instance Argon2, requires KDBX 4.
* Argon2 (KDBX 4 - recommended): KDBX 4, the Argon2 key derivation function can be used for transforming the composite master key (as protection against dictionary attacks). The main advantage of Argon2 over AES-KDF is that it provides a better resistance against GPU/ASIC attacks (due to being a memory-hard function). The number of iterations scales linearly with the required time. By increasing the memory parameter, GPU/ASIC attacks become harder (and the required time increases). The parallelism parameter can be used to specify how many threads should be used.
=== Creating a YubiKey backup
It is advisable to have a backup replica YubiKey In case your main YubiKey gets damaged, lost, or stolen. The same HMAC key will need to be written to both keys. To do this you can either use the YubiKey Personalization Tool GUI or the ykpersonalize CLI tool. The steps for the CLI tool are shown:
1. Create a 20 byte HMAC key:
+
```
dd status=none if=/dev/random bs=20 count=1 | xxd -p -c 40
```
2. Write the HMAC key to slot 2 _(Set through the first switch. Out of the box the YubiKey OTP resides in slot 1)_:
+
```
ykpersonalize -2 -a -ochal-resp -ochal-hmac -ohmac-lt64 -oserial-api-visible -oallow-update
```
You will be asked to enter the HMAC key you created earlier, copy/paste they key output in the first step. Repeat both steps for your second YubiKey. We recommend storing your HMAC key in a safe place (e.g., printed on paper) in case you need to recreate another key.
// end::advanced[]
== Storing a Database File

View File

@ -44,6 +44,7 @@ ORIG_BRANCH=""
ORIG_CWD="$(pwd)"
MACOSX_DEPLOYMENT_TARGET=10.12
GREP="grep"
TIMESTAMP_SERVER="http://timestamp.sectigo.com"
# -----------------------------------------------------------------------
# helper functions
@ -114,6 +115,7 @@ Options:
--appimage Build a Linux AppImage after compilation.
If this option is set, --install-prefix has no effect
--appsign Perform platform specific App Signing before packaging
--timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
-k, --key Specify the App Signing Key/Identity
-c, --cmake-options Additional CMake options for compiling the sources
--compiler Compiler to use (default: '${COMPILER}')
@ -145,6 +147,8 @@ Options:
-f, --files Files to sign (required)
-k, --key, -i, --identity
Signing Key or Apple Developer ID (required)
--timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
-u, --username Apple username for notarization (required on macOS)
-h, --help Show this help
EOF
elif [ "notarize" == "$cmd" ]; then
@ -494,6 +498,10 @@ merge() {
GPG_GIT_KEY="$2"
shift ;;
--timestamp)
TIMESTAMP_SERVER="$2"
shift ;;
-r|--release-branch)
SOURCE_BRANCH="$2"
shift ;;
@ -810,6 +818,10 @@ build() {
--appsign)
build_appsign=true ;;
--timestamp)
TIMESTAMP_SERVER="$2"
shift ;;
-k|--key)
build_key="$2"
shift ;;
@ -1268,8 +1280,10 @@ appsign() {
# osslsigncode does not succeed at signing MSI files at this time...
logInfo "Signing file '${f}' using Microsoft signtool..."
if ! signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \
-fd sha256 -tr "http://timestamp.comodoca.com/authenticode" "${f}"; then
signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \
-fd sha256 -tr "${TIMESTAMP_SERVER}" "${f}"
if [ 0 -ne $? ]; then
exitError "Signing failed!"
fi
else

View File

@ -50,6 +50,23 @@
</screenshots>
<releases>
<release version="2.6.4" date="2021-01-31">
<description>
<ul>
<li>Automatically adapt to light/dark system theme changes (Windows/macOS only) [#6034]</li>
<li>Show window title as tooltip on system tray [#5948]</li>
<li>Compress Snap release as LZO for faster initial startup [#5877]</li>
<li>Password generator: Set maximum selectable password length to 999 [#5937]</li>
<li>Fix crash on app close when using SSH agent [#5935]</li>
<li>Fix KDF selection showing wrong item when using Argon2id [#5923]</li>
<li>Automatically close About dialog on database lock if it is still open [#5947]</li>
<li>Linux: Fix automatic launch at system startup with AppImages [#5901]</li>
<li>Linux: Fix click-to-move on empty area activating when using menus [#5971]</li>
<li>Linux: Try multiple times to show tray icon if tray is not ready yet [#5948]</li>
<li>macOS: Fix KeePassXC blocking clean shutdown [#6002]</li>
</ul>
</description>
</release>
<release version="2.6.3" date="2021-01-12">
<description>
<ul>

View File

@ -8,6 +8,7 @@ description: |
published under the terms of the GNU General Public License.
confinement: strict
base: core18
compression: lzo
plugs: # plugs for theming, font settings, cursor and to use gtk3 file chooser
gtk-3-themes:

View File

@ -30,8 +30,7 @@
* or associated data. KeePass uses the latest version of Argon2, v1.3.
*/
Argon2Kdf::Argon2Kdf(Type type)
: Kdf::Kdf(KeePass2::KDF_ARGON2D)
, m_type(type)
: Kdf::Kdf(type == Type::Argon2d ? KeePass2::KDF_ARGON2D : KeePass2::KDF_ARGON2ID)
, m_version(0x13)
, m_memory(1 << 16)
, m_parallelism(static_cast<quint32>(QThread::idealThreadCount()))
@ -57,12 +56,7 @@ bool Argon2Kdf::setVersion(quint32 version)
Argon2Kdf::Type Argon2Kdf::type() const
{
return m_type;
}
void Argon2Kdf::setType(Type type)
{
m_type = type;
return uuid() == KeePass2::KDF_ARGON2D ? Type::Argon2d : Type::Argon2id;
}
quint64 Argon2Kdf::memory() const
@ -144,11 +138,7 @@ bool Argon2Kdf::processParameters(const QVariantMap& p)
QVariantMap Argon2Kdf::writeParameters()
{
QVariantMap p;
if (type() == Type::Argon2d) {
p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2D.toRfc4122());
} else {
p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2ID.toRfc4122());
}
p.insert(KeePass2::KDFPARAM_UUID, uuid().toRfc4122());
p.insert(KeePass2::KDFPARAM_ARGON2_VERSION, version());
p.insert(KeePass2::KDFPARAM_ARGON2_PARALLELISM, parallelism());
p.insert(KeePass2::KDFPARAM_ARGON2_MEMORY, memory() * 1024);

View File

@ -39,7 +39,6 @@ public:
quint32 version() const;
bool setVersion(quint32 version);
Type type() const;
void setType(Type type);
quint64 memory() const;
bool setMemory(quint64 kibibytes);
quint32 parallelism() const;
@ -49,7 +48,6 @@ public:
protected:
int benchmarkImpl(int msec) const override;
Type m_type;
quint32 m_version;
quint64 m_memory;
quint32 m_parallelism;

View File

@ -214,13 +214,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_EntrySearcher = new EntrySearcher(false);
m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
#ifdef WITH_XC_SSHAGENT
if (sshAgent()->isEnabled()) {
connect(this, SIGNAL(databaseLocked()), sshAgent(), SLOT(databaseLocked()));
connect(this, SIGNAL(databaseUnlocked()), sshAgent(), SLOT(databaseUnlocked()));
}
#endif
#ifdef WITH_XC_KEESHARE
// We need to reregister the database to allow exports
// from a newly created database
@ -1091,6 +1084,9 @@ void DatabaseWidget::loadDatabase(bool accepted)
m_entryBeforeLock = QUuid();
m_saveAttempts = 0;
emit databaseUnlocked();
#ifdef WITH_XC_SSHAGENT
sshAgent()->databaseUnlocked(m_db);
#endif
if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
getMainWindow()->minimizeOrHide();
}
@ -1178,6 +1174,10 @@ void DatabaseWidget::unlockDatabase(bool accepted)
processAutoOpen();
emit databaseUnlocked();
#ifdef WITH_XC_SSHAGENT
sshAgent()->databaseUnlocked(m_db);
#endif
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::AutoType) {
QList<QSharedPointer<Database>> dbList;
dbList.append(m_db);
@ -1599,6 +1599,10 @@ bool DatabaseWidget::lock()
m_entryBeforeLock = currentEntry->uuid();
}
#ifdef WITH_XC_SSHAGENT
sshAgent()->databaseLocked(m_db);
#endif
endSearch();
clearAllWidgets();
switchToOpenDatabase(m_db->filePath());

View File

@ -411,7 +411,6 @@ MainWindow::MainWindow()
connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle()));
connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int)));
connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState()));
connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateTrayIcon()));
connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*)));
connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*)));
connect(m_ui->tabWidget, SIGNAL(tabVisibilityChanged(bool)), SLOT(updateToolbarSeparatorVisibility()));
@ -565,6 +564,9 @@ MainWindow::MainWindow()
MessageWidget::Error);
}
// Properly shutdown on logoff, restart, and shutdown
connect(qApp, &QGuiApplication::commitDataRequest, this, [this] { m_appExitCalled = true; });
#if defined(KEEPASSXC_BUILD_TYPE_SNAPSHOT) || defined(KEEPASSXC_BUILD_TYPE_PRE_RELEASE)
auto* hidePreRelWarn = new QAction(tr("Don't show again for this version"), m_ui->globalMessageWidget);
m_ui->globalMessageWidget->addAction(hidePreRelWarn);
@ -617,6 +619,9 @@ MainWindow::MainWindow()
MainWindow::~MainWindow()
{
#ifdef WITH_XC_SSHAGENT
sshAgent()->removeAllIdentities();
#endif
}
/**
@ -968,11 +973,20 @@ void MainWindow::updateWindowTitle()
setWindowTitle(windowTitle);
setWindowModified(isModified);
updateTrayIcon();
}
void MainWindow::showAboutDialog()
{
auto* aboutDialog = new AboutDialog(this);
// Auto close the about dialog before attempting database locks
if (m_ui->tabWidget->currentDatabaseWidget()) {
connect(m_ui->tabWidget->currentDatabaseWidget(),
&DatabaseWidget::databaseLockRequested,
aboutDialog,
&AboutDialog::close);
}
aboutDialog->open();
}
@ -1208,8 +1222,7 @@ void MainWindow::closeEvent(QCloseEvent* event)
void MainWindow::changeEvent(QEvent* event)
{
if ((event->type() == QEvent::WindowStateChange) && isMinimized()) {
if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible()
&& config()->get(Config::GUI_MinimizeToTray).toBool()) {
if (isTrayIconEnabled() && config()->get(Config::GUI_MinimizeToTray).toBool()) {
event->ignore();
hide();
}
@ -1309,9 +1322,7 @@ bool MainWindow::saveLastDatabases()
void MainWindow::updateTrayIcon()
{
if (isTrayIconEnabled()) {
QApplication::setQuitOnLastWindowClosed(false);
if (config()->get(Config::GUI_ShowTrayIcon).toBool()) {
if (!m_trayIcon) {
m_trayIcon = new QSystemTrayIcon(this);
auto* menu = new QMenu(this);
@ -1323,40 +1334,46 @@ void MainWindow::updateTrayIcon()
menu->addAction(m_ui->actionLockDatabases);
#ifdef Q_OS_MACOS
QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu);
menu->addAction(actionQuit);
auto actionQuit = new QAction(tr("Quit KeePassXC"), menu);
connect(actionQuit, SIGNAL(triggered()), SLOT(appExit()));
menu->addAction(actionQuit);
#else
menu->addAction(m_ui->actionQuit);
#endif
m_trayIcon->setContextMenu(menu);
connect(m_trayIcon,
SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason)));
connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow()));
m_trayIcon->setContextMenu(menu);
m_trayIcon->setIcon(icons()->trayIcon());
m_trayIcon->show();
}
if (m_ui->tabWidget->count() == 0) {
m_trayIcon->setIcon(icons()->trayIcon());
} else if (m_ui->tabWidget->hasLockableDatabases()) {
if (m_ui->tabWidget->hasLockableDatabases()) {
m_trayIcon->setIcon(icons()->trayIconUnlocked());
} else {
m_trayIcon->setIcon(icons()->trayIconLocked());
}
} else {
QApplication::setQuitOnLastWindowClosed(true);
m_trayIcon->setToolTip(windowTitle().replace("[*]", isWindowModified() ? "*" : ""));
m_trayIcon->show();
if (!isTrayIconEnabled() || !QSystemTrayIcon::isSystemTrayAvailable()) {
// Try to show tray icon after 5 seconds, try 5 times
// This can happen if KeePassXC starts before the system tray is available
static int trayIconAttempts = 0;
if (trayIconAttempts < 5) {
QTimer::singleShot(5000, this, &MainWindow::updateTrayIcon);
++trayIconAttempts;
}
}
} else {
if (m_trayIcon) {
m_trayIcon->hide();
delete m_trayIcon;
}
}
QApplication::setQuitOnLastWindowClosed(!isTrayIconEnabled());
}
void MainWindow::obtainContextFocusLock()
@ -1598,7 +1615,7 @@ void MainWindow::forgetTouchIDAfterInactivity()
bool MainWindow::isTrayIconEnabled() const
{
return config()->get(Config::GUI_ShowTrayIcon).toBool() && QSystemTrayIcon::isSystemTrayAvailable();
return m_trayIcon && m_trayIcon->isVisible();
}
void MainWindow::displayGlobalMessage(const QString& text,
@ -1833,9 +1850,11 @@ bool MainWindowEventFilter::eventFilter(QObject* watched, QEvent* event)
if (event->type() == QEvent::MouseButtonPress) {
if (watched == mainWindow->m_ui->menubar) {
mainWindow->windowHandle()->startSystemMove();
// Continue processing events, so menus keep working.
return false;
auto* m = static_cast<QMouseEvent*>(event);
if (!mainWindow->m_ui->menubar->actionAt(m->pos())) {
mainWindow->windowHandle()->startSystemMove();
return false;
}
} else if (watched == mainWindow->m_ui->toolBar) {
if (!mainWindow->m_ui->toolBar->isMovable() || mainWindow->m_ui->toolBar->cursor() != Qt::SizeAllCursor) {
mainWindow->windowHandle()->startSystemMove();

View File

@ -259,6 +259,9 @@ QProgressBar::chunk {
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>20</number>
</property>

View File

@ -115,13 +115,18 @@ void NixUtils::setLaunchAtStartup(bool enable)
qWarning("Failed to create autostart desktop file.");
return;
}
const QString appImagePath = QString::fromLocal8Bit(qgetenv("APPIMAGE"));
const bool isAppImage = !appImagePath.isNull() && QFile::exists(appImagePath);
const QString executeablePath = isAppImage ? appImagePath : QApplication::applicationFilePath();
QTextStream stream(&desktopFile);
stream.setCodec("UTF-8");
stream << QStringLiteral("[Desktop Entry]") << '\n'
<< QStringLiteral("Name=") << QApplication::applicationDisplayName() << '\n'
<< QStringLiteral("GenericName=") << tr("Password Manager") << '\n'
<< QStringLiteral("Exec=") << QApplication::applicationFilePath() << '\n'
<< QStringLiteral("TryExec=") << QApplication::applicationFilePath() << '\n'
<< QStringLiteral("Exec=") << executeablePath << '\n'
<< QStringLiteral("TryExec=") << executeablePath << '\n'
<< QStringLiteral("Icon=") << QApplication::applicationName().toLower() << '\n'
<< QStringLiteral("StartupWMClass=keepassxc") << '\n'
<< QStringLiteral("StartupNotify=true") << '\n'

View File

@ -19,6 +19,9 @@
#include "SSHAgent.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "crypto/ssh/BinaryStream.h"
#include "crypto/ssh/OpenSSHKey.h"
#include "sshagent/KeeAgentSettings.h"
@ -31,11 +34,6 @@
Q_GLOBAL_STATIC(SSHAgent, s_sshAgent);
SSHAgent::~SSHAgent()
{
removeAllIdentities();
}
SSHAgent* SSHAgent::instance()
{
return s_sshAgent;
@ -427,18 +425,15 @@ void SSHAgent::setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove)
}
}
void SSHAgent::databaseLocked()
void SSHAgent::databaseLocked(QSharedPointer<Database> db)
{
auto* widget = qobject_cast<DatabaseWidget*>(sender());
if (!widget) {
if (!db) {
return;
}
QUuid databaseUuid = widget->database()->uuid();
auto it = m_addedKeys.begin();
while (it != m_addedKeys.end()) {
if (it.value().first != databaseUuid) {
if (it.value().first != db->uuid()) {
++it;
continue;
}
@ -452,16 +447,14 @@ void SSHAgent::databaseLocked()
}
}
void SSHAgent::databaseUnlocked()
void SSHAgent::databaseUnlocked(QSharedPointer<Database> db)
{
auto* widget = qobject_cast<DatabaseWidget*>(sender());
if (!widget) {
if (!db || !isEnabled()) {
return;
}
for (Entry* e : widget->database()->rootGroup()->entriesRecursive()) {
if (widget->database()->metadata()->recycleBinEnabled()
&& e->group() == widget->database()->metadata()->recycleBin()) {
for (Entry* e : db->rootGroup()->entriesRecursive()) {
if (db->metadata()->recycleBinEnabled() && e->group() == db->metadata()->recycleBin()) {
continue;
}
@ -483,7 +476,7 @@ void SSHAgent::databaseUnlocked()
// Add key to agent; ignore errors if we have previously added the key
bool known_key = m_addedKeys.contains(key);
if (!addIdentity(key, settings, widget->database()->uuid()) && !known_key) {
if (!addIdentity(key, settings, db->uuid()) && !known_key) {
emit error(m_error);
}
}

View File

@ -24,7 +24,6 @@
#include <QtCore>
#include "crypto/ssh/OpenSSHKey.h"
#include "gui/DatabaseWidget.h"
#include "sshagent/KeeAgentSettings.h"
class SSHAgent : public QObject
@ -32,7 +31,7 @@ class SSHAgent : public QObject
Q_OBJECT
public:
~SSHAgent() override;
~SSHAgent() override = default;
static SSHAgent* instance();
bool isEnabled() const;
@ -59,8 +58,8 @@ signals:
void enabledChanged(bool enabled);
public slots:
void databaseLocked();
void databaseUnlocked();
void databaseLocked(QSharedPointer<Database> db);
void databaseUnlocked(QSharedPointer<Database> db);
private:
const quint8 SSH_AGENT_FAILURE = 5;