-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathYubiKeyInterfacePCSC.cpp
More file actions
774 lines (687 loc) · 30.7 KB
/
YubiKeyInterfacePCSC.cpp
File metadata and controls
774 lines (687 loc) · 30.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
/*
* 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 "core/Tools.h"
#include "crypto/Random.h"
#include <QScopeGuard>
// 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;
typedef uint32_t RETVAL;
#else
typedef unsigned long SCUINT;
typedef long RETVAL;
#endif
// LPWSTR is needs to be defined when compiling outside of Windows
#ifndef Q_OS_WIN
#define LPWSTR char *
#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
*/
RETVAL ensureValidContext(SCARDCONTEXT& context)
{
// This check only tests if the handle pointer is valid in memory
// but it does not actually verify that it works
RETVAL 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
// On Linux, restarting the pcsc daemon causes the API server to die as well
if (rv == SCARD_E_SERVICE_STOPPED || rv == SCARD_E_NO_SERVICE) {
// 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
auto 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, static_cast<LPWSTR>(mszReaders), &dwReaders);
if (rv == SCARD_S_SUCCESS) {
char* readhead = mszReaders;
// Names are separated 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
*/
RETVAL getCardStatus(SCARDHANDLE handle, SCUINT& dwProt, const SCARD_IO_REQUEST*& pioSendPci)
{
char pbReader[MAX_READERNAME] = {0}; // Name of the reader the card is placed in
SCUINT dwReaderLen = sizeof(pbReader); // String length of the reader name
SCUINT dwState = 0; // Unused. Contents differ depending on API implementation.
uint8_t pbAtr[MAX_ATR_SIZE] = {0}; // ATR record
SCUINT dwAtrLen = sizeof(pbAtr); // ATR record size
auto rv = SCardStatus(handle, static_cast<LPWSTR>(pbReader), &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv == 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 = 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
*/
RETVAL transactRetry(SCARDHANDLE handle, const std::function<RETVAL()>& atomic_action)
{
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
const SCARD_IO_REQUEST* pioSendPci = nullptr;
auto rv = getCardStatus(handle, dwProt, pioSendPci);
if (rv == SCARD_S_SUCCESS) {
// Begin a transaction. This locks out any other process from interfacing with the card
rv = SCardBeginTransaction(handle);
if (rv == 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
RETVAL rv_act = atomic_action();
if (rv_act == 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("Smartcard 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 = SCARD_W_RESET_CARD;
qDebug("Smartcard 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
*/
RETVAL transmit(SCARDHANDLE handle,
const uint8_t* pbSendBuffer,
SCUINT dwSendLength,
uint8_t* pbRecvBuffer,
SCUINT& dwRecvLength)
{
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
const SCARD_IO_REQUEST* pioSendPci = nullptr;
auto rv = getCardStatus(handle, dwProt, pioSendPci);
if (rv == SCARD_S_SUCCESS) {
// Write to and read from the card
// pioRecvPci is nullptr because we do not expect any PCI response header
const SCUINT dwRecvBufferSize = dwRecvLength;
rv = SCardTransmit(handle, pioSendPci, pbSendBuffer, dwSendLength, nullptr, pbRecvBuffer, &dwRecvLength);
if (dwRecvLength < 2) {
// Any valid response should be at least 2 bytes (response status)
// However the protocol itself could fail
return SCARD_E_UNEXPECTED;
}
uint8_t SW1 = pbRecvBuffer[dwRecvLength - 2];
// Check for the MoreDataAvailable SW1 code. If present, send GetResponse command repeatedly, until success
// SW, or filling the receiving buffer.
if (SW1 == SW_MORE_DATA_HIGH) {
while (true) {
if (dwRecvBufferSize < dwRecvLength) {
// No free buffer space remaining
return SCARD_E_UNEXPECTED;
}
// Overwrite Status Word in the receiving buffer
dwRecvLength -= 2;
SCUINT dwRecvLength_sr = dwRecvBufferSize - dwRecvLength; // at least 2 bytes for SW are available
const uint8_t bRecvDataSize =
qBound(static_cast<SCUINT>(0), dwRecvLength_sr - 2, static_cast<SCUINT>(255));
uint8_t pbSendBuffer_sr[] = {CLA_ISO, INS_GET_RESPONSE, 0, 0, bRecvDataSize};
rv = SCardTransmit(handle,
pioSendPci,
pbSendBuffer_sr,
sizeof pbSendBuffer_sr,
nullptr,
pbRecvBuffer + dwRecvLength,
&dwRecvLength_sr);
// Check if any new data are received. Break if the smart card's status is other than success,
// or no new bytes were received.
if (!(rv == SCARD_S_SUCCESS && dwRecvLength_sr >= 2)) {
break;
}
dwRecvLength += dwRecvLength_sr;
SW1 = pbRecvBuffer[dwRecvLength - 2];
// Break the loop if there is no continuation status
if (SW1 != SW_MORE_DATA_HIGH) {
break;
}
}
}
if (rv == SCARD_S_SUCCESS) {
if (dwRecvLength < 2) {
// Any valid response should be at least 2 bytes (response status)
// However the protocol itself could fail
rv = SCARD_E_UNEXPECTED;
} else {
const uint8_t SW_HIGH = pbRecvBuffer[dwRecvLength - 2];
const uint8_t SW_LOW = pbRecvBuffer[dwRecvLength - 1];
if (SW_HIGH == SW_OK_HIGH && SW_LOW == SW_OK_LOW) {
rv = SCARD_S_SUCCESS;
} else if (SW_HIGH == SW_PRECOND_HIGH && SW_LOW == 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 = SCARD_W_CARD_NOT_AUTHENTICATED;
} else if ((SW_HIGH == SW_NOTFOUND_HIGH && SW_LOW == SW_NOTFOUND_LOW) || SW_HIGH == SW_UNSUP_HIGH) {
// This happens eg. during a select command when the AID is not found
rv = SCARD_E_FILE_NOT_FOUND;
} else {
rv = 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
*/
RETVAL 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());
// Give it more space in case custom implementations have longer answer to select
uint8_t pbRecvBuffer[64] = {
0}; // 3 bytes version, 1 byte program counter, other stuff for various implementations, 2 bytes status
SCUINT dwRecvLength = sizeof pbRecvBuffer;
auto 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
auto 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
*/
RETVAL getSerial(const SCardAID& handle, unsigned int& serial)
{
// Ensure the transmission is retransmitted after card resets
return transactRetry(handle.first, [&handle, &serial]() {
// Ensure that the card is always selected before sending the command
auto rv = selectApplet(handle);
if (rv != SCARD_S_SUCCESS) {
return rv;
}
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 = transmit(handle.first, pbSendBuffer, 5, pbRecvBuffer, dwRecvLength);
if (rv == 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;
});
}
/***
* @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
*/
RETVAL openKeySerial(const unsigned int target_serial,
SCARDCONTEXT& context,
const QList<QByteArray>& aid_codes,
SCardAID* handle)
{
// Ensure the Smartcard API handle is still valid
auto rv = ensureValidContext(context);
if (rv != SCARD_S_SUCCESS) {
return rv;
}
auto 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,
static_cast<const LPWSTR>(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
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
SCUINT dwState = 0;
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
SCUINT dwAtrLen = sizeof(pbAtr);
rv = SCardStatus(hCard, static_cast<LPWSTR>(pbReader), &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv == SCARD_S_SUCCESS && (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;
}
}
}
SCardDisconnect(hCard, SCARD_LEAVE_CARD);
}
}
return SCARD_E_NO_SMARTCARD;
}
/***
* @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
*/
RETVAL 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]() {
auto rv = selectApplet(handle);
// Ensure that the card is always selected before sending the command
if (rv != SCARD_S_SUCCESS) {
return rv;
}
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 = transmit(handle.first, pbSendBuffer, 5 + 64, pbRecvBuffer, dwRecvLength);
if (rv == 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 == SCARD_S_SUCCESS && dwRecvLength != 22) {
return SCARD_E_FILE_NOT_FOUND;
}
return rv;
});
}
} // namespace
YubiKeyInterfacePCSC::YubiKeyInterfacePCSC()
: YubiKeyInterface()
{
if (ensureValidContext(m_sc_context) != SCARD_S_SUCCESS) {
qDebug("YubiKey: Failed to establish PC/SC context.");
} else {
m_initialized = true;
}
}
YubiKeyInterfacePCSC::~YubiKeyInterfacePCSC()
{
if (m_initialized && SCardReleaseContext(m_sc_context) != SCARD_S_SUCCESS) {
qDebug("YubiKey: Failed to release PC/SC context.");
}
}
YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::m_instance(nullptr);
YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::instance()
{
if (!m_instance) {
m_instance = new YubiKeyInterfacePCSC();
}
return m_instance;
}
YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys(int& connectedKeys)
{
m_error.clear();
if (!isInitialized()) {
return {};
}
YubiKey::KeyMap foundKeys;
// Connect to each reader and look for cards
for (const auto& reader_name : getReaders(m_sc_context)) {
/* 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;
auto rv = SCardConnect(m_sc_context,
static_cast<const LPWSTR>(reader_name.toStdString().c_str()),
SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
&hCard,
&dwActiveProtocol);
if (rv != SCARD_S_SUCCESS) {
// Cannot connect to the reader
continue;
}
auto finally = qScopeGuard([hCard]() { SCardDisconnect(hCard, SCARD_LEAVE_CARD); });
// Read the protocol and the ATR record
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
SCUINT dwState = 0;
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
SCUINT dwAtrLen = sizeof(pbAtr);
rv = SCardStatus(hCard, static_cast<LPWSTR>(pbReader), &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv != SCARD_S_SUCCESS || (dwProt != SCARD_PROTOCOL_T0 && dwProt != SCARD_PROTOCOL_T1)) {
// Could not read the ATR record or the protocol is not supported
continue;
}
// 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(reinterpret_cast<char*>(pbAtr), dwAtrLen);
QString name("Unknown Key");
if (m_atr_names.contains(atr)) {
name = m_atr_names.value(atr);
}
unsigned int serial = 0;
getSerial(satr, serial);
++connectedKeys;
/* 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 interaction request will be displayed. */
for (int slot = 1; slot <= 2; ++slot) {
if (performTestChallenge(&satr, slot, &wouldBlock)) {
auto display =
tr("(NFC) %1 [%2] - Slot %3, %4", "YubiKey display fields")
.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"));
foundKeys.insert({serial, slot}, display);
}
}
}
}
return foundKeys;
}
bool YubiKeyInterfacePCSC::testChallenge(YubiKeySlot slot, bool* wouldBlock)
{
bool ret = false;
SCardAID hCard;
auto 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 PC/SC interface has not been initialized.");
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
// 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) {
auto 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 comment above) */
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS) {
emit challengeCompleted();
return ret;
}
}
if (--tries > 0) {
Tools::sleep(250);
}
}
emit challengeCompleted();
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
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 preceding 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));
}
auto c = reinterpret_cast<const unsigned char*>(paddedChallenge.constData());
auto r = reinterpret_cast<unsigned char*>(response.data());
auto rv = getHMAC(*static_cast<SCardAID*>(key), yk_cmd, c, r);
if (rv != SCARD_S_SUCCESS) {
if (rv == 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 == SCARD_E_FILE_NOT_FOUND) {
m_error = tr("Hardware key was not found or is not configured.");
} else {
m_error =
tr("Failed to complete a challenge-response, the PC/SC error code was: %1").arg(QString::number(rv));
}
return YubiKey::ChallengeResult::YCR_ERROR;
}
return YubiKey::ChallengeResult::YCR_SUCCESS;
}