/* * Copyright (C) 2024 KeePassXC Team * * 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 3 of the License, or * (at your option) any later version. * * 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 . */ #include "PasskeyUtils.h" #include "BrowserMessageBuilder.h" #include "BrowserPasskeys.h" #include "core/Tools.h" #include "core/UrlTools.h" #include #include Q_GLOBAL_STATIC(PasskeyUtils, s_passkeyUtils); PasskeyUtils* PasskeyUtils::instance() { return s_passkeyUtils; } int PasskeyUtils::checkLimits(const QJsonObject& pkOptions) const { const auto challenge = pkOptions["challenge"].toString(); if (challenge.isEmpty() || challenge.length() < 16) { return ERROR_PASSKEYS_INVALID_CHALLENGE; } const auto userIdBase64 = pkOptions["user"]["id"].toString(); const auto userId = browserMessageBuilder()->getArrayFromBase64(userIdBase64); if (userId.isEmpty() || (userId.length() < 1 || userId.length() > 64)) { return ERROR_PASSKEYS_INVALID_USER_ID; } return PASSKEYS_SUCCESS; } // Basic check for the object that it contains necessary variables in a correct form bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialCreationOptions) const { if (!credentialCreationOptions["attestation"].isString() || credentialCreationOptions["attestation"].toString().isEmpty() || !credentialCreationOptions["clientDataJSON"].isObject() || credentialCreationOptions["clientDataJSON"].toObject().isEmpty() || !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty() || !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty() || !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined() || !credentialCreationOptions["userPresence"].isBool() || credentialCreationOptions["userPresence"].isUndefined() || !credentialCreationOptions["userVerification"].isBool() || credentialCreationOptions["userVerification"].isUndefined() || !credentialCreationOptions["credTypesAndPubKeyAlgs"].isArray() || credentialCreationOptions["credTypesAndPubKeyAlgs"].toArray().isEmpty() || !credentialCreationOptions["excludeCredentials"].isArray() || credentialCreationOptions["excludeCredentials"].isUndefined()) { return false; } return true; } // Basic check for the object that it contains necessary variables in a correct form bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const { if (!assertionOptions["clientDataJson"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty() || !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty() || !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined() || !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) { return false; } return true; } int PasskeyUtils::getEffectiveDomain(const QString& origin, QString* result) const { if (!result) { return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED; } if (origin.isEmpty()) { return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED; } const auto effectiveDomain = QUrl::fromUserInput(origin).host(); if (!isDomain(effectiveDomain)) { return ERROR_PASSKEYS_DOMAIN_IS_NOT_VALID; } *result = effectiveDomain; return PASSKEYS_SUCCESS; } int PasskeyUtils::validateRpId(const QJsonValue& rpIdValue, const QString& effectiveDomain, QString* result) const { if (!result) { return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH; } if (effectiveDomain.isEmpty()) { return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED; } // The RP ID defaults to being the caller's origin's effective domain unless the caller has explicitly set // options.rp.id if (rpIdValue.isUndefined() || rpIdValue.isNull()) { *result = effectiveDomain; return PASSKEYS_SUCCESS; } const auto rpId = rpIdValue.toString(); if (!isRegistrableDomainSuffix(rpId, effectiveDomain)) { return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH; } if (rpId == effectiveDomain) { *result = effectiveDomain; return PASSKEYS_SUCCESS; } *result = rpId; return PASSKEYS_SUCCESS; } // https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dom-publickeycredentialcreationoptions-attestation QString PasskeyUtils::parseAttestation(const QString& attestation) const { return attestation == BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT ? BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT : BrowserPasskeys::PASSKEYS_ATTESTATION_NONE; } QJsonArray PasskeyUtils::parseCredentialTypes(const QJsonArray& credentialTypes) const { QJsonArray credTypesAndPubKeyAlgs; if (credentialTypes.isEmpty()) { // Set default values credTypesAndPubKeyAlgs.push_back(QJsonObject({ {"type", BrowserPasskeys::PUBLIC_KEY}, {"alg", WebAuthnAlgorithms::ES256}, })); credTypesAndPubKeyAlgs.push_back(QJsonObject({ {"type", BrowserPasskeys::PUBLIC_KEY}, {"alg", WebAuthnAlgorithms::RS256}, })); } else { for (const auto current : credentialTypes) { if (current["type"] != BrowserPasskeys::PUBLIC_KEY || current["alg"].isUndefined()) { continue; } const auto currentAlg = current["alg"].toInt(); if (currentAlg != WebAuthnAlgorithms::ES256 && currentAlg != WebAuthnAlgorithms::RS256 && currentAlg != WebAuthnAlgorithms::EDDSA) { continue; } credTypesAndPubKeyAlgs.push_back(QJsonObject({ {"type", current["type"]}, {"alg", currentAlg}, })); } } return credTypesAndPubKeyAlgs; } bool PasskeyUtils::isAuthenticatorSelectionValid(const QJsonObject& authenticatorSelection) const { const auto authenticatorAttachment = authenticatorSelection["authenticatorAttachment"].toString(); if (!authenticatorAttachment.isEmpty() && authenticatorAttachment != BrowserPasskeys::ATTACHMENT_PLATFORM && authenticatorAttachment != BrowserPasskeys::ATTACHMENT_CROSS_PLATFORM) { return false; } const auto requireResidentKey = authenticatorSelection["requireResidentKey"].toBool(); if (requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) { return false; } const auto residentKey = authenticatorSelection["residentKey"].toString(); if (residentKey == "required" && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) { return false; } if (residentKey.isEmpty() && requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) { return false; } const auto userVerification = authenticatorSelection["userVerification"].toBool(); if (userVerification && !BrowserPasskeys::SUPPORT_USER_VERIFICATION) { return false; } return true; } bool PasskeyUtils::isRegistrableDomainSuffix(const QString& hostSuffixString, const QString& originalHost) const { if (hostSuffixString.isEmpty()) { return false; } if (!isDomain(originalHost)) { return false; } const auto hostSuffix = QUrl::fromUserInput(hostSuffixString).host(); if (hostSuffix == originalHost) { return true; } if (!isDomain(hostSuffix)) { return false; } const auto prefixedHostSuffix = QString(".%1").arg(hostSuffix); if (!originalHost.endsWith(prefixedHostSuffix)) { return false; } if (hostSuffix == urlTools()->getTopLevelDomainFromUrl(hostSuffix)) { return false; } const auto originalPublicSuffix = urlTools()->getTopLevelDomainFromUrl(originalHost); if (originalPublicSuffix.isEmpty()) { return false; } if (originalPublicSuffix.endsWith(prefixedHostSuffix)) { return false; } if (!hostSuffix.endsWith(QString(".%1").arg(originalPublicSuffix))) { return false; } return true; } bool PasskeyUtils::isDomain(const QString& hostName) const { const auto domain = QUrl::fromUserInput(hostName).host(); return !domain.isEmpty() && !domain.endsWith('.') && Tools::isAsciiString(domain) && !urlTools()->domainHasIllegalCharacters(domain) && !urlTools()->isIpAddress(hostName); } bool PasskeyUtils::isUserVerificationValid(const QString& userVerification) const { return QStringList({BrowserPasskeys::REQUIREMENT_PREFERRED, BrowserPasskeys::REQUIREMENT_REQUIRED, BrowserPasskeys::REQUIREMENT_DISCOURAGED}) .contains(userVerification); } bool PasskeyUtils::isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const { if (origin.startsWith("https://") || (allowLocalhostWithPasskeys && origin.startsWith("file://"))) { return true; } if (!allowLocalhostWithPasskeys) { return false; } const auto host = QUrl::fromUserInput(origin).host(); return host == "localhost" || host == "localhost." || host.endsWith(".localhost") || host.endsWith(".localhost."); } bool PasskeyUtils::isResidentKeyRequired(const QJsonObject& authenticatorSelection) const { if (authenticatorSelection.isEmpty()) { return false; } const auto residentKey = authenticatorSelection["residentKey"].toString(); if (residentKey == BrowserPasskeys::REQUIREMENT_REQUIRED || (BrowserPasskeys::SUPPORT_RESIDENT_KEYS && residentKey == BrowserPasskeys::REQUIREMENT_PREFERRED)) { return true; } else if (residentKey == BrowserPasskeys::REQUIREMENT_DISCOURAGED) { return false; } return authenticatorSelection["requireResidentKey"].toBool(); } bool PasskeyUtils::isUserVerificationRequired(const QJsonObject& authenticatorSelection) const { const auto userVerification = authenticatorSelection["userVerification"].toString(); return userVerification == BrowserPasskeys::REQUIREMENT_REQUIRED || (userVerification == BrowserPasskeys::REQUIREMENT_PREFERRED && BrowserPasskeys::SUPPORT_USER_VERIFICATION); } ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const { const QStringList allowedKeys = {"credProps", "uvm"}; // Remove unsupported keys for (const auto& key : extensionObject.keys()) { if (!allowedKeys.contains(key)) { extensionObject.remove(key); } } // Create response object QJsonObject extensionJSON; // https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension if (extensionObject.contains("credProps") && extensionObject["credProps"].toBool()) { extensionJSON["credProps"] = QJsonObject({{"rk", true}}); } // https://w3c.github.io/webauthn/#sctn-uvm-extension if (extensionObject.contains("uvm") && extensionObject["uvm"].toBool()) { QJsonArray uvmResponse; QJsonArray uvmArray = { 1, // userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001) 1, // keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001) 1, // matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001) }; uvmResponse.append(uvmArray); extensionJSON["uvm"] = uvmResponse; } if (extensionJSON.isEmpty()) { return {}; } auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject); if (!extensionData.isEmpty()) { ExtensionResult result; result.extensionData = extensionData; result.extensionObject = extensionJSON; return result; } return {}; } QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const { QJsonObject clientData; clientData["challenge"] = publicKey["challenge"]; clientData["crossOrigin"] = false; clientData["origin"] = origin; clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create"); return clientData; } QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const { QStringList allowedCredentials; for (const auto& credential : assertionOptions["allowCredentials"].toArray()) { const auto cred = credential.toObject(); const auto id = cred["id"].toString(); const auto transports = cred["transports"].toArray(); const auto hasSupportedTransport = transports.isEmpty() || (transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_INTERNAL) || transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_NFC) || transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_USB)); if (cred["type"].toString() == BrowserPasskeys::PUBLIC_KEY && hasSupportedTransport && !id.isEmpty()) { allowedCredentials << id; } } return allowedCredentials; } // For compatibility with StrongBox (and other possible clients in the future) QString PasskeyUtils::getCredentialIdFromEntry(const Entry* entry) const { if (!entry) { return {}; } return entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID) ? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID) : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID); } // For compatibility with StrongBox (and other possible clients in the future) QString PasskeyUtils::getUsernameFromEntry(const Entry* entry) const { if (!entry) { return {}; } return entry->attributes()->hasKey(BrowserPasskeys::KPXC_PASSKEY_USERNAME) ? entry->attributes()->value(BrowserPasskeys::KPXC_PASSKEY_USERNAME) : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME); }