diff --git a/.travis.yml b/.travis.yml index 34bc0add5..be05d6e47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ git: before_install: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get -qq update; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get -qq install cmake libmicrohttpd10 libmicrohttpd-dev libxi-dev qtbase5-dev libqt5x11extras5-dev qttools5-dev qttools5-dev-tools libgcrypt20-dev zlib1g-dev libxtst-dev xvfb; fi + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get -qq install cmake libmicrohttpd10 libmicrohttpd-dev libxi-dev qtbase5-dev libqt5x11extras5-dev qttools5-dev qttools5-dev-tools libgcrypt20-dev zlib1g-dev libxtst-dev xvfb libyubikey-dev libykpers-1-dev; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew ls | grep -wq cmake || brew install cmake; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew ls | grep -wq qt5 || brew install qt5; fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 048bbf68a..ef1743df7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ option(WITH_COVERAGE "Use to build with coverage tests. (GCC ONLY)." OFF) option(WITH_XC_AUTOTYPE "Include Auto-Type." ON) option(WITH_XC_HTTP "Include KeePassHTTP and Custom Icon Downloads." OFF) -option(WITH_XC_YUBIKEY "Include Yubikey support." OFF) +option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) set(KEEPASSXC_VERSION "2.1.3") set(KEEPASSXC_VERSION_NUM "2.1.3") @@ -207,6 +207,13 @@ if(NOT ZLIB_SUPPORTS_GZIP) message(FATAL_ERROR "zlib 1.2.x or higher is required to use the gzip format") endif() +# Optional +if(WITH_XC_YUBIKEY) + find_package(YubiKey REQUIRED) + + include_directories(SYSTEM ${YUBIKEY_INCLUDE_DIRS}) +endif() + if(UNIX) check_cxx_source_compiles("#include int main() { prctl(PR_SET_DUMPABLE, 0); return 0; }" diff --git a/cmake/FindYubiKey.cmake b/cmake/FindYubiKey.cmake new file mode 100644 index 000000000..e5e0bb681 --- /dev/null +++ b/cmake/FindYubiKey.cmake @@ -0,0 +1,27 @@ +# Copyright (C) 2014 Kyle Manna +# +# 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 . + +find_path(YUBIKEY_CORE_INCLUDE_DIR yubikey.h) +find_path(YUBIKEY_PERS_INCLUDE_DIR ykcore.h PATH_SUFFIXES ykpers-1) +set(YUBIKEY_INCLUDE_DIRS ${YUBIKEY_CORE_INCLUDE_DIR} ${YUBIKEY_PERS_INCLUDE_DIR}) + +find_library(YUBIKEY_CORE_LIBRARY yubikey) +find_library(YUBIKEY_PERS_LIBRARY ykpers-1) +set(YUBIKEY_LIBRARIES ${YUBIKEY_CORE_LIBRARY} ${YUBIKEY_PERS_LIBRARY}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(YubiKey DEFAULT_MSG YUBIKEY_LIBRARIES YUBIKEY_INCLUDE_DIRS) + +mark_as_advanced(YUBIKEY_LIBRARIES YUBIKEY_INCLUDE_DIRS) diff --git a/release-tool b/release-tool index a508d79f7..e5a49b05e 100755 --- a/release-tool +++ b/release-tool @@ -37,7 +37,7 @@ DOCKER_CONTAINER_NAME="keepassxc-build-container" CMAKE_OPTIONS="" COMPILER="g++" MAKE_OPTIONS="-j8" -BUILD_PLUGINS="autotype http" +BUILD_PLUGINS="autotype http yubikey" INSTALL_PREFIX="/usr/local" BUILD_SOURCE_TARBALL=true ORIG_BRANCH="" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 94a685737..5e221b916 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -115,9 +115,11 @@ set(keepassx_SOURCES gui/group/GroupView.cpp keys/CompositeKey.cpp keys/CompositeKey_p.h + keys/drivers/YubiKey.h keys/FileKey.cpp keys/Key.h keys/PasswordKey.cpp + keys/YkChallengeResponseKey.cpp streams/HashedBlockStream.cpp streams/LayeredStream.cpp streams/qtiocompressor.cpp @@ -152,8 +154,9 @@ set(keepassx_FORMS gui/group/EditGroupWidgetMain.ui ) -add_feature_info(KeePassHTTP WITH_XC_HTTP "KeePassHTTP support for ChromeIPass and PassIFox") -add_feature_info(Autotype WITH_XC_AUTOTYPE "Auto-type passwords in Input fields") +add_feature_info(AutoType WITH_XC_AUTOTYPE "Automatic password typing") +add_feature_info(KeePassHTTP WITH_XC_HTTP "Browser integration compatible with ChromeIPass and PassIFox") +add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") add_subdirectory(http) if(WITH_XC_HTTP) @@ -181,6 +184,12 @@ if(MINGW) ${CMAKE_SOURCE_DIR}/share/windows/icon.rc) endif() +if(WITH_XC_YUBIKEY) + set(keepassx_SOURCES ${keepassx_SOURCES} keys/drivers/YubiKey.cpp) +else() + set(keepassx_SOURCES ${keepassx_SOURCES} keys/drivers/YubiKeyStub.cpp) +endif() + qt5_wrap_ui(keepassx_SOURCES ${keepassx_FORMS}) add_library(zxcvbn STATIC zxcvbn/zxcvbn.cpp) @@ -197,6 +206,7 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL target_link_libraries(keepassx_core ${keepasshttp_LIB} ${autotype_LIB} + ${YUBIKEY_LIBRARIES} zxcvbn Qt5::Core Qt5::Concurrent diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 60874fd8f..23b564143 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -176,6 +176,17 @@ QByteArray Database::transformedMasterKey() const return m_data.transformedMasterKey; } +QByteArray Database::challengeResponseKey() const +{ + return m_data.challengeResponseKey; +} + +bool Database::challengeMasterSeed(const QByteArray& masterSeed) +{ + m_data.masterSeed = masterSeed; + return m_data.key.challenge(masterSeed, m_data.challengeResponseKey); +} + void Database::setCipher(const Uuid& cipher) { Q_ASSERT(!cipher.isNull()); @@ -246,6 +257,20 @@ bool Database::verifyKey(const CompositeKey& key) const { Q_ASSERT(hasKey()); + if (!m_data.challengeResponseKey.isEmpty()) { + QByteArray result; + + if (!key.challenge(m_data.masterSeed, result)) { + // challenge failed, (YubiKey?) removed? + return false; + } + + if (m_data.challengeResponseKey != result) { + // wrong response from challenged device(s) + return false; + } + } + return (m_data.key.rawKey() == key.rawKey()); } diff --git a/src/core/Database.h b/src/core/Database.h index e781e1aa3..3767bb30d 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -59,6 +59,8 @@ public: QByteArray transformedMasterKey; CompositeKey key; bool hasKey; + QByteArray masterSeed; + QByteArray challengeResponseKey; }; Database(); @@ -89,6 +91,8 @@ public: quint64 transformRounds() const; QByteArray transformedMasterKey() const; const CompositeKey & key() const; + QByteArray challengeResponseKey() const; + bool challengeMasterSeed(const QByteArray& masterSeed); void setCipher(const Uuid& cipher); void setCompressionAlgo(Database::CompressionAlgorithm algo); diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 668165c5f..ffe4e94fc 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -113,8 +113,14 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke return nullptr; } + if (m_db->challengeMasterSeed(m_masterSeed) == false) { + raiseError(tr("Unable to issue challenge-response.")); + return nullptr; + } + CryptoHash hash(CryptoHash::Sha256); hash.addData(m_masterSeed); + hash.addData(m_db->challengeResponseKey()); hash.addData(m_db->transformedMasterKey()); QByteArray finalKey = hash.result(); diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index e6ec5f600..d63151c84 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -51,8 +51,14 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) QByteArray startBytes = randomGen()->randomArray(32); QByteArray endOfHeader = "\r\n\r\n"; + if (db->challengeMasterSeed(masterSeed) == false) { + raiseError("Unable to issue challenge-response."); + return; + } + CryptoHash hash(CryptoHash::Sha256); hash.addData(masterSeed); + hash.addData(db->challengeResponseKey()); Q_ASSERT(!db->transformedMasterKey().isEmpty()); hash.addData(db->transformedMasterKey()); QByteArray finalKey = hash.result(); diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 98c373ed3..f64a766f5 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -87,6 +87,11 @@ Application::Application(int& argc, char** argv) #endif } +QWidget* Application::mainWindow() const +{ + return m_mainWindow; +} + void Application::setMainWindow(QWidget* mainWindow) { m_mainWindow = mainWindow; diff --git a/src/gui/Application.h b/src/gui/Application.h index f4324fa2c..fd5ad12e0 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -29,6 +29,7 @@ class Application : public QApplication public: Application(int& argc, char** argv); + QWidget* mainWindow() const; void setMainWindow(QWidget* mainWindow); bool event(QEvent* event) override; diff --git a/src/gui/ChangeMasterKeyWidget.cpp b/src/gui/ChangeMasterKeyWidget.cpp index f7e5b33ab..616b0ee01 100644 --- a/src/gui/ChangeMasterKeyWidget.cpp +++ b/src/gui/ChangeMasterKeyWidget.cpp @@ -21,8 +21,16 @@ #include "core/FilePath.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" +#include "keys/YkChallengeResponseKey.h" #include "gui/FileDialog.h" #include "gui/MessageBox.h" +#include "crypto/Random.h" +#include "MainWindow.h" + +#include "config-keepassx.h" + +#include +#include ChangeMasterKeyWidget::ChangeMasterKeyWidget(QWidget* parent) : DialogyWidget(parent) @@ -32,13 +40,35 @@ ChangeMasterKeyWidget::ChangeMasterKeyWidget(QWidget* parent) m_ui->messageWidget->setHidden(true); - connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(generateKey())); - connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); - connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->enterPasswordEdit, SLOT(setShowPassword(bool))); m_ui->repeatPasswordEdit->enableVerifyMode(m_ui->enterPasswordEdit); + + connect(m_ui->passwordGroup, SIGNAL(clicked(bool)), SLOT(setOkEnabled())); + connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->enterPasswordEdit, SLOT(setShowPassword(bool))); + + connect(m_ui->keyFileGroup, SIGNAL(clicked(bool)), SLOT(setOkEnabled())); connect(m_ui->createKeyFileButton, SIGNAL(clicked()), SLOT(createKeyFile())); connect(m_ui->browseKeyFileButton, SIGNAL(clicked()), SLOT(browseKeyFile())); + connect(m_ui->keyFileCombo, SIGNAL(editTextChanged(QString)), SLOT(setOkEnabled())); + + connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(generateKey())); + connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); + +#ifdef WITH_XC_YUBIKEY + m_ui->yubikeyProgress->setVisible(false); + QSizePolicy sp = m_ui->yubikeyProgress->sizePolicy(); + sp.setRetainSizeWhenHidden(true); + m_ui->yubikeyProgress->setSizePolicy(sp); + + connect(m_ui->challengeResponseGroup, SIGNAL(clicked(bool)), SLOT(challengeResponseGroupToggled(bool))); + connect(m_ui->challengeResponseGroup, SIGNAL(clicked(bool)), SLOT(setOkEnabled())); + connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey())); + + connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection); + connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection); +#else + m_ui->challengeResponseGroup->setVisible(false); +#endif } ChangeMasterKeyWidget::~ChangeMasterKeyWidget() @@ -81,7 +111,11 @@ void ChangeMasterKeyWidget::clearForms() m_ui->repeatPasswordEdit->setText(""); m_ui->keyFileGroup->setChecked(false); m_ui->togglePasswordButton->setChecked(false); - // TODO: clear m_ui->keyFileCombo + +#ifdef WITH_XC_YUBIKEY + m_ui->challengeResponseGroup->setChecked(false); + m_ui->comboChallengeResponse->clear(); +#endif m_ui->enterPasswordEdit->setFocus(); } @@ -103,9 +137,9 @@ void ChangeMasterKeyWidget::generateKey() if (m_ui->passwordGroup->isChecked()) { if (m_ui->enterPasswordEdit->text() == m_ui->repeatPasswordEdit->text()) { if (m_ui->enterPasswordEdit->text().isEmpty()) { - if (MessageBox::question(this, tr("Question"), - tr("Do you really want to use an empty string as password?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + if (MessageBox::warning(this, tr("Empty password"), + tr("Do you really want to use an empty string as password?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { return; } } @@ -130,6 +164,25 @@ void ChangeMasterKeyWidget::generateKey() m_key.addKey(fileKey); } +#ifdef WITH_XC_YUBIKEY + if (m_ui->challengeResponseGroup->isChecked()) { + int selectionIndex = m_ui->comboChallengeResponse->currentIndex(); + int comboPayload = m_ui->comboChallengeResponse->itemData(selectionIndex).toInt(); + + if (0 == comboPayload) { + m_ui->messageWidget->showMessage(tr("Changing master key failed: no YubiKey inserted."), + MessageWidget::Error); + return; + } + + // read blocking mode from LSB and slot index number from second LSB + bool blocking = comboPayload & 1; + int slot = comboPayload >> 1; + auto key = QSharedPointer(new YkChallengeResponseKey(slot, blocking)); + m_key.addChallengeResponseKey(key); + } +#endif + m_ui->messageWidget->hideMessage(); emit editFinished(true); } @@ -140,6 +193,51 @@ void ChangeMasterKeyWidget::reject() emit editFinished(false); } +void ChangeMasterKeyWidget::challengeResponseGroupToggled(bool checked) +{ + if (checked) + pollYubikey(); +} + +void ChangeMasterKeyWidget::pollYubikey() +{ + m_ui->buttonRedetectYubikey->setEnabled(false); + m_ui->comboChallengeResponse->setEnabled(false); + m_ui->comboChallengeResponse->clear(); + m_ui->yubikeyProgress->setVisible(true); + setOkEnabled(); + + // YubiKey init is slow, detect asynchronously to not block the UI + QtConcurrent::run(YubiKey::instance(), &YubiKey::detect); +} + +void ChangeMasterKeyWidget::yubikeyDetected(int slot, bool blocking) +{ + YkChallengeResponseKey yk(slot, blocking); + // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB + m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1) | blocking)); + m_ui->comboChallengeResponse->setEnabled(m_ui->challengeResponseGroup->isChecked()); + m_ui->buttonRedetectYubikey->setEnabled(m_ui->challengeResponseGroup->isChecked()); + m_ui->yubikeyProgress->setVisible(false); + setOkEnabled(); +} + +void ChangeMasterKeyWidget::noYubikeyFound() +{ + m_ui->buttonRedetectYubikey->setEnabled(m_ui->challengeResponseGroup->isChecked()); + m_ui->yubikeyProgress->setVisible(false); + setOkEnabled(); +} + +void ChangeMasterKeyWidget::setOkEnabled() +{ + bool ok = m_ui->passwordGroup->isChecked() || + (m_ui->challengeResponseGroup->isChecked() && !m_ui->comboChallengeResponse->currentText().isEmpty()) || + (m_ui->keyFileGroup->isChecked() && !m_ui->keyFileCombo->currentText().isEmpty()); + + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ok); +} + void ChangeMasterKeyWidget::setCancelEnabled(bool enabled) { m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(enabled); diff --git a/src/gui/ChangeMasterKeyWidget.h b/src/gui/ChangeMasterKeyWidget.h index 1f90c4c5d..b3e097276 100644 --- a/src/gui/ChangeMasterKeyWidget.h +++ b/src/gui/ChangeMasterKeyWidget.h @@ -38,6 +38,9 @@ public: void clearForms(); CompositeKey newMasterKey(); QLabel* headlineLabel(); + +public slots: + void setOkEnabled(); void setCancelEnabled(bool enabled); signals: @@ -48,6 +51,10 @@ private slots: void reject(); void createKeyFile(); void browseKeyFile(); + void yubikeyDetected(int slot, bool blocking); + void noYubikeyFound(); + void challengeResponseGroupToggled(bool checked); + void pollYubikey(); private: const QScopedPointer m_ui; diff --git a/src/gui/ChangeMasterKeyWidget.ui b/src/gui/ChangeMasterKeyWidget.ui index 8d879f759..693d8ac1d 100644 --- a/src/gui/ChangeMasterKeyWidget.ui +++ b/src/gui/ChangeMasterKeyWidget.ui @@ -7,7 +7,7 @@ 0 0 818 - 397 + 471 @@ -90,7 +90,7 @@ - Key file + &Key file true @@ -126,6 +126,67 @@ + + + + true + + + Cha&llenge Response + + + true + + + true + + + + + + 0 + + + + + Refresh + + + + + + + + 0 + 0 + + + + + + + + + 16777215 + 2 + + + + 0 + + + -1 + + + false + + + + + + + + diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index b6220d93a..f7d432479 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -27,6 +27,14 @@ #include "format/KeePass2Reader.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" +#include "crypto/Random.h" +#include "keys/YkChallengeResponseKey.h" + +#include "config-keepassx.h" + +#include +#include + DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) : DialogyWidget(parent) @@ -42,8 +50,6 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) font.setPointSize(font.pointSize() + 2); m_ui->labelHeadline->setFont(font); - m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - m_ui->buttonTogglePassword->setIcon(filePath()->onOffIcon("actions", "password-show")); connect(m_ui->buttonTogglePassword, SIGNAL(toggled(bool)), m_ui->editPassword, SLOT(setShowPassword(bool))); @@ -54,7 +60,25 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); - + +#ifdef WITH_XC_YUBIKEY + m_ui->yubikeyProgress->setVisible(false); + QSizePolicy sp = m_ui->yubikeyProgress->sizePolicy(); + sp.setRetainSizeWhenHidden(true); + m_ui->yubikeyProgress->setSizePolicy(sp); + + connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey())); + connect(m_ui->comboChallengeResponse, SIGNAL(activated(int)), SLOT(activateChallengeResponse())); + + connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection); + connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection); +#else + m_ui->checkChallengeResponse->setVisible(false); + m_ui->buttonRedetectYubikey->setVisible(false); + m_ui->comboChallengeResponse->setVisible(false); + m_ui->yubikeyProgress->setVisible(false); +#endif + #ifdef Q_OS_MACOS // add random padding to layouts to align widgets properly m_ui->dialogButtonsLayout->setContentsMargins(10, 0, 15, 0); @@ -71,6 +95,10 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event) { DialogyWidget::showEvent(event); m_ui->editPassword->setFocus(); + +#ifdef WITH_XC_YUBIKEY + pollYubikey(); +#endif } void DatabaseOpenWidget::load(const QString& filename) @@ -87,7 +115,6 @@ void DatabaseOpenWidget::load(const QString& filename) } } - m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_ui->editPassword->setFocus(); } @@ -148,6 +175,7 @@ CompositeKey DatabaseOpenWidget::databaseKey() } QHash lastKeyFiles = config()->get("LastKeyFiles").toHash(); + QHash lastChallengeResponse = config()->get("LastChallengeResponse").toHash(); if (m_ui->checkKeyFile->isChecked()) { FileKey key; @@ -160,15 +188,37 @@ CompositeKey DatabaseOpenWidget::databaseKey() } masterKey.addKey(key); lastKeyFiles[m_filename] = keyFilename; - } - else { + } else { lastKeyFiles.remove(m_filename); } + if (m_ui->checkChallengeResponse->isChecked()) { + lastChallengeResponse[m_filename] = true; + } else { + lastChallengeResponse.remove(m_filename); + } + if (config()->get("RememberLastKeyFiles").toBool()) { config()->set("LastKeyFiles", lastKeyFiles); } +#ifdef WITH_XC_YUBIKEY + if (config()->get("RememberLastKeyFiles").toBool()) { + config()->set("LastChallengeResponse", lastChallengeResponse); + } + + if (m_ui->checkChallengeResponse->isChecked()) { + int selectionIndex = m_ui->comboChallengeResponse->currentIndex(); + int comboPayload = m_ui->comboChallengeResponse->itemData(selectionIndex).toInt(); + + // read blocking mode from LSB and slot index number from second LSB + bool blocking = comboPayload & 1; + int slot = comboPayload >> 1; + auto key = QSharedPointer(new YkChallengeResponseKey(slot, blocking)); + masterKey.addChallengeResponseKey(key); + } +#endif + return masterKey; } @@ -187,6 +237,11 @@ void DatabaseOpenWidget::activateKeyFile() m_ui->checkKeyFile->setChecked(true); } +void DatabaseOpenWidget::activateChallengeResponse() +{ + m_ui->checkChallengeResponse->setChecked(true); +} + void DatabaseOpenWidget::browseKeyFile() { QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files")); @@ -196,3 +251,40 @@ void DatabaseOpenWidget::browseKeyFile() m_ui->comboKeyFile->lineEdit()->setText(filename); } } + +void DatabaseOpenWidget::pollYubikey() +{ + m_ui->buttonRedetectYubikey->setEnabled(false); + m_ui->checkChallengeResponse->setEnabled(false); + m_ui->checkChallengeResponse->setChecked(false); + m_ui->comboChallengeResponse->setEnabled(false); + m_ui->comboChallengeResponse->clear(); + m_ui->yubikeyProgress->setVisible(true); + + // YubiKey init is slow, detect asynchronously to not block the UI + QtConcurrent::run(YubiKey::instance(), &YubiKey::detect); +} + +void DatabaseOpenWidget::yubikeyDetected(int slot, bool blocking) +{ + YkChallengeResponseKey yk(slot, blocking); + // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB + m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1) | blocking)); + m_ui->comboChallengeResponse->setEnabled(true); + m_ui->checkChallengeResponse->setEnabled(true); + m_ui->buttonRedetectYubikey->setEnabled(true); + m_ui->yubikeyProgress->setVisible(false); + + if (config()->get("RememberLastKeyFiles").toBool()) { + QHash lastChallengeResponse = config()->get("LastChallengeResponse").toHash(); + if (lastChallengeResponse.contains(m_filename)) { + m_ui->checkChallengeResponse->setChecked(true); + } + } +} + +void DatabaseOpenWidget::noYubikeyFound() +{ + m_ui->buttonRedetectYubikey->setEnabled(true); + m_ui->yubikeyProgress->setVisible(false); +} diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 405c7f3b8..caba70a61 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -41,6 +41,9 @@ public: void enterKey(const QString& pw, const QString& keyFile); Database* database(); +public slots: + void pollYubikey(); + signals: void editFinished(bool accepted); @@ -55,7 +58,10 @@ protected slots: private slots: void activatePassword(); void activateKeyFile(); + void activateChallengeResponse(); void browseKeyFile(); + void yubikeyDetected(int slot, bool blocking); + void noYubikeyFound(); protected: const QScopedPointer m_ui; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 4b85c1740..dba71d0fa 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -7,7 +7,7 @@ 0 0 596 - 250 + 302 @@ -85,7 +85,7 @@ - + 5 @@ -118,7 +118,7 @@ - + 5 @@ -142,6 +142,87 @@ + + + + 5 + + + 5 + + + 0 + + + + + true + + + Refresh + + + + + + + false + + + + 0 + 0 + + + + false + + + + + + + + 16777215 + 2 + + + + 0 + + + 0 + + + -1 + + + false + + + + + + + + + 0 + + + 2 + + + + + false + + + Challenge Response: + + + + + diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index d679ce294..6626b3428 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -54,7 +54,7 @@ const int DatabaseTabWidget::LastDatabasesCount = 5; DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) : QTabWidget(parent) - , m_dbWidgetSateSync(new DatabaseWidgetStateSync(this)) + , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this)) { DragTabBar* tabBar = new DragTabBar(this); setTabBar(tabBar); @@ -62,7 +62,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabase(int))); connect(this, SIGNAL(currentChanged(int)), SLOT(emitActivateDatabaseChanged())); - connect(this, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), m_dbWidgetSateSync, SLOT(setActive(DatabaseWidget*))); + connect(this, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*))); connect(autoType(), SIGNAL(globalShortcutTriggered()), SLOT(performGlobalAutoType())); } @@ -331,17 +331,18 @@ bool DatabaseTabWidget::closeAllDatabases() bool DatabaseTabWidget::saveDatabase(Database* db) { DatabaseManagerStruct& dbStruct = m_dbList[db]; - // temporarily disable autoreload - dbStruct.dbWidget->ignoreNextAutoreload(); if (dbStruct.saveToFilename) { QSaveFile saveFile(dbStruct.canonicalFilePath); if (saveFile.open(QIODevice::WriteOnly)) { // write the database to the file + dbStruct.dbWidget->blockAutoReload(true); m_writer.writeDatabase(&saveFile, db); + dbStruct.dbWidget->blockAutoReload(false); + if (m_writer.hasError()) { emit messageTab(tr("Writing the database failed.").append("\n") - .append(m_writer.errorString()), MessageWidget::Error); + .append(m_writer.errorString()), MessageWidget::Error); return false; } @@ -352,20 +353,17 @@ bool DatabaseTabWidget::saveDatabase(Database* db) updateTabName(db); emit messageDismissTab(); return true; - } - else { + } else { emit messageTab(tr("Writing the database failed.").append("\n") - .append(saveFile.errorString()), MessageWidget::Error); + .append(saveFile.errorString()), MessageWidget::Error); return false; } - } - else { + } else { emit messageTab(tr("Writing the database failed.").append("\n") - .append(saveFile.errorString()), MessageWidget::Error); + .append(saveFile.errorString()), MessageWidget::Error); return false; } - } - else { + } else { return saveDatabaseAs(db); } } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index d4955c6ea..6fa783dd0 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -116,7 +116,7 @@ private: KeePass2Writer m_writer; QHash m_dbList; - DatabaseWidgetStateSync* m_dbWidgetSateSync; + DatabaseWidgetStateSync* m_dbWidgetStateSync; }; #endif // KEEPASSX_DATABASETABWIDGET_H diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index f68fecdc4..8a40b6364 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -169,14 +169,14 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile())); - connect(&m_ignoreWatchTimer, SIGNAL(timeout()), this, SLOT(onWatchedFileChanged())); + connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(unblockAutoReload())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); m_databaseModified = false; m_fileWatchTimer.setSingleShot(true); - m_ignoreWatchTimer.setSingleShot(true); - m_ignoreNextAutoreload = false; + m_fileWatchUnblockTimer.setSingleShot(true); + m_ignoreAutoReload = false; m_searchCaseSensitive = false; @@ -1004,7 +1004,7 @@ void DatabaseWidget::lock() void DatabaseWidget::updateFilename(const QString& fileName) { - if (! m_filename.isEmpty()) { + if (!m_filename.isEmpty()) { m_fileWatcher.removePath(m_filename); } @@ -1012,26 +1012,31 @@ void DatabaseWidget::updateFilename(const QString& fileName) m_filename = fileName; } -void DatabaseWidget::ignoreNextAutoreload() +void DatabaseWidget::blockAutoReload(bool block) { - m_ignoreNextAutoreload = true; - m_ignoreWatchTimer.start(100); + if (block) { + m_ignoreAutoReload = true; + m_fileWatchTimer.stop(); + } else { + m_fileWatchUnblockTimer.start(500); + } +} + +void DatabaseWidget::unblockAutoReload() +{ + m_ignoreAutoReload = false; + updateFilename(m_filename); } void DatabaseWidget::onWatchedFileChanged() { - if (m_ignoreNextAutoreload) { - // Reset the watch - m_ignoreNextAutoreload = false; - m_ignoreWatchTimer.stop(); - m_fileWatcher.addPath(m_filename); + if (m_ignoreAutoReload) { + return; } - else { - if (m_fileWatchTimer.isActive()) - return; + if (m_fileWatchTimer.isActive()) + return; - m_fileWatchTimer.start(500); - } + m_fileWatchTimer.start(500); } void DatabaseWidget::reloadDatabaseFile() diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 3781af49f..2aa818df6 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -98,7 +98,7 @@ public: EntryView* entryView(); void showUnlockDialog(); void closeUnlockDialog(); - void ignoreNextAutoreload(); + void blockAutoReload(bool block = true); void refreshSearch(); signals: @@ -175,6 +175,7 @@ private slots: void onWatchedFileChanged(); void reloadDatabaseFile(); void restoreGroupEntryFocus(Uuid groupUuid, Uuid EntryUuid); + void unblockAutoReload(); private: void setClipboardTextAndMinimize(const QString& text); @@ -212,8 +213,8 @@ private: // Autoreload QFileSystemWatcher m_fileWatcher; QTimer m_fileWatchTimer; - bool m_ignoreNextAutoreload; - QTimer m_ignoreWatchTimer; + QTimer m_fileWatchUnblockTimer; + bool m_ignoreAutoReload; bool m_databaseModified; }; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 763ada1ae..f93678474 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -871,13 +871,15 @@ bool MainWindow::isTrayIconEnabled() const #endif } -void MainWindow::displayGlobalMessage(const QString& text, MessageWidget::MessageType type) +void MainWindow::displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton) { + m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); m_ui->globalMessageWidget->showMessage(text, type); } -void MainWindow::displayTabMessage(const QString& text, MessageWidget::MessageType type) +void MainWindow::displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton) { + m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type); } @@ -893,3 +895,14 @@ void MainWindow::hideTabMessage() } } +void MainWindow::showYubiKeyPopup() +{ + displayGlobalMessage(tr("Please touch the button on your YubiKey!"), MessageWidget::Information, false); + setEnabled(false); +} + +void MainWindow::hideYubiKeyPopup() +{ + hideGlobalMessage(); + setEnabled(true); +} diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 7a314df8c..cc7037959 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -24,6 +24,7 @@ #include "core/SignalMultiplexer.h" #include "gui/DatabaseWidget.h" +#include "gui/Application.h" namespace Ui { class MainWindow; @@ -43,6 +44,11 @@ public slots: void openDatabase(const QString& fileName, const QString& pw = QString(), const QString& keyFile = QString()); void appExit(); + void displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true); + void displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true); + void hideGlobalMessage(); + void showYubiKeyPopup(); + void hideYubiKeyPopup(); protected: void closeEvent(QCloseEvent* event) override; @@ -76,9 +82,6 @@ private slots: void toggleWindow(); void lockDatabasesAfterInactivity(); void repairDatabase(); - void displayGlobalMessage(const QString& text, MessageWidget::MessageType type); - void displayTabMessage(const QString& text, MessageWidget::MessageType type); - void hideGlobalMessage(); void hideTabMessage(); private: @@ -107,4 +110,7 @@ private: bool appExitCalled; }; +#define KEEPASSXC_MAIN_WINDOW (qobject_cast(qApp) ? \ + qobject_cast(qobject_cast(qApp)->mainWindow()) : nullptr) + #endif // KEEPASSX_MAINWINDOW_H diff --git a/src/gui/UnlockDatabaseWidget.cpp b/src/gui/UnlockDatabaseWidget.cpp index a005d0e60..d6beb1339 100644 --- a/src/gui/UnlockDatabaseWidget.cpp +++ b/src/gui/UnlockDatabaseWidget.cpp @@ -33,6 +33,7 @@ void UnlockDatabaseWidget::clearForms() m_ui->comboKeyFile->clear(); m_ui->checkPassword->setChecked(false); m_ui->checkKeyFile->setChecked(false); + m_ui->checkChallengeResponse->setChecked(false); m_ui->buttonTogglePassword->setChecked(false); m_db = nullptr; } diff --git a/src/keys/ChallengeResponseKey.h b/src/keys/ChallengeResponseKey.h new file mode 100644 index 000000000..ac8c81650 --- /dev/null +++ b/src/keys/ChallengeResponseKey.h @@ -0,0 +1,31 @@ +/* +* Copyright (C) 2014 Kyle Manna +* +* 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 . +*/ + +#ifndef KEEPASSX_CHALLENGE_RESPONSE_KEY_H +#define KEEPASSX_CHALLENGE_RESPONSE_KEY_H + +#include + +class ChallengeResponseKey +{ +public: + virtual ~ChallengeResponseKey() {} + virtual QByteArray rawKey() const = 0; + virtual bool challenge(const QByteArray& challenge) = 0; +}; + +#endif // KEEPASSX_CHALLENGE_RESPONSE_KEY_H diff --git a/src/keys/CompositeKey.cpp b/src/keys/CompositeKey.cpp index 88116c104..6114fd366 100644 --- a/src/keys/CompositeKey.cpp +++ b/src/keys/CompositeKey.cpp @@ -17,6 +17,7 @@ #include "CompositeKey.h" #include "CompositeKey_p.h" +#include "ChallengeResponseKey.h" #include #include @@ -46,11 +47,12 @@ void CompositeKey::clear() { qDeleteAll(m_keys); m_keys.clear(); + m_challengeResponseKeys.clear(); } bool CompositeKey::isEmpty() const { - return m_keys.isEmpty(); + return m_keys.isEmpty() && m_challengeResponseKeys.isEmpty(); } CompositeKey* CompositeKey::clone() const @@ -70,6 +72,9 @@ CompositeKey& CompositeKey::operator=(const CompositeKey& key) for (const Key* subKey : asConst(key.m_keys)) { addKey(*subKey); } + for (const auto subKey : asConst(key.m_challengeResponseKeys)) { + addChallengeResponseKey(subKey); + } return *this; } @@ -168,11 +173,40 @@ QByteArray CompositeKey::transformKeyRaw(const QByteArray& key, const QByteArray return result; } +bool CompositeKey::challenge(const QByteArray& seed, QByteArray& result) const +{ + // if no challenge response was requested, return nothing to + // maintain backwards compatibility with regular databases. + if (m_challengeResponseKeys.length() == 0) { + result.clear(); + return true; + } + + CryptoHash cryptoHash(CryptoHash::Sha256); + + for (const auto key : m_challengeResponseKeys) { + // if the device isn't present or fails, return an error + if (!key->challenge(seed)) { + return false; + } + cryptoHash.addData(key->rawKey()); + } + + result = cryptoHash.result(); + return true; +} + void CompositeKey::addKey(const Key& key) { m_keys.append(key.clone()); } +void CompositeKey::addChallengeResponseKey(QSharedPointer key) +{ + m_challengeResponseKeys.append(key); +} + + int CompositeKey::transformKeyBenchmark(int msec) { TransformKeyBenchmarkThread thread1(msec); diff --git a/src/keys/CompositeKey.h b/src/keys/CompositeKey.h index f8666aadc..50b2f699a 100644 --- a/src/keys/CompositeKey.h +++ b/src/keys/CompositeKey.h @@ -20,8 +20,10 @@ #include #include +#include #include "keys/Key.h" +#include "keys/ChallengeResponseKey.h" class CompositeKey : public Key { @@ -37,7 +39,10 @@ public: QByteArray rawKey() const; QByteArray transform(const QByteArray& seed, quint64 rounds, bool* ok, QString* errorString) const; + bool challenge(const QByteArray& seed, QByteArray &result) const; + void addKey(const Key& key); + void addChallengeResponseKey(QSharedPointer key); static int transformKeyBenchmark(int msec); static CompositeKey readFromLine(QString line); @@ -47,6 +52,7 @@ private: quint64 rounds, bool* ok, QString* errorString); QList m_keys; + QList> m_challengeResponseKeys; }; #endif // KEEPASSX_COMPOSITEKEY_H diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp new file mode 100644 index 000000000..dcd583358 --- /dev/null +++ b/src/keys/YkChallengeResponseKey.cpp @@ -0,0 +1,109 @@ +/* +* Copyright (C) 2014 Kyle Manna +* +* 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 . +*/ +#include "keys/YkChallengeResponseKey.h" +#include "keys/drivers/YubiKey.h" + +#include "core/Tools.h" +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "gui/MainWindow.h" + +#include +#include +#include +#include +#include +#include + +YkChallengeResponseKey::YkChallengeResponseKey(int slot, bool blocking) + : m_slot(slot), + m_blocking(blocking) +{ + if (KEEPASSXC_MAIN_WINDOW) { + connect(this, SIGNAL(userInteractionRequired()), KEEPASSXC_MAIN_WINDOW, SLOT(showYubiKeyPopup())); + connect(this, SIGNAL(userConfirmed()), KEEPASSXC_MAIN_WINDOW, SLOT(hideYubiKeyPopup())); + } +} + +QByteArray YkChallengeResponseKey::rawKey() const +{ + return m_key; +} + +/** + * Assumes yubikey()->init() was called + */ +bool YkChallengeResponseKey::challenge(const QByteArray& challenge) +{ + return this->challenge(challenge, 1); +} + +bool YkChallengeResponseKey::challenge(const QByteArray& challenge, unsigned retries) +{ + Q_ASSERT(retries > 0); + + do { + --retries; + + if (m_blocking) { + emit userInteractionRequired(); + } + + QFuture future = QtConcurrent::run([this, challenge]() { + return YubiKey::instance()->challenge(m_slot, true, challenge, m_key); + }); + + QEventLoop loop; + QFutureWatcher watcher; + watcher.setFuture(future); + connect(&watcher, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + if (m_blocking) { + emit userConfirmed(); + } + + if (future.result() != YubiKey::ERROR) { + return true; + } + + // if challenge failed, retry to detect YubiKeys in the event the YubiKey was un-plugged and re-plugged + if (retries > 0 && YubiKey::instance()->init() != true) { + continue; + } + + } while (retries > 0); + + return false; +} + +QString YkChallengeResponseKey::getName() const +{ + unsigned int serial; + QString fmt(QObject::tr("YubiKey[%1] Challenge Response - Slot %2 - %3")); + + YubiKey::instance()->getSerial(serial); + + return fmt.arg(QString::number(serial), + QString::number(m_slot), + (m_blocking) ? QObject::tr("Press") : QObject::tr("Passive")); +} + +bool YkChallengeResponseKey::isBlocking() const +{ + return m_blocking; +} diff --git a/src/keys/YkChallengeResponseKey.h b/src/keys/YkChallengeResponseKey.h new file mode 100644 index 000000000..8c566ca41 --- /dev/null +++ b/src/keys/YkChallengeResponseKey.h @@ -0,0 +1,59 @@ +/* +* Copyright (C) 2011 Felix Geyer +* +* 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 . +*/ + +#ifndef KEEPASSX_YK_CHALLENGERESPONSEKEY_H +#define KEEPASSX_YK_CHALLENGERESPONSEKEY_H + +#include "core/Global.h" +#include "keys/ChallengeResponseKey.h" +#include "keys/drivers/YubiKey.h" + +#include + +class YkChallengeResponseKey : public QObject, public ChallengeResponseKey +{ + Q_OBJECT + +public: + + YkChallengeResponseKey(int slot = -1, bool blocking = false); + + QByteArray rawKey() const; + bool challenge(const QByteArray& challenge); + bool challenge(const QByteArray& challenge, unsigned retries); + QString getName() const; + bool isBlocking() const; + +signals: + /** + * Emitted whenever user interaction is required to proceed with the challenge-response protocol. + * You can use this to show a helpful dialog informing the user that his assistance is required. + */ + void userInteractionRequired(); + + /** + * Emitted when the user has provided their required input. + */ + void userConfirmed(); + +private: + QByteArray m_key; + int m_slot; + bool m_blocking; +}; + +#endif // KEEPASSX_YK_CHALLENGERESPONSEKEY_H diff --git a/src/keys/drivers/YubiKey.cpp b/src/keys/drivers/YubiKey.cpp new file mode 100644 index 000000000..dfbc57c69 --- /dev/null +++ b/src/keys/drivers/YubiKey.cpp @@ -0,0 +1,212 @@ +/* +* Copyright (C) 2014 Kyle Manna +* +* 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 . +*/ + +#include + +#include + +#include +#include +#include +#include + +#include "core/Global.h" +#include "crypto/Random.h" + +#include "YubiKey.h" + +// Cast the void pointer from the generalized class definition +// to the proper pointer type from the now included system headers +#define m_yk (static_cast(m_yk_void)) +#define m_ykds (static_cast(m_ykds_void)) + +YubiKey::YubiKey() : m_yk_void(NULL), m_ykds_void(NULL), m_mutex(QMutex::Recursive) +{ +} + +YubiKey* YubiKey::m_instance(Q_NULLPTR); + +YubiKey* YubiKey::instance() +{ + if (!m_instance) { + m_instance = new YubiKey(); + } + + return m_instance; +} + +bool YubiKey::init() +{ + m_mutex.lock(); + + // previously initialized + if (m_yk != NULL && m_ykds != NULL) { + + if (yk_get_status(m_yk, m_ykds)) { + // Still connected + m_mutex.unlock(); + return true; + } else { + // Initialized but not connected anymore, re-init + deinit(); + } + } + + if (!yk_init()) { + m_mutex.unlock(); + return false; + } + + // TODO: handle multiple attached hardware devices + m_yk_void = static_cast(yk_open_first_key()); + if (m_yk == NULL) { + m_mutex.unlock(); + return false; + } + + m_ykds_void = static_cast(ykds_alloc()); + if (m_ykds == NULL) { + yk_close_key(m_yk); + m_yk_void = NULL; + m_mutex.unlock(); + return false; + } + + m_mutex.unlock(); + return true; +} + +bool YubiKey::deinit() +{ + m_mutex.lock(); + + if (m_yk) { + yk_close_key(m_yk); + m_yk_void = NULL; + } + + if (m_ykds) { + ykds_free(m_ykds); + m_ykds_void = NULL; + } + + m_mutex.unlock(); + + return true; +} + +void YubiKey::detect() +{ + if (init()) { + for (int i = 1; i < 3; i++) { + YubiKey::ChallengeResult result; + QByteArray rand = randomGen()->randomArray(1); + QByteArray resp; + + result = challenge(i, false, rand, resp); + if (result == YubiKey::ALREADY_RUNNING) { + emit alreadyRunning(); + return; + } else if (result != YubiKey::ERROR) { + emit detected(i, result == YubiKey::WOULDBLOCK); + return; + } + } + } + emit notFound(); +} + +bool YubiKey::getSerial(unsigned int& serial) +{ + m_mutex.lock(); + int result = yk_get_serial(m_yk, 1, 0, &serial); + m_mutex.unlock(); + + if (!result) { + return false; + } + + return true; +} + +YubiKey::ChallengeResult YubiKey::challenge(int slot, bool mayBlock, const QByteArray& challenge, QByteArray& response) +{ + if (!m_mutex.tryLock()) { + return ALREADY_RUNNING; + } + + int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2; + QByteArray paddedChallenge = challenge; + + // ensure that YubiKey::init() succeeded + if (m_yk == NULL) { + m_mutex.unlock(); + return ERROR; + } + + // yk_challenge_response() insists on 64 byte response buffer */ + response.resize(64); + + /* The challenge sent to the yubikey should always be 64 bytes for + * compatibility with all configurations. Follow PKCS7 padding. + * + * There is some question whether or not 64 byte fixed length + * configurations even work, some docs say avoid it. + */ + const int padLen = 64 - paddedChallenge.size(); + if (padLen > 0) { + paddedChallenge.append(QByteArray(padLen, padLen)); + } + + const unsigned char *c; + unsigned char *r; + c = reinterpret_cast(paddedChallenge.constData()); + r = reinterpret_cast(response.data()); + + int ret = yk_challenge_response(m_yk, yk_cmd, mayBlock, paddedChallenge.size(), c, response.size(), r); + emit challenged(); + + m_mutex.unlock(); + + if (!ret) { + if (yk_errno == YK_EWOULDBLOCK) { + return WOULDBLOCK; + } else if (yk_errno == YK_ETIMEOUT) { + return ERROR; + } else if (yk_errno) { + + /* Something went wrong, close the key, so that the next call to + * can try to re-open. + * + * Likely caused by the YubiKey being unplugged. + */ + + if (yk_errno == YK_EUSBERR) { + qWarning() << "USB error:" << yk_usb_strerror(); + } else { + qWarning() << "YubiKey core error:" << yk_strerror(yk_errno); + } + + return ERROR; + } + } + + // actual HMAC-SHA1 response is only 20 bytes + response.resize(20); + + return SUCCESS; +} diff --git a/src/keys/drivers/YubiKey.h b/src/keys/drivers/YubiKey.h new file mode 100644 index 000000000..47341f9a2 --- /dev/null +++ b/src/keys/drivers/YubiKey.h @@ -0,0 +1,117 @@ +/* +* Copyright (C) 2014 Kyle Manna +* +* 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 . +*/ + +#ifndef KEEPASSX_YUBIKEY_H +#define KEEPASSX_YUBIKEY_H + +#include +#include + +/** + * Singleton class to manage the interface to the hardware + */ +class YubiKey : public QObject +{ + Q_OBJECT + +public: + enum ChallengeResult { ERROR = -1, SUCCESS = 0, WOULDBLOCK, ALREADY_RUNNING }; + + /** + * @brief YubiKey::instance - get instance of singleton + * @return instance + */ + static YubiKey* instance(); + + /** + * @brief YubiKey::init - initialize yubikey library and hardware + * @return true on success + */ + bool init(); + + /** + * @brief YubiKey::deinit - cleanup after init + * @return true on success + */ + bool deinit(); + + /** + * @brief YubiKey::challenge - issue a challenge + * + * This operation could block if the YubiKey requires a touch to trigger. + * + * TODO: Signal to the UI that the system is waiting for challenge response + * touch. + * + * @param slot YubiKey configuration slot + * @param mayBlock operation is allowed to block + * @param challenge challenge input to YubiKey + * @param response response output from YubiKey + * @return true on success + */ + ChallengeResult challenge(int slot, bool mayBlock, const QByteArray& challenge, QByteArray& response); + + /** + * @brief YubiKey::getSerial - serial number of YubiKey + * @param serial serial number + * @return true on success + */ + bool getSerial(unsigned int& serial); + + /** + * @brief YubiKey::detect - probe for attached YubiKeys + */ + void detect(); + +Q_SIGNALS: + /** Emitted in response to detect() when a device is found + * + * @slot is the slot number detected + * @blocking signifies if the YK is setup in passive mode or if requires + * the user to touch it for a response + */ + void detected(int slot, bool blocking); + + /** + * Emitted when the YubiKey was challenged and has returned a response. + */ + void challenged(); + + /** + * Emitted when no Yubikey could be found. + */ + void notFound(); + + /** + * Emitted when detection is already running. + */ + void alreadyRunning(); + +private: + explicit YubiKey(); + static YubiKey* m_instance; + + // Create void ptr here to avoid ifdef header include mess + void* m_yk_void; + void* m_ykds_void; + + QMutex m_mutex; + + Q_DISABLE_COPY(YubiKey) +}; + +#endif // KEEPASSX_YUBIKEY_H diff --git a/src/keys/drivers/YubiKeyStub.cpp b/src/keys/drivers/YubiKeyStub.cpp new file mode 100644 index 000000000..15eef27ad --- /dev/null +++ b/src/keys/drivers/YubiKeyStub.cpp @@ -0,0 +1,69 @@ +/* +* Copyright (C) 2014 Kyle Manna +* +* 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 . +*/ + +#include + +#include "core/Global.h" +#include "crypto/Random.h" + +#include "YubiKey.h" + +YubiKey::YubiKey() : m_yk_void(NULL), m_ykds_void(NULL) +{ +} + +YubiKey* YubiKey::m_instance(Q_NULLPTR); + +YubiKey* YubiKey::instance() +{ + if (!m_instance) { + m_instance = new YubiKey(); + } + + return m_instance; +} + +bool YubiKey::init() +{ + return false; +} + +bool YubiKey::deinit() +{ + return false; +} + +void YubiKey::detect() +{ +} + +bool YubiKey::getSerial(unsigned int& serial) +{ + Q_UNUSED(serial); + + return false; +} + +YubiKey::ChallengeResult YubiKey::challenge(int slot, bool mayBlock, const QByteArray& chal, QByteArray& resp) +{ + Q_UNUSED(slot); + Q_UNUSED(mayBlock); + Q_UNUSED(chal); + Q_UNUSED(resp); + + return ERROR; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5840a5b4b..db1750d7d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -100,6 +100,10 @@ set(testsupport_SOURCES modeltest.cpp FailDevice.cpp) add_library(testsupport STATIC ${testsupport_SOURCES}) target_link_libraries(testsupport ${MHD_LIBRARIES} Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test) +if(YUBIKEY_FOUND) + set(TEST_LIBRARIES ${TEST_LIBRARIES} ${YUBIKEY_LIBRARIES}) +endif() + add_unit_test(NAME testgroup SOURCES TestGroup.cpp LIBS ${TEST_LIBRARIES}) @@ -166,6 +170,10 @@ add_unit_test(NAME testexporter SOURCES TestExporter.cpp add_unit_test(NAME testcsvexporter SOURCES TestCsvExporter.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testykchallengeresponsekey + SOURCES TestYkChallengeResponseKey.cpp TestYkChallengeResponseKey.h + LIBS ${TEST_LIBRARIES}) + if(WITH_GUI_TESTS) add_subdirectory(gui) endif(WITH_GUI_TESTS) diff --git a/tests/TestYkChallengeResponseKey.cpp b/tests/TestYkChallengeResponseKey.cpp new file mode 100644 index 000000000..40eda3bf9 --- /dev/null +++ b/tests/TestYkChallengeResponseKey.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2014 Kyle Manna + * + * + * 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 . + */ + +#include "TestYkChallengeResponseKey.h" + +#include +#include + +#include "crypto/Crypto.h" +#include "keys/YkChallengeResponseKey.h" + +QTEST_GUILESS_MAIN(TestYubiKeyChalResp) + +void TestYubiKeyChalResp::initTestCase() +{ + m_detected = 0; + m_key = NULL; + + // crypto subsystem needs to be initialized for YubiKey testing + QVERIFY(Crypto::init()); +} + +void TestYubiKeyChalResp::cleanupTestCase() +{ + if (m_key) + delete m_key; +} + +void TestYubiKeyChalResp::init() +{ + bool result = YubiKey::instance()->init(); + + if (!result) { + QSKIP("Unable to connect to YubiKey", SkipAll); + } +} + +void TestYubiKeyChalResp::detectDevices() +{ + connect(YubiKey::instance(), SIGNAL(detected(int,bool)), + SLOT(ykDetected(int,bool)), + Qt::QueuedConnection); + QtConcurrent::run(YubiKey::instance(), &YubiKey::detect); + + // need to wait for the hardware (that's hopefully plugged in)... + QTest::qWait(2000); + QVERIFY2(m_detected > 0, "Is a YubiKey attached?"); +} + +void TestYubiKeyChalResp::getSerial() +{ + unsigned int serial; + QVERIFY(YubiKey::instance()->getSerial(serial)); +} + +void TestYubiKeyChalResp::keyGetName() +{ + QVERIFY(m_key); + QVERIFY(m_key->getName().length() > 0); +} + +void TestYubiKeyChalResp::keyIssueChallenge() +{ + QVERIFY(m_key); + if (m_key->isBlocking()) { + /* Testing active mode in unit tests is unreasonable */ + QSKIP("YubiKey not in passive mode", SkipSingle); + } + + QByteArray ba("UnitTest"); + QVERIFY(m_key->challenge(ba)); + + /* TODO Determine if it's reasonable to provide a fixed secret key for + * verification testing. Obviously simple technically, but annoying + * if devs need to re-program their yubikeys or have a spare test key + * for unit tests to past. + * + * Might be worth it for integrity verification though. + */ +} + +void TestYubiKeyChalResp::ykDetected(int slot, bool blocking) +{ + Q_UNUSED(blocking); + + if (slot > 0) + m_detected++; + + /* Key used for later testing */ + if (!m_key) + m_key = new YkChallengeResponseKey(slot, blocking); +} + +void TestYubiKeyChalResp::deinit() +{ + QVERIFY(YubiKey::instance()->deinit()); +} diff --git a/tests/TestYkChallengeResponseKey.h b/tests/TestYkChallengeResponseKey.h new file mode 100644 index 000000000..4699b9101 --- /dev/null +++ b/tests/TestYkChallengeResponseKey.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2014 Kyle Manna + * + * 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 . + */ + +#ifndef KEEPASSX_TESTYUBIKEYCHALRESP_H +#define KEEPASSX_TESTYUBIKEYCHALRESP_H + +#include + +#include "keys/YkChallengeResponseKey.h" + +class TestYubiKeyChalResp: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void init(); + + /* Order is important! + * Need to init and detectDevices() before proceeding + */ + void detectDevices(); + + void getSerial(); + void keyGetName(); + void keyIssueChallenge(); + + void deinit(); + + /* Callback for detectDevices() */ + void ykDetected(int slot, bool blocking); + +private: + int m_detected; + YkChallengeResponseKey *m_key; +}; + +#endif // KEEPASSX_TESTYUBIKEYCHALRESP_H