Passkeys improvements (#10318)

Refactors the Passkey implementation to include more checks and a structure that is more aligned with the official specification.
Notable changes:
- _BrowserService_ no longer does the checks by itself. A new class _BrowserPasskeysClient_ constructs the relevant objects, acting as a client. _BrowserService_ only acts as a bridge between the client and _BrowserPasskeys_ (authenticator) and calls the relevant popups for user interaction.
- A new helper class _PasskeyUtils_ includes the actual checks and parses the objects.
- _BrowserPasskeys_ is pretty much intact, but some functions have been moved to PasskeyUtils.
- Fixes Ed25519 encoding in _BrowserCBOR_.
- Adds new error messages.
- User confirmation for Passkey retrieval is also asked even if `discouraged` is used. This goes against the specification, but currently there's no other way to verify the user.
- `cross-platform` is also accepted for compatibility. This could be removed if there's a potential issue with it.
- Extension data is now handled correctly during Authentication.
- Allowed and excluded credentials are now handled correctly.
- `KPEX_PASSKEY_GENERATED_USER_ID` is renamed to `KPEX_PASSKEY_CREDENTIAL_ID`
- Adds a new option "Allow localhost with Passkeys" to Browser Integration -> Advanced tab. By default it's not allowed to access HTTP sites, but `http://localhost` can be allowed for debugging and testing purposes for local servers.
- Add tag `Passkey` to a Passkey entry, or an entry with an imported Passkey.

Fixes #10287.
This commit is contained in:
Sami Vänttinen 2024-03-06 14:42:01 +02:00 committed by GitHub
parent dff2f186ce
commit ac2b445db6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1248 additions and 269 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -19,6 +19,7 @@
#include "BrowserMessageBuilder.h"
#ifdef WITH_XC_BROWSER_PASSKEYS
#include "BrowserPasskeys.h"
#include "PasskeyUtils.h"
#endif
#include "BrowserSettings.h"
#include "core/Global.h"
@ -541,7 +542,7 @@ QJsonObject BrowserAction::handlePasskeysGet(const QJsonObject& json, const QStr
}
const auto origin = browserRequest.getString("origin");
if (!origin.startsWith("https://")) {
if (!passkeyUtils()->isOriginAllowedWithLocalhost(browserSettings()->allowLocalhostWithPasskeys(), origin)) {
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
}
@ -574,8 +575,8 @@ QJsonObject BrowserAction::handlePasskeysRegister(const QJsonObject& json, const
}
const auto origin = browserRequest.getString("origin");
if (!origin.startsWith("https://")) {
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
if (!passkeyUtils()->isOriginAllowedWithLocalhost(browserSettings()->allowLocalhostWithPasskeys(), origin)) {
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
}
const auto keyList = getConnectionKeys(browserRequest);

View file

@ -44,6 +44,11 @@ struct BrowserRequest
return decrypted.value(param).toArray();
}
inline bool getBool(const QString& param) const
{
return decrypted.value(param).toBool();
}
inline QJsonObject getObject(const QString& param) const
{
return decrypted.value(param).toObject();

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -48,6 +48,16 @@ QByteArray BrowserCbor::cborEncodeAttestation(const QByteArray& authData) const
// https://w3c.github.io/webauthn/#authdata-attestedcredentialdata-credentialpublickey
QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, const QByteArray& second) const
{
const auto keyType = getCoseKeyType(alg);
if (keyType == 0) {
return {};
}
const auto curveParameter = getCurveParameter(alg);
if ((alg == WebAuthnAlgorithms::ES256 || alg == WebAuthnAlgorithms::EDDSA) && curveParameter == 0) {
return {};
}
QByteArray result;
QCborStreamWriter writer(&result);
@ -56,7 +66,7 @@ QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, co
// Key type
writer.append(1);
writer.append(getCoseKeyType(alg));
writer.append(keyType);
// Signature algorithm
writer.append(3);
@ -64,7 +74,7 @@ QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, co
// Curve parameter
writer.append(-1);
writer.append(getCurveParameter(alg));
writer.append(curveParameter);
// Key x-coordinate
writer.append(-2);
@ -80,7 +90,7 @@ QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, co
// Key type
writer.append(1);
writer.append(getCoseKeyType(alg));
writer.append(keyType);
// Signature algorithm
writer.append(3);
@ -96,21 +106,24 @@ QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, co
writer.endMap();
} else if (alg == WebAuthnAlgorithms::EDDSA) {
// https://www.rfc-editor.org/rfc/rfc8152#section-13.2
writer.startMap(3);
writer.startMap(4);
// Key type
writer.append(1);
writer.append(keyType);
// Algorithm
writer.append(3);
writer.append(alg);
// Curve parameter
writer.append(-1);
writer.append(getCurveParameter(alg));
writer.append(curveParameter);
// Public key
writer.append(-2);
writer.append(first);
// Private key
writer.append(-4);
writer.append(second);
writer.endMap();
}
@ -230,7 +243,7 @@ unsigned int BrowserCbor::getCurveParameter(int alg) const
case WebAuthnAlgorithms::EDDSA:
return WebAuthnCurveKey::ED25519;
default:
return WebAuthnCurveKey::P256;
return WebAuthnCurveKey::INVALID_CURVE_KEY;
}
}
@ -240,14 +253,15 @@ unsigned int BrowserCbor::getCoseKeyType(int alg) const
{
switch (alg) {
case WebAuthnAlgorithms::ES256:
return WebAuthnCoseKeyType::EC2;
case WebAuthnAlgorithms::ES384:
case WebAuthnAlgorithms::ES512:
return WebAuthnCoseKeyType::EC2;
return WebAuthnCoseKeyType::INVALID_COSE_KEY_TYPE;
case WebAuthnAlgorithms::EDDSA:
return WebAuthnCoseKeyType::OKP;
case WebAuthnAlgorithms::RS256:
return WebAuthnCoseKeyType::RSA;
default:
return WebAuthnCoseKeyType::EC2;
return WebAuthnCoseKeyType::INVALID_COSE_KEY_TYPE;
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -35,6 +35,7 @@ enum WebAuthnAlgorithms : int
// https://www.rfc-editor.org/rfc/rfc9053#section-7.1
enum WebAuthnCurveKey : int
{
INVALID_CURVE_KEY = 0,
P256 = 1, // EC2, NIST P-256, also known as secp256r1
P384 = 2, // EC2, NIST P-384, also known as secp384r1
P521 = 3, // EC2, NIST P-521, also known as secp521r1
@ -48,6 +49,7 @@ enum WebAuthnCurveKey : int
// For RSA: https://www.rfc-editor.org/rfc/rfc8230#section-4
enum WebAuthnCoseKeyType : int
{
INVALID_COSE_KEY_TYPE = 0,
OKP = 1, // Octet Keypair
EC2 = 2, // Elliptic Curve
RSA = 3 // RSA

View file

@ -140,8 +140,22 @@ QString BrowserMessageBuilder::getErrorMessage(const int errorCode) const
return QObject::tr("Empty public key");
case ERROR_PASSKEYS_INVALID_URL_PROVIDED:
return QObject::tr("Invalid URL provided");
case ERROR_PASSKEYS_RESIDENT_KEYS_NOT_SUPPORTED:
return QObject::tr("Resident Keys are not supported");
case ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED:
return QObject::tr("Origin is empty or not allowed");
case ERROR_PASSKEYS_DOMAIN_IS_NOT_VALID:
return QObject::tr("Effective domain is not a valid domain");
case ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH:
return QObject::tr("Origin and RP ID do not match");
case ERROR_PASSKEYS_NO_SUPPORTED_ALGORITHMS:
return QObject::tr("No supported algorithms were provided");
case ERROR_PASSKEYS_WAIT_FOR_LIFETIMER:
return QObject::tr("Wait for timer to expire");
case ERROR_PASSKEYS_UNKNOWN_ERROR:
return QObject::tr("Unknown Passkeys error");
case ERROR_PASSKEYS_INVALID_CHALLENGE:
return QObject::tr("Challenge is shorter than required minimum length");
case ERROR_PASSKEYS_INVALID_USER_ID:
return QObject::tr("user.id does not match the required length");
default:
return QObject::tr("Unknown error");
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -55,7 +55,14 @@ namespace
ERROR_PASSKEYS_INVALID_USER_VERIFICATION = 23,
ERROR_PASSKEYS_EMPTY_PUBLIC_KEY = 24,
ERROR_PASSKEYS_INVALID_URL_PROVIDED = 25,
ERROR_PASSKEYS_RESIDENT_KEYS_NOT_SUPPORTED = 26,
ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED = 26,
ERROR_PASSKEYS_DOMAIN_IS_NOT_VALID = 27,
ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH = 28,
ERROR_PASSKEYS_NO_SUPPORTED_ALGORITHMS = 29,
ERROR_PASSKEYS_WAIT_FOR_LIFETIMER = 30,
ERROR_PASSKEYS_UNKNOWN_ERROR = 31,
ERROR_PASSKEYS_INVALID_CHALLENGE = 32,
ERROR_PASSKEYS_INVALID_USER_ID = 33,
};
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -18,8 +18,8 @@
#include "BrowserPasskeys.h"
#include "BrowserMessageBuilder.h"
#include "BrowserService.h"
#include "PasskeyUtils.h"
#include "crypto/Random.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtEndian>
@ -40,6 +40,13 @@ Q_GLOBAL_STATIC(BrowserPasskeys, s_browserPasskeys);
// KeePassXC AAGUID: fdb141b2-5d84-443e-8a35-4698c205a502
const QString BrowserPasskeys::AAGUID = QStringLiteral("fdb141b25d84443e8a354698c205a502");
// Authenticator capabilities
const QString BrowserPasskeys::ATTACHMENT_CROSS_PLATFORM = QStringLiteral("cross-platform");
const QString BrowserPasskeys::ATTACHMENT_PLATFORM = QStringLiteral("platform");
const QString BrowserPasskeys::AUTHENTICATOR_TRANSPORT = QStringLiteral("internal");
const bool BrowserPasskeys::SUPPORT_RESIDENT_KEYS = true;
const bool BrowserPasskeys::SUPPORT_USER_VERIFICATION = true;
const QString BrowserPasskeys::PUBLIC_KEY = QStringLiteral("public-key");
const QString BrowserPasskeys::REQUIREMENT_DISCOURAGED = QStringLiteral("discouraged");
const QString BrowserPasskeys::REQUIREMENT_PREFERRED = QStringLiteral("preferred");
@ -49,7 +56,7 @@ const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("dir
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
const QString BrowserPasskeys::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
const QString BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
const QString BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID = QStringLiteral("KPEX_PASSKEY_CREDENTIAL_ID");
const QString BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
const QString BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
const QString BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
@ -59,56 +66,80 @@ BrowserPasskeys* BrowserPasskeys::instance()
return s_browserPasskeys;
}
PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
const QString& origin,
PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJsonObject& credentialCreationOptions,
const TestingVariables& testingVariables)
{
QJsonObject publicKeyCredential;
if (!passkeyUtils()->checkCredentialCreationOptions(credentialCreationOptions)) {
return {};
}
const auto authenticatorAttachment = credentialCreationOptions["authenticatorAttachment"];
const auto clientDataJson = credentialCreationOptions["clientDataJSON"].toObject();
const auto extensions = credentialCreationOptions["extensions"].toString();
const auto credentialId = testingVariables.credentialId.isEmpty()
? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
: testingVariables.credentialId;
// Extensions
auto extensionObject = publicKeyCredentialOptions["extensions"].toObject();
const auto extensionData = buildExtensionData(extensionObject);
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
// Credential private key
const auto alg = getAlgorithmFromPublicKey(credentialCreationOptions);
const auto privateKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
if (privateKey.cborEncodedPublicKey.isEmpty() && privateKey.privateKeyPem.isEmpty()) {
// Key creation failed
return {};
}
// Attestation
const auto attestationObject = buildAttestationObject(
credentialCreationOptions, extensions, credentialId, privateKey.cborEncodedPublicKey, testingVariables);
if (attestationObject.isEmpty()) {
return {};
}
// Response
QJsonObject responseObject;
const auto clientData = buildClientDataJson(publicKeyCredentialOptions, origin, false);
const auto attestationObject =
buildAttestationObject(publicKeyCredentialOptions, extensions, credentialId, testingVariables);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientData);
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject.cborEncoded);
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
// PublicKeyCredential
publicKeyCredential["authenticatorAttachment"] = QString("platform");
QJsonObject publicKeyCredential;
publicKeyCredential["authenticatorAttachment"] = authenticatorAttachment;
publicKeyCredential["id"] = credentialId;
publicKeyCredential["response"] = responseObject;
publicKeyCredential["type"] = PUBLIC_KEY;
return {credentialId, publicKeyCredential, attestationObject.pem};
PublicKeyCredential result;
result.credentialId = credentialId;
result.key = privateKey.privateKeyPem;
result.response = publicKeyCredential;
return result;
}
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
const QString& origin,
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
const QString& credentialId,
const QString& userHandle,
const QString& privateKeyPem)
{
const auto authenticatorData = buildGetAttestationObject(publicKeyCredentialRequestOptions);
const auto clientData = buildClientDataJson(publicKeyCredentialRequestOptions, origin, true);
const auto clientDataArray = QJsonDocument(clientData).toJson(QJsonDocument::Compact);
if (!passkeyUtils()->checkCredentialAssertionOptions(assertionOptions)) {
return {};
}
const auto authenticatorData = buildAuthenticatorData(assertionOptions);
const auto clientDataJson = assertionOptions["clientDataJson"].toObject();
const auto clientDataArray = QJsonDocument(clientDataJson).toJson(QJsonDocument::Compact);
const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
if (signature.isEmpty()) {
return {};
}
QJsonObject responseObject;
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientDataJson);
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
responseObject["userHandle"] = userHandle;
QJsonObject publicKeyCredential;
publicKeyCredential["authenticatorAttachment"] = QString("platform");
publicKeyCredential["authenticatorAttachment"] = BrowserPasskeys::ATTACHMENT_PLATFORM;
publicKeyCredential["id"] = credentialId;
publicKeyCredential["response"] = responseObject;
publicKeyCredential["type"] = PUBLIC_KEY;
@ -116,68 +147,22 @@ QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& publ
return publicKeyCredential;
}
bool BrowserPasskeys::isUserVerificationValid(const QString& userVerification) const
{
return QStringList({REQUIREMENT_PREFERRED, REQUIREMENT_REQUIRED, REQUIREMENT_DISCOURAGED})
.contains(userVerification);
}
// See https://w3c.github.io/webauthn/#sctn-createCredential for default timeout values when not set in the request
int BrowserPasskeys::getTimeout(const QString& userVerification, int timeout) const
{
if (timeout == 0) {
return userVerification == REQUIREMENT_DISCOURAGED ? DEFAULT_DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT;
}
return timeout;
}
QStringList BrowserPasskeys::getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const
{
QStringList allowedCredentials;
for (const auto& cred : publicKey["allowCredentials"].toArray()) {
const auto c = cred.toObject();
const auto id = c["id"].toString();
if (c["type"].toString() == PUBLIC_KEY && !id.isEmpty()) {
allowedCredentials << id;
}
}
return allowedCredentials;
}
QJsonObject BrowserPasskeys::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get)
{
QJsonObject clientData;
clientData["challenge"] = publicKey["challenge"];
clientData["crossOrigin"] = false;
clientData["origin"] = origin;
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
return clientData;
}
// https://w3c.github.io/webauthn/#attestation-object
PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credentialCreationOptions,
const QString& extensions,
const QString& credentialId,
const QByteArray& cborEncodedPublicKey,
const TestingVariables& testingVariables)
{
QByteArray result;
// Create SHA256 hash from rpId
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rp"]["id"].toString());
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(credentialCreationOptions["rp"]["id"].toString());
result.append(rpIdHash);
// Use default flags
const auto flags =
setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
{"AT", true},
{"BS", false},
{"BE", false},
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
{"UP", true}}));
const auto flags = setFlagsFromJson(QJsonObject(
{{"ED", !extensions.isEmpty()}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
result.append(flags);
// Signature counter (not supported, always 0
@ -188,7 +173,7 @@ PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
result.append(browserMessageBuilder()->getArrayFromHexString(AAGUID));
// Credential length
const char credentialLength[2] = {0x00, 0x20};
const char credentialLength[2] = {0x00, ID_BYTES};
result.append(QByteArray::fromRawData(credentialLength, 2));
// Credential Id
@ -196,10 +181,8 @@ PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
testingVariables.credentialId.isEmpty() ? credentialId.toUtf8() : testingVariables.credentialId.toUtf8(),
QByteArray::Base64UrlEncoding));
// Credential private key
const auto alg = getAlgorithmFromPublicKey(publicKey);
const auto credentialPublicKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
result.append(credentialPublicKey.cborEncoded);
// Credential public key
result.append(cborEncodedPublicKey);
// Add extension data if available
if (!extensions.isEmpty()) {
@ -207,35 +190,35 @@ PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
}
// The final result should be CBOR encoded
return {m_browserCbor.cborEncodeAttestation(result), credentialPublicKey.pem};
return m_browserCbor.cborEncodeAttestation(result);
}
// Build a short version of the attestation object for webauthn.get
QByteArray BrowserPasskeys::buildGetAttestationObject(const QJsonObject& publicKey)
QByteArray BrowserPasskeys::buildAuthenticatorData(const QJsonObject& publicKey)
{
QByteArray result;
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rpId"].toString());
result.append(rpIdHash);
const auto flags =
setFlagsFromJson(QJsonObject({{"ED", false},
{"AT", false},
{"BS", false},
{"BE", false},
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
{"UP", true}}));
const auto extensions = publicKey["extensions"].toString();
const auto flags = setFlagsFromJson(QJsonObject(
{{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
result.append(flags);
// Signature counter (not supported, always 0
const char counter[4] = {0x00, 0x00, 0x00, 0x00};
result.append(QByteArray::fromRawData(counter, 4));
if (!extensions.isEmpty()) {
result.append(browserMessageBuilder()->getArrayFromBase64(extensions));
}
return result;
}
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
PrivateKey
AttestationKeyPair
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
{
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
@ -299,7 +282,14 @@ BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFir
}
auto result = m_browserCbor.cborEncodePublicKey(alg, firstPart, secondPart);
return {result, pem};
if (result.isEmpty()) {
return {};
}
AttestationKeyPair attestationKeyPair;
attestationKeyPair.cborEncodedPublicKey = result;
attestationKeyPair.privateKeyPem = pem;
return attestationKeyPair;
}
QByteArray BrowserPasskeys::buildSignature(const QByteArray& authenticatorData,
@ -339,7 +329,8 @@ QByteArray BrowserPasskeys::buildSignature(const QByteArray& authenticatorData,
rawSignature = signer.signature(*randomGen()->getRng());
} else if (algName == "Ed25519") {
Botan::Ed25519_PrivateKey privateKey(algId, privateKeyBytes);
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "SHA-512");
// "Pure" here means signing message directly. SHA-512 is only used with pre-hashed Ed25519 (Ed25519ph).
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "Pure");
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
rawSignature = signer.signature(*randomGen()->getRng());
@ -356,26 +347,6 @@ QByteArray BrowserPasskeys::buildSignature(const QByteArray& authenticatorData,
}
}
QByteArray BrowserPasskeys::buildExtensionData(QJsonObject& extensionObject) const
{
// Only supports "credProps" and "uvm" for now
const QStringList allowedKeys = {"credProps", "uvm"};
// Remove unsupported keys
for (const auto& key : extensionObject.keys()) {
if (!allowedKeys.contains(key)) {
extensionObject.remove(key);
}
}
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
if (!extensionData.isEmpty()) {
return extensionData;
}
return {};
}
// Parse authentication data byte array to JSON
// See: https://www.w3.org/TR/webauthn/images/fido-attestation-structures.svg
// And: https://w3c.github.io/webauthn/#attested-credential-data
@ -420,6 +391,9 @@ QJsonObject BrowserPasskeys::parseFlags(const QByteArray& flags) const
{"UP", flagBits.test(AuthenticatorFlags::UP)}});
}
// https://w3c.github.io/webauthn/#table-authData
// ED - Extension Data, AT - Attested Credential, BS - Reserved
// BE - Reserved , UV - User Verified, UP - User Present
char BrowserPasskeys::setFlagsFromJson(const QJsonObject& flags) const
{
if (flags.isEmpty()) {
@ -444,9 +418,9 @@ char BrowserPasskeys::setFlagsFromJson(const QJsonObject& flags) const
}
// Returns the first supported algorithm from the pubKeyCredParams list (only support ES256, RS256 and EdDSA for now)
WebAuthnAlgorithms BrowserPasskeys::getAlgorithmFromPublicKey(const QJsonObject& publicKey) const
WebAuthnAlgorithms BrowserPasskeys::getAlgorithmFromPublicKey(const QJsonObject& credentialCreationOptions) const
{
const auto pubKeyCredParams = publicKey["pubKeyCredParams"].toArray();
const auto pubKeyCredParams = credentialCreationOptions["credTypesAndPubKeyAlgs"].toArray();
if (!pubKeyCredParams.isEmpty()) {
const auto alg = pubKeyCredParams.first()["alg"].toInt();
if (alg == WebAuthnAlgorithms::ES256 || alg == WebAuthnAlgorithms::RS256 || alg == WebAuthnAlgorithms::EDDSA) {

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 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
@ -27,8 +27,6 @@
#define ID_BYTES 32
#define HASH_BYTES 32
#define DEFAULT_TIMEOUT 300000
#define DEFAULT_DISCOURAGED_TIMEOUT 120000
#define RSA_BITS 2048
#define RSA_EXPONENT 65537
@ -59,10 +57,10 @@ struct PublicKeyCredential
QByteArray key;
};
struct PrivateKey
struct AttestationKeyPair
{
QByteArray cborEncoded;
QByteArray pem;
QByteArray cborEncodedPublicKey;
QByteArray privateKeyPem;
};
// Predefined variables used for testing the class
@ -82,19 +80,21 @@ public:
~BrowserPasskeys() = default;
static BrowserPasskeys* instance();
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
const QString& origin,
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& credentialCreationOptions,
const TestingVariables& predefinedVariables = {});
QJsonObject buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
const QString& origin,
QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
const QString& credentialId,
const QString& userHandle,
const QString& privateKeyPem);
bool isUserVerificationValid(const QString& userVerification) const;
int getTimeout(const QString& userVerification, int timeout) const;
QStringList getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const;
static const QString AAGUID;
static const QString ATTACHMENT_CROSS_PLATFORM;
static const QString ATTACHMENT_PLATFORM;
static const QString AUTHENTICATOR_TRANSPORT;
static const bool SUPPORT_RESIDENT_KEYS;
static const bool SUPPORT_USER_VERIFICATION;
static const QString PUBLIC_KEY;
static const QString REQUIREMENT_DISCOURAGED;
static const QString REQUIREMENT_PREFERRED;
@ -104,28 +104,27 @@ public:
static const QString PASSKEYS_ATTESTATION_NONE;
static const QString KPEX_PASSKEY_USERNAME;
static const QString KPEX_PASSKEY_GENERATED_USER_ID;
static const QString KPEX_PASSKEY_CREDENTIAL_ID;
static const QString KPEX_PASSKEY_PRIVATE_KEY_PEM;
static const QString KPEX_PASSKEY_RELYING_PARTY;
static const QString KPEX_PASSKEY_USER_HANDLE;
private:
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get);
PrivateKey buildAttestationObject(const QJsonObject& publicKey,
QByteArray buildAttestationObject(const QJsonObject& credentialCreationOptions,
const QString& extensions,
const QString& credentialId,
const QByteArray& cborEncodedPublicKey,
const TestingVariables& predefinedVariables = {});
QByteArray buildGetAttestationObject(const QJsonObject& publicKey);
PrivateKey buildCredentialPrivateKey(int alg,
const QString& predefinedFirst = QString(),
const QString& predefinedSecond = QString());
QByteArray buildAuthenticatorData(const QJsonObject& publicKey);
AttestationKeyPair buildCredentialPrivateKey(int alg,
const QString& predefinedFirst = QString(),
const QString& predefinedSecond = QString());
QByteArray
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
QByteArray buildExtensionData(QJsonObject& extensionObject) const;
QJsonObject parseAuthData(const QByteArray& authData) const;
QJsonObject parseFlags(const QByteArray& flags) const;
char setFlagsFromJson(const QJsonObject& flags) const;
WebAuthnAlgorithms getAlgorithmFromPublicKey(const QJsonObject& publicKey) const;
WebAuthnAlgorithms getAlgorithmFromPublicKey(const QJsonObject& credentialCreationOptions) const;
QByteArray bigIntToQByteArray(Botan::BigInt& bigInt) const;
Q_DISABLE_COPY(BrowserPasskeys);

View file

@ -0,0 +1,173 @@
/*
* Copyright (C) 2024 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 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 <http://www.gnu.org/licenses/>.
*/
#include "BrowserPasskeysClient.h"
#include "BrowserMessageBuilder.h"
#include "BrowserPasskeys.h"
#include "PasskeyUtils.h"
#include <QJsonDocument>
Q_GLOBAL_STATIC(BrowserPasskeysClient, s_browserPasskeysClient);
BrowserPasskeysClient* BrowserPasskeysClient::instance()
{
return s_browserPasskeysClient;
}
// Constructs CredentialCreationOptions from the original PublicKeyCredential
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#createCredential
int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publicKeyOptions,
const QString& origin,
QJsonObject* result) const
{
if (!result || publicKeyOptions.isEmpty()) {
return ERROR_PASSKEYS_EMPTY_PUBLIC_KEY;
}
// Check validity of some basic values
const auto checkResultError = passkeyUtils()->checkLimits(publicKeyOptions);
if (checkResultError > 0) {
return checkResultError;
}
// Get effective domain
QString effectiveDomain;
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
if (effectiveDomainResponse > 0) {
return effectiveDomainResponse;
}
// Validate RP ID
QString rpId;
const auto rpName = publicKeyOptions["rp"]["name"].toString();
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rp"]["id"], effectiveDomain, &rpId);
if (rpIdResponse > 0) {
return rpIdResponse;
}
// Check PublicKeyCredentialTypes
const auto pubKeyCredParams = passkeyUtils()->parseCredentialTypes(publicKeyOptions["pubKeyCredParams"].toArray());
if (pubKeyCredParams.isEmpty() && !publicKeyOptions["pubKeyCredParams"].toArray().isEmpty()) {
return ERROR_PASSKEYS_NO_SUPPORTED_ALGORITHMS;
}
// Check Attestation
const auto attestation = passkeyUtils()->parseAttestation(publicKeyOptions["attestation"].toString());
// Check validity of AuthenticatorSelection
auto authenticatorSelection = publicKeyOptions["authenticatorSelection"].toObject();
const bool isAuthenticatorSelectionValid = passkeyUtils()->isAuthenticatorSelectionValid(authenticatorSelection);
if (!isAuthenticatorSelectionValid) {
return ERROR_PASSKEYS_WAIT_FOR_LIFETIMER;
}
// Add default values for compatibility
if (authenticatorSelection.isEmpty()) {
authenticatorSelection = QJsonObject({{"userVerification", BrowserPasskeys::REQUIREMENT_PREFERRED}});
} else if (authenticatorSelection["userVerification"].toString().isEmpty()) {
authenticatorSelection["userVerification"] = BrowserPasskeys::REQUIREMENT_PREFERRED;
}
auto authenticatorAttachment = authenticatorSelection["authenticatorAttachment"].toString();
if (authenticatorAttachment.isEmpty()) {
authenticatorAttachment = BrowserPasskeys::ATTACHMENT_PLATFORM;
}
// Unknown values are ignored, but a warning will be still shown just in case
const auto userVerification = authenticatorSelection["userVerification"].toString();
if (!passkeyUtils()->isUserVerificationValid(userVerification)) {
qWarning() << browserMessageBuilder()->getErrorMessage(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
}
// Parse requireResidentKey and userVerification
const auto isResidentKeyRequired = passkeyUtils()->isResidentKeyRequired(authenticatorSelection);
const auto isUserVerificationRequired = passkeyUtils()->isUserVerificationRequired(authenticatorSelection);
// Extensions
auto extensionObject = publicKeyOptions["extensions"].toObject();
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
// Construct the final object
QJsonObject credentialCreationOptions;
credentialCreationOptions["attestation"] = attestation; // Set this, even if only "none" is supported
credentialCreationOptions["authenticatorAttachment"] = authenticatorAttachment;
credentialCreationOptions["clientDataJSON"] = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, false);
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
credentialCreationOptions["excludeCredentials"] = publicKeyOptions["excludeCredentials"];
credentialCreationOptions["extensions"] = extensions;
credentialCreationOptions["residentKey"] = isResidentKeyRequired;
credentialCreationOptions["rp"] = QJsonObject({{"id", rpId}, {"name", rpName}});
credentialCreationOptions["user"] = publicKeyOptions["user"];
credentialCreationOptions["userPresence"] = !isUserVerificationRequired;
credentialCreationOptions["userVerification"] = isUserVerificationRequired;
*result = credentialCreationOptions;
return 0;
}
// Use an existing credential
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#getAssertion
int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptions,
const QString& origin,
QJsonObject* result) const
{
if (!result || publicKeyOptions.isEmpty()) {
return ERROR_PASSKEYS_EMPTY_PUBLIC_KEY;
}
// Get effective domain
QString effectiveDomain;
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
if (effectiveDomainResponse > 0) {
return effectiveDomainResponse;
}
// Validate RP ID
QString rpId;
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rpId"], effectiveDomain, &rpId);
if (rpIdResponse > 0) {
return rpIdResponse;
}
// Extensions
auto extensionObject = publicKeyOptions["extensions"].toObject();
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
// clientDataJson
const auto clientDataJson = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, true);
// Unknown values are ignored, but a warning will be still shown just in case
const auto userVerification = publicKeyOptions["userVerification"].toString();
if (!passkeyUtils()->isUserVerificationValid(userVerification)) {
qWarning() << browserMessageBuilder()->getErrorMessage(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
}
const auto isUserVerificationRequired = passkeyUtils()->isUserVerificationRequired(publicKeyOptions);
QJsonObject assertionOptions;
assertionOptions["allowCredentials"] = publicKeyOptions["allowCredentials"];
assertionOptions["clientDataJson"] = clientDataJson;
assertionOptions["extensions"] = extensions;
assertionOptions["rpId"] = rpId;
assertionOptions["userPresence"] = true;
assertionOptions["userVerification"] = isUserVerificationRequired;
*result = assertionOptions;
return 0;
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 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 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 <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERPASSKEYSCLIENT_H
#define BROWSERPASSKEYSCLIENT_H
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
class BrowserPasskeysClient : public QObject
{
Q_OBJECT
public:
explicit BrowserPasskeysClient() = default;
~BrowserPasskeysClient() = default;
static BrowserPasskeysClient* instance();
int
getCredentialCreationOptions(const QJsonObject& publicKeyOptions, const QString& origin, QJsonObject* result) const;
int getAssertionOptions(const QJsonObject& publicKeyOptions, const QString& origin, QJsonObject* result) const;
private:
Q_DISABLE_COPY(BrowserPasskeysClient);
friend class TestPasskeys;
};
static inline BrowserPasskeysClient* browserPasskeysClient()
{
return BrowserPasskeysClient::instance();
}
#endif // BROWSERPASSKEYSCLIENT_H

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand
*
@ -31,7 +31,9 @@
#include "gui/osutils/OSUtils.h"
#ifdef WITH_XC_BROWSER_PASSKEYS
#include "BrowserPasskeys.h"
#include "BrowserPasskeysClient.h"
#include "BrowserPasskeysConfirmationDialog.h"
#include "PasskeyUtils.h"
#endif
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
@ -611,7 +613,7 @@ QString BrowserService::getKey(const QString& id)
#ifdef WITH_XC_BROWSER_PASSKEYS
// Passkey registration
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKey,
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
const QString& origin,
const StringPairList& keyList)
{
@ -620,39 +622,23 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
const auto userJson = publicKey["user"].toObject();
const auto username = userJson["name"].toString();
const auto userHandle = userJson["id"].toString();
const auto rpId = publicKey["rp"]["id"].toString();
const auto rpName = publicKey["rp"]["name"].toString();
const auto timeoutValue = publicKey["timeout"].toInt();
const auto excludeCredentials = publicKey["excludeCredentials"].toArray();
const auto attestation = publicKey["attestation"].toString();
// Check Resident Key requirement
const auto authenticatorSelection = publicKey["authenticatorSelection"].toObject();
const auto requireResidentKey = authenticatorSelection["requireResidentKey"].toBool();
if (requireResidentKey) {
return getPasskeyError(ERROR_PASSKEYS_RESIDENT_KEYS_NOT_SUPPORTED);
QJsonObject credentialCreationOptions;
const auto pkOptionsResult =
browserPasskeysClient()->getCredentialCreationOptions(publicKeyOptions, origin, &credentialCreationOptions);
if (pkOptionsResult > 0 || credentialCreationOptions.isEmpty()) {
return getPasskeyError(pkOptionsResult);
}
// Only support these two for now
if (attestation != BrowserPasskeys::PASSKEYS_ATTESTATION_NONE
&& attestation != BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT) {
return getPasskeyError(ERROR_PASSKEYS_ATTESTATION_NOT_SUPPORTED);
}
const auto excludeCredentials = credentialCreationOptions["excludeCredentials"].toArray();
const auto rpId = publicKeyOptions["rp"]["id"].toString();
const auto timeout = publicKeyOptions["timeout"].toInt();
const auto username = credentialCreationOptions["user"].toObject()["name"].toString();
const auto userVerification = authenticatorSelection["userVerification"].toString();
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
}
if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, origin, keyList)) {
// Parse excludeCredentialDescriptorList
if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, rpId, keyList)) {
return getPasskeyError(ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED);
}
const auto existingEntries = getPasskeyEntries(rpId, keyList);
const auto timeout = browserPasskeys()->getTimeout(userVerification, timeoutValue);
raiseWindow();
BrowserPasskeysConfirmationDialog confirmDialog;
@ -660,7 +646,16 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
auto dialogResult = confirmDialog.exec();
if (dialogResult == QDialog::Accepted) {
const auto publicKeyCredentials = browserPasskeys()->buildRegisterPublicKeyCredential(publicKey, origin);
const auto publicKeyCredentials =
browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions);
if (publicKeyCredentials.credentialId.isEmpty() || publicKeyCredentials.key.isEmpty()
|| publicKeyCredentials.response.isEmpty()) {
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
}
const auto rpName = publicKeyOptions["rp"]["name"].toString();
const auto user = credentialCreationOptions["user"].toObject();
const auto userId = user["id"].toString();
if (confirmDialog.isPasskeyUpdated()) {
addPasskeyToEntry(confirmDialog.getSelectedEntry(),
@ -668,7 +663,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
rpName,
username,
publicKeyCredentials.credentialId,
userHandle,
userId,
publicKeyCredentials.key);
} else {
addPasskeyToGroup(nullptr,
@ -677,7 +672,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
rpName,
username,
publicKeyCredentials.credentialId,
userHandle,
userId,
publicKeyCredentials.key);
}
@ -690,7 +685,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
}
// Passkey authentication
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
const QString& origin,
const StringPairList& keyList)
{
@ -699,24 +694,21 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
const auto userVerification = publicKey["userVerification"].toString();
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
QJsonObject assertionOptions;
const auto assertionResult =
browserPasskeysClient()->getAssertionOptions(publicKeyOptions, origin, &assertionOptions);
if (assertionResult > 0 || assertionOptions.isEmpty()) {
return getPasskeyError(assertionResult);
}
// Parse "allowCredentials"
const auto rpId = publicKey["rpId"].toString();
const auto entries = getPasskeyAllowedEntries(publicKey, rpId, keyList);
// Get allowed entries from RP ID
const auto rpId = assertionOptions["rpId"].toString();
const auto entries = getPasskeyAllowedEntries(assertionOptions, rpId, keyList);
if (entries.isEmpty()) {
return getPasskeyError(ERROR_KEEPASS_NO_LOGINS_FOUND);
}
// With single entry, if no verification is needed, return directly
if (entries.count() == 1 && userVerification == BrowserPasskeys::REQUIREMENT_DISCOURAGED) {
return getPublicKeyCredentialFromEntry(entries.first(), publicKey, origin);
}
const auto timeout = browserPasskeys()->getTimeout(userVerification, publicKey["timeout"].toInt());
const auto timeout = publicKeyOptions["timeout"].toInt();
raiseWindow();
BrowserPasskeysConfirmationDialog confirmDialog;
@ -725,7 +717,21 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
if (dialogResult == QDialog::Accepted) {
hideWindow();
const auto selectedEntry = confirmDialog.getSelectedEntry();
return getPublicKeyCredentialFromEntry(selectedEntry, publicKey, origin);
if (!selectedEntry) {
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
}
const auto privateKeyPem = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
const auto credentialId = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
const auto userHandle = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
auto publicKeyCredential =
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
if (publicKeyCredential.isEmpty()) {
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
}
return publicKeyCredential;
}
hideWindow();
@ -797,10 +803,11 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
entry->beginUpdate();
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID, credentialId, true);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
entry->addTag(tr("Passkey"));
entry->endUpdate();
}
@ -1342,18 +1349,21 @@ QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const Strin
}
// Get all entries for the site that are allowed by the server
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& publicKey,
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& assertionOptions,
const QString& rpId,
const StringPairList& keyList)
{
QList<Entry*> entries;
const auto allowedCredentials = browserPasskeys()->getAllowedCredentialsFromPublicKey(publicKey);
const auto allowedCredentials = passkeyUtils()->getAllowedCredentialsFromAssertionOptions(assertionOptions);
if (!assertionOptions["allowCredentials"].toArray().isEmpty() && allowedCredentials.isEmpty()) {
return {};
}
for (const auto& entry : getPasskeyEntries(rpId, keyList)) {
// If allowedCredentials.isEmpty() check if entry contains an extra attribute for user handle.
// If that is found, the entry should be allowed.
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
if (allowedCredentials.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID))
if (allowedCredentials.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID))
|| (allowedCredentials.isEmpty()
&& entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
entries << entry;
@ -1363,18 +1373,9 @@ QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& public
return entries;
}
QJsonObject
BrowserService::getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin)
{
const auto privateKeyPem = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
const auto credentialId = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID);
const auto userHandle = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
return browserPasskeys()->buildGetPublicKeyCredential(publicKey, origin, credentialId, userHandle, privateKeyPem);
}
// Checks if the same user ID already exists for the current site
// Checks if the same user ID already exists for the current RP ID
bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
const QString& origin,
const QString& rpId,
const StringPairList& keyList)
{
QStringList allIds;
@ -1382,9 +1383,9 @@ bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCreden
allIds << cred["id"].toString();
}
const auto passkeyEntries = getPasskeyEntries(origin, keyList);
const auto passkeyEntries = getPasskeyEntries(rpId, keyList);
return std::any_of(passkeyEntries.begin(), passkeyEntries.end(), [&](const auto& entry) {
return allIds.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID));
return allIds.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID));
});
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand
*
@ -88,9 +88,10 @@ public:
QSharedPointer<Database> selectedDatabase();
QList<QSharedPointer<Database>> getOpenDatabases();
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
QJsonObject showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
QJsonObject showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
const QString& origin,
const StringPairList& keyList);
QJsonObject showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
const QString& origin,
const StringPairList& keyList);
void addPasskeyToGroup(Group* group,
@ -177,18 +178,15 @@ private:
Access checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm);
Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {});
int sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl);
bool schemeFound(const QString& url);
bool removeFirstDomain(QString& hostname);
bool
shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false);
#ifdef WITH_XC_BROWSER_PASSKEYS
QList<Entry*> getPasskeyEntries(const QString& rpId, const StringPairList& keyList);
QList<Entry*>
getPasskeyAllowedEntries(const QJsonObject& publicKey, const QString& rpId, const StringPairList& keyList);
QJsonObject
getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin);
getPasskeyAllowedEntries(const QJsonObject& assertionOptions, const QString& rpId, const StringPairList& keyList);
bool isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
const QString& origin,
const QString& rpId,
const StringPairList& keyList);
QJsonObject getPasskeyError(int errorCode) const;
#endif

View file

@ -1,7 +1,7 @@
/*
* Copyright (C) 2013 Francois Ferrand
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2013 Francois Ferrand
*
* 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
@ -145,6 +145,16 @@ void BrowserSettings::setNoMigrationPrompt(bool prompt)
config()->set(Config::Browser_NoMigrationPrompt, prompt);
}
bool BrowserSettings::allowLocalhostWithPasskeys()
{
return config()->get(Config::Browser_AllowLocalhostWithPasskeys).toBool();
}
void BrowserSettings::setAllowLocalhostWithPasskeys(bool enabled)
{
config()->set(Config::Browser_AllowLocalhostWithPasskeys, enabled);
}
bool BrowserSettings::useCustomProxy()
{
return config()->get(Config::Browser_UseCustomProxy).toBool();

View file

@ -1,7 +1,7 @@
/*
* Copyright (C) 2013 Francois Ferrand
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2013 Francois Ferrand
*
* 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
@ -51,6 +51,8 @@ public:
void setSupportKphFields(bool supportKphFields);
bool noMigrationPrompt();
void setNoMigrationPrompt(bool prompt);
bool allowLocalhostWithPasskeys();
void setAllowLocalhostWithPasskeys(bool enabled);
bool useCustomProxy();
void setUseCustomProxy(bool enabled);

View file

@ -123,6 +123,7 @@ void BrowserSettingsWidget::loadSettings()
m_ui->httpAuthPermission->setChecked(settings->httpAuthPermission());
m_ui->searchInAllDatabases->setChecked(settings->searchInAllDatabases());
m_ui->supportKphFields->setChecked(settings->supportKphFields());
m_ui->allowLocalhostWithPasskeys->setChecked(settings->allowLocalhostWithPasskeys());
m_ui->noMigrationPrompt->setChecked(settings->noMigrationPrompt());
m_ui->useCustomProxy->setChecked(settings->useCustomProxy());
m_ui->customProxyLocation->setText(settings->replaceHomePath(settings->customProxyLocation()));
@ -253,6 +254,7 @@ void BrowserSettingsWidget::saveSettings()
settings->setHttpAuthPermission(m_ui->httpAuthPermission->isChecked());
settings->setSearchInAllDatabases(m_ui->searchInAllDatabases->isChecked());
settings->setSupportKphFields(m_ui->supportKphFields->isChecked());
settings->setAllowLocalhostWithPasskeys(m_ui->allowLocalhostWithPasskeys->isChecked());
settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked());
#ifdef QT_DEBUG

View file

@ -310,6 +310,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="allowLocalhostWithPasskeys">
<property name="toolTip">
<string>Allows using insecure http://localhost with Passkeys for testing purposes.</string>
</property>
<property name="text">
<string>Allow using localhost with Passkeys</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="noMigrationPrompt">
<property name="toolTip">

View file

@ -1,4 +1,4 @@
# Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
# Copyright (C) 2024 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
@ -35,7 +35,9 @@ if(WITH_XC_BROWSER)
list(APPEND keepassxcbrowser_SOURCES
BrowserCbor.cpp
BrowserPasskeys.cpp
BrowserPasskeysConfirmationDialog.cpp)
BrowserPasskeysClient.cpp
BrowserPasskeysConfirmationDialog.cpp
PasskeyUtils.cpp)
endif()
add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES})

View file

@ -0,0 +1,352 @@
/*
* Copyright (C) 2024 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 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 <http://www.gnu.org/licenses/>.
*/
#include "PasskeyUtils.h"
#include "BrowserMessageBuilder.h"
#include "BrowserPasskeys.h"
#include "core/Tools.h"
#include "core/UrlTools.h"
#include <QList>
#include <QUrl>
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 (rpIdValue.isUndefined()) {
return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH;
}
if (effectiveDomain.isEmpty()) {
return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
}
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);
}
QByteArray PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const
{
// Only supports "credProps" and "uvm" for now
const QStringList allowedKeys = {"credProps", "uvm"};
// Remove unsupported keys
for (const auto& key : extensionObject.keys()) {
if (!allowedKeys.contains(key)) {
extensionObject.remove(key);
}
}
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
if (!extensionData.isEmpty()) {
return extensionData;
}
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);
if (cred["type"].toString() == BrowserPasskeys::PUBLIC_KEY && hasSupportedTransport && !id.isEmpty()) {
allowedCredentials << id;
}
}
return allowedCredentials;
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (C) 2024 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 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 <http://www.gnu.org/licenses/>.
*/
#ifndef PASSKEYUTILS_H
#define PASSKEYUTILS_H
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QStringList>
#include "BrowserCbor.h"
#define DEFAULT_TIMEOUT 300000
#define DEFAULT_DISCOURAGED_TIMEOUT 120000
#define PASSKEYS_SUCCESS 0
class PasskeyUtils : public QObject
{
Q_OBJECT
public:
explicit PasskeyUtils() = default;
~PasskeyUtils() = default;
static PasskeyUtils* instance();
int checkLimits(const QJsonObject& pkOptions) const;
bool checkCredentialCreationOptions(const QJsonObject& credentialCreationOptions) const;
bool checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const;
int getEffectiveDomain(const QString& origin, QString* result) const;
int validateRpId(const QJsonValue& rpIdValue, const QString& effectiveDomain, QString* result) const;
QString parseAttestation(const QString& attestation) const;
QJsonArray parseCredentialTypes(const QJsonArray& credentialTypes) const;
bool isAuthenticatorSelectionValid(const QJsonObject& authenticatorSelection) const;
bool isUserVerificationValid(const QString& userVerification) const;
bool isResidentKeyRequired(const QJsonObject& authenticatorSelection) const;
bool isUserVerificationRequired(const QJsonObject& authenticatorSelection) const;
bool isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const;
QByteArray buildExtensionData(QJsonObject& extensionObject) const;
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const;
QStringList getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const;
private:
Q_DISABLE_COPY(PasskeyUtils);
bool isRegistrableDomainSuffix(const QString& hostSuffixString, const QString& originalHost) const;
bool isDomain(const QString& hostName) const;
friend class TestPasskeys;
private:
BrowserCbor m_browserCbor;
};
static inline PasskeyUtils* passkeyUtils()
{
return PasskeyUtils::instance();
}
#endif // PASSKEYUTILS_H