mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Implement support for Yubikeys and potential other tokens via wireless NFC using smartcard readers (Rebase) (#6895)
* Support NFC readers for hardware tokens using PC/SC This requires a new library dependency: PCSC. The PCSC library provides methods to access smartcards. On Linux, the third-party pcsc-lite package is used. On Windows, the native Windows API (Winscard.dll) is used. On Mac OSX, the native OSX API (framework-PCSC) is used. * Split hardware key access into multiple classes to handle different methods of communicating with the keys. * Since the Yubikey can now be a wireless token as well, the verb "plug in" was replaced with a more generic "interface with". This shall indicate that the user has to present their token to the reader, or plug it in via USB. * Add PC/SC interface for YubiKey challenge-response This new interface uses the PC/SC protocol and API instead of the USB protocol via ykpers. Many YubiKeys expose their functionality as a CCID device, which can be interfaced with using PC/SC. This is especially useful for NFC-only or NFC-capable Yubikeys, when they are used together with a PC/SC compliant NFC reader device. Although many (not all) Yubikeys expose their CCID functionality over their own USB connection as well, the HMAC-SHA1 functionality is often locked in this mode, as it requires eg. a touch on the gold button. When accessing the CCID functionality wirelessly via NFC (like this code can do using a reader), then the user interaction is to present the key to the reader. This implementation has been tested on Linux using pcsc-lite, Windows using the native Winscard.dll library, and Mac OSX using the native PCSC-framework library. * Remove PC/SC ATR whitelist, instead scan for AIDs Before, a whitelist of ATR codes (answer to reset, hardware-specific) was used to scan for compatible (Yubi)Keys. Now, every connected smartcard is scanned for AIDs (applet identifier), which are known to implement the HMAC-SHA1 protocol. This enables the support of currently unknown or unreleased hardware. Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
parent
cc39f9ec23
commit
6d1fc31e96
@ -476,6 +476,11 @@ if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0")
|
||||
endif()
|
||||
include_directories(SYSTEM ${ZLIB_INCLUDE_DIR})
|
||||
|
||||
if(WITH_XC_YUBIKEY)
|
||||
find_package(PCSC REQUIRED)
|
||||
include_directories(SYSTEM ${PCSC_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
if(UNIX)
|
||||
check_cxx_source_compiles("#include <sys/prctl.h>
|
||||
int main() { prctl(PR_SET_DUMPABLE, 0); return 0; }"
|
||||
|
@ -25,6 +25,7 @@ The following libraries are required:
|
||||
* readline (for completion in cli)
|
||||
* libqt5x11extras5, libxi, and libxtst (for auto-type on X11)
|
||||
* qrencode
|
||||
* libusb-1.0, pcsclite (optional to support YubiKey on Linux)
|
||||
|
||||
Prepare the Building Environment
|
||||
================================
|
||||
|
39
cmake/FindPCSC.cmake
Normal file
39
cmake/FindPCSC.cmake
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# Use pkgconfig on Linux
|
||||
if(NOT WIN32)
|
||||
find_package(PkgConfig QUIET)
|
||||
pkg_check_modules(PCSC libpcsclite)
|
||||
endif()
|
||||
|
||||
if(NOT PCSC_FOUND)
|
||||
# Search for PC/SC headers on Mac and Windows
|
||||
find_path(PCSC_INCLUDE_DIRS winscard.h
|
||||
HINTS
|
||||
${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}
|
||||
/usr/include/PCSC
|
||||
PATH_SUFFIXES PCSC)
|
||||
|
||||
# MAC library is PCSC, Windows library is WinSCard
|
||||
find_library(PCSC_LIBRARIES NAMES pcsclite libpcsclite WinSCard PCSC
|
||||
HINTS
|
||||
${CMAKE_C_IMPLICIT_LINK_DIRECTORIES})
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(PCSC DEFAULT_MSG PCSC_LIBRARIES PCSC_INCLUDE_DIRS)
|
||||
|
||||
mark_as_advanced(PCSC_LIBRARIES PCSC_INCLUDE_DIRS)
|
@ -1442,10 +1442,6 @@ If you do not have a key file, please leave the field empty.</source>
|
||||
<source>Key file to unlock the database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please touch the button on your YubiKey!</source>
|
||||
<translation type="unfinished">Please touch the button on your YubiKey!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Detecting hardware keys…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -1479,6 +1475,10 @@ If you do not have a key file, please leave the field empty.</source>
|
||||
<source>You are using an old key file format which KeePassXC may<br>stop supporting in the future.<br><br>Please consider generating a new key file by going to:<br><strong>Database &gt; Database Security &gt; Change Key File.</strong><br></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please present or touch your YubiKey to continue…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DatabaseSettingWidgetMetaData</name>
|
||||
@ -4755,10 +4755,6 @@ Are you sure you want to continue with this file?</source>
|
||||
<source>Quit KeePassXC</source>
|
||||
<translation>Quit KeePassXC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please touch the button on your YubiKey!</source>
|
||||
<translation>Please touch the button on your YubiKey!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>&Donate</source>
|
||||
<translation>&Donate</translation>
|
||||
@ -5119,6 +5115,10 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
|
||||
We recommend you use the AppImage available on our downloads page.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please present or touch your YubiKey to continue…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ManageDatabase</name>
|
||||
@ -6889,10 +6889,6 @@ Kernel: %3 %4</source>
|
||||
<source>Invalid YubiKey serial %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please touch the button on your YubiKey to continue…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to create a database with an empty password? [y/N]: </source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -7233,6 +7229,10 @@ Please consider generating a new key file.</source>
|
||||
<source>Warning: Failed to prevent screenshots on a top level window!</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please present or touch your YubiKey to continue…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
@ -8268,49 +8268,15 @@ Example: JBSWY3DPEHPK3PXP</source>
|
||||
<context>
|
||||
<name>YubiKey</name>
|
||||
<message>
|
||||
<source>%1 [%2] Configured Slot - %3</source>
|
||||
<source>%1 No interface, slot %2</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>%1 Invalid slot specified - %2</source>
|
||||
<source>General: </source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The YubiKey interface has not been initialized.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key is currently in use.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not find hardware key with serial number %1. Please plug it in to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key timed out waiting for user interaction.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to complete a challenge-response, the specific error was: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>%1 [%2] Challenge-Response - Slot %3 - %4</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press</source>
|
||||
<comment>Challenge-Response Key interaction request</comment>
|
||||
<translation type="unfinished">Press</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passive</source>
|
||||
<comment>Challenge-Response Key no interaction required</comment>
|
||||
<translation type="unfinished">Passive</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>A USB error occurred when accessing the hardware key: %1</source>
|
||||
<source>Could not find interface for hardware key with serial number %1. Please connect it to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
@ -8369,4 +8335,91 @@ Example: JBSWY3DPEHPK3PXP</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>YubiKeyInterface</name>
|
||||
<message>
|
||||
<source>%1 Invalid slot specified - %2</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>YubiKeyInterfacePCSC</name>
|
||||
<message>
|
||||
<source>(PCSC) %1 [%2] Challenge-Response - Slot %3</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The YubiKey PCSC interface has not been initialized.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key is currently in use.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key is locked or timed out. Unlock or re-present it to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key was not found or is misconfigured.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to complete a challenge-response, the PCSC error code was: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>YubiKeyInterfaceUSB</name>
|
||||
<message>
|
||||
<source>Unknown</source>
|
||||
<translation type="unfinished">Unknown</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>(USB) %1 [%2] Configured Slot - %3</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>(USB) %1 [%2] Challenge-Response - Slot %3 - %4</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press</source>
|
||||
<comment>USB Challenge-Response Key interaction request</comment>
|
||||
<translation type="unfinished">Press</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passive</source>
|
||||
<comment>USB Challenge-Response Key no interaction required</comment>
|
||||
<translation type="unfinished">Passive</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>The YubiKey USB interface has not been initialized.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key is currently in use.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Could not find hardware key with serial number %1. Please plug it in to continue.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hardware key timed out waiting for user interaction.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>A USB error occurred when accessing the hardware key: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to complete a challenge-response, the specific error was: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
@ -280,9 +280,16 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
if(WITH_XC_YUBIKEY)
|
||||
list(APPEND keepassx_SOURCES keys/drivers/YubiKey.cpp)
|
||||
list(APPEND keepassx_SOURCES
|
||||
keys/drivers/YubiKey.h
|
||||
keys/drivers/YubiKey.cpp
|
||||
keys/drivers/YubiKeyInterface.cpp
|
||||
keys/drivers/YubiKeyInterfaceUSB.cpp
|
||||
keys/drivers/YubiKeyInterfacePCSC.cpp)
|
||||
else()
|
||||
list(APPEND keepassx_SOURCES keys/drivers/YubiKey.h keys/drivers/YubiKeyStub.cpp)
|
||||
list(APPEND keepassx_SOURCES
|
||||
keys/drivers/YubiKey.h
|
||||
keys/drivers/YubiKeyStub.cpp)
|
||||
endif()
|
||||
|
||||
if(WITH_XC_NETWORKING)
|
||||
@ -320,6 +327,7 @@ target_link_libraries(keepassx_core
|
||||
Qt5::Network
|
||||
Qt5::Widgets
|
||||
${BOTAN2_LIBRARIES}
|
||||
${PCSC_LIBRARIES}
|
||||
${ZXCVBN_LIBRARIES}
|
||||
${ZLIB_LIBRARIES}
|
||||
${thirdparty_LIBRARIES}
|
||||
|
@ -168,7 +168,7 @@ namespace Utils
|
||||
}
|
||||
|
||||
auto conn = QObject::connect(YubiKey::instance(), &YubiKey::userInteractionRequest, [&] {
|
||||
err << QObject::tr("Please touch the button on your YubiKey to continue…") << "\n\n" << flush;
|
||||
err << QObject::tr("Please present or touch your YubiKey to continue…") << "\n\n" << flush;
|
||||
});
|
||||
|
||||
auto key = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey({serial, slot}));
|
||||
|
@ -84,7 +84,7 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
|
||||
// Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog)
|
||||
if (window() != getMainWindow()) {
|
||||
m_ui->messageWidget->showMessage(tr("Please touch the button on your YubiKey!"),
|
||||
m_ui->messageWidget->showMessage(tr("Please present or touch your YubiKey to continue…"),
|
||||
MessageWidget::Information,
|
||||
MessageWidget::DisableAutoHide);
|
||||
}
|
||||
|
@ -1708,7 +1708,7 @@ void MainWindow::hideGlobalMessage()
|
||||
|
||||
void MainWindow::showYubiKeyPopup()
|
||||
{
|
||||
displayGlobalMessage(tr("Please touch the button on your YubiKey!"),
|
||||
displayGlobalMessage(tr("Please present or touch your YubiKey to continue…"),
|
||||
MessageWidget::Information,
|
||||
false,
|
||||
MessageWidget::DisableAutoHide);
|
||||
|
@ -44,11 +44,11 @@ bool ChallengeResponseKey::challenge(const QByteArray& challenge)
|
||||
auto result =
|
||||
AsyncTask::runAndWaitForFuture([&] { return YubiKey::instance()->challenge(m_keySlot, challenge, m_key); });
|
||||
|
||||
if (result != YubiKey::SUCCESS) {
|
||||
if (result != YubiKey::ChallengeResult::YCR_SUCCESS) {
|
||||
// Record the error message
|
||||
m_key.clear();
|
||||
m_error = YubiKey::instance()->errorMessage();
|
||||
}
|
||||
|
||||
return result == YubiKey::SUCCESS;
|
||||
return result == YubiKey::ChallengeResult::YCR_SUCCESS;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -17,97 +17,58 @@
|
||||
*/
|
||||
|
||||
#include "YubiKey.h"
|
||||
|
||||
#include "core/Tools.h"
|
||||
#include "crypto/Random.h"
|
||||
|
||||
#include "thirdparty/ykcore/ykcore.h"
|
||||
#include "thirdparty/ykcore/ykdef.h"
|
||||
#include "thirdparty/ykcore/ykstatus.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int MAX_KEYS = 4;
|
||||
|
||||
YK_KEY* openKey(int index)
|
||||
{
|
||||
static const int vids[] = {YUBICO_VID, ONLYKEY_VID};
|
||||
static const int pids[] = {YUBIKEY_PID,
|
||||
NEO_OTP_PID,
|
||||
NEO_OTP_CCID_PID,
|
||||
NEO_OTP_U2F_PID,
|
||||
NEO_OTP_U2F_CCID_PID,
|
||||
YK4_OTP_PID,
|
||||
YK4_OTP_U2F_PID,
|
||||
YK4_OTP_CCID_PID,
|
||||
YK4_OTP_U2F_CCID_PID,
|
||||
PLUS_U2F_OTP_PID,
|
||||
ONLYKEY_PID};
|
||||
|
||||
return yk_open_key_vid_pid(vids, sizeof(vids) / sizeof(vids[0]), pids, sizeof(pids) / sizeof(pids[0]), index);
|
||||
}
|
||||
|
||||
void closeKey(YK_KEY* key)
|
||||
{
|
||||
yk_close_key(key);
|
||||
}
|
||||
|
||||
unsigned int getSerial(YK_KEY* key)
|
||||
{
|
||||
unsigned int serial;
|
||||
yk_get_serial(key, 1, 0, &serial);
|
||||
return serial;
|
||||
}
|
||||
|
||||
YK_KEY* openKeySerial(unsigned int serial)
|
||||
{
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
auto* yk_key = openKey(i);
|
||||
if (yk_key) {
|
||||
// If the provided serial number is 0, or the key matches the serial, return it
|
||||
if (serial == 0 || getSerial(yk_key) == serial) {
|
||||
return yk_key;
|
||||
}
|
||||
closeKey(yk_key);
|
||||
} else if (yk_errno == YK_ENOKEY) {
|
||||
// No more connected keys
|
||||
break;
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
qWarning("Hardware key USB error: %s", yk_usb_strerror());
|
||||
} else {
|
||||
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
#include "YubiKeyInterfacePCSC.h"
|
||||
#include "YubiKeyInterfaceUSB.h"
|
||||
|
||||
YubiKey::YubiKey()
|
||||
: m_mutex(QMutex::Recursive)
|
||||
: m_interfaces_detect_mutex(QMutex::Recursive)
|
||||
{
|
||||
m_interactionTimer.setSingleShot(true);
|
||||
m_interactionTimer.setInterval(300);
|
||||
int num_interfaces = 0;
|
||||
|
||||
if (!yk_init()) {
|
||||
qDebug("YubiKey: Failed to initialize USB interface.");
|
||||
if (YubiKeyInterfaceUSB::instance()->isInitialized()) {
|
||||
++num_interfaces;
|
||||
} else {
|
||||
qDebug("YubiKey: USB interface is not initialized.");
|
||||
}
|
||||
connect(YubiKeyInterfaceUSB::instance(), SIGNAL(challengeStarted()), this, SIGNAL(challengeStarted()));
|
||||
connect(YubiKeyInterfaceUSB::instance(), SIGNAL(challengeCompleted()), this, SIGNAL(challengeCompleted()));
|
||||
|
||||
if (YubiKeyInterfacePCSC::instance()->isInitialized()) {
|
||||
++num_interfaces;
|
||||
} else {
|
||||
qDebug("YubiKey: PCSC interface is disabled or not initialized.");
|
||||
}
|
||||
connect(YubiKeyInterfacePCSC::instance(), SIGNAL(challengeStarted()), this, SIGNAL(challengeStarted()));
|
||||
connect(YubiKeyInterfacePCSC::instance(), SIGNAL(challengeCompleted()), this, SIGNAL(challengeCompleted()));
|
||||
|
||||
// Collapse the detectComplete signals from all interfaces into one signal
|
||||
// If multiple interfaces are used, wait for them all to finish
|
||||
auto detect_handler = [this, num_interfaces](bool found) {
|
||||
if (!m_interfaces_detect_mutex.tryLock(1000)) {
|
||||
return;
|
||||
}
|
||||
m_interfaces_detect_found |= found;
|
||||
m_interfaces_detect_completed++;
|
||||
if (m_interfaces_detect_completed != -1 && m_interfaces_detect_completed == num_interfaces) {
|
||||
m_interfaces_detect_completed = -1;
|
||||
emit detectComplete(m_interfaces_detect_found);
|
||||
}
|
||||
m_interfaces_detect_mutex.unlock();
|
||||
};
|
||||
connect(YubiKeyInterfaceUSB::instance(), &YubiKeyInterfaceUSB::detectComplete, this, detect_handler);
|
||||
connect(YubiKeyInterfacePCSC::instance(), &YubiKeyInterfacePCSC::detectComplete, this, detect_handler);
|
||||
|
||||
if (num_interfaces != 0) {
|
||||
m_initialized = true;
|
||||
// clang-format off
|
||||
connect(&m_interactionTimer, SIGNAL(timeout()), this, SIGNAL(userInteractionRequest()));
|
||||
connect(this, &YubiKey::challengeStarted, this, [this] { m_interactionTimer.start(); }, Qt::QueuedConnection);
|
||||
connect(this, &YubiKey::challengeCompleted, this, [this] { m_interactionTimer.stop(); }, Qt::QueuedConnection);
|
||||
connect(this, &YubiKey::challengeStarted, this, [this] { m_interactionTimer.start(); });
|
||||
connect(this, &YubiKey::challengeCompleted, this, [this] { m_interactionTimer.stop(); });
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
YubiKey::~YubiKey()
|
||||
{
|
||||
yk_release();
|
||||
}
|
||||
|
||||
YubiKey* YubiKey::m_instance(Q_NULLPTR);
|
||||
YubiKey* YubiKey::m_instance(nullptr);
|
||||
|
||||
YubiKey* YubiKey::instance()
|
||||
{
|
||||
@ -125,110 +86,90 @@ bool YubiKey::isInitialized()
|
||||
|
||||
void YubiKey::findValidKeys()
|
||||
{
|
||||
m_error.clear();
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QtConcurrent::run([this] {
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
emit detectComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all known keys
|
||||
m_foundKeys.clear();
|
||||
|
||||
// Try to detect up to 4 connected hardware keys
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
auto yk_key = openKey(i);
|
||||
if (yk_key) {
|
||||
auto serial = getSerial(yk_key);
|
||||
if (serial == 0) {
|
||||
closeKey(yk_key);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto st = ykds_alloc();
|
||||
yk_get_status(yk_key, st);
|
||||
int vid, pid;
|
||||
yk_get_key_vid_pid(yk_key, &vid, &pid);
|
||||
|
||||
auto vendor = vid == 0x1d50 ? QStringLiteral("OnlyKey") : QStringLiteral("YubiKey");
|
||||
|
||||
bool wouldBlock;
|
||||
QList<QPair<int, QString>> ykSlots;
|
||||
for (int slot = 1; slot <= 2; ++slot) {
|
||||
auto config = (slot == 1 ? CONFIG1_VALID : CONFIG2_VALID);
|
||||
if (!(ykds_touch_level(st) & config)) {
|
||||
// Slot is not configured
|
||||
continue;
|
||||
}
|
||||
// Don't actually challenge a YubiKey Neo or below, they always require button press
|
||||
// if it is enabled for the slot resulting in failed detection
|
||||
if (pid <= NEO_OTP_U2F_CCID_PID) {
|
||||
auto display = tr("%1 [%2] Configured Slot - %3")
|
||||
.arg(vendor, QString::number(serial), QString::number(slot));
|
||||
ykSlots.append({slot, display});
|
||||
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
|
||||
auto display =
|
||||
tr("%1 [%2] Challenge-Response - Slot %3 - %4")
|
||||
.arg(vendor,
|
||||
QString::number(serial),
|
||||
QString::number(slot),
|
||||
wouldBlock ? tr("Press", "Challenge-Response Key interaction request")
|
||||
: tr("Passive", "Challenge-Response Key no interaction required"));
|
||||
ykSlots.append({slot, display});
|
||||
}
|
||||
}
|
||||
|
||||
if (!ykSlots.isEmpty()) {
|
||||
m_foundKeys.insert(serial, ykSlots);
|
||||
}
|
||||
|
||||
ykds_free(st);
|
||||
closeKey(yk_key);
|
||||
|
||||
Tools::wait(100);
|
||||
} else if (yk_errno == YK_ENOKEY) {
|
||||
// No more keys are connected
|
||||
break;
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
qWarning("Hardware key USB error: %s", yk_usb_strerror());
|
||||
} else {
|
||||
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
|
||||
}
|
||||
}
|
||||
|
||||
m_mutex.unlock();
|
||||
emit detectComplete(!m_foundKeys.isEmpty());
|
||||
});
|
||||
m_interfaces_detect_completed = 0;
|
||||
m_interfaces_detect_found = false;
|
||||
YubiKeyInterfaceUSB::instance()->findValidKeys();
|
||||
YubiKeyInterfacePCSC::instance()->findValidKeys();
|
||||
}
|
||||
|
||||
QList<YubiKeySlot> YubiKey::foundKeys()
|
||||
{
|
||||
QList<YubiKeySlot> keys;
|
||||
for (auto serial : m_foundKeys.uniqueKeys()) {
|
||||
for (auto key : m_foundKeys.value(serial)) {
|
||||
keys.append({serial, key.first});
|
||||
QList<YubiKeySlot> foundKeys;
|
||||
|
||||
auto keys = YubiKeyInterfaceUSB::instance()->foundKeys();
|
||||
QList<unsigned int> handledSerials = keys.uniqueKeys();
|
||||
for (auto serial : handledSerials) {
|
||||
for (const auto& key : keys.values(serial)) {
|
||||
foundKeys.append({serial, key.first});
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
|
||||
keys = YubiKeyInterfacePCSC::instance()->foundKeys();
|
||||
for (auto serial : keys.uniqueKeys()) {
|
||||
// Ignore keys that were detected on USB interface already
|
||||
if (handledSerials.contains(serial)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& key : keys.values(serial)) {
|
||||
foundKeys.append({serial, key.first});
|
||||
}
|
||||
}
|
||||
|
||||
return foundKeys;
|
||||
}
|
||||
|
||||
QString YubiKey::getDisplayName(YubiKeySlot slot)
|
||||
{
|
||||
for (auto key : m_foundKeys.value(slot.first)) {
|
||||
if (slot.second == key.first) {
|
||||
return key.second;
|
||||
QString name;
|
||||
name.clear();
|
||||
|
||||
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
|
||||
name += YubiKeyInterfaceUSB::instance()->getDisplayName(slot);
|
||||
}
|
||||
|
||||
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
|
||||
// In some cases, the key might present on two interfaces
|
||||
// This should usually never happen, because the PCSC interface
|
||||
// filters the "virtual yubikey reader device".
|
||||
if (!name.isNull()) {
|
||||
name += " = ";
|
||||
}
|
||||
return tr("%1 Invalid slot specified - %2").arg(QString::number(slot.first), QString::number(slot.second));
|
||||
name += YubiKeyInterfacePCSC::instance()->getDisplayName(slot);
|
||||
}
|
||||
|
||||
if (!name.isNull()) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return tr("%1 No interface, slot %2").arg(QString::number(slot.first), QString::number(slot.second));
|
||||
}
|
||||
|
||||
QString YubiKey::errorMessage()
|
||||
{
|
||||
return m_error;
|
||||
QString error;
|
||||
error.clear();
|
||||
if (!m_error.isNull()) {
|
||||
error += tr("General: ") + m_error;
|
||||
}
|
||||
|
||||
QString usb_error = YubiKeyInterfaceUSB::instance()->errorMessage();
|
||||
if (!usb_error.isNull()) {
|
||||
if (!error.isNull()) {
|
||||
error += " | ";
|
||||
}
|
||||
error += "USB: " + usb_error;
|
||||
}
|
||||
|
||||
QString pcsc_error = YubiKeyInterfacePCSC::instance()->errorMessage();
|
||||
if (!pcsc_error.isNull()) {
|
||||
if (!error.isNull()) {
|
||||
error += " | ";
|
||||
}
|
||||
error += "PCSC: " + pcsc_error;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,25 +182,14 @@ QString YubiKey::errorMessage()
|
||||
*/
|
||||
bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
|
||||
{
|
||||
bool ret = false;
|
||||
auto* yk_key = openKeySerial(slot.first);
|
||||
if (yk_key) {
|
||||
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
|
||||
}
|
||||
return ret;
|
||||
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
|
||||
return YubiKeyInterfaceUSB::instance()->testChallenge(slot, wouldBlock);
|
||||
}
|
||||
|
||||
bool YubiKey::performTestChallenge(void* key, int slot, bool* wouldBlock)
|
||||
{
|
||||
auto chall = randomGen()->randomArray(1);
|
||||
Botan::secure_vector<char> resp;
|
||||
auto ret = performChallenge(static_cast<YK_KEY*>(key), slot, false, chall, resp);
|
||||
if (ret == SUCCESS || ret == WOULDBLOCK) {
|
||||
if (wouldBlock) {
|
||||
*wouldBlock = ret == WOULDBLOCK;
|
||||
}
|
||||
return true;
|
||||
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
|
||||
return YubiKeyInterfacePCSC::instance()->testChallenge(slot, wouldBlock);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -276,88 +206,17 @@ YubiKey::ChallengeResult
|
||||
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
if (!m_initialized) {
|
||||
m_error = tr("The YubiKey interface has not been initialized.");
|
||||
return ERROR;
|
||||
|
||||
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
|
||||
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
|
||||
}
|
||||
|
||||
// Try to grab a lock for 1 second, fail out if not possible
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
m_error = tr("Hardware key is currently in use.");
|
||||
return ERROR;
|
||||
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
|
||||
return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
|
||||
}
|
||||
|
||||
auto* yk_key = openKeySerial(slot.first);
|
||||
if (!yk_key) {
|
||||
// Key with specified serial number is not connected
|
||||
m_error =
|
||||
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
|
||||
m_mutex.unlock();
|
||||
return ERROR;
|
||||
}
|
||||
|
||||
emit challengeStarted();
|
||||
auto ret = performChallenge(yk_key, slot.second, true, challenge, response);
|
||||
|
||||
closeKey(yk_key);
|
||||
emit challengeCompleted();
|
||||
m_mutex.unlock();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKey::performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2;
|
||||
QByteArray paddedChallenge = challenge;
|
||||
|
||||
// yk_challenge_response() insists on 64 bytes response buffer */
|
||||
response.clear();
|
||||
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 bytes 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(
|
||||
static_cast<YK_KEY*>(key), yk_cmd, mayBlock, paddedChallenge.size(), c, response.size(), r);
|
||||
|
||||
// actual HMAC-SHA1 response is only 20 bytes
|
||||
response.resize(20);
|
||||
|
||||
if (!ret) {
|
||||
if (yk_errno == YK_EWOULDBLOCK) {
|
||||
return WOULDBLOCK;
|
||||
} else if (yk_errno) {
|
||||
if (yk_errno == YK_ETIMEOUT) {
|
||||
m_error = tr("Hardware key timed out waiting for user interaction.");
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
m_error = tr("A USB error occurred when accessing the hardware key: %1").arg(yk_usb_strerror());
|
||||
} else {
|
||||
m_error = tr("Failed to complete a challenge-response, the specific error was: %1")
|
||||
.arg(yk_strerror(yk_errno));
|
||||
}
|
||||
|
||||
return ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
|
||||
.arg(slot.first);
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -36,11 +36,11 @@ class YubiKey : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ChallengeResult
|
||||
enum class ChallengeResult : int
|
||||
{
|
||||
ERROR,
|
||||
SUCCESS,
|
||||
WOULDBLOCK
|
||||
YCR_ERROR = 0,
|
||||
YCR_SUCCESS = 1,
|
||||
YCR_WOULDBLOCK = 2
|
||||
};
|
||||
|
||||
static YubiKey* instance();
|
||||
@ -76,30 +76,17 @@ signals:
|
||||
void challengeStarted();
|
||||
void challengeCompleted();
|
||||
|
||||
/**
|
||||
* Emitted when an error occurred during challenge/response
|
||||
*/
|
||||
void challengeError(QString error);
|
||||
|
||||
private:
|
||||
explicit YubiKey();
|
||||
~YubiKey();
|
||||
|
||||
static YubiKey* m_instance;
|
||||
|
||||
ChallengeResult performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response);
|
||||
bool performTestChallenge(void* key, int slot, bool* wouldBlock);
|
||||
|
||||
QHash<unsigned int, QList<QPair<int, QString>>> m_foundKeys;
|
||||
|
||||
QMutex m_mutex;
|
||||
QTimer m_interactionTimer;
|
||||
bool m_initialized = false;
|
||||
QString m_error;
|
||||
int m_interfaces_detect_completed = -1;
|
||||
bool m_interfaces_detect_found = false;
|
||||
QMutex m_interfaces_detect_mutex;
|
||||
|
||||
Q_DISABLE_COPY(YubiKey)
|
||||
};
|
||||
|
61
src/keys/drivers/YubiKeyInterface.cpp
Normal file
61
src/keys/drivers/YubiKeyInterface.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 "YubiKeyInterface.h"
|
||||
|
||||
YubiKeyInterface::YubiKeyInterface()
|
||||
: m_mutex(QMutex::Recursive)
|
||||
{
|
||||
m_interactionTimer.setSingleShot(true);
|
||||
m_interactionTimer.setInterval(300);
|
||||
}
|
||||
|
||||
bool YubiKeyInterface::isInitialized() const
|
||||
{
|
||||
return m_initialized;
|
||||
}
|
||||
|
||||
QMultiMap<unsigned int, QPair<int, QString>> YubiKeyInterface::foundKeys()
|
||||
{
|
||||
return m_foundKeys;
|
||||
}
|
||||
|
||||
bool YubiKeyInterface::hasFoundKey(YubiKeySlot slot)
|
||||
{
|
||||
for (const auto& key : m_foundKeys.values(slot.first)) {
|
||||
if (slot.second == key.first) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString YubiKeyInterface::getDisplayName(YubiKeySlot slot)
|
||||
{
|
||||
for (const auto& key : m_foundKeys.values(slot.first)) {
|
||||
if (slot.second == key.first) {
|
||||
return key.second;
|
||||
}
|
||||
}
|
||||
return tr("%1 Invalid slot specified - %2").arg(QString::number(slot.first), QString::number(slot.second));
|
||||
}
|
||||
|
||||
QString YubiKeyInterface::errorMessage()
|
||||
{
|
||||
return m_error;
|
||||
}
|
81
src/keys/drivers/YubiKeyInterface.h
Normal file
81
src/keys/drivers/YubiKeyInterface.h
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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_INTERFACE_H
|
||||
#define KEEPASSX_YUBIKEY_INTERFACE_H
|
||||
|
||||
#include "YubiKey.h"
|
||||
|
||||
#include <QMultiMap>
|
||||
|
||||
/**
|
||||
* Abstract base class to manage the interfaces to hardware key(s)
|
||||
*/
|
||||
class YubiKeyInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
bool isInitialized() const;
|
||||
QMultiMap<unsigned int, QPair<int, QString>> foundKeys();
|
||||
bool hasFoundKey(YubiKeySlot slot);
|
||||
QString getDisplayName(YubiKeySlot slot);
|
||||
|
||||
virtual void findValidKeys() = 0;
|
||||
virtual YubiKey::ChallengeResult
|
||||
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) = 0;
|
||||
virtual bool testChallenge(YubiKeySlot slot, bool* wouldBlock) = 0;
|
||||
|
||||
QString errorMessage();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Emitted when a detection process completes. Use the `detectedSlots`
|
||||
* accessor function to get information on the available slots.
|
||||
*
|
||||
* @param found - true if a key was found
|
||||
*/
|
||||
void detectComplete(bool found);
|
||||
|
||||
/**
|
||||
* Emitted before/after a challenge-response is performed
|
||||
*/
|
||||
void challengeStarted();
|
||||
void challengeCompleted();
|
||||
|
||||
protected:
|
||||
explicit YubiKeyInterface();
|
||||
|
||||
virtual YubiKey::ChallengeResult performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response) = 0;
|
||||
virtual bool performTestChallenge(void* key, int slot, bool* wouldBlock) = 0;
|
||||
|
||||
QMultiMap<unsigned int, QPair<int, QString>> m_foundKeys;
|
||||
|
||||
QMutex m_mutex;
|
||||
QTimer m_interactionTimer;
|
||||
bool m_initialized = false;
|
||||
QString m_error;
|
||||
|
||||
Q_DISABLE_COPY(YubiKeyInterface)
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_YUBIKEY_INTERFACE_H
|
783
src/keys/drivers/YubiKeyInterfacePCSC.cpp
Normal file
783
src/keys/drivers/YubiKeyInterfacePCSC.cpp
Normal file
@ -0,0 +1,783 @@
|
||||
/*
|
||||
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 "YubiKeyInterfacePCSC.h"
|
||||
|
||||
#include "crypto/Random.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
|
||||
// MSYS2 does not define these macros
|
||||
// So set them to the value used by pcsc-lite
|
||||
#ifndef MAX_ATR_SIZE
|
||||
#define MAX_ATR_SIZE 33
|
||||
#endif
|
||||
#ifndef MAX_READERNAME
|
||||
#define MAX_READERNAME 128
|
||||
#endif
|
||||
|
||||
// PCSC framework on OSX uses unsigned int
|
||||
// Windows winscard and Linux pcsc-lite use unsigned long
|
||||
#ifdef Q_OS_MACOS
|
||||
typedef uint32_t SCUINT;
|
||||
#else
|
||||
typedef unsigned long SCUINT;
|
||||
#endif
|
||||
|
||||
// This namescape contains static wrappers for the smart card API
|
||||
// Which enable the communication with a Yubikey via PCSC ADPUs
|
||||
namespace
|
||||
{
|
||||
|
||||
/***
|
||||
* @brief Check if a smartcard API context is valid and reopen it if it is not
|
||||
*
|
||||
* @param context Smartcard API context, valid or not
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t ensureValidContext(SCARDCONTEXT& context)
|
||||
{
|
||||
// This check only tests if the handle pointer is valid in memory
|
||||
// but it does not actually verify that it works
|
||||
int32_t rv = SCardIsValidContext(context);
|
||||
|
||||
// If the handle is broken, create it
|
||||
// This happens e.g. on application launch
|
||||
if (rv != SCARD_S_SUCCESS) {
|
||||
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, nullptr, nullptr, &context);
|
||||
if (rv != SCARD_S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the handle actually works
|
||||
SCUINT dwReaders = 0;
|
||||
rv = SCardListReaders(context, nullptr, nullptr, &dwReaders);
|
||||
// On windows, USB hot-plugging causes the underlying API server to die
|
||||
// So on every USB unplug event, the API context has to be recreated
|
||||
if (rv == static_cast<int32_t>(SCARD_E_SERVICE_STOPPED)) {
|
||||
// Dont care if the release works since the handle might be broken
|
||||
SCardReleaseContext(context);
|
||||
rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, nullptr, nullptr, &context);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief return the names of all connected smartcard readers
|
||||
*
|
||||
* @param context A pre-established smartcard API context
|
||||
* @return New list of smartcard readers
|
||||
*/
|
||||
QList<QString> getReaders(SCARDCONTEXT& context)
|
||||
{
|
||||
// Ensure the Smartcard API handle is still valid
|
||||
ensureValidContext(context);
|
||||
|
||||
QList<QString> readers_list;
|
||||
SCUINT dwReaders = 0;
|
||||
|
||||
// Read size of required string buffer
|
||||
// OSX does not support auto-allocate
|
||||
int32_t rv = SCardListReaders(context, nullptr, nullptr, &dwReaders);
|
||||
if (rv != SCARD_S_SUCCESS) {
|
||||
return readers_list;
|
||||
}
|
||||
if (dwReaders == 0 || dwReaders > 16384) { // max 16kb
|
||||
return readers_list;
|
||||
}
|
||||
char* mszReaders = new char[dwReaders + 2];
|
||||
|
||||
rv = SCardListReaders(context, nullptr, mszReaders, &dwReaders);
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
char* readhead = mszReaders;
|
||||
// Names are seperated by a null byte
|
||||
// The list is terminated by two null bytes
|
||||
while (*readhead != '\0') {
|
||||
QString reader = QString::fromUtf8(readhead);
|
||||
readers_list.append(reader);
|
||||
readhead += reader.size() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
delete[] mszReaders;
|
||||
return readers_list;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Reads the status of a smartcard handle
|
||||
*
|
||||
* This function does not actually transmit data,
|
||||
* instead it only reads the OS API state
|
||||
*
|
||||
* @param handle Smartcard handle
|
||||
* @param dwProt Protocol currently used
|
||||
* @param pioSendPci Pointer to the PCI header used for sending
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t getCardStatus(SCARDHANDLE handle, SCUINT& dwProt, const SCARD_IO_REQUEST*& pioSendPci)
|
||||
{
|
||||
int32_t rv = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
uint8_t pbAtr[MAX_ATR_SIZE] = {0}; // ATR record
|
||||
char pbReader[MAX_READERNAME] = {0}; // Name of the reader the card is placed in
|
||||
SCUINT dwAtrLen = sizeof(pbAtr); // ATR record size
|
||||
SCUINT dwReaderLen = sizeof(pbReader); // String length of the reader name
|
||||
SCUINT dwState = 0; // Unused. Contents differ depending on API implementation.
|
||||
|
||||
if ((rv = SCardStatus(handle, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen))
|
||||
== SCARD_S_SUCCESS) {
|
||||
switch (dwProt) {
|
||||
case SCARD_PROTOCOL_T0:
|
||||
pioSendPci = SCARD_PCI_T0;
|
||||
break;
|
||||
case SCARD_PROTOCOL_T1:
|
||||
pioSendPci = SCARD_PCI_T1;
|
||||
break;
|
||||
default:
|
||||
// This should not happen during normal use
|
||||
rv = static_cast<int32_t>(SCARD_E_PROTO_MISMATCH);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Executes a sequence of transmissions, and retries it if the card is reset during transmission
|
||||
*
|
||||
* A card not opened in exclusive mode (like here) can be reset by another process.
|
||||
* The application has to acknowledge the reset and retransmit the transaction.
|
||||
*
|
||||
* @param handle Smartcard handle
|
||||
* @param atomic_action Lambda that contains the sequence to be executed as a transaction. Expected to return
|
||||
* SCARD_S_SUCCESS on success.
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t transactRetry(SCARDHANDLE handle, const std::function<int32_t()>& atomic_action)
|
||||
{
|
||||
int32_t rv = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
|
||||
const SCARD_IO_REQUEST* pioSendPci = nullptr;
|
||||
if ((rv = getCardStatus(handle, dwProt, pioSendPci)) == SCARD_S_SUCCESS) {
|
||||
// Begin a transaction. This locks out any other process from interfacing with the card
|
||||
if ((rv = SCardBeginTransaction(handle)) == SCARD_S_SUCCESS) {
|
||||
int i;
|
||||
for (i = 4; i > 0; i--) { // 3 tries for reconnecting after reset
|
||||
// Run the lambda payload and store its return code
|
||||
int32_t rv_act = atomic_action();
|
||||
if (rv_act == static_cast<int32_t>(SCARD_W_RESET_CARD)) {
|
||||
// The card was reset during the transmission.
|
||||
SCUINT dwProt_new = SCARD_PROTOCOL_UNDEFINED;
|
||||
// Acknowledge the reset and reestablish the connection and handle
|
||||
rv = SCardReconnect(handle, SCARD_SHARE_SHARED, dwProt, SCARD_LEAVE_CARD, &dwProt_new);
|
||||
// On Windows, the transaction has to be re-started.
|
||||
// On Linux and OSX (which use pcsc-lite), the transaction continues to be valid.
|
||||
#ifdef Q_OS_WIN
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
rv = SCardBeginTransaction(handle);
|
||||
}
|
||||
#endif
|
||||
qDebug("Smardcard was reset and had to be reconnected");
|
||||
} else {
|
||||
// This does not mean that the payload returned SCARD_S_SUCCESS
|
||||
// just that the card was not reset during communication.
|
||||
// Return the return code of the payload function
|
||||
rv = rv_act;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == 0) {
|
||||
rv = static_cast<int32_t>(SCARD_W_RESET_CARD);
|
||||
qDebug("Smardcard was reset and failed to reconnect after 3 tries");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This could return SCARD_W_RESET_CARD or SCARD_E_NOT_TRANSACTED, but we dont care
|
||||
// because then the transaction would have already been ended implicitly
|
||||
SCardEndTransaction(handle, SCARD_LEAVE_CARD);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Transmits a buffer to the smartcard, and reads the response
|
||||
*
|
||||
* @param handle Smartcard handle
|
||||
* @param pbSendBuffer Pointer to the data to be sent
|
||||
* @param dwSendLength Size of the data to be sent in bytes
|
||||
* @param pbRecvBuffer Pointer to the data to be received
|
||||
* @param dwRecvLength Size of the data to be received in bytes
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t transmit(SCARDHANDLE handle,
|
||||
const uint8_t* pbSendBuffer,
|
||||
SCUINT dwSendLength,
|
||||
uint8_t* pbRecvBuffer,
|
||||
SCUINT& dwRecvLength)
|
||||
{
|
||||
int32_t rv = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
|
||||
const SCARD_IO_REQUEST* pioSendPci = nullptr;
|
||||
if ((rv = getCardStatus(handle, dwProt, pioSendPci)) == SCARD_S_SUCCESS) {
|
||||
// Write to and read from the card
|
||||
// pioRecvPci is nullptr because we do not expect any PCI response header
|
||||
if ((rv = SCardTransmit(
|
||||
handle, pioSendPci, pbSendBuffer, dwSendLength, nullptr, pbRecvBuffer, &dwRecvLength))
|
||||
== SCARD_S_SUCCESS) {
|
||||
if (dwRecvLength < 2) {
|
||||
// Any valid response should be at least 2 bytes (response status)
|
||||
// However the protocol itself could fail
|
||||
rv = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
} else {
|
||||
if (pbRecvBuffer[dwRecvLength - 2] == SW_OK_HIGH && pbRecvBuffer[dwRecvLength - 1] == SW_OK_LOW) {
|
||||
rv = SCARD_S_SUCCESS;
|
||||
} else if (pbRecvBuffer[dwRecvLength - 2] == SW_PRECOND_HIGH
|
||||
&& pbRecvBuffer[dwRecvLength - 1] == SW_PRECOND_LOW) {
|
||||
// This happens if the key requires eg. a button press or if the applet times out
|
||||
// Solution: Re-present the card to the reader
|
||||
rv = static_cast<int32_t>(SCARD_W_CARD_NOT_AUTHENTICATED);
|
||||
} else if ((pbRecvBuffer[dwRecvLength - 2] == SW_NOTFOUND_HIGH
|
||||
&& pbRecvBuffer[dwRecvLength - 1] == SW_NOTFOUND_LOW)
|
||||
|| pbRecvBuffer[dwRecvLength - 2] == SW_UNSUP_HIGH) {
|
||||
// This happens eg. during a select command when the AID is not found
|
||||
rv = static_cast<int32_t>(SCARD_E_FILE_NOT_FOUND);
|
||||
} else {
|
||||
rv = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Transmits an applet selection APDU to select the challenge-response applet
|
||||
*
|
||||
* @param handle Smartcard handle and applet ID bytestring pair
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t selectApplet(const SCardAID& handle)
|
||||
{
|
||||
uint8_t pbSendBuffer_head[5] = {
|
||||
CLA_ISO, INS_SELECT, SEL_APP_AID, 0, static_cast<uint8_t>(handle.second.size())};
|
||||
auto pbSendBuffer = new uint8_t[5 + handle.second.size()];
|
||||
memcpy(pbSendBuffer, pbSendBuffer_head, 5);
|
||||
memcpy(pbSendBuffer + 5, handle.second.constData(), handle.second.size());
|
||||
uint8_t pbRecvBuffer[12] = {
|
||||
0}; // 3 bytes version, 1 byte program counter, other stuff for various implementations, 2 bytes status
|
||||
SCUINT dwRecvLength = 12;
|
||||
|
||||
int32_t rv = transmit(handle.first, pbSendBuffer, 5 + handle.second.size(), pbRecvBuffer, dwRecvLength);
|
||||
|
||||
delete[] pbSendBuffer;
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Finds the AID a card uses by checking a list of AIDs
|
||||
*
|
||||
* @param handle Smartcard handle
|
||||
* @param aid Application identifier byte string
|
||||
* @param result Smartcard handle and AID bytestring pair that will be populated on success
|
||||
*
|
||||
* @return true on success
|
||||
*/
|
||||
bool findAID(SCARDHANDLE handle, const QList<QByteArray>& aid_codes, SCardAID& result)
|
||||
{
|
||||
for (const auto& aid : aid_codes) {
|
||||
// Ensure the transmission is retransmitted after card resets
|
||||
int32_t rv = transactRetry(handle, [&handle, &aid]() {
|
||||
// Try to select the card using the specified AID
|
||||
return selectApplet({handle, aid});
|
||||
});
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
result.first = handle;
|
||||
result.second = aid;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Reads the serial number of a key
|
||||
*
|
||||
* @param handle Smartcard handle and applet ID bytestring pair
|
||||
* @param serial The serial number
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t getSerial(const SCardAID& handle, unsigned int& serial)
|
||||
{
|
||||
// Ensure the transmission is retransmitted after card resets
|
||||
return transactRetry(handle.first, [&handle, &serial]() {
|
||||
int32_t rv_l = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
// Ensure that the card is always selected before sending the command
|
||||
if ((rv_l = selectApplet(handle)) != SCARD_S_SUCCESS) {
|
||||
return rv_l;
|
||||
}
|
||||
|
||||
uint8_t pbSendBuffer[5] = {CLA_ISO, INS_API_REQ, CMD_GET_SERIAL, 0, 6};
|
||||
uint8_t pbRecvBuffer[6] = {0}; // 4 bytes serial, 2 bytes status
|
||||
SCUINT dwRecvLength = 6;
|
||||
|
||||
rv_l = transmit(handle.first, pbSendBuffer, 5, pbRecvBuffer, dwRecvLength);
|
||||
if (rv_l == SCARD_S_SUCCESS && dwRecvLength >= 4) {
|
||||
// The serial number is encoded MSB first
|
||||
serial = (pbRecvBuffer[0] << 24) + (pbRecvBuffer[1] << 16) + (pbRecvBuffer[2] << 8) + (pbRecvBuffer[3]);
|
||||
}
|
||||
|
||||
return rv_l;
|
||||
});
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Creates a smartcard handle and applet select bytestring pair by looking up a serial key
|
||||
*
|
||||
* @param target_serial The serial number to search for
|
||||
* @param context A pre-established smartcard API context
|
||||
* @param aid_codes A list which contains the AIDs to scan for
|
||||
* @param handle The created smartcard handle and applet select bytestring pair
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t openKeySerial(const unsigned int target_serial,
|
||||
SCARDCONTEXT& context,
|
||||
const QList<QByteArray>& aid_codes,
|
||||
SCardAID* handle)
|
||||
{
|
||||
// Ensure the Smartcard API handle is still valid
|
||||
ensureValidContext(context);
|
||||
|
||||
int32_t rv = SCARD_S_SUCCESS;
|
||||
QList<QString> readers_list = getReaders(context);
|
||||
|
||||
// Iterate all connected readers
|
||||
foreach (const QString& reader_name, readers_list) {
|
||||
SCARDHANDLE hCard;
|
||||
SCUINT dwActiveProtocol = SCARD_PROTOCOL_UNDEFINED;
|
||||
rv = SCardConnect(context,
|
||||
reader_name.toStdString().c_str(),
|
||||
SCARD_SHARE_SHARED,
|
||||
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
|
||||
&hCard,
|
||||
&dwActiveProtocol);
|
||||
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
// Read the ATR record of the card
|
||||
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
|
||||
char pbReader[MAX_READERNAME] = {0};
|
||||
SCUINT dwAtrLen = sizeof(pbAtr);
|
||||
SCUINT dwReaderLen = sizeof(pbReader);
|
||||
SCUINT dwState = 0, dwProt = SCARD_PROTOCOL_UNDEFINED;
|
||||
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
if (dwProt == SCARD_PROTOCOL_T0 || dwProt == SCARD_PROTOCOL_T1) {
|
||||
// Find which AID to use
|
||||
SCardAID satr;
|
||||
if (findAID(hCard, aid_codes, satr)) {
|
||||
unsigned int serial = 0;
|
||||
// Read the serial number of the card
|
||||
getSerial(satr, serial);
|
||||
if (serial == target_serial) {
|
||||
handle->first = satr.first;
|
||||
handle->second = satr.second;
|
||||
return SCARD_S_SUCCESS;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rv = static_cast<int32_t>(SCARD_E_PROTO_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
rv = SCardDisconnect(hCard, SCARD_LEAVE_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
if (rv != SCARD_S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
return static_cast<int32_t>(SCARD_E_NO_SMARTCARD);
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Reads the status of a key
|
||||
*
|
||||
* The status is used for the firmware version only atm.
|
||||
*
|
||||
* @param handle Smartcard handle and applet ID bytestring pair
|
||||
* @param version The firmware version in [major, minor, patch] format
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t getStatus(const SCardAID& handle, uint8_t version[3])
|
||||
{
|
||||
// Ensure the transmission is retransmitted after card resets
|
||||
return transactRetry(handle.first, [&handle, &version]() {
|
||||
int32_t rv_l = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
// Ensure that the card is always selected before sending the command
|
||||
if ((rv_l = selectApplet(handle)) != SCARD_S_SUCCESS) {
|
||||
return rv_l;
|
||||
}
|
||||
|
||||
uint8_t pbSendBuffer[5] = {CLA_ISO, INS_STATUS, 0, 0, 6};
|
||||
uint8_t pbRecvBuffer[8] = {0}; // 4 bytes serial, 2 bytes other stuff, 2 bytes status
|
||||
SCUINT dwRecvLength = 8;
|
||||
|
||||
rv_l = transmit(handle.first, pbSendBuffer, 5, pbRecvBuffer, dwRecvLength);
|
||||
if (rv_l == SCARD_S_SUCCESS && dwRecvLength >= 3) {
|
||||
memcpy(version, pbRecvBuffer, 3);
|
||||
}
|
||||
|
||||
return rv_l;
|
||||
});
|
||||
}
|
||||
|
||||
/***
|
||||
* @brief Performs a challenge-response transmission
|
||||
*
|
||||
* The card computes the SHA1-HMAC of the challenge
|
||||
* using its pre-programmed secret key and return the response
|
||||
*
|
||||
* @param handle Smartcard handle and applet ID bytestring pair
|
||||
* @param slot_cmd Either CMD_HMAC_1 for slot 1 or CMD_HMAC_2 for slot 2
|
||||
* @param input Challenge byte buffer, exactly 64 bytes and padded using PKCS#7 or Yubikey padding
|
||||
* @param output Response byte buffer, exactly 20 bytes
|
||||
*
|
||||
* @return SCARD_S_SUCCESS on success
|
||||
*/
|
||||
int32_t getHMAC(const SCardAID& handle, uint8_t slot_cmd, const uint8_t input[64], uint8_t output[20])
|
||||
{
|
||||
// Ensure the transmission is retransmitted after card resets
|
||||
return transactRetry(handle.first, [&handle, &slot_cmd, &input, &output]() {
|
||||
int32_t rv_l = static_cast<int32_t>(SCARD_E_UNEXPECTED);
|
||||
|
||||
// Ensure that the card is always selected before sending the command
|
||||
if ((rv_l = selectApplet(handle)) != SCARD_S_SUCCESS) {
|
||||
return rv_l;
|
||||
}
|
||||
|
||||
uint8_t pbSendBuffer[5 + 64] = {CLA_ISO, INS_API_REQ, slot_cmd, 0, 64};
|
||||
memcpy(pbSendBuffer + 5, input, 64);
|
||||
uint8_t pbRecvBuffer[22] = {0}; // 20 bytes hmac, 2 bytes status
|
||||
SCUINT dwRecvLength = 22;
|
||||
|
||||
rv_l = transmit(handle.first, pbSendBuffer, 5 + 64, pbRecvBuffer, dwRecvLength);
|
||||
if (rv_l == SCARD_S_SUCCESS && dwRecvLength >= 20) {
|
||||
memcpy(output, pbRecvBuffer, 20);
|
||||
}
|
||||
|
||||
// If transmission is successful but no data is returned
|
||||
// then the slot is probably not configured for HMAC-SHA1
|
||||
// but for OTP or nothing instead
|
||||
if (rv_l == SCARD_S_SUCCESS && dwRecvLength != 22) {
|
||||
return static_cast<int32_t>(SCARD_E_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
return rv_l;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
YubiKeyInterfacePCSC::YubiKeyInterfacePCSC()
|
||||
: YubiKeyInterface()
|
||||
{
|
||||
if (ensureValidContext(m_sc_context) != SCARD_S_SUCCESS) {
|
||||
qDebug("YubiKey: Failed to establish PCSC context.");
|
||||
} else {
|
||||
m_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
YubiKeyInterfacePCSC::~YubiKeyInterfacePCSC()
|
||||
{
|
||||
if (m_initialized && SCardReleaseContext(m_sc_context) != SCARD_S_SUCCESS) {
|
||||
qDebug("YubiKey: Failed to release PCSC context.");
|
||||
}
|
||||
}
|
||||
|
||||
YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::m_instance(nullptr);
|
||||
|
||||
YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::instance()
|
||||
{
|
||||
if (!m_instance) {
|
||||
m_instance = new YubiKeyInterfacePCSC();
|
||||
}
|
||||
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void YubiKeyInterfacePCSC::findValidKeys()
|
||||
{
|
||||
m_error.clear();
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QtConcurrent::run([this] {
|
||||
// This mutex protects the smartcard against concurrent transmissions
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
emit detectComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all known keys
|
||||
m_foundKeys.clear();
|
||||
|
||||
// Connect to each reader and look for cards
|
||||
QList<QString> readers_list = getReaders(m_sc_context);
|
||||
foreach (const QString& reader_name, readers_list) {
|
||||
|
||||
/* Some Yubikeys present their PCSC interface via USB as well
|
||||
Although this would not be a problem in itself,
|
||||
we filter these connections because in USB mode,
|
||||
the PCSC challenge-response interface is usually locked
|
||||
Instead, the other USB (HID) interface should pick up and
|
||||
interface the key.
|
||||
For more info see the comment block further below. */
|
||||
if (reader_name.contains("yubikey", Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SCARDHANDLE hCard;
|
||||
SCUINT dwActiveProtocol = SCARD_PROTOCOL_UNDEFINED;
|
||||
int32_t rv = SCardConnect(m_sc_context,
|
||||
reader_name.toStdString().c_str(),
|
||||
SCARD_SHARE_SHARED,
|
||||
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
|
||||
&hCard,
|
||||
&dwActiveProtocol);
|
||||
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
// Read the potocol and the ATR record
|
||||
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
|
||||
char pbReader[MAX_READERNAME] = {0};
|
||||
SCUINT dwAtrLen = sizeof(pbAtr);
|
||||
SCUINT dwReaderLen = sizeof(pbReader);
|
||||
SCUINT dwState = 0, dwProt = SCARD_PROTOCOL_UNDEFINED;
|
||||
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
// Check for a valid protocol
|
||||
if (dwProt == SCARD_PROTOCOL_T0 || dwProt == SCARD_PROTOCOL_T1) {
|
||||
// Find which AID to use
|
||||
SCardAID satr;
|
||||
if (findAID(hCard, m_aid_codes, satr)) {
|
||||
// Build the UI name using the display name found in the ATR map
|
||||
QByteArray atr = QByteArray(reinterpret_cast<char*>(pbAtr), dwAtrLen);
|
||||
QString name = "Unknown Key";
|
||||
if (m_atr_names.contains(atr)) {
|
||||
name = m_atr_names.value(atr);
|
||||
}
|
||||
// Add the firmware version and the serial number
|
||||
uint8_t version[3] = {0};
|
||||
getStatus(satr, version);
|
||||
name += QString(" v%1.%2.%3")
|
||||
.arg(QString::number(version[0]),
|
||||
QString::number(version[1]),
|
||||
QString::number(version[2]));
|
||||
unsigned int serial = 0;
|
||||
getSerial(satr, serial);
|
||||
|
||||
/* This variable indicates that the key is locked / timed out.
|
||||
When using the key via NFC, the user has to re-present the key to clear the timeout.
|
||||
Also, the key can be programmatically reset (see below).
|
||||
When using the key via USB (where the Yubikey presents as a PCSC reader in itself),
|
||||
the non-HMAC-SHA1 slots (eg. OTP) are incorrectly recognized as locked HMAC-SHA1 slots.
|
||||
Due to this conundrum, we exclude "locked" keys from the key enumeration,
|
||||
but only if the reader is the "virtual yubikey reader device".
|
||||
This also has the nice side effect of de-duplicating interfaces when a key
|
||||
Is connected via USB and also accessible via PCSC */
|
||||
bool wouldBlock = false;
|
||||
/* When the key is Used via NFC, the lock state / time-out is cleared when
|
||||
The smartcard connection is re-established / the applet is selected
|
||||
So the next call to performTestChallenge actually clears the lock.
|
||||
Due to this, the key is unlocked and we display it as such.
|
||||
When the key times out in the time between the key listing and
|
||||
the database unlock /save, an intercation request will be displayed. */
|
||||
for (int slot = 1; slot <= 2; ++slot) {
|
||||
if (performTestChallenge(&satr, slot, &wouldBlock)) {
|
||||
auto display = tr("(PCSC) %1 [%2] Challenge-Response - Slot %3")
|
||||
.arg(name, QString::number(serial), QString::number(slot));
|
||||
m_foundKeys.insert(serial, {slot, display});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rv = SCardDisconnect(hCard, SCARD_LEAVE_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
m_mutex.unlock();
|
||||
emit detectComplete(!m_foundKeys.isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
bool YubiKeyInterfacePCSC::testChallenge(YubiKeySlot slot, bool* wouldBlock)
|
||||
{
|
||||
bool ret = false;
|
||||
SCardAID hCard;
|
||||
int32_t rv = openKeySerial(slot.first, m_sc_context, m_aid_codes, &hCard);
|
||||
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
ret = performTestChallenge(&hCard, slot.second, wouldBlock);
|
||||
SCardDisconnect(hCard.first, SCARD_LEAVE_CARD);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool YubiKeyInterfacePCSC::performTestChallenge(void* key, int slot, bool* wouldBlock)
|
||||
{
|
||||
// Array has to be at least one byte or else the yubikey would interpret everything as padding
|
||||
auto chall = randomGen()->randomArray(1);
|
||||
Botan::secure_vector<char> resp;
|
||||
auto ret = performChallenge(static_cast<SCardAID*>(key), slot, false, chall, resp);
|
||||
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS || ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK) {
|
||||
if (wouldBlock) {
|
||||
*wouldBlock = ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult
|
||||
YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
if (!m_initialized) {
|
||||
m_error = tr("The YubiKey PCSC interface has not been initialized.");
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
// Try to grab a lock for 1 second, fail out if not possible
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
m_error = tr("Hardware key is currently in use.");
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
// Try for a few seconds to find the key
|
||||
emit challengeStarted();
|
||||
|
||||
SCardAID hCard;
|
||||
int tries = 20; // 5 seconds, test every 250 ms
|
||||
while (tries > 0) {
|
||||
int32_t rv = openKeySerial(slot.first, m_sc_context, m_aid_codes, &hCard);
|
||||
// Key with specified serial number is found
|
||||
if (rv == SCARD_S_SUCCESS) {
|
||||
auto ret = performChallenge(&hCard, slot.second, true, challenge, response);
|
||||
SCardDisconnect(hCard.first, SCARD_LEAVE_CARD);
|
||||
|
||||
/* If this would be YCR_WOULDBLOCK, the key is locked.
|
||||
So we wait for the user to re-present it to clear the time-out
|
||||
This condition usually only happens when the key times out after
|
||||
the initial key listing, because performTestChallenge implicitly
|
||||
resets the key (see commnt above) */
|
||||
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS) {
|
||||
emit challengeCompleted();
|
||||
m_mutex.unlock();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (--tries > 0) {
|
||||
QThread::msleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
m_error = tr("Could not find or access hardware key with serial number %1. Please present it to continue. ")
|
||||
.arg(slot.first)
|
||||
+ m_error;
|
||||
emit challengeCompleted();
|
||||
m_mutex.unlock();
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKeyInterfacePCSC::performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response)
|
||||
{
|
||||
// Always block (i.e. wait for the user to touch the key to the reader)
|
||||
Q_UNUSED(mayBlock);
|
||||
|
||||
m_error.clear();
|
||||
int yk_cmd = (slot == 1) ? CMD_HMAC_1 : CMD_HMAC_2;
|
||||
QByteArray paddedChallenge = challenge;
|
||||
|
||||
response.clear();
|
||||
response.resize(20);
|
||||
|
||||
/*
|
||||
* 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 bytes fixed length
|
||||
* configurations even work, some docs say avoid it.
|
||||
*
|
||||
* In fact, the Yubikey always assumes the last byte (nr. 64)
|
||||
* and all bytes of the same value preceeding it to be padding.
|
||||
* This does not conform fully to PKCS7, because the the actual value
|
||||
* of the padding bytes is ignored.
|
||||
*/
|
||||
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());
|
||||
|
||||
int32_t rv = getHMAC(*static_cast<SCardAID*>(key), yk_cmd, c, r);
|
||||
|
||||
if (rv != SCARD_S_SUCCESS) {
|
||||
if (rv == static_cast<int32_t>(SCARD_W_CARD_NOT_AUTHENTICATED)) {
|
||||
m_error = tr("Hardware key is locked or timed out. Unlock or re-present it to continue.");
|
||||
return YubiKey::ChallengeResult::YCR_WOULDBLOCK;
|
||||
} else if (rv == static_cast<int32_t>(SCARD_E_FILE_NOT_FOUND)) {
|
||||
m_error = tr("Hardware key was not found or is misconfigured.");
|
||||
} else {
|
||||
m_error =
|
||||
tr("Failed to complete a challenge-response, the PCSC error code was: %1").arg(QString::number(rv));
|
||||
}
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_SUCCESS;
|
||||
}
|
112
src/keys/drivers/YubiKeyInterfacePCSC.h
Normal file
112
src/keys/drivers/YubiKeyInterfacePCSC.h
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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_INTERFACE_PCSC_H
|
||||
#define KEEPASSX_YUBIKEY_INTERFACE_PCSC_H
|
||||
|
||||
#include "YubiKeyInterface.h"
|
||||
|
||||
#include <winscard.h>
|
||||
|
||||
#define CLA_ISO 0x00
|
||||
#define INS_SELECT 0xA4
|
||||
#define SEL_APP_AID 0x04
|
||||
#define INS_API_REQ 0x01
|
||||
#define INS_STATUS 0x03
|
||||
#define CMD_GET_SERIAL 0x10
|
||||
#define CMD_HMAC_1 0x30
|
||||
#define CMD_HMAC_2 0x38
|
||||
#define SW_OK_HIGH 0x90
|
||||
#define SW_OK_LOW 0x00
|
||||
#define SW_PRECOND_HIGH 0x69
|
||||
#define SW_PRECOND_LOW 0x85
|
||||
#define SW_NOTFOUND_HIGH 0x6A
|
||||
#define SW_NOTFOUND_LOW 0x82
|
||||
#define SW_UNSUP_HIGH 0x6D
|
||||
|
||||
typedef QPair<SCARDHANDLE, QByteArray> SCardAID;
|
||||
|
||||
/**
|
||||
* Singleton class to manage the PCSC interface to hardware key(s)
|
||||
*/
|
||||
class YubiKeyInterfacePCSC : public YubiKeyInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static YubiKeyInterfacePCSC* instance();
|
||||
|
||||
void findValidKeys() override;
|
||||
|
||||
YubiKey::ChallengeResult
|
||||
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
|
||||
bool testChallenge(YubiKeySlot slot, bool* wouldBlock) override;
|
||||
|
||||
private:
|
||||
explicit YubiKeyInterfacePCSC();
|
||||
~YubiKeyInterfacePCSC();
|
||||
|
||||
static YubiKeyInterfacePCSC* m_instance;
|
||||
|
||||
YubiKey::ChallengeResult performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response) override;
|
||||
bool performTestChallenge(void* key, int slot, bool* wouldBlock) override;
|
||||
|
||||
SCARDCONTEXT m_sc_context;
|
||||
|
||||
// This list contains all the AID (application identifier) codes for the Yubikey HMAC-SHA1 applet
|
||||
// and also for compatible third-party ones. They will be tried one by one.
|
||||
const QList<QByteArray> m_aid_codes = {
|
||||
QByteArrayLiteral("\xA0\x00\x00\x05\x27\x20\x01"), // Yubico Yubikey
|
||||
QByteArrayLiteral("\xA0\x00\x00\x06\x17\x00\x07\x53\x4E\xAF\x01") // Fidesmo development
|
||||
};
|
||||
|
||||
// This map provides display names for the various hardware-specific ATR (answer to reset) codes
|
||||
// of the Yubikeys (and other compatible tokens)
|
||||
const QHash<QByteArray, QString> m_atr_names = {
|
||||
// Yubico Yubikeys
|
||||
{QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\x58"), "YubiKey NEO"},
|
||||
{QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\xFF\x94"),
|
||||
"YubiKey NEO via NFC"},
|
||||
{QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\xF9"),
|
||||
"YubiKey 5 NFC via NFC"},
|
||||
{QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\xFF\x7F"),
|
||||
"YubiKey 5 NFC via ACR122U"},
|
||||
{QByteArrayLiteral("\x3B\xF8\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x34\xD4"),
|
||||
"YubiKey 4 OTP+CCID"},
|
||||
{QByteArrayLiteral("\x3B\xF9\x18\x00\xFF\x81\x31\xFE\x45\x50\x56\x5F\x4A\x33\x41\x30\x34\x30\x40"),
|
||||
"YubiKey NEO OTP+U2F+CCID (PKI)"},
|
||||
{QByteArrayLiteral("\x3B\xFA\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\xA6"),
|
||||
"YubiKey NEO"},
|
||||
{QByteArrayLiteral("\x3B\xFC\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\xE1"),
|
||||
"YubiKey NEO (PKI)"},
|
||||
{QByteArrayLiteral("\x3B\xFC\x13\x00\x00\x81\x31\xFE\x45\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\xB1"),
|
||||
"YubiKey NEO"},
|
||||
{QByteArrayLiteral(
|
||||
"\x3B\xFD\x13\x00\x00\x81\x31\xFE\x15\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\x40"),
|
||||
"YubiKey 5 NFC (PKI)"},
|
||||
{QByteArrayLiteral(
|
||||
"\x3B\xFD\x13\x00\x00\x81\x31\xFE\x45\x41\x37\x30\x30\x36\x43\x47\x20\x32\x34\x32\x52\x31\xD6"),
|
||||
"YubiKey NEO (token)"},
|
||||
// Other tokens implementing the Yubikey challenge-response protocol
|
||||
{QByteArrayLiteral("\x3B\x80\x80\x01\x01"), "Fidesmo Card 2.0"}};
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_YUBIKEY_INTERFACE_PCSC_H
|
325
src/keys/drivers/YubiKeyInterfaceUSB.cpp
Normal file
325
src/keys/drivers/YubiKeyInterfaceUSB.cpp
Normal file
@ -0,0 +1,325 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 "YubiKeyInterfaceUSB.h"
|
||||
|
||||
#include "core/Tools.h"
|
||||
#include "crypto/Random.h"
|
||||
#include "thirdparty/ykcore/ykcore.h"
|
||||
#include "thirdparty/ykcore/ykdef.h"
|
||||
#include "thirdparty/ykcore/ykstatus.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int MAX_KEYS = 4;
|
||||
|
||||
YK_KEY* openKey(int index)
|
||||
{
|
||||
static const int vids[] = {YUBICO_VID, ONLYKEY_VID};
|
||||
static const int pids[] = {YUBIKEY_PID,
|
||||
NEO_OTP_PID,
|
||||
NEO_OTP_CCID_PID,
|
||||
NEO_OTP_U2F_PID,
|
||||
NEO_OTP_U2F_CCID_PID,
|
||||
YK4_OTP_PID,
|
||||
YK4_OTP_U2F_PID,
|
||||
YK4_OTP_CCID_PID,
|
||||
YK4_OTP_U2F_CCID_PID,
|
||||
PLUS_U2F_OTP_PID,
|
||||
ONLYKEY_PID};
|
||||
|
||||
return yk_open_key_vid_pid(vids, sizeof(vids) / sizeof(vids[0]), pids, sizeof(pids) / sizeof(pids[0]), index);
|
||||
}
|
||||
|
||||
void closeKey(YK_KEY* key)
|
||||
{
|
||||
yk_close_key(key);
|
||||
}
|
||||
|
||||
unsigned int getSerial(YK_KEY* key)
|
||||
{
|
||||
unsigned int serial;
|
||||
yk_get_serial(key, 1, 0, &serial);
|
||||
return serial;
|
||||
}
|
||||
|
||||
YK_KEY* openKeySerial(unsigned int serial)
|
||||
{
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
auto* yk_key = openKey(i);
|
||||
if (yk_key) {
|
||||
// If the provided serial number is 0, or the key matches the serial, return it
|
||||
if (serial == 0 || getSerial(yk_key) == serial) {
|
||||
return yk_key;
|
||||
}
|
||||
closeKey(yk_key);
|
||||
} else if (yk_errno == YK_ENOKEY) {
|
||||
// No more connected keys
|
||||
break;
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
qWarning("Hardware key USB error: %s", yk_usb_strerror());
|
||||
} else {
|
||||
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
YubiKeyInterfaceUSB::YubiKeyInterfaceUSB()
|
||||
: YubiKeyInterface()
|
||||
{
|
||||
if (!yk_init()) {
|
||||
qDebug("YubiKey: Failed to initialize USB interface.");
|
||||
} else {
|
||||
m_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
YubiKeyInterfaceUSB::~YubiKeyInterfaceUSB()
|
||||
{
|
||||
yk_release();
|
||||
}
|
||||
|
||||
YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::m_instance(Q_NULLPTR);
|
||||
|
||||
YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::instance()
|
||||
{
|
||||
if (!m_instance) {
|
||||
m_instance = new YubiKeyInterfaceUSB();
|
||||
}
|
||||
|
||||
return m_instance;
|
||||
}
|
||||
|
||||
void YubiKeyInterfaceUSB::findValidKeys()
|
||||
{
|
||||
m_error.clear();
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QtConcurrent::run([this] {
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
emit detectComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all known keys
|
||||
m_foundKeys.clear();
|
||||
|
||||
// Try to detect up to 4 connected hardware keys
|
||||
for (int i = 0; i < MAX_KEYS; ++i) {
|
||||
auto yk_key = openKey(i);
|
||||
if (yk_key) {
|
||||
auto serial = getSerial(yk_key);
|
||||
if (serial == 0) {
|
||||
closeKey(yk_key);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto st = ykds_alloc();
|
||||
yk_get_status(yk_key, st);
|
||||
int vid, pid;
|
||||
yk_get_key_vid_pid(yk_key, &vid, &pid);
|
||||
|
||||
QString name = m_pid_names.value(pid, tr("Unknown"));
|
||||
if (vid == 0x1d50) {
|
||||
name = QStringLiteral("OnlyKey");
|
||||
}
|
||||
name += QString(" v%1.%2.%3")
|
||||
.arg(QString::number(ykds_version_major(st)),
|
||||
QString::number(ykds_version_minor(st)),
|
||||
QString::number(ykds_version_build(st)));
|
||||
|
||||
bool wouldBlock;
|
||||
for (int slot = 1; slot <= 2; ++slot) {
|
||||
auto config = (slot == 1 ? CONFIG1_VALID : CONFIG2_VALID);
|
||||
if (!(ykds_touch_level(st) & config)) {
|
||||
// Slot is not configured
|
||||
continue;
|
||||
}
|
||||
// Don't actually challenge a YubiKey Neo or below, they always require button press
|
||||
// if it is enabled for the slot resulting in failed detection
|
||||
if (pid <= NEO_OTP_U2F_CCID_PID) {
|
||||
auto display = tr("(USB) %1 [%2] Configured Slot - %3")
|
||||
.arg(name, QString::number(serial), QString::number(slot));
|
||||
m_foundKeys.insert(serial, {slot, display});
|
||||
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
|
||||
auto display =
|
||||
tr("(USB) %1 [%2] Challenge-Response - Slot %3 - %4")
|
||||
.arg(name,
|
||||
QString::number(serial),
|
||||
QString::number(slot),
|
||||
wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request")
|
||||
: tr("Passive", "USB Challenge-Response Key no interaction required"));
|
||||
m_foundKeys.insert(serial, {slot, display});
|
||||
}
|
||||
}
|
||||
|
||||
ykds_free(st);
|
||||
closeKey(yk_key);
|
||||
|
||||
Tools::wait(100);
|
||||
} else if (yk_errno == YK_ENOKEY) {
|
||||
// No more keys are connected
|
||||
break;
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
qWarning("Hardware key USB error: %s", yk_usb_strerror());
|
||||
} else {
|
||||
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
|
||||
}
|
||||
}
|
||||
|
||||
m_mutex.unlock();
|
||||
emit detectComplete(!m_foundKeys.isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a test challenge to the specified slot to determine if challenge
|
||||
* response is properly configured.
|
||||
*
|
||||
* @param slot YubiKey configuration slot
|
||||
* @param wouldBlock return if the operation requires user input
|
||||
* @return whether the challenge succeeded
|
||||
*/
|
||||
bool YubiKeyInterfaceUSB::testChallenge(YubiKeySlot slot, bool* wouldBlock)
|
||||
{
|
||||
bool ret = false;
|
||||
auto* yk_key = openKeySerial(slot.first);
|
||||
if (yk_key) {
|
||||
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool YubiKeyInterfaceUSB::performTestChallenge(void* key, int slot, bool* wouldBlock)
|
||||
{
|
||||
auto chall = randomGen()->randomArray(1);
|
||||
Botan::secure_vector<char> resp;
|
||||
auto ret = performChallenge(static_cast<YK_KEY*>(key), slot, false, chall, resp);
|
||||
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS || ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK) {
|
||||
if (wouldBlock) {
|
||||
*wouldBlock = ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a challenge to the specified slot
|
||||
* This operation could block if the YubiKey requires a touch to trigger.
|
||||
*
|
||||
* @param slot YubiKey configuration slot
|
||||
* @param challenge challenge input to YubiKey
|
||||
* @param response response output from YubiKey
|
||||
* @return challenge result
|
||||
*/
|
||||
YubiKey::ChallengeResult
|
||||
YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
if (!m_initialized) {
|
||||
m_error = tr("The YubiKey USB interface has not been initialized.");
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
// Try to grab a lock for 1 second, fail out if not possible
|
||||
if (!m_mutex.tryLock(1000)) {
|
||||
m_error = tr("Hardware key is currently in use.");
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
auto* yk_key = openKeySerial(slot.first);
|
||||
if (!yk_key) {
|
||||
// Key with specified serial number is not connected
|
||||
m_error =
|
||||
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
|
||||
m_mutex.unlock();
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
||||
emit challengeStarted();
|
||||
auto ret = performChallenge(yk_key, slot.second, true, challenge, response);
|
||||
|
||||
closeKey(yk_key);
|
||||
emit challengeCompleted();
|
||||
m_mutex.unlock();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKeyInterfaceUSB::performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response)
|
||||
{
|
||||
m_error.clear();
|
||||
int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2;
|
||||
QByteArray paddedChallenge = challenge;
|
||||
|
||||
// yk_challenge_response() insists on 64 bytes response buffer */
|
||||
response.clear();
|
||||
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 bytes 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(
|
||||
static_cast<YK_KEY*>(key), yk_cmd, mayBlock, paddedChallenge.size(), c, response.size(), r);
|
||||
|
||||
// actual HMAC-SHA1 response is only 20 bytes
|
||||
response.resize(20);
|
||||
|
||||
if (!ret) {
|
||||
if (yk_errno == YK_EWOULDBLOCK) {
|
||||
return YubiKey::ChallengeResult::YCR_WOULDBLOCK;
|
||||
} else if (yk_errno) {
|
||||
if (yk_errno == YK_ETIMEOUT) {
|
||||
m_error = tr("Hardware key timed out waiting for user interaction.");
|
||||
} else if (yk_errno == YK_EUSBERR) {
|
||||
m_error = tr("A USB error occurred when accessing the hardware key: %1").arg(yk_usb_strerror());
|
||||
} else {
|
||||
m_error = tr("Failed to complete a challenge-response, the specific error was: %1")
|
||||
.arg(yk_strerror(yk_errno));
|
||||
}
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_SUCCESS;
|
||||
}
|
74
src/keys/drivers/YubiKeyInterfaceUSB.h
Normal file
74
src/keys/drivers/YubiKeyInterfaceUSB.h
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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_INTERFACE_USB_H
|
||||
#define KEEPASSX_YUBIKEY_INTERFACE_USB_H
|
||||
|
||||
#include "thirdparty/ykcore/ykdef.h"
|
||||
|
||||
#include "YubiKeyInterface.h"
|
||||
|
||||
/**
|
||||
* Singleton class to manage the USB interface to hardware key(s)
|
||||
*/
|
||||
class YubiKeyInterfaceUSB : public YubiKeyInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static YubiKeyInterfaceUSB* instance();
|
||||
|
||||
void findValidKeys() override;
|
||||
|
||||
YubiKey::ChallengeResult
|
||||
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
|
||||
bool testChallenge(YubiKeySlot slot, bool* wouldBlock) override;
|
||||
|
||||
private:
|
||||
explicit YubiKeyInterfaceUSB();
|
||||
~YubiKeyInterfaceUSB();
|
||||
|
||||
static YubiKeyInterfaceUSB* m_instance;
|
||||
|
||||
YubiKey::ChallengeResult performChallenge(void* key,
|
||||
int slot,
|
||||
bool mayBlock,
|
||||
const QByteArray& challenge,
|
||||
Botan::secure_vector<char>& response) override;
|
||||
bool performTestChallenge(void* key, int slot, bool* wouldBlock) override;
|
||||
|
||||
// This map provides display names for the various USB PIDs of the Yubikeys
|
||||
const QHash<int, QString> m_pid_names = {{YUBIKEY_PID, "YubiKey 1/2"},
|
||||
{NEO_OTP_PID, "YubiKey NEO - OTP only"},
|
||||
{NEO_OTP_CCID_PID, "YubiKey NEO - OTP and CCID"},
|
||||
{NEO_CCID_PID, "YubiKey NEO - CCID only"},
|
||||
{NEO_U2F_PID, "YubiKey NEO - U2F only"},
|
||||
{NEO_OTP_U2F_PID, "YubiKey NEO - OTP and U2F"},
|
||||
{NEO_U2F_CCID_PID, "YubiKey NEO - U2F and CCID"},
|
||||
{NEO_OTP_U2F_CCID_PID, "YubiKey NEO - OTP, U2F and CCID"},
|
||||
{YK4_OTP_PID, "YubiKey 4/5 - OTP only"},
|
||||
{YK4_U2F_PID, "YubiKey 4/5 - U2F only"},
|
||||
{YK4_OTP_U2F_PID, "YubiKey 4/5 - OTP and U2F"},
|
||||
{YK4_CCID_PID, "YubiKey 4/5 - CCID only"},
|
||||
{YK4_OTP_CCID_PID, "YubiKey 4/5 - OTP and CCID"},
|
||||
{YK4_U2F_CCID_PID, "YubiKey 4/5 - U2F and CCID"},
|
||||
{YK4_OTP_U2F_CCID_PID, "YubiKey 4/5 - OTP, U2F and CCID"},
|
||||
{PLUS_U2F_OTP_PID, "YubiKey plus - OTP+U2F"}};
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_YUBIKEY_INTERFACE_USB_H
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
|
||||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -22,11 +22,7 @@ YubiKey::YubiKey()
|
||||
{
|
||||
}
|
||||
|
||||
YubiKey::~YubiKey()
|
||||
{
|
||||
}
|
||||
|
||||
YubiKey* YubiKey::m_instance(nullptr);
|
||||
YubiKey* YubiKey::m_instance(Q_NULLPTR);
|
||||
|
||||
YubiKey* YubiKey::instance()
|
||||
{
|
||||
@ -62,18 +58,18 @@ QString YubiKey::errorMessage()
|
||||
return {};
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKey::challenge(YubiKeySlot slot, const QByteArray& chal, Botan::secure_vector<char>& resp)
|
||||
{
|
||||
Q_UNUSED(slot);
|
||||
Q_UNUSED(chal);
|
||||
Q_UNUSED(resp);
|
||||
|
||||
return ERROR;
|
||||
}
|
||||
|
||||
bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
|
||||
{
|
||||
Q_UNUSED(slot);
|
||||
Q_UNUSED(wouldBlock);
|
||||
return false;
|
||||
}
|
||||
|
||||
YubiKey::ChallengeResult YubiKey::challenge(YubiKeySlot slot, const QByteArray& chal, Botan::secure_vector<char>& resp)
|
||||
{
|
||||
Q_UNUSED(slot);
|
||||
Q_UNUSED(chal);
|
||||
Q_UNUSED(resp);
|
||||
|
||||
return YubiKey::ChallengeResult::YCR_ERROR;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user