mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-18 13:24:10 -05: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()
|
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; }"
|
||||||
|
@ -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
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>
|
<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<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>
|
<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>
|
<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>&Donate</source>
|
<source>&Donate</source>
|
||||||
<translation>&Donate</translation>
|
<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>
|
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>
|
||||||
|
@ -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}
|
||||||
|
@ -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}));
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
|
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) 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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user