Skip to content

Commit

Permalink
Significantly enhance hardware key robustness
Browse files Browse the repository at this point in the history
* Significantly improve user experience when using hardware keys on databases in both GUI and CLI modes. Prevent locking up the YubiKey USB interface for prolonged periods of time. Allows for other apps to use the key concurrently with KeePassXC.

* Improve messages displayed to user when finding keys and when user interaction is required. Output specific error messages when handling hardware keys during database read/write.

* Only poll for keys when previously used or upon user request. Prevent continuously polling keys when accessing the UI such as switching tabs and minimize/maximize.

* Add support for using multiple hardware keys simultaneously. Keys are identified by their serial number which prevents using the wrong key during open and save operations.

* Fixes #4400
* Fixes #4065
* Fixes #1050
* Fixes #1215
* Fixes #3087
* Fixes #1088
* Fixes #1869
  • Loading branch information
droidmonkey committed May 11, 2020
1 parent dcff507 commit 64367d2
Show file tree
Hide file tree
Showing 32 changed files with 664 additions and 665 deletions.
15 changes: 12 additions & 3 deletions src/cli/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <cstdlib>
#include <utility>

#include <QFileInfo>
#include <QMap>

#include "Command.h"
Expand Down Expand Up @@ -72,8 +73,8 @@ const QCommandLineOption Command::NoPasswordOption =
const QCommandLineOption Command::YubiKeyOption =
QCommandLineOption(QStringList() << "y"
<< "yubikey",
QObject::tr("Yubikey slot used to encrypt the database."),
QObject::tr("slot"));
QObject::tr("Yubikey slot and optional serial used to access the database (e.g., 1:7370001)."),
QObject::tr("slot[:serial]"));

namespace
{
Expand Down Expand Up @@ -121,7 +122,15 @@ QString Command::getDescriptionLine()

QString Command::getHelpText()
{
return buildParser(this)->helpText().replace("[options]", name + " [options]");
auto help = buildParser(this)->helpText();
// Fix spacing of options parameter
help.replace(QStringLiteral("[options]"), name + QStringLiteral(" [options]"));
// Remove application directory from command line example
auto appname = QFileInfo(QCoreApplication::applicationFilePath()).fileName();
auto regex = QRegularExpression(QStringLiteral(" .*%1").arg(QRegularExpression::escape(appname)));
help.replace(regex, appname.prepend(" "));

return help;
}

QSharedPointer<QCommandLineParser> Command::getCommandLineParser(const QStringList& arguments)
Expand Down
35 changes: 23 additions & 12 deletions src/cli/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

#include "Utils.h"

#ifdef WITH_XC_YUBIKEY
#include "keys/YkChallengeResponseKeyCLI.h"
#endif

#ifdef Q_OS_WIN
#include <windows.h>
#else
Expand Down Expand Up @@ -159,25 +163,32 @@ namespace Utils

#ifdef WITH_XC_YUBIKEY
if (!yubiKeySlot.isEmpty()) {
unsigned int serial = 0;
int slot;

bool ok = false;
int slot = yubiKeySlot.toInt(&ok, 10);
auto parts = yubiKeySlot.split(":");
slot = parts[0].toInt(&ok);

if (!ok || (slot != 1 && slot != 2)) {
err << QObject::tr("Invalid YubiKey slot %1").arg(yubiKeySlot) << endl;
err << QObject::tr("Invalid YubiKey slot %1").arg(parts[0]) << endl;
return {};
}

QString errorMessage;
bool blocking = YubiKey::instance()->checkSlotIsBlocking(slot, errorMessage);
if (!errorMessage.isEmpty()) {
err << errorMessage << endl;
return {};
if (parts.size() > 1) {
serial = parts[1].toUInt(&ok, 10);
if (!ok) {
err << QObject::tr("Invalid YubiKey serial %1").arg(parts[1]) << endl;
return {};
}
}

auto key = QSharedPointer<YkChallengeResponseKeyCLI>(new YkChallengeResponseKeyCLI(
slot,
blocking,
QObject::tr("Please touch the button on your YubiKey to unlock %1").arg(databaseFilename),
outputDescriptor));
// clang-format off
auto key = QSharedPointer<YkChallengeResponseKeyCLI>(
new YkChallengeResponseKeyCLI({serial, slot},
QObject::tr("Please touch the button on your YubiKey to continue…"),
errorDescriptor));
// clang-format on
compositeKey->addChallengeResponseKey(key);
}
#else
Expand Down
6 changes: 0 additions & 6 deletions src/cli/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@
#include "keys/PasswordKey.h"
#include <QtCore/qglobal.h>

#ifdef WITH_XC_YUBIKEY
#include "keys/YkChallengeResponseKey.h"
#include "keys/YkChallengeResponseKeyCLI.h"
#include "keys/drivers/YubiKey.h"
#endif

namespace Utils
{
extern FILE* STDOUT;
Expand Down
3 changes: 3 additions & 0 deletions src/core/Alloc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ void operator delete[](void* ptr) noexcept
::operator delete(ptr);
}

// clang-format versions less than 10.0 refuse to put a space before "noexcept"
// clang-format off
/**
* Custom insecure delete operator that does not zero out memory before
* freeing a buffer. Can be used for better performance.
Expand All @@ -87,6 +89,7 @@ void operator delete(void* ptr, bool) noexcept
{
std::free(ptr);
}
// clang-format on

void operator delete[](void* ptr, bool) noexcept
{
Expand Down
26 changes: 6 additions & 20 deletions src/core/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -659,10 +659,11 @@ QByteArray Database::challengeResponseKey() const

bool Database::challengeMasterSeed(const QByteArray& masterSeed)
{
m_keyError.clear();
if (m_data.key) {
m_data.masterSeed->setHash(masterSeed);
QByteArray response;
bool ok = m_data.key->challenge(masterSeed, response);
bool ok = m_data.key->challenge(masterSeed, response, &m_keyError);
if (ok && !response.isEmpty()) {
m_data.challengeResponseKey->setHash(response);
} else if (ok && response.isEmpty()) {
Expand Down Expand Up @@ -703,6 +704,7 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
bool transformKey)
{
Q_ASSERT(!m_data.isReadOnly);
m_keyError.clear();

if (!key) {
m_data.key.reset();
Expand All @@ -724,7 +726,7 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key,

if (!transformKey) {
transformedMasterKey = QByteArray(oldTransformedMasterKey.rawKey());
} else if (!key->transform(*m_data.kdf, transformedMasterKey)) {
} else if (!key->transform(*m_data.kdf, transformedMasterKey, &m_keyError)) {
return false;
}

Expand All @@ -743,25 +745,9 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
return true;
}

bool Database::verifyKey(const QSharedPointer<CompositeKey>& key) const
QString Database::keyError()
{
Q_ASSERT(!m_data.key->isEmpty());

if (!m_data.challengeResponseKey->rawKey().isEmpty()) {
QByteArray result;

if (!key->challenge(m_data.masterSeed->rawKey(), result)) {
// challenge failed, (YubiKey?) removed?
return false;
}

if (m_data.challengeResponseKey->rawKey() != result) {
// wrong response from challenged device(s)
return false;
}
}

return (m_data.key->rawKey() == key->rawKey());
return m_keyError;
}

QVariantMap& Database::publicCustomData()
Expand Down
3 changes: 2 additions & 1 deletion src/core/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ class Database : public QObject
bool updateChangedTime = true,
bool updateTransformSalt = false,
bool transformKey = true);
QString keyError();
QByteArray challengeResponseKey() const;
bool challengeMasterSeed(const QByteArray& masterSeed);
bool verifyKey(const QSharedPointer<CompositeKey>& key) const;
const QUuid& cipher() const;
void setCipher(const QUuid& cipher);
Database::CompressionAlgorithm compressionAlgorithm() const;
Expand Down Expand Up @@ -210,6 +210,7 @@ public slots:
QPointer<FileWatcher> m_fileWatcher;
bool m_modified = false;
bool m_emitModified;
QString m_keyError;

QList<QString> m_commonUsernames;

Expand Down
2 changes: 1 addition & 1 deletion src/format/Kdbx3Reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
}

if (!db->challengeMasterSeed(m_masterSeed)) {
raiseError(tr("Unable to issue challenge-response."));
raiseError(tr("Unable to issue challenge-response: %1").arg(db->keyError()));
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/format/Kdbx3Writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db)
QByteArray endOfHeader = "\r\n\r\n";

if (!db->challengeMasterSeed(masterSeed)) {
raiseError(tr("Unable to issue challenge-response."));
raiseError(tr("Unable to issue challenge-response: %1").arg(db->keyError()));
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/format/Kdbx4Reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,

bool ok = AsyncTask::runAndWaitForFuture([&] { return db->setKey(key, false, false); });
if (!ok) {
raiseError(tr("Unable to calculate master key"));
raiseError(tr("Unable to calculate master key: %1").arg(db->keyError()));
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/format/Kdbx4Writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
QByteArray endOfHeader = "\r\n\r\n";

if (!db->setKey(db->key(), false, true)) {
raiseError(tr("Unable to calculate master key"));
raiseError(tr("Unable to calculate master key: %1").arg(db->keyError()));
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/gui/ApplicationSettingsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ void ApplicationSettingsWidget::saveSettings()

if (!config()->get(Config::RememberLastKeyFiles).toBool()) {
config()->remove(Config::LastKeyFiles);
config()->remove(Config::LastChallengeResponse);
config()->remove(Config::LastDir);
}

Expand Down
Loading

0 comments on commit 64367d2

Please sign in to comment.