2
* Copyright (C) 2014 Kyle Manna <kyle@kylemanna.com>
3
* Copyright (C) 2017-2021 KeePassXC Team <team@keepassxc.org>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 2 or (at your option)
8
* version 3 of the License.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#include "YubiKeyInterfaceUSB.h"
21
#include "core/Tools.h"
22
#include "crypto/Random.h"
23
#include "thirdparty/ykcore/ykcore.h"
24
#include "thirdparty/ykcore/ykstatus.h"
28
constexpr int MAX_KEYS = 4;
30
YK_KEY* openKey(int index)
32
static const int vids[] = {YUBICO_VID, ONLYKEY_VID};
33
static const int pids[] = {YUBIKEY_PID,
45
return yk_open_key_vid_pid(vids, sizeof(vids) / sizeof(vids[0]), pids, sizeof(pids) / sizeof(pids[0]), index);
48
void closeKey(YK_KEY* key)
55
if (yk_errno == YK_EUSBERR) {
56
qWarning("Hardware key USB error: %s", yk_usb_strerror());
58
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
62
unsigned int getSerial(YK_KEY* key)
65
if (!yk_get_serial(key, 1, 0, &serial)) {
72
YK_KEY* openKeySerial(unsigned int serial)
74
for (int i = 0; i < MAX_KEYS; ++i) {
75
auto* yk_key = openKey(i);
77
// If the provided serial number is 0, or the key matches the serial, return it
78
if (serial == 0 || getSerial(yk_key) == serial) {
82
} else if (yk_errno == YK_ENOKEY) {
83
// No more connected keys
92
YubiKeyInterfaceUSB::YubiKeyInterfaceUSB()
95
qDebug("YubiKey: Failed to initialize USB interface.");
101
YubiKeyInterfaceUSB::~YubiKeyInterfaceUSB()
106
YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::m_instance(Q_NULLPTR);
108
YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::instance()
111
m_instance = new YubiKeyInterfaceUSB();
117
YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
120
if (!isInitialized()) {
124
YubiKey::KeyMap keyMap;
126
// Try to detect up to 4 connected hardware keys
127
for (int i = 0; i < MAX_KEYS; ++i) {
128
auto yk_key = openKey(i);
130
auto serial = getSerial(yk_key);
136
auto st = ykds_alloc();
137
yk_get_status(yk_key, st);
139
yk_get_key_vid_pid(yk_key, &vid, &pid);
141
QString name = m_pid_names.value(pid, tr("Unknown"));
142
if (vid == ONLYKEY_VID) {
143
name = QStringLiteral("OnlyKey %ver");
145
if (name.contains("%ver")) {
146
name = name.replace("%ver", QString::number(ykds_version_major(st)));
150
for (int slot = 1; slot <= 2; ++slot) {
151
auto config = (slot == 1 ? CONFIG1_VALID : CONFIG2_VALID);
152
if (!(ykds_touch_level(st) & config)) {
153
// Slot is not configured
156
// Don't actually challenge a YubiKey Neo or below, they always require button press
157
// if it is enabled for the slot resulting in failed detection
158
if (pid <= NEO_OTP_U2F_CCID_PID) {
159
auto display = tr("%1 [%2] - Slot %3", "YubiKey NEO display fields")
160
.arg(name, QString::number(serial), QString::number(slot));
161
keyMap.insert({serial, slot}, display);
162
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
164
tr("%1 [%2] - Slot %3, %4", "YubiKey display fields")
166
QString::number(serial),
167
QString::number(slot),
168
wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request")
169
: tr("Passive", "USB Challenge-Response Key no interaction required"));
170
keyMap.insert({serial, slot}, display);
176
} else if (yk_errno == YK_ENOKEY) {
177
// No more keys are connected
179
} else if (yk_errno == YK_EUSBERR) {
180
qWarning("Hardware key USB error: %s", yk_usb_strerror());
182
qWarning("Hardware key error: %s", yk_strerror(yk_errno));
190
* Issue a test challenge to the specified slot to determine if challenge
191
* response is properly configured.
193
* @param slot YubiKey configuration slot
194
* @param wouldBlock return if the operation requires user input
195
* @return whether the challenge succeeded
197
bool YubiKeyInterfaceUSB::testChallenge(YubiKeySlot slot, bool* wouldBlock)
200
auto* yk_key = openKeySerial(slot.first);
202
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
208
bool YubiKeyInterfaceUSB::performTestChallenge(void* key, int slot, bool* wouldBlock)
210
auto chall = randomGen()->randomArray(1);
211
Botan::secure_vector<char> resp;
212
auto ret = performChallenge(static_cast<YK_KEY*>(key), slot, false, chall, resp);
213
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS || ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK) {
215
*wouldBlock = ret == YubiKey::ChallengeResult::YCR_WOULDBLOCK;
223
* Issue a challenge to the specified slot
224
* This operation could block if the YubiKey requires a touch to trigger.
226
* @param slot YubiKey configuration slot
227
* @param challenge challenge input to YubiKey
228
* @param response response output from YubiKey
229
* @return challenge result
231
YubiKey::ChallengeResult
232
YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
235
if (!m_initialized) {
236
m_error = tr("The YubiKey USB interface has not been initialized.");
237
return YubiKey::ChallengeResult::YCR_ERROR;
240
auto* yk_key = openKeySerial(slot.first);
242
// Key with specified serial number is not connected
244
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
245
return YubiKey::ChallengeResult::YCR_ERROR;
248
emit challengeStarted();
249
auto ret = performChallenge(yk_key, slot.second, true, challenge, response);
252
emit challengeCompleted();
257
YubiKey::ChallengeResult YubiKeyInterfaceUSB::performChallenge(void* key,
260
const QByteArray& challenge,
261
Botan::secure_vector<char>& response)
264
int yk_cmd = (slot == 1) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_HMAC2;
265
QByteArray paddedChallenge = challenge;
267
// yk_challenge_response() insists on 64 bytes response buffer */
271
/* The challenge sent to the yubikey should always be 64 bytes for
272
* compatibility with all configurations. Follow PKCS7 padding.
274
* There is some question whether or not 64 bytes fixed length
275
* configurations even work, some docs say avoid it.
277
const int padLen = 64 - paddedChallenge.size();
279
paddedChallenge.append(QByteArray(padLen, padLen));
282
const unsigned char* c;
284
c = reinterpret_cast<const unsigned char*>(paddedChallenge.constData());
285
r = reinterpret_cast<unsigned char*>(response.data());
287
int ret = yk_challenge_response(
288
static_cast<YK_KEY*>(key), yk_cmd, mayBlock, paddedChallenge.size(), c, response.size(), r);
290
// actual HMAC-SHA1 response is only 20 bytes
294
if (yk_errno == YK_EWOULDBLOCK) {
295
return YubiKey::ChallengeResult::YCR_WOULDBLOCK;
296
} else if (yk_errno) {
297
if (yk_errno == YK_ETIMEOUT) {
298
m_error = tr("Hardware key timed out waiting for user interaction.");
299
} else if (yk_errno == YK_EUSBERR) {
300
m_error = tr("A USB error occurred when accessing the hardware key: %1").arg(yk_usb_strerror());
302
m_error = tr("Failed to complete a challenge-response, the specific error was: %1")
303
.arg(yk_strerror(yk_errno));
306
return YubiKey::ChallengeResult::YCR_ERROR;
310
return YubiKey::ChallengeResult::YCR_SUCCESS;