spicetools/external/scard/scard.cpp

368 lines
13 KiB
C++

/**
* MIT-License
* Copyright (c) 2018 by nolm <nolan@nolm.name>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Modified version.
*/
#include "scard.h"
#include <windows.h>
#include <winscard.h>
#include <thread>
#include "util/utils.h"
#include "util/logging.h"
#include "hooks/sleephook.h"
#define MAX_APDU_SIZE 255
#define SCARD_POLL_INTERVAL_MS 100
// set to detect all cards, reduce polling rate to 500ms.
// based off acr122u reader, see page 26 in api document.
// https://www.acs.com.hk/en/download-manual/419/API-ACR122U-2.04.pdf
#define PICC_OPERATING_PARAMS 0xDFu
BYTE PICC_OPERATING_PARAM_CMD[5] = {0xFFu, 0x00u, 0x51u, PICC_OPERATING_PARAMS, 0x00u};
// return bytes from device
#define PICC_SUCCESS 0x90u
#define PICC_ERROR 0x63u
static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u };
enum scard_atr_protocol {
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
SCARD_ATR_PROTOCOL_FELICA_424K = 0x12,
};
volatile bool should_exit = false;
winscard_config_t WINSCARD_CONFIG = {};
void scard_update(SCARDCONTEXT hContext, LPCTSTR readerName, uint8_t unit_no) {
// Connect to the smart card.
LONG lRet = 0;
SCARDHANDLE hCard{};
DWORD dwActiveProtocol{};
for (int retry = 0; retry < 10; retry++) {
if ((lRet = SCardConnect(hContext, readerName, SCARD_SHARE_EXCLUSIVE, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
&hCard, &dwActiveProtocol)) == SCARD_S_SUCCESS) {
break;
}
Sleep(20);
}
if (lRet != SCARD_S_SUCCESS) {
log_warning("scard", "error connecting to the card: 0x{:08x}", static_cast<unsigned>(lRet));
return;
}
// set the reader params
lRet = 0;
auto pci = dwActiveProtocol == SCARD_PROTOCOL_T1 ? SCARD_PCI_T1 : SCARD_PCI_T0;
DWORD cbRecv = MAX_APDU_SIZE;
BYTE pbRecv[MAX_APDU_SIZE];
if ((lRet = SCardTransmit(hCard, pci, PICC_OPERATING_PARAM_CMD, sizeof(PICC_OPERATING_PARAM_CMD),
nullptr, pbRecv, &cbRecv)) != SCARD_S_SUCCESS) {
log_warning("scard", "error setting PICC params: 0x{:08x}", static_cast<unsigned>(lRet));
return;
}
if(cbRecv > 2 && pbRecv[0] != PICC_SUCCESS && pbRecv[1] != PICC_OPERATING_PARAMS)
{
log_warning("scard", "PICC params not valid 0x{:02x} != 0x{:02x}",
static_cast<unsigned>(pbRecv[1]), static_cast<unsigned>(PICC_OPERATING_PARAMS));
return;
}
// Read ATR to determine card type.
TCHAR szReader[200];
DWORD cchReader = 200;
BYTE atr[32];
DWORD cByteAtr = 32;
lRet = SCardStatus(hCard, szReader, &cchReader, nullptr, nullptr, atr, &cByteAtr);
if (lRet != SCARD_S_SUCCESS) {
log_warning("scard", "error getting card status: 0x{:08x}", static_cast<unsigned>(lRet));
return;
}
// Only care about 20-byte ATRs returned by arcade-type smart cards
if (cByteAtr != 20) {
log_warning("scard", "ignoring card with len(atr) = {} ({})", cByteAtr, bin2hex(atr, cByteAtr));
return;
}
//log_info("scard", "atr Return: len({}), {}", cByteAtr, bin2hex(atr, cByteAtr));
// Figure out if we should reverse the UID returned by the card based on the ATR protocol
BYTE cardProtocol = atr[12];
BOOL shouldReverseUid = false;
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
log_info("scard", "card protocol: ISO15693_PART3");
shouldReverseUid = true;
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
log_info("scard", "card protocol: ISO14443_PART3");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
log_info("scard", "card protocol: FELICA_212K");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_424K) {
log_info("scard", "card protocol: FELICA_424K");
} else {
log_warning("scard", "Unknown NFC Protocol: 0x{:02x}", static_cast<unsigned>(cardProtocol));
//return;
}
// Read UID
cbRecv = MAX_APDU_SIZE;
if ((lRet = SCardTransmit(hCard, pci, UID_CMD, sizeof(UID_CMD),nullptr, pbRecv, &cbRecv)) != SCARD_S_SUCCESS) {
log_warning("scard", "error querying card UID: 0x{:08x}", static_cast<unsigned>(lRet));
return;
}
if (cbRecv > 1 && pbRecv[0] == PICC_ERROR) {
log_warning("scard", "UID query failed");
return;
}
if ((lRet = SCardDisconnect(hCard, SCARD_LEAVE_CARD)) != SCARD_S_SUCCESS) {
log_info("scard", "failed SCardDisconnect: 0x{:08x}", static_cast<unsigned>(lRet));
}
if (cbRecv < 8) {
log_info("scard", "padding card uid to 8 bytes (read uid: {})", bin2hex(pbRecv, cbRecv));
memset(&pbRecv[cbRecv], 0, 8 - cbRecv);
} else if (cbRecv > 8) {
log_info("scard", "taking first 8 bytes of len(uid) = {}", cbRecv);
}
// Copy UID to struct, reversing if necessary
card_info_t card_info;
if (shouldReverseUid) {
for (DWORD i = 0; i < 8; i++) {
card_info.uid[i] = pbRecv[7 - i];
}
} else {
memcpy(card_info.uid, pbRecv, 8);
}
log_info("scard", "reader unit no: {}, read card: {}", unit_no, bin2hex(card_info.uid, 8));
// set card type to 1
card_info.card_type = 1;
// update toggle
bool flip_order = WINSCARD_CONFIG.flip_order;
if (WINSCARD_CONFIG.flip_order) {
log_info("scard", "Flip order of readers since -scardflip option is set");
}
if (WINSCARD_CONFIG.toggle_order && (GetKeyState(VK_NUMLOCK) & 1) > 0) {
log_info("scard", "Flip order of readers since Num Lock is on");
flip_order = !flip_order;
}
// callback
if (WINSCARD_CONFIG.cardinfo_callback) {
WINSCARD_CONFIG.cardinfo_callback(flip_order ? ~unit_no : unit_no, &card_info);
}
}
void scard_clear(uint8_t unitNo) {
// reset
card_info_t empty_cardinfo{};
// callback
if (WINSCARD_CONFIG.cardinfo_callback)
WINSCARD_CONFIG.cardinfo_callback(WINSCARD_CONFIG.flip_order ? ~unitNo : unitNo, &empty_cardinfo);
}
void scard_loop(SCARDCONTEXT hContext, LPCTSTR slot0_reader_name, LPCTSTR slot1_reader_name, uint8_t reader_count) {
LONG lRet = 0;
SCARD_READERSTATE reader_states[2] = {
{
.szReader = slot0_reader_name,
.pvUserData = nullptr,
.dwCurrentState = SCARD_STATE_UNAWARE,
.dwEventState = 0,
.cbAtr = 0,
.rgbAtr = { 0 },
},
{
.szReader = slot1_reader_name,
.pvUserData = nullptr,
.dwCurrentState = SCARD_STATE_UNAWARE,
.dwEventState = 0,
.cbAtr = 0,
.rgbAtr = { 0 },
},
};
if (reader_count < 1) {
return;
}
while (!should_exit) {
lRet = SCardGetStatusChange(hContext, SCARD_POLL_INTERVAL_MS, reader_states, reader_count);
if (lRet == SCARD_E_TIMEOUT) {
continue;
} else if (lRet != SCARD_S_SUCCESS) {
log_warning("scard", "failed SCardGetStatusChange: 0x{:08x}", static_cast<unsigned>(lRet));
continue;
}
for (uint8_t unit_no = 0; unit_no < reader_count; unit_no++) {
if (!(reader_states[unit_no].dwEventState & SCARD_STATE_CHANGED)) {
continue;
}
DWORD newState = reader_states[unit_no].dwEventState ^ SCARD_STATE_CHANGED;
bool wasCardPresent = (reader_states[unit_no].dwCurrentState & SCARD_STATE_PRESENT) > 0;
if (newState & SCARD_STATE_UNAVAILABLE) {
log_info("scard", "new card state: unavailable");
Sleep(SCARD_POLL_INTERVAL_MS);
} else if (newState & SCARD_STATE_EMPTY) {
log_info("scard", "new card state: empty");
scard_clear(unit_no);
} else if (newState & SCARD_STATE_PRESENT && !wasCardPresent) {
log_info("scard", "new card state: present");
scard_update(hContext, reader_states[unit_no].szReader, unit_no);
}
reader_states[unit_no].dwCurrentState = reader_states[unit_no].dwEventState;
}
}
}
int scard_threadmain() {
LONG lRet = 0;
SCARDCONTEXT hContext = 0;
if ((lRet = SCardEstablishContext(SCARD_SCOPE_USER, nullptr, nullptr, &hContext)) != SCARD_S_SUCCESS) {
log_warning("scard", "failed to establish SCard context: {}", bin2hex(&lRet, sizeof(LONG)));
return lRet;
}
LPCTSTR reader = nullptr;
LPTSTR reader_name_slots[2] = {nullptr, nullptr };
int readerNameLen = 0;
while (!(reader_name_slots[0] || reader_name_slots[1])) {
// get list of readers
LPTSTR reader_list = nullptr;
auto pcchReaders = SCARD_AUTOALLOCATE;
lRet = SCardListReaders(hContext, nullptr, (LPTSTR) &reader_list, &pcchReaders);
int slot0_idx = -1;
int slot1_idx = -1;
int readerCount = 0;
switch (lRet) {
case SCARD_E_NO_READERS_AVAILABLE:
log_info("scard", "no readers available, waiting 1000ms...");
Sleep(1000);
break;
case SCARD_S_SUCCESS:
// So WinAPI has this terrible "multi-string" concept wherein you have a list
// of null-terminated strings, terminated by a double-null.
for (reader = reader_list; *reader; reader = reader + lstrlen(reader) + 1) {
log_info("scard", "found reader: {}", reader);
if (WINSCARD_CONFIG.slot0_reader_name && !lstrcmp(WINSCARD_CONFIG.slot0_reader_name, reader))
slot0_idx = readerCount;
if (WINSCARD_CONFIG.slot1_reader_name && !lstrcmp(WINSCARD_CONFIG.slot1_reader_name, reader))
slot1_idx = readerCount;
readerCount++;
}
// If we have at least two readers, assign readers to slots as necessary.
if (readerCount >= 2) {
if (slot1_idx != 0)
slot0_idx = 0;
if (slot0_idx != 1)
slot1_idx = 1;
}
// if the reader count is 1 and no reader was set, set first reader
if (readerCount == 1 && slot0_idx < 0 && slot1_idx < 0)
slot0_idx = 0;
// If we somehow only found slot 1, promote slot 1 to slot 0.
if (slot0_idx < 0 && slot1_idx >= 0) {
slot0_idx = slot1_idx;
slot1_idx = -1;
}
// Extract the relevant names from the multi-string.
int i;
for (i = 0, reader = reader_list; *reader; reader = reader + lstrlen(reader) + 1, i++) {
if (slot0_idx == i) {
readerNameLen = lstrlen(reader);
reader_name_slots[0] = (LPTSTR) HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS,
sizeof(TCHAR) * (readerNameLen + 1));
memcpy(reader_name_slots[0], &reader[0], (size_t) (readerNameLen + 1));
}
if (slot1_idx == i) {
readerNameLen = lstrlen(reader);
reader_name_slots[1] = (LPTSTR) HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS,
sizeof(TCHAR) * (readerNameLen + 1));
memcpy(reader_name_slots[1], &reader[0], (size_t) (readerNameLen + 1));
}
}
break;
default:
log_warning("scard", "failed SCardListReaders: 0x{:08x}", lRet);
Sleep(5000);
break;
}
if (reader_list) {
SCardFreeMemory(hContext, reader_list);
}
}
if (reader_name_slots[0]) {
log_info("scard", "using reader slot 0: {}", reader_name_slots[0]);
}
if (reader_name_slots[1]) {
log_info("scard", "using reader slot 1: {}", reader_name_slots[0]);
}
scard_loop(hContext, reader_name_slots[0], reader_name_slots[1], (uint8_t) (reader_name_slots[1] ? 2 : 1));
if (reader_name_slots[0]) {
HeapFree(GetProcessHeap(), 0, reader_name_slots[0]);
}
if (reader_name_slots[1]) {
HeapFree(GetProcessHeap(), 0, reader_name_slots[1]);
}
return 0;
}
void scard_threadstart() {
std::thread t(scard_threadmain);
t.detach();
}
void scard_fini() {
should_exit = true;
}