0
0
mirror of https://github.com/keepassxreboot/keepassxc.git synced 2024-09-19 20:02:18 +02: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:
Christoph Honal 2021-10-01 16:39:07 +02:00 committed by GitHub
parent cc39f9ec23
commit 6d1fc31e96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1740 additions and 356 deletions

View File

@ -476,6 +476,11 @@ if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0")
endif() endif()
include_directories(SYSTEM ${ZLIB_INCLUDE_DIR}) include_directories(SYSTEM ${ZLIB_INCLUDE_DIR})
if(WITH_XC_YUBIKEY)
find_package(PCSC REQUIRED)
include_directories(SYSTEM ${PCSC_INCLUDE_DIRS})
endif()
if(UNIX) if(UNIX)
check_cxx_source_compiles("#include <sys/prctl.h> check_cxx_source_compiles("#include <sys/prctl.h>
int main() { prctl(PR_SET_DUMPABLE, 0); return 0; }" int main() { prctl(PR_SET_DUMPABLE, 0); return 0; }"

View File

@ -25,6 +25,7 @@ The following libraries are required:
* readline (for completion in cli) * readline (for completion in cli)
* libqt5x11extras5, libxi, and libxtst (for auto-type on X11) * libqt5x11extras5, libxi, and libxtst (for auto-type on X11)
* qrencode * qrencode
* libusb-1.0, pcsclite (optional to support YubiKey on Linux)
Prepare the Building Environment Prepare the Building Environment
================================ ================================

39
cmake/FindPCSC.cmake Normal file
View 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)

View File

@ -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> <source>Key file to unlock the database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please touch the button on your YubiKey!</source>
<translation type="unfinished">Please touch the button on your YubiKey!</translation>
</message>
<message> <message>
<source>Detecting hardware keys</source> <source>Detecting hardware keys</source>
<translation type="unfinished"></translation> <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&lt;br&gt;stop supporting in the future.&lt;br&gt;&lt;br&gt;Please consider generating a new key file by going to:&lt;br&gt;&lt;strong&gt;Database &amp;gt; Database Security &amp;gt; Change Key File.&lt;/strong&gt;&lt;br&gt;</source> <source>You are using an old key file format which KeePassXC may&lt;br&gt;stop supporting in the future.&lt;br&gt;&lt;br&gt;Please consider generating a new key file by going to:&lt;br&gt;&lt;strong&gt;Database &amp;gt; Database Security &amp;gt; Change Key File.&lt;/strong&gt;&lt;br&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please present or touch your YubiKey to continue</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DatabaseSettingWidgetMetaData</name> <name>DatabaseSettingWidgetMetaData</name>
@ -4755,10 +4755,6 @@ Are you sure you want to continue with this file?</source>
<source>Quit KeePassXC</source> <source>Quit KeePassXC</source>
<translation>Quit KeePassXC</translation> <translation>Quit KeePassXC</translation>
</message> </message>
<message>
<source>Please touch the button on your YubiKey!</source>
<translation>Please touch the button on your YubiKey!</translation>
</message>
<message> <message>
<source>&amp;Donate</source> <source>&amp;Donate</source>
<translation>&amp;Donate</translation> <translation>&amp;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> We recommend you use the AppImage available on our downloads page.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please present or touch your YubiKey to continue</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>ManageDatabase</name> <name>ManageDatabase</name>
@ -6889,10 +6889,6 @@ Kernel: %3 %4</source>
<source>Invalid YubiKey serial %1</source> <source>Invalid YubiKey serial %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please touch the button on your YubiKey to continue</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Do you want to create a database with an empty password? [y/N]: </source> <source>Do you want to create a database with an empty password? [y/N]: </source>
<translation type="unfinished"></translation> <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> <source>Warning: Failed to prevent screenshots on a top level window!</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Please present or touch your YubiKey to continue</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>
@ -8268,49 +8268,15 @@ Example: JBSWY3DPEHPK3PXP</source>
<context> <context>
<name>YubiKey</name> <name>YubiKey</name>
<message> <message>
<source>%1 [%2] Configured Slot - %3</source> <source>%1 No interface, slot %2</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>%1 Invalid slot specified - %2</source> <source>General: </source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>The YubiKey interface has not been initialized.</source> <source>Could not find interface for hardware key with serial number %1. Please connect it to continue.</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>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -8369,4 +8335,91 @@ Example: JBSWY3DPEHPK3PXP</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </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> </TS>

View File

@ -280,9 +280,16 @@ if(WIN32)
endif() endif()
if(WITH_XC_YUBIKEY) 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() 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() endif()
if(WITH_XC_NETWORKING) if(WITH_XC_NETWORKING)
@ -320,6 +327,7 @@ target_link_libraries(keepassx_core
Qt5::Network Qt5::Network
Qt5::Widgets Qt5::Widgets
${BOTAN2_LIBRARIES} ${BOTAN2_LIBRARIES}
${PCSC_LIBRARIES}
${ZXCVBN_LIBRARIES} ${ZXCVBN_LIBRARIES}
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
${thirdparty_LIBRARIES} ${thirdparty_LIBRARIES}

View File

@ -168,7 +168,7 @@ namespace Utils
} }
auto conn = QObject::connect(YubiKey::instance(), &YubiKey::userInteractionRequest, [&] { 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})); auto key = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey({serial, slot}));

View File

@ -84,7 +84,7 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] { connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
// Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog) // Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog)
if (window() != getMainWindow()) { 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::Information,
MessageWidget::DisableAutoHide); MessageWidget::DisableAutoHide);
} }

View File

@ -1708,7 +1708,7 @@ void MainWindow::hideGlobalMessage()
void MainWindow::showYubiKeyPopup() void MainWindow::showYubiKeyPopup()
{ {
displayGlobalMessage(tr("Please touch the button on your YubiKey!"), displayGlobalMessage(tr("Please present or touch your YubiKey to continue…"),
MessageWidget::Information, MessageWidget::Information,
false, false,
MessageWidget::DisableAutoHide); MessageWidget::DisableAutoHide);

View File

@ -44,11 +44,11 @@ bool ChallengeResponseKey::challenge(const QByteArray& challenge)
auto result = auto result =
AsyncTask::runAndWaitForFuture([&] { return YubiKey::instance()->challenge(m_keySlot, challenge, m_key); }); 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 // Record the error message
m_key.clear(); m_key.clear();
m_error = YubiKey::instance()->errorMessage(); m_error = YubiKey::instance()->errorMessage();
} }
return result == YubiKey::SUCCESS; return result == YubiKey::ChallengeResult::YCR_SUCCESS;
} }

View File

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com> * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -17,97 +17,58 @@
*/ */
#include "YubiKey.h" #include "YubiKey.h"
#include "YubiKeyInterfacePCSC.h"
#include "core/Tools.h" #include "YubiKeyInterfaceUSB.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
YubiKey::YubiKey() YubiKey::YubiKey()
: m_mutex(QMutex::Recursive) : m_interfaces_detect_mutex(QMutex::Recursive)
{ {
m_interactionTimer.setSingleShot(true); int num_interfaces = 0;
m_interactionTimer.setInterval(300);
if (!yk_init()) { if (YubiKeyInterfaceUSB::instance()->isInitialized()) {
qDebug("YubiKey: Failed to initialize USB interface."); ++num_interfaces;
} else { } 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; m_initialized = true;
// clang-format off // clang-format off
connect(&m_interactionTimer, SIGNAL(timeout()), this, SIGNAL(userInteractionRequest())); connect(&m_interactionTimer, SIGNAL(timeout()), this, SIGNAL(userInteractionRequest()));
connect(this, &YubiKey::challengeStarted, this, [this] { m_interactionTimer.start(); }, Qt::QueuedConnection); connect(this, &YubiKey::challengeStarted, this, [this] { m_interactionTimer.start(); });
connect(this, &YubiKey::challengeCompleted, this, [this] { m_interactionTimer.stop(); }, Qt::QueuedConnection); connect(this, &YubiKey::challengeCompleted, this, [this] { m_interactionTimer.stop(); });
// clang-format on // clang-format on
} }
} }
YubiKey::~YubiKey() YubiKey* YubiKey::m_instance(nullptr);
{
yk_release();
}
YubiKey* YubiKey::m_instance(Q_NULLPTR);
YubiKey* YubiKey::instance() YubiKey* YubiKey::instance()
{ {
@ -125,110 +86,90 @@ bool YubiKey::isInitialized()
void YubiKey::findValidKeys() void YubiKey::findValidKeys()
{ {
m_error.clear(); m_interfaces_detect_completed = 0;
if (!isInitialized()) { m_interfaces_detect_found = false;
return; YubiKeyInterfaceUSB::instance()->findValidKeys();
} YubiKeyInterfacePCSC::instance()->findValidKeys();
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());
});
} }
QList<YubiKeySlot> YubiKey::foundKeys() QList<YubiKeySlot> YubiKey::foundKeys()
{ {
QList<YubiKeySlot> keys; QList<YubiKeySlot> foundKeys;
for (auto serial : m_foundKeys.uniqueKeys()) {
for (auto key : m_foundKeys.value(serial)) { auto keys = YubiKeyInterfaceUSB::instance()->foundKeys();
keys.append({serial, key.first}); 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) QString YubiKey::getDisplayName(YubiKeySlot slot)
{ {
for (auto key : m_foundKeys.value(slot.first)) { QString name;
if (slot.second == key.first) { name.clear();
return key.second;
} if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
name += YubiKeyInterfaceUSB::instance()->getDisplayName(slot);
} }
return tr("%1 Invalid slot specified - %2").arg(QString::number(slot.first), QString::number(slot.second));
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 += " = ";
}
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() 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 YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
{ {
bool ret = false; if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
auto* yk_key = openKeySerial(slot.first); return YubiKeyInterfaceUSB::instance()->testChallenge(slot, wouldBlock);
if (yk_key) {
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
} }
return ret;
}
bool YubiKey::performTestChallenge(void* key, int slot, bool* wouldBlock) if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
{ return YubiKeyInterfacePCSC::instance()->testChallenge(slot, 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;
} }
return false; return false;
} }
@ -276,88 +206,17 @@ YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{ {
m_error.clear(); m_error.clear();
if (!m_initialized) {
m_error = tr("The YubiKey interface has not been initialized."); if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
return ERROR; return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
} }
// Try to grab a lock for 1 second, fail out if not possible if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
if (!m_mutex.tryLock(1000)) { return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
m_error = tr("Hardware key is currently in use.");
return ERROR;
} }
auto* yk_key = openKeySerial(slot.first); m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
if (!yk_key) { .arg(slot.first);
// 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(); return YubiKey::ChallengeResult::YCR_ERROR;
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;
} }

View File

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com> * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -36,11 +36,11 @@ class YubiKey : public QObject
Q_OBJECT Q_OBJECT
public: public:
enum ChallengeResult enum class ChallengeResult : int
{ {
ERROR, YCR_ERROR = 0,
SUCCESS, YCR_SUCCESS = 1,
WOULDBLOCK YCR_WOULDBLOCK = 2
}; };
static YubiKey* instance(); static YubiKey* instance();
@ -76,30 +76,17 @@ signals:
void challengeStarted(); void challengeStarted();
void challengeCompleted(); void challengeCompleted();
/**
* Emitted when an error occurred during challenge/response
*/
void challengeError(QString error);
private: private:
explicit YubiKey(); explicit YubiKey();
~YubiKey();
static YubiKey* m_instance; 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; QTimer m_interactionTimer;
bool m_initialized = false; bool m_initialized = false;
QString m_error; QString m_error;
int m_interfaces_detect_completed = -1;
bool m_interfaces_detect_found = false;
QMutex m_interfaces_detect_mutex;
Q_DISABLE_COPY(YubiKey) Q_DISABLE_COPY(YubiKey)
}; };

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

View 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

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

View 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

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

View 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

View File

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com> * 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 * 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 * 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(Q_NULLPTR);
{
}
YubiKey* YubiKey::m_instance(nullptr);
YubiKey* YubiKey::instance() YubiKey* YubiKey::instance()
{ {
@ -62,18 +58,18 @@ QString YubiKey::errorMessage()
return {}; 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) bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
{ {
Q_UNUSED(slot); Q_UNUSED(slot);
Q_UNUSED(wouldBlock); Q_UNUSED(wouldBlock);
return false; 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;
}