mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Merge pull request #127 from keepassxreboot/feature/yubikey
Add Yubikey 2FA for unlocking databases
This commit is contained in:
commit
3e84c0a91a
@ -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
|
||||
|
@ -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
27
cmake/FindYubiKey.cmake
Normal 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)
|
@ -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=""
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,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<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);
|
||||
|
@ -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;
|
||||
|
@ -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>&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&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">
|
||||
|
@ -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)));
|
||||
@ -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<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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ private:
|
||||
|
||||
KeePass2Writer m_writer;
|
||||
QHash<Database*, DatabaseManagerStruct> m_dbList;
|
||||
DatabaseWidgetStateSync* m_dbWidgetSateSync;
|
||||
DatabaseWidgetStateSync* m_dbWidgetStateSync;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_DATABASETABWIDGET_H
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
31
src/keys/ChallengeResponseKey.h
Normal file
31
src/keys/ChallengeResponseKey.h
Normal 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
|
@ -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);
|
||||
|
@ -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
|
||||
|
109
src/keys/YkChallengeResponseKey.cpp
Normal file
109
src/keys/YkChallengeResponseKey.cpp
Normal 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;
|
||||
}
|
59
src/keys/YkChallengeResponseKey.h
Normal file
59
src/keys/YkChallengeResponseKey.h
Normal 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
|
212
src/keys/drivers/YubiKey.cpp
Normal file
212
src/keys/drivers/YubiKey.cpp
Normal 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
117
src/keys/drivers/YubiKey.h
Normal 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
|
69
src/keys/drivers/YubiKeyStub.cpp
Normal file
69
src/keys/drivers/YubiKeyStub.cpp
Normal 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;
|
||||
}
|
@ -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)
|
||||
|
112
tests/TestYkChallengeResponseKey.cpp
Normal file
112
tests/TestYkChallengeResponseKey.cpp
Normal 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());
|
||||
}
|
54
tests/TestYkChallengeResponseKey.h
Normal file
54
tests/TestYkChallengeResponseKey.h
Normal 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
|
Loading…
Reference in New Issue
Block a user