mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
* 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>
784 lines
32 KiB
C++
784 lines
32 KiB
C++
/*
|
|
* 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;
|
|
}
|