Merge pull request #127 from keepassxreboot/feature/yubikey

Add Yubikey 2FA for unlocking databases
This commit is contained in:
Janek Bevendorff 2017-03-10 22:48:00 +01:00 committed by GitHub
commit 3e84c0a91a
35 changed files with 1334 additions and 63 deletions

View File

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

View File

@ -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 <sys/prctl.h>
int main() { prctl(PR_SET_DUMPABLE, 0); return 0; }"

27
cmake/FindYubiKey.cmake Normal file
View File

@ -0,0 +1,27 @@
# Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
find_path(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)

View File

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

View File

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

View File

@ -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());
}

View File

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

View File

@ -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();

View File

@ -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();

View File

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

View File

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

View File

@ -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 <QtConcurrentRun>
#include <QSharedPointer>
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,7 +137,7 @@ 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"),
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<YkChallengeResponseKey>(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);

View File

@ -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<Ui::ChangeMasterKeyWidget> m_ui;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>818</width>
<height>397</height>
<height>471</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -90,7 +90,7 @@
<item>
<widget class="QGroupBox" name="keyFileGroup">
<property name="title">
<string>Key file</string>
<string>&amp;Key file</string>
</property>
<property name="checkable">
<bool>true</bool>
@ -126,6 +126,67 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="challengeResponseGroup">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>Cha&amp;llenge Response</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_4">
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="buttonRedetectYubikey">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@ -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 <QtConcurrentRun>
#include <QSharedPointer>
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)));
@ -55,6 +61,24 @@ 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<QString, QVariant> lastKeyFiles = config()->get("LastKeyFiles").toHash();
QHash<QString, QVariant> 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<YkChallengeResponseKey>(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<QString, QVariant> 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);
}

View File

@ -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<Ui::DatabaseOpenWidget> m_ui;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>596</width>
<height>250</height>
<height>302</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0,1,0,0,3">
@ -85,7 +85,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="1" column="2">
<layout class="QHBoxLayout" name="keyFileLayout">
<property name="leftMargin">
<number>5</number>
@ -118,7 +118,7 @@
</item>
</layout>
</item>
<item row="0" column="1">
<item row="0" column="2">
<layout class="QHBoxLayout" name="passwordLayout">
<property name="leftMargin">
<number>5</number>
@ -142,6 +142,87 @@
</item>
</layout>
</item>
<item row="5" column="2">
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QPushButton" name="buttonRedetectYubikey">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="comboChallengeResponse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="QCheckBox" name="checkChallengeResponse">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Challenge Response:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>

View File

@ -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,14 +331,15 @@ 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);
@ -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);
return false;
}
}
else {
} else {
emit messageTab(tr("Writing the database failed.").append("\n")
.append(saveFile.errorString()), MessageWidget::Error);
return false;
}
}
else {
} else {
return saveDatabaseAs(db);
}
}

View File

@ -116,7 +116,7 @@ private:
KeePass2Writer m_writer;
QHash<Database*, DatabaseManagerStruct> m_dbList;
DatabaseWidgetStateSync* m_dbWidgetSateSync;
DatabaseWidgetStateSync* m_dbWidgetStateSync;
};
#endif // KEEPASSX_DATABASETABWIDGET_H

View File

@ -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;
m_fileWatchTimer.start(500);
}
}
void DatabaseWidget::reloadDatabaseFile()

View File

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

View File

@ -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);
}

View File

@ -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<Application*>(qApp) ? \
qobject_cast<MainWindow*>(qobject_cast<Application*>(qApp)->mainWindow()) : nullptr)
#endif // KEEPASSX_MAINWINDOW_H

View File

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

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 KEEPASSX_CHALLENGE_RESPONSE_KEY_H
#define KEEPASSX_CHALLENGE_RESPONSE_KEY_H
#include <QByteArray>
class ChallengeResponseKey
{
public:
virtual ~ChallengeResponseKey() {}
virtual QByteArray rawKey() const = 0;
virtual bool challenge(const QByteArray& challenge) = 0;
};
#endif // KEEPASSX_CHALLENGE_RESPONSE_KEY_H

View File

@ -17,6 +17,7 @@
#include "CompositeKey.h"
#include "CompositeKey_p.h"
#include "ChallengeResponseKey.h"
#include <QElapsedTimer>
#include <QFile>
@ -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<ChallengeResponseKey> key)
{
m_challengeResponseKeys.append(key);
}
int CompositeKey::transformKeyBenchmark(int msec)
{
TransformKeyBenchmarkThread thread1(msec);

View File

@ -20,8 +20,10 @@
#include <QList>
#include <QString>
#include <QSharedPointer>
#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<ChallengeResponseKey> key);
static int transformKeyBenchmark(int msec);
static CompositeKey readFromLine(QString line);
@ -47,6 +52,7 @@ private:
quint64 rounds, bool* ok, QString* errorString);
QList<Key*> m_keys;
QList<QSharedPointer<ChallengeResponseKey>> m_challengeResponseKeys;
};
#endif // KEEPASSX_COMPOSITEKEY_H

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 "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 <QFile>
#include <QXmlStreamReader>
#include <QtConcurrent>
#include <QApplication>
#include <QEventLoop>
#include <QFutureWatcher>
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<YubiKey::ChallengeResult> future = QtConcurrent::run([this, challenge]() {
return YubiKey::instance()->challenge(m_slot, true, challenge, m_key);
});
QEventLoop loop;
QFutureWatcher<YubiKey::ChallengeResult> 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;
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
*
* 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 KEEPASSX_YK_CHALLENGERESPONSEKEY_H
#define KEEPASSX_YK_CHALLENGERESPONSEKEY_H
#include "core/Global.h"
#include "keys/ChallengeResponseKey.h"
#include "keys/drivers/YubiKey.h"
#include <QObject>
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

View File

@ -0,0 +1,212 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 <stdio.h>
#include <QDebug>
#include <ykcore.h>
#include <yubikey.h>
#include <ykdef.h>
#include <ykstatus.h>
#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<YK_KEY*>(m_yk_void))
#define m_ykds (static_cast<YK_STATUS*>(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<void*>(yk_open_first_key());
if (m_yk == NULL) {
m_mutex.unlock();
return false;
}
m_ykds_void = static_cast<void*>(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<const unsigned char*>(paddedChallenge.constData());
r = reinterpret_cast<unsigned char*>(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;
}

117
src/keys/drivers/YubiKey.h Normal file
View File

@ -0,0 +1,117 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 KEEPASSX_YUBIKEY_H
#define KEEPASSX_YUBIKEY_H
#include <QObject>
#include <QMutex>
/**
* 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

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 <stdio.h>
#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;
}

View File

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

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
*
* 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 "TestYkChallengeResponseKey.h"
#include <QTest>
#include <QtConcurrentRun>
#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());
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
*
* 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 KEEPASSX_TESTYUBIKEYCHALRESP_H
#define KEEPASSX_TESTYUBIKEYCHALRESP_H
#include <QObject>
#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