diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 12 | ||||
-rw-r--r-- | src/core/Database.cpp | 27 | ||||
-rw-r--r-- | src/core/Database.h | 4 | ||||
-rw-r--r-- | src/format/KeePass2Reader.cpp | 6 | ||||
-rw-r--r-- | src/format/KeePass2Writer.cpp | 6 | ||||
-rw-r--r-- | src/gui/ChangeMasterKeyWidget.cpp | 29 | ||||
-rw-r--r-- | src/gui/ChangeMasterKeyWidget.h | 1 | ||||
-rw-r--r-- | src/gui/ChangeMasterKeyWidget.ui | 25 | ||||
-rw-r--r-- | src/gui/DatabaseOpenWidget.cpp | 51 | ||||
-rw-r--r-- | src/gui/DatabaseOpenWidget.h | 3 | ||||
-rw-r--r-- | src/gui/DatabaseOpenWidget.ui | 17 | ||||
-rw-r--r-- | src/gui/UnlockDatabaseWidget.cpp | 1 | ||||
-rw-r--r-- | src/keys/ChallengeResponseKey.h | 32 | ||||
-rw-r--r-- | src/keys/CompositeKey.cpp | 37 | ||||
-rw-r--r-- | src/keys/CompositeKey.h | 5 | ||||
-rw-r--r-- | src/keys/YkChallengeResponseKey.cpp | 94 | ||||
-rw-r--r-- | src/keys/YkChallengeResponseKey.h | 45 | ||||
-rw-r--r-- | src/keys/drivers/YubiKey.cpp | 250 | ||||
-rw-r--r-- | src/keys/drivers/YubiKey.h | 70 | ||||
-rw-r--r-- | src/keys/drivers/YubiKeyStub.cpp | 71 |
20 files changed, 785 insertions, 1 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30332c71e..cf4d593ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -111,9 +111,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 @@ -190,6 +192,12 @@ if(MINGW) ${CMAKE_SOURCE_DIR}/share/windows/icon.rc) endif() +if(YUBIKEY_FOUND) + 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) @@ -220,6 +228,10 @@ target_link_libraries(${PROGNAME} ${GCRYPT_LIBRARIES} ${ZLIB_LIBRARIES}) +if(YUBIKEY_FOUND) + target_link_libraries(keepassx_core ${YUBIKEY_LIBRARIES}) +endif() + set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON) if(APPLE) diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 336820381..22fc07230 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -176,6 +176,17 @@ QByteArray Database::transformedMasterKey() const return m_data.transformedMasterKey; } +QByteArray Database::challengeResponseKey() const +{ + return m_data.challengeResponseKey; +} + +bool Database::challengeMasterSeed(const QByteArray& masterSeed) +{ + m_data.masterSeed = masterSeed; + return m_data.key.challenge(masterSeed, m_data.challengeResponseKey); +} + void Database::setCipher(const Uuid& cipher) { Q_ASSERT(!cipher.isNull()); @@ -246,6 +257,22 @@ bool Database::verifyKey(const CompositeKey& key) const { Q_ASSERT(hasKey()); + /* If the database has challenge response keys, then the the verification + * key better as well */ + if (!m_data.challengeResponseKey.isEmpty()) { + QByteArray result; + + if (!key.challenge(m_data.masterSeed, result)) { + /* Challenge failed, (YubiKey?) removed? */ + return false; + } + + if (m_data.challengeResponseKey != result) { + /* Wrong response from challenged device(s) */ + return false; + } + } + return (m_data.key.rawKey() == key.rawKey()); } diff --git a/src/core/Database.h b/src/core/Database.h index 3cd5ed1b1..3f946a1cf 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -59,6 +59,8 @@ public: QByteArray transformedMasterKey; CompositeKey key; bool hasKey; + QByteArray masterSeed; + QByteArray challengeResponseKey; }; Database(); @@ -89,6 +91,8 @@ public: quint64 transformRounds() const; QByteArray transformedMasterKey() const; const CompositeKey & key() const; + QByteArray challengeResponseKey() const; + bool challengeMasterSeed(const QByteArray& masterSeed); void setCipher(const Uuid& cipher); void setCompressionAlgo(Database::CompressionAlgorithm algo); diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 1371aaa6a..0512be601 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -113,8 +113,14 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke return nullptr; } + if (m_db->challengeMasterSeed(m_masterSeed) == false) { + raiseError(tr("Unable to issue challenge-response.")); + return Q_NULLPTR; + } + CryptoHash hash(CryptoHash::Sha256); hash.addData(m_masterSeed); + hash.addData(m_db->challengeResponseKey()); hash.addData(m_db->transformedMasterKey()); QByteArray finalKey = hash.result(); diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index dfbbf3532..6bb316bac 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -51,8 +51,14 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) QByteArray startBytes = randomGen()->randomArray(32); QByteArray endOfHeader = "\r\n\r\n"; + if (db->challengeMasterSeed(masterSeed) == false) { + raiseError("Unable to issue challenge-response."); + return; + } + CryptoHash hash(CryptoHash::Sha256); hash.addData(masterSeed); + hash.addData(db->challengeResponseKey()); Q_ASSERT(!db->transformedMasterKey().isEmpty()); hash.addData(db->transformedMasterKey()); QByteArray finalKey = hash.result(); diff --git a/src/gui/ChangeMasterKeyWidget.cpp b/src/gui/ChangeMasterKeyWidget.cpp index 3e346bc10..c69cf2dcc 100644 --- a/src/gui/ChangeMasterKeyWidget.cpp +++ b/src/gui/ChangeMasterKeyWidget.cpp @@ -15,14 +15,18 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QtConcurrentRun> + #include "ChangeMasterKeyWidget.h" #include "ui_ChangeMasterKeyWidget.h" #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" ChangeMasterKeyWidget::ChangeMasterKeyWidget(QWidget* parent) : DialogyWidget(parent) @@ -81,6 +85,15 @@ void ChangeMasterKeyWidget::clearForms() m_ui->togglePasswordButton->setChecked(false); // TODO: clear m_ui->keyFileCombo + m_ui->challengeResponseGroup->setChecked(false); + m_ui->challengeResponseCombo->clear(); + + /* YubiKey init is slow */ + connect(YubiKey::instance(), SIGNAL(detected(int,bool)), + SLOT(ykDetected(int,bool)), + Qt::QueuedConnection); + QtConcurrent::run(YubiKey::instance(), &YubiKey::detect); + m_ui->enterPasswordEdit->setFocus(); } @@ -128,6 +141,14 @@ void ChangeMasterKeyWidget::generateKey() m_key.addKey(fileKey); } + if (m_ui->challengeResponseGroup->isChecked()) { + int i = m_ui->challengeResponseCombo->currentIndex(); + i = m_ui->challengeResponseCombo->itemData(i).toInt(); + YkChallengeResponseKey key(i); + + m_key.addChallengeResponseKey(key); + } + Q_EMIT editFinished(true); } @@ -136,3 +157,11 @@ void ChangeMasterKeyWidget::reject() { Q_EMIT editFinished(false); } + + +void ChangeMasterKeyWidget::ykDetected(int slot, bool blocking) +{ + YkChallengeResponseKey yk(slot, blocking); + m_ui->challengeResponseCombo->addItem(yk.getName(), QVariant(slot)); + m_ui->challengeResponseGroup->setEnabled(true); +} diff --git a/src/gui/ChangeMasterKeyWidget.h b/src/gui/ChangeMasterKeyWidget.h index 8985ff7a8..9b765c205 100644 --- a/src/gui/ChangeMasterKeyWidget.h +++ b/src/gui/ChangeMasterKeyWidget.h @@ -47,6 +47,7 @@ private Q_SLOTS: void reject(); void createKeyFile(); void browseKeyFile(); + void ykDetected(int slot, bool blocking); private: const QScopedPointer<Ui::ChangeMasterKeyWidget> m_ui; diff --git a/src/gui/ChangeMasterKeyWidget.ui b/src/gui/ChangeMasterKeyWidget.ui index d14941ccc..335a67c92 100644 --- a/src/gui/ChangeMasterKeyWidget.ui +++ b/src/gui/ChangeMasterKeyWidget.ui @@ -124,6 +124,31 @@ </widget> </item> <item> + <widget class="QGroupBox" name="challengeResponseGroup"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string>Challenge Response</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <widget class="QComboBox" name="challengeResponseCombo"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="challengeResponseLabel"> + <property name="text"> + <string>Challenge Response:</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 781b836fd..0b63bc0fc 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -15,6 +15,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QtConcurrentRun> + #include "DatabaseOpenWidget.h" #include "ui_DatabaseOpenWidget.h" @@ -27,6 +29,9 @@ #include "format/KeePass2Reader.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" +#include "keys/YkChallengeResponseKey.h" +#include "crypto/Random.h" + DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) : DialogyWidget(parent) @@ -49,9 +54,20 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) connect(m_ui->editPassword, SIGNAL(textChanged(QString)), SLOT(activatePassword())); connect(m_ui->comboKeyFile, SIGNAL(editTextChanged(QString)), SLOT(activateKeyFile())); + connect(m_ui->comboChallengeResponse, SIGNAL(activated(int)), SLOT(activateChallengeResponse())); + + connect(m_ui->checkPassword, SIGNAL(toggled(bool)), SLOT(setOkButtonEnabled())); + connect(m_ui->checkKeyFile, SIGNAL(toggled(bool)), SLOT(setOkButtonEnabled())); + connect(m_ui->comboKeyFile, SIGNAL(editTextChanged(QString)), SLOT(setOkButtonEnabled())); + connect(m_ui->checkChallengeResponse, SIGNAL(toggled(bool)), SLOT(setOkButtonEnabled())); + connect(m_ui->comboChallengeResponse, SIGNAL(activated(int)), SLOT(setOkButtonEnabled())); connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); + + connect(YubiKey::instance(), SIGNAL(detected(int,bool)), + SLOT(ykDetected(int,bool)), + Qt::QueuedConnection); } DatabaseOpenWidget::~DatabaseOpenWidget() @@ -79,6 +95,11 @@ void DatabaseOpenWidget::load(const QString& filename) } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + + /* YubiKey init is slow, detect asynchronously to not block the UI */ + m_ui->comboChallengeResponse->clear(); + QtConcurrent::run(YubiKey::instance(), &YubiKey::detect); + m_ui->editPassword->setFocus(); } @@ -156,6 +177,15 @@ CompositeKey DatabaseOpenWidget::databaseKey() config()->set("LastKeyFiles", lastKeyFiles); } + + if (m_ui->checkChallengeResponse->isChecked()) { + int i = m_ui->comboChallengeResponse->currentIndex(); + i = m_ui->comboChallengeResponse->itemData(i).toInt(); + YkChallengeResponseKey key(i); + + masterKey.addChallengeResponseKey(key); + } + return masterKey; } @@ -174,6 +204,19 @@ void DatabaseOpenWidget::activateKeyFile() m_ui->checkKeyFile->setChecked(true); } +void DatabaseOpenWidget::activateChallengeResponse() +{ + m_ui->checkChallengeResponse->setChecked(true); +} + +void DatabaseOpenWidget::setOkButtonEnabled() +{ + bool enable = m_ui->checkPassword->isChecked() || m_ui->checkChallengeResponse->isChecked() + || (m_ui->checkKeyFile->isChecked() && !m_ui->comboKeyFile->currentText().isEmpty()); + + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable); +} + void DatabaseOpenWidget::browseKeyFile() { QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files")); @@ -183,3 +226,11 @@ void DatabaseOpenWidget::browseKeyFile() m_ui->comboKeyFile->lineEdit()->setText(filename); } } + +void DatabaseOpenWidget::ykDetected(int slot, bool blocking) +{ + YkChallengeResponseKey yk(slot, blocking); + m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant(slot)); + m_ui->comboChallengeResponse->setEnabled(true); + m_ui->checkChallengeResponse->setEnabled(true); +} diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 34f401a09..fadb5ee70 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -55,7 +55,10 @@ protected Q_SLOTS: private Q_SLOTS: void activatePassword(); void activateKeyFile(); + void activateChallengeResponse(); + void setOkButtonEnabled(); void browseKeyFile(); + void ykDetected(int slot, bool blocking); protected: const QScopedPointer<Ui::DatabaseOpenWidget> m_ui; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 4aae5faf2..3d651ee8d 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -111,6 +111,23 @@ </item> </layout> </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="checkChallengeResponse"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Challenge Response:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="comboChallengeResponse"> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> </layout> </item> <item> diff --git a/src/gui/UnlockDatabaseWidget.cpp b/src/gui/UnlockDatabaseWidget.cpp index a005d0e60..d6beb1339 100644 --- a/src/gui/UnlockDatabaseWidget.cpp +++ b/src/gui/UnlockDatabaseWidget.cpp @@ -33,6 +33,7 @@ void UnlockDatabaseWidget::clearForms() m_ui->comboKeyFile->clear(); m_ui->checkPassword->setChecked(false); m_ui->checkKeyFile->setChecked(false); + m_ui->checkChallengeResponse->setChecked(false); m_ui->buttonTogglePassword->setChecked(false); m_db = nullptr; } diff --git a/src/keys/ChallengeResponseKey.h b/src/keys/ChallengeResponseKey.h new file mode 100644 index 000000000..e03a2f9f9 --- /dev/null +++ b/src/keys/ChallengeResponseKey.h @@ -0,0 +1,32 @@ +/* +* 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 ChallengeResponseKey* clone() const = 0; + virtual bool challenge(const QByteArray& challenge) = 0; +}; + +#endif // KEEPASSX_CHALLENGE_RESPONSE_KEY_H diff --git a/src/keys/CompositeKey.cpp b/src/keys/CompositeKey.cpp index 88116c104..4e79cd05c 100644 --- a/src/keys/CompositeKey.cpp +++ b/src/keys/CompositeKey.cpp @@ -17,6 +17,7 @@ #include "CompositeKey.h" #include "CompositeKey_p.h" +#include "ChallengeResponseKey.h" #include <QElapsedTimer> #include <QFile> @@ -45,12 +46,14 @@ CompositeKey::~CompositeKey() void CompositeKey::clear() { qDeleteAll(m_keys); + qDeleteAll(m_challengeResponseKeys); 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 +73,9 @@ CompositeKey& CompositeKey::operator=(const CompositeKey& key) for (const Key* subKey : asConst(key.m_keys)) { addKey(*subKey); } + for (const ChallengeResponseKey* subKey : asConst(key.m_challengeResponseKeys)) { + addChallengeResponseKey(*subKey); + } return *this; } @@ -168,11 +174,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 compatability with regular databases. + */ + if (m_challengeResponseKeys.length() == 0) { + result.clear(); + return true; + } + + CryptoHash cryptoHash(CryptoHash::Sha256); + + for (ChallengeResponseKey* key : m_challengeResponseKeys) { + /* If the device isn't present or fails, return an error */ + if (key->challenge(seed) == false) { + 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(const ChallengeResponseKey& key) +{ + m_challengeResponseKeys.append(key.clone()); +} + int CompositeKey::transformKeyBenchmark(int msec) { TransformKeyBenchmarkThread thread1(msec); diff --git a/src/keys/CompositeKey.h b/src/keys/CompositeKey.h index f8666aadc..531c2d9b2 100644 --- a/src/keys/CompositeKey.h +++ b/src/keys/CompositeKey.h @@ -22,6 +22,7 @@ #include <QString> #include "keys/Key.h" +#include "keys/ChallengeResponseKey.h" class CompositeKey : public Key { @@ -37,7 +38,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(const ChallengeResponseKey& key); static int transformKeyBenchmark(int msec); static CompositeKey readFromLine(QString line); @@ -47,6 +51,7 @@ private: quint64 rounds, bool* ok, QString* errorString); QList<Key*> m_keys; + QList<ChallengeResponseKey*> m_challengeResponseKeys; }; #endif // KEEPASSX_COMPOSITEKEY_H diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp new file mode 100644 index 000000000..17982ab62 --- /dev/null +++ b/src/keys/YkChallengeResponseKey.cpp @@ -0,0 +1,94 @@ +/* +* 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 <QFile> +#include <QXmlStreamReader> + +#include "core/Tools.h" +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" + +#include "keys/YkChallengeResponseKey.h" +#include "keys/drivers/YubiKey.h" + +#include <QDebug> + +YkChallengeResponseKey::YkChallengeResponseKey(int slot, + bool blocking) + : m_slot(slot), + m_blocking(blocking) +{ +} + +QByteArray YkChallengeResponseKey::rawKey() const +{ + return m_key; +} + +YkChallengeResponseKey* YkChallengeResponseKey::clone() const +{ + return new YkChallengeResponseKey(*this); +} + + +/** Assumes yubikey()->init() was called */ +bool YkChallengeResponseKey::challenge(const QByteArray& chal) +{ + return challenge(chal, 1); +} + +bool YkChallengeResponseKey::challenge(const QByteArray& chal, int retries) +{ + if (YubiKey::instance()->challenge(m_slot, true, chal, m_key) != YubiKey::ERROR) { + return true; + } + + /* If challenge failed, retry to detect YubiKeys int the event the YubiKey + * was un-plugged and re-plugged */ + while (retries > 0) { + qDebug() << "Attempt" << retries << "to re-detect YubiKey(s)"; + retries--; + + if (YubiKey::instance()->init() != true) { + continue; + } + + if (YubiKey::instance()->challenge(m_slot, true, chal, m_key) != YubiKey::ERROR) { + return true; + } + } + + return false; +} + +QString YkChallengeResponseKey::getName() const +{ + unsigned int serial; + QString fmt("YubiKey[%1] Challenge Response - Slot %2 - %3"); + + YubiKey::instance()->getSerial(serial); + + return fmt.arg(QString::number(serial), + QString::number(m_slot), + (m_blocking) ? "Press" : "Passive"); +} + +bool YkChallengeResponseKey::isBlocking() const +{ + return m_blocking; +} diff --git a/src/keys/YkChallengeResponseKey.h b/src/keys/YkChallengeResponseKey.h new file mode 100644 index 000000000..8acb0f9e9 --- /dev/null +++ b/src/keys/YkChallengeResponseKey.h @@ -0,0 +1,45 @@ +/* +* 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" + +class YkChallengeResponseKey : public ChallengeResponseKey +{ +public: + + YkChallengeResponseKey(int slot = -1, + bool blocking = false); + + QByteArray rawKey() const; + YkChallengeResponseKey* clone() const; + bool challenge(const QByteArray& chal); + bool challenge(const QByteArray& chal, int retries); + QString getName() const; + bool isBlocking() const; + +private: + QByteArray m_key; + int m_slot; + bool m_blocking; +}; + +#endif // KEEPASSX_YK_CHALLENGERESPONSEKEY_H diff --git a/src/keys/drivers/YubiKey.cpp b/src/keys/drivers/YubiKey.cpp new file mode 100644 index 000000000..5ca175836 --- /dev/null +++ b/src/keys/drivers/YubiKey.cpp @@ -0,0 +1,250 @@ +/* +* 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) +{ +} + +YubiKey* YubiKey::m_instance(Q_NULLPTR); + +/** + * @brief YubiKey::instance - get instance of singleton + * @return + */ +YubiKey* YubiKey::instance() +{ + if (!m_instance) { + m_instance = new YubiKey(); + } + + return m_instance; +} + +/** + * @brief YubiKey::init - initialize yubikey library and hardware + * @return + */ +bool YubiKey::init() +{ + /* Previously initalized */ + if (m_yk != NULL && m_ykds != NULL) { + + if (yk_get_status(m_yk, m_ykds)) { + /* Still connected */ + return true; + } else { + /* Initialized but not connected anymore, re-init */ + deinit(); + } + } + + if (!yk_init()) { + return false; + } + + /* TODO: handle multiple attached hardware devices, currently own one */ + m_yk_void = static_cast<void*>(yk_open_first_key()); + if (m_yk == NULL) { + return false; + } + + m_ykds_void = static_cast<void*>(ykds_alloc()); + if (m_ykds == NULL) { + yk_close_key(m_yk); + m_yk_void = NULL; + return false; + } + + return true; +} + +/** + * @brief YubiKey::deinit - cleanup after init + * @return true on success + */ +bool YubiKey::deinit() +{ + if (m_yk) { + yk_close_key(m_yk); + m_yk_void = NULL; + } + + if (m_ykds) { + ykds_free(m_ykds); + m_ykds_void = NULL; + } + + return true; +} + +/** + * @brief YubiKey::detect - probe for attached YubiKeys + */ +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::ERROR) { + Q_EMIT detected(i, result == YubiKey::WOULDBLOCK ? true : false); + } + } + } +} + +/** + * @brief YubiKey::getSerial - serial number of yubikey + * @param serial + * @return + */ +bool YubiKey::getSerial(unsigned int& serial) const +{ + if (!yk_get_serial(m_yk, 1, 0, &serial)) { + return false; + } + + return true; +} + +#ifdef QT_DEBUG +/** + * @brief printByteArray - debug raw data + * @param a array input + * @return string representation of array + */ +static inline QString printByteArray(const QByteArray& a) +{ + QString s; + for (int i = 0; i < a.size(); i++) + s.append(QString::number(a[i] & 0xff, 16).rightJustified(2, '0')); + return s; +} +#endif + +/** + * @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 chal challenge input to YubiKey + * @param resp response output from YubiKey + * @return SUCCESS when successful + */ +YubiKey::ChallengeResult YubiKey::challenge(int slot, bool mayBlock, + const QByteArray& chal, + QByteArray& resp) const +{ + int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2; + QByteArray paddedChal = chal; + + /* Ensure that YubiKey::init() succeeded */ + if (m_yk == NULL) { + return ERROR; + } + + /* yk_challenge_response() insists on 64 byte response buffer */ + resp.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 - paddedChal.size(); + if (padLen > 0) { + paddedChal.append(QByteArray(padLen, padLen)); + } + + const unsigned char *c; + unsigned char *r; + c = reinterpret_cast<const unsigned char*>(paddedChal.constData()); + r = reinterpret_cast<unsigned char*>(resp.data()); + +#ifdef QT_DEBUG + qDebug().nospace() << __func__ << "(" << slot << ") c = " + << printByteArray(paddedChal); +#endif + + int ret = yk_challenge_response(m_yk, yk_cmd, mayBlock, + paddedChal.size(), c, + resp.size(), r); + + 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 */ + resp.resize(20); + +#ifdef QT_DEBUG + qDebug().nospace() << __func__ << "(" << slot << ") r = " + << printByteArray(resp) << ", ret = " << ret; +#endif + + return SUCCESS; +} diff --git a/src/keys/drivers/YubiKey.h b/src/keys/drivers/YubiKey.h new file mode 100644 index 000000000..492fba01d --- /dev/null +++ b/src/keys/drivers/YubiKey.h @@ -0,0 +1,70 @@ +/* +* 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> + +/** + * Singleton class to manage the interface to the hardware + */ +class YubiKey : public QObject +{ + Q_OBJECT + +public: + enum ChallengeResult { ERROR = -1, SUCCESS = 0, WOULDBLOCK }; + + static YubiKey* instance(); + + /** Initialize the underlying yubico libraries */ + bool init(); + bool deinit(); + + /** Issue a challenge to the hardware */ + ChallengeResult challenge(int slot, bool mayBlock, + const QByteArray& chal, + QByteArray& resp) const; + + /** Read the serial number from the hardware */ + bool getSerial(unsigned int& serial) const; + + /** Start looking for attached hardware devices */ + 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); + +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; + + Q_DISABLE_COPY(YubiKey) +}; + +#endif // KEEPASSX_YUBIKEY_H diff --git a/src/keys/drivers/YubiKeyStub.cpp b/src/keys/drivers/YubiKeyStub.cpp new file mode 100644 index 000000000..c00790f38 --- /dev/null +++ b/src/keys/drivers/YubiKeyStub.cpp @@ -0,0 +1,71 @@ +/* +* 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) const +{ + Q_UNUSED(serial); + + return false; +} + +YubiKey::ChallengeResult YubiKey::challenge(int slot, bool mayBlock, + const QByteArray& chal, + QByteArray& resp) const +{ + Q_UNUSED(slot); + Q_UNUSED(mayBlock); + Q_UNUSED(chal); + Q_UNUSED(resp); + + return ERROR; +} |