2
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 3 of the License, or
7
* (at your option) any later version.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "PasskeyUtils.h"
19
#include "BrowserMessageBuilder.h"
20
#include "BrowserPasskeys.h"
21
#include "core/Tools.h"
22
#include "core/UrlTools.h"
27
Q_GLOBAL_STATIC(PasskeyUtils, s_passkeyUtils);
29
PasskeyUtils* PasskeyUtils::instance()
31
return s_passkeyUtils;
34
int PasskeyUtils::checkLimits(const QJsonObject& pkOptions) const
36
const auto challenge = pkOptions["challenge"].toString();
37
if (challenge.isEmpty() || challenge.length() < 16) {
38
return ERROR_PASSKEYS_INVALID_CHALLENGE;
41
const auto userIdBase64 = pkOptions["user"]["id"].toString();
42
const auto userId = browserMessageBuilder()->getArrayFromBase64(userIdBase64);
43
if (userId.isEmpty() || (userId.length() < 1 || userId.length() > 64)) {
44
return ERROR_PASSKEYS_INVALID_USER_ID;
47
return PASSKEYS_SUCCESS;
50
// Basic check for the object that it contains necessary variables in a correct form
51
bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialCreationOptions) const
53
if (!credentialCreationOptions["attestation"].isString()
54
|| credentialCreationOptions["attestation"].toString().isEmpty()
55
|| !credentialCreationOptions["clientDataJSON"].isObject()
56
|| credentialCreationOptions["clientDataJSON"].toObject().isEmpty()
57
|| !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty()
58
|| !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty()
59
|| !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined()
60
|| !credentialCreationOptions["userPresence"].isBool()
61
|| credentialCreationOptions["userPresence"].isUndefined()
62
|| !credentialCreationOptions["userVerification"].isBool()
63
|| credentialCreationOptions["userVerification"].isUndefined()
64
|| !credentialCreationOptions["credTypesAndPubKeyAlgs"].isArray()
65
|| credentialCreationOptions["credTypesAndPubKeyAlgs"].toArray().isEmpty()
66
|| !credentialCreationOptions["excludeCredentials"].isArray()
67
|| credentialCreationOptions["excludeCredentials"].isUndefined()) {
74
// Basic check for the object that it contains necessary variables in a correct form
75
bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const
77
if (!assertionOptions["clientDataJson"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty()
78
|| !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty()
79
|| !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined()
80
|| !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) {
87
int PasskeyUtils::getEffectiveDomain(const QString& origin, QString* result) const
90
return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
93
if (origin.isEmpty()) {
94
return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
97
const auto effectiveDomain = QUrl::fromUserInput(origin).host();
98
if (!isDomain(effectiveDomain)) {
99
return ERROR_PASSKEYS_DOMAIN_IS_NOT_VALID;
102
*result = effectiveDomain;
103
return PASSKEYS_SUCCESS;
106
int PasskeyUtils::validateRpId(const QJsonValue& rpIdValue, const QString& effectiveDomain, QString* result) const
109
return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH;
112
if (effectiveDomain.isEmpty()) {
113
return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
116
// The RP ID defaults to being the caller's origin's effective domain unless the caller has explicitly set
118
if (rpIdValue.isUndefined() || rpIdValue.isNull()) {
119
*result = effectiveDomain;
120
return PASSKEYS_SUCCESS;
123
const auto rpId = rpIdValue.toString();
124
if (!isRegistrableDomainSuffix(rpId, effectiveDomain)) {
125
return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH;
128
if (rpId == effectiveDomain) {
129
*result = effectiveDomain;
130
return PASSKEYS_SUCCESS;
134
return PASSKEYS_SUCCESS;
137
// https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dom-publickeycredentialcreationoptions-attestation
138
QString PasskeyUtils::parseAttestation(const QString& attestation) const
140
return attestation == BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT ? BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT
141
: BrowserPasskeys::PASSKEYS_ATTESTATION_NONE;
144
QJsonArray PasskeyUtils::parseCredentialTypes(const QJsonArray& credentialTypes) const
146
QJsonArray credTypesAndPubKeyAlgs;
148
if (credentialTypes.isEmpty()) {
149
// Set default values
150
credTypesAndPubKeyAlgs.push_back(QJsonObject({
151
{"type", BrowserPasskeys::PUBLIC_KEY},
152
{"alg", WebAuthnAlgorithms::ES256},
154
credTypesAndPubKeyAlgs.push_back(QJsonObject({
155
{"type", BrowserPasskeys::PUBLIC_KEY},
156
{"alg", WebAuthnAlgorithms::RS256},
159
for (const auto current : credentialTypes) {
160
if (current["type"] != BrowserPasskeys::PUBLIC_KEY || current["alg"].isUndefined()) {
164
const auto currentAlg = current["alg"].toInt();
165
if (currentAlg != WebAuthnAlgorithms::ES256 && currentAlg != WebAuthnAlgorithms::RS256
166
&& currentAlg != WebAuthnAlgorithms::EDDSA) {
170
credTypesAndPubKeyAlgs.push_back(QJsonObject({
171
{"type", current["type"]},
177
return credTypesAndPubKeyAlgs;
180
bool PasskeyUtils::isAuthenticatorSelectionValid(const QJsonObject& authenticatorSelection) const
182
const auto authenticatorAttachment = authenticatorSelection["authenticatorAttachment"].toString();
183
if (!authenticatorAttachment.isEmpty() && authenticatorAttachment != BrowserPasskeys::ATTACHMENT_PLATFORM
184
&& authenticatorAttachment != BrowserPasskeys::ATTACHMENT_CROSS_PLATFORM) {
188
const auto requireResidentKey = authenticatorSelection["requireResidentKey"].toBool();
189
if (requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
193
const auto residentKey = authenticatorSelection["residentKey"].toString();
194
if (residentKey == "required" && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
198
if (residentKey.isEmpty() && requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
202
const auto userVerification = authenticatorSelection["userVerification"].toBool();
203
if (userVerification && !BrowserPasskeys::SUPPORT_USER_VERIFICATION) {
210
bool PasskeyUtils::isRegistrableDomainSuffix(const QString& hostSuffixString, const QString& originalHost) const
212
if (hostSuffixString.isEmpty()) {
216
if (!isDomain(originalHost)) {
220
const auto hostSuffix = QUrl::fromUserInput(hostSuffixString).host();
221
if (hostSuffix == originalHost) {
225
if (!isDomain(hostSuffix)) {
229
const auto prefixedHostSuffix = QString(".%1").arg(hostSuffix);
230
if (!originalHost.endsWith(prefixedHostSuffix)) {
234
if (hostSuffix == urlTools()->getTopLevelDomainFromUrl(hostSuffix)) {
238
const auto originalPublicSuffix = urlTools()->getTopLevelDomainFromUrl(originalHost);
239
if (originalPublicSuffix.isEmpty()) {
243
if (originalPublicSuffix.endsWith(prefixedHostSuffix)) {
247
if (!hostSuffix.endsWith(QString(".%1").arg(originalPublicSuffix))) {
254
bool PasskeyUtils::isDomain(const QString& hostName) const
256
const auto domain = QUrl::fromUserInput(hostName).host();
257
return !domain.isEmpty() && !domain.endsWith('.') && Tools::isAsciiString(domain)
258
&& !urlTools()->domainHasIllegalCharacters(domain) && !urlTools()->isIpAddress(hostName);
261
bool PasskeyUtils::isUserVerificationValid(const QString& userVerification) const
263
return QStringList({BrowserPasskeys::REQUIREMENT_PREFERRED,
264
BrowserPasskeys::REQUIREMENT_REQUIRED,
265
BrowserPasskeys::REQUIREMENT_DISCOURAGED})
266
.contains(userVerification);
269
bool PasskeyUtils::isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const
271
if (origin.startsWith("https://") || (allowLocalhostWithPasskeys && origin.startsWith("file://"))) {
275
if (!allowLocalhostWithPasskeys) {
279
const auto host = QUrl::fromUserInput(origin).host();
280
return host == "localhost" || host == "localhost." || host.endsWith(".localhost") || host.endsWith(".localhost.");
283
bool PasskeyUtils::isResidentKeyRequired(const QJsonObject& authenticatorSelection) const
285
if (authenticatorSelection.isEmpty()) {
289
const auto residentKey = authenticatorSelection["residentKey"].toString();
290
if (residentKey == BrowserPasskeys::REQUIREMENT_REQUIRED
291
|| (BrowserPasskeys::SUPPORT_RESIDENT_KEYS && residentKey == BrowserPasskeys::REQUIREMENT_PREFERRED)) {
293
} else if (residentKey == BrowserPasskeys::REQUIREMENT_DISCOURAGED) {
297
return authenticatorSelection["requireResidentKey"].toBool();
300
bool PasskeyUtils::isUserVerificationRequired(const QJsonObject& authenticatorSelection) const
302
const auto userVerification = authenticatorSelection["userVerification"].toString();
303
return userVerification == BrowserPasskeys::REQUIREMENT_REQUIRED
304
|| (userVerification == BrowserPasskeys::REQUIREMENT_PREFERRED
305
&& BrowserPasskeys::SUPPORT_USER_VERIFICATION);
308
ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const
310
const QStringList allowedKeys = {"credProps", "uvm"};
312
// Remove unsupported keys
313
for (const auto& key : extensionObject.keys()) {
314
if (!allowedKeys.contains(key)) {
315
extensionObject.remove(key);
319
// Create response object
320
QJsonObject extensionJSON;
322
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
323
if (extensionObject.contains("credProps") && extensionObject["credProps"].toBool()) {
324
extensionJSON["credProps"] = QJsonObject({{"rk", true}});
327
// https://w3c.github.io/webauthn/#sctn-uvm-extension
328
if (extensionObject.contains("uvm") && extensionObject["uvm"].toBool()) {
329
QJsonArray uvmResponse;
330
QJsonArray uvmArray = {
331
1, // userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001)
332
1, // keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
333
1, // matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
335
uvmResponse.append(uvmArray);
336
extensionJSON["uvm"] = uvmResponse;
339
if (extensionJSON.isEmpty()) {
343
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
344
if (!extensionData.isEmpty()) {
345
ExtensionResult result;
346
result.extensionData = extensionData;
347
result.extensionObject = extensionJSON;
354
QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
356
QJsonObject clientData;
357
clientData["challenge"] = publicKey["challenge"];
358
clientData["crossOrigin"] = false;
359
clientData["origin"] = origin;
360
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
365
QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const
367
QStringList allowedCredentials;
368
for (const auto& credential : assertionOptions["allowCredentials"].toArray()) {
369
const auto cred = credential.toObject();
370
const auto id = cred["id"].toString();
371
const auto transports = cred["transports"].toArray();
372
const auto hasSupportedTransport = transports.isEmpty()
373
|| (transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_INTERNAL)
374
|| transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_NFC)
375
|| transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_USB));
377
if (cred["type"].toString() == BrowserPasskeys::PUBLIC_KEY && hasSupportedTransport && !id.isEmpty()) {
378
allowedCredentials << id;
382
return allowedCredentials;
385
// For compatibility with StrongBox (and other possible clients in the future)
386
QString PasskeyUtils::getCredentialIdFromEntry(const Entry* entry) const
392
return entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
393
? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
394
: entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
397
// For compatibility with StrongBox (and other possible clients in the future)
398
QString PasskeyUtils::getUsernameFromEntry(const Entry* entry) const
404
return entry->attributes()->hasKey(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
405
? entry->attributes()->value(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
406
: entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);