mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
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:
parent
dff2f186ce
commit
ac2b445db6
@ -976,6 +976,10 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
|
||||
<source>KeePassXC - New key association request</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passkey</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserSettingsWidget</name>
|
||||
@ -1222,6 +1226,14 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
|
||||
<source><b>Error:</b> The installed proxy executable is missing from the expected location: %1<br/>Please set a custom proxy location in the advanced settings or reinstall the application.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Allows using insecure http://localhost with Passkeys for testing purposes.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Allow using localhost with Passkeys</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CloneDialog</name>
|
||||
@ -8419,10 +8431,6 @@ Kernel: %3 %4</source>
|
||||
<source>Invalid URL provided</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Resident Keys are not supported</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passkeys</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -8483,6 +8491,38 @@ Kernel: %3 %4</source>
|
||||
<source>Failed to decrypt key data.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Origin is empty or not allowed</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Effective domain is not a valid domain</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Origin and RP ID do not match</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No supported algorithms were provided</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Wait for timer to expire</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unknown Passkeys error</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Challenge is shorter than required minimum length</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>user.id does not match the required length</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
173
src/browser/BrowserPasskeysClient.cpp
Normal file
173
src/browser/BrowserPasskeysClient.cpp
Normal 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;
|
||||
}
|
49
src/browser/BrowserPasskeysClient.h
Normal file
49
src/browser/BrowserPasskeysClient.h
Normal 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
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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})
|
||||
|
352
src/browser/PasskeyUtils.cpp
Normal file
352
src/browser/PasskeyUtils.cpp
Normal 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;
|
||||
}
|
74
src/browser/PasskeyUtils.h
Normal file
74
src/browser/PasskeyUtils.h
Normal 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
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@ -167,6 +167,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
||||
{Config::Browser_UseCustomBrowser, {QS("Browser/UseCustomBrowser"), Local, false}},
|
||||
{Config::Browser_CustomBrowserType, {QS("Browser/CustomBrowserType"), Local, -1}},
|
||||
{Config::Browser_CustomBrowserLocation, {QS("Browser/CustomBrowserLocation"), Local, {}}},
|
||||
{Config::Browser_AllowLocalhostWithPasskeys, {QS("Browser/Browser_AllowLocalhostWithPasskeys"), Roaming, false}},
|
||||
#ifdef QT_DEBUG
|
||||
{Config::Browser_CustomExtensionId, {QS("Browser/CustomExtensionId"), Local, {}}},
|
||||
#endif
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@ -147,6 +147,7 @@ public:
|
||||
Browser_UseCustomBrowser,
|
||||
Browser_CustomBrowserType,
|
||||
Browser_CustomBrowserLocation,
|
||||
Browser_AllowLocalhostWithPasskeys,
|
||||
#ifdef QT_DEBUG
|
||||
Browser_CustomExtensionId,
|
||||
#endif
|
||||
|
@ -230,6 +230,13 @@ namespace Tools
|
||||
return regexp.exactMatch(base64);
|
||||
}
|
||||
|
||||
bool isAsciiString(const QString& str)
|
||||
{
|
||||
constexpr auto pattern = R"(^[\x00-\x7F]+$)";
|
||||
QRegularExpression regexp(pattern, QRegularExpression::CaseInsensitiveOption);
|
||||
return regexp.match(str).hasMatch();
|
||||
}
|
||||
|
||||
void sleep(int ms)
|
||||
{
|
||||
Q_ASSERT(ms >= 0);
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
||||
* Copyright (C) 2023 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
|
||||
@ -37,6 +37,7 @@ namespace Tools
|
||||
bool readAllFromDevice(QIODevice* device, QByteArray& data);
|
||||
bool isHex(const QByteArray& ba);
|
||||
bool isBase64(const QByteArray& ba);
|
||||
bool isAsciiString(const QString& str);
|
||||
void sleep(int ms);
|
||||
void wait(int ms);
|
||||
QString uuidToHex(const QUuid& uuid);
|
||||
|
@ -102,7 +102,7 @@ QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const
|
||||
cookie.setDomain(host);
|
||||
|
||||
// Check if dummy cookie's domain/TLD matches with public suffix list
|
||||
if (!QNetworkCookieJar{}.setCookiesFromUrl(QList{cookie}, url)) {
|
||||
if (!QNetworkCookieJar{}.setCookiesFromUrl(QList{cookie}, QUrl::fromUserInput(url))) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
@ -112,7 +112,9 @@ QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const
|
||||
|
||||
bool UrlTools::isIpAddress(const QString& host) const
|
||||
{
|
||||
QHostAddress address(host);
|
||||
// Handle IPv6 host with brackets, e.g [::1]
|
||||
const auto hostAddress = host.startsWith('[') && host.endsWith(']') ? host.mid(1, host.length() - 2) : host;
|
||||
QHostAddress address(hostAddress);
|
||||
return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol;
|
||||
}
|
||||
#endif
|
||||
@ -171,3 +173,9 @@ bool UrlTools::isUrlValid(const QString& urlField) const
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UrlTools::domainHasIllegalCharacters(const QString& domain) const
|
||||
{
|
||||
QRegularExpression re(R"([\s\^#|/:<>\?@\[\]\\])");
|
||||
return re.match(domain).hasMatch();
|
||||
}
|
||||
|
@ -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
|
||||
@ -40,6 +40,7 @@ public:
|
||||
#endif
|
||||
bool isUrlIdentical(const QString& first, const QString& second) const;
|
||||
bool isUrlValid(const QString& urlField) const;
|
||||
bool domainHasIllegalCharacters(const QString& domain) const;
|
||||
|
||||
private:
|
||||
QUrl convertVariantToUrl(const QVariant& var) const;
|
||||
|
@ -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
|
||||
@ -91,7 +91,7 @@ void PasskeyExporter::exportSelectedEntry(const Entry* entry, const QString& fol
|
||||
passkeyObject["relyingParty"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY);
|
||||
passkeyObject["url"] = entry->url();
|
||||
passkeyObject["username"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);
|
||||
passkeyObject["credentialId"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID);
|
||||
passkeyObject["credentialId"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
|
||||
passkeyObject["userHandle"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
|
||||
passkeyObject["privateKey"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
|
||||
|
||||
|
@ -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,7 +18,9 @@
|
||||
#include "TestPasskeys.h"
|
||||
#include "browser/BrowserCbor.h"
|
||||
#include "browser/BrowserMessageBuilder.h"
|
||||
#include "browser/BrowserPasskeysClient.h"
|
||||
#include "browser/BrowserService.h"
|
||||
#include "browser/PasskeyUtils.h"
|
||||
#include "core/Database.h"
|
||||
#include "core/Entry.h"
|
||||
#include "core/Group.h"
|
||||
@ -98,6 +100,18 @@ const QString PublicKeyCredentialRequestOptions = R"(
|
||||
"userVerification": "required"
|
||||
}
|
||||
)";
|
||||
|
||||
const QJsonArray validPubKeyCredParams = {
|
||||
QJsonObject({
|
||||
{"type", "public-key"},
|
||||
{"alg", -7}
|
||||
}),
|
||||
QJsonObject({
|
||||
{"type", "public-key"},
|
||||
{"alg", -257}
|
||||
}),
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
|
||||
void TestPasskeys::initTestCase()
|
||||
@ -252,14 +266,21 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
|
||||
const auto predefinedSecond = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
|
||||
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
QJsonObject credentialCreationOptions;
|
||||
browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
|
||||
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond};
|
||||
auto result = browserPasskeys()->buildAttestationObject(publicKeyCredentialOptions, "", id, testingVariables);
|
||||
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
||||
const auto credentialPrivateKey =
|
||||
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedFirst, predefinedSecond);
|
||||
auto result = browserPasskeys()->buildAttestationObject(
|
||||
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
||||
QCOMPARE(
|
||||
QString(result.cborEncoded),
|
||||
result,
|
||||
QString("\xA3"
|
||||
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
|
||||
"9\x7F)%\x0B`\x84\x1E\xF0"
|
||||
@ -272,7 +293,7 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
|
||||
|
||||
// Double check that the result can be decoded
|
||||
BrowserCbor browserCbor;
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result.cborEncoded);
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
|
||||
|
||||
// Parse authData
|
||||
auto authDataJsonObject = attestationJsonObject["authData"].toString();
|
||||
@ -311,18 +332,25 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
|
||||
QJsonArray pubKeyCredParams;
|
||||
pubKeyCredParams.append(QJsonObject({{"type", "public-key"}, {"alg", -257}}));
|
||||
|
||||
auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
publicKeyCredentialOptions["pubKeyCredParams"] = pubKeyCredParams;
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
QJsonObject credentialCreationOptions;
|
||||
browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, QString("https://webauthn.io"), &credentialCreationOptions);
|
||||
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
|
||||
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent};
|
||||
auto result = browserPasskeys()->buildAttestationObject(publicKeyCredentialOptions, "", id, testingVariables);
|
||||
const auto alg = browserPasskeys()->getAlgorithmFromPublicKey(credentialCreationOptions);
|
||||
auto credentialPrivateKey =
|
||||
browserPasskeys()->buildCredentialPrivateKey(alg, predefinedModulus, predefinedExponent);
|
||||
auto result = browserPasskeys()->buildAttestationObject(
|
||||
credentialCreationOptions, "", id, credentialPrivateKey.cborEncodedPublicKey, testingVariables);
|
||||
|
||||
// Double check that the result can be decoded
|
||||
BrowserCbor browserCbor;
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result.cborEncoded);
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result);
|
||||
|
||||
// Parse authData
|
||||
auto authDataJsonObject = attestationJsonObject["authData"].toString();
|
||||
@ -356,9 +384,13 @@ void TestPasskeys::testRegister()
|
||||
const auto testDataResponse = testDataPublicKey["response"];
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
|
||||
QJsonObject credentialCreationOptions;
|
||||
const auto creationResult = browserPasskeysClient()->getCredentialCreationOptions(
|
||||
publicKeyCredentialOptions, origin, &credentialCreationOptions);
|
||||
QVERIFY(creationResult == 0);
|
||||
|
||||
TestingVariables testingVariables = {predefinedId, predefinedX, predefinedY};
|
||||
auto result =
|
||||
browserPasskeys()->buildRegisterPublicKeyCredential(publicKeyCredentialOptions, origin, testingVariables);
|
||||
auto result = browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions, testingVariables);
|
||||
auto publicKeyCredential = result.response;
|
||||
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
|
||||
QCOMPARE(publicKeyCredential["authenticatorAttachment"], QString("platform"));
|
||||
@ -390,8 +422,12 @@ void TestPasskeys::testGet()
|
||||
const auto publicKeyCredentialRequestOptions =
|
||||
browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
|
||||
|
||||
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(
|
||||
publicKeyCredentialRequestOptions, origin, id, {}, privateKeyPem);
|
||||
QJsonObject assertionOptions;
|
||||
const auto assertionResult =
|
||||
browserPasskeysClient()->getAssertionOptions(publicKeyCredentialRequestOptions, origin, &assertionOptions);
|
||||
QVERIFY(assertionResult == 0);
|
||||
|
||||
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, id, {}, privateKeyPem);
|
||||
QVERIFY(!publicKeyCredential.isEmpty());
|
||||
QCOMPARE(publicKeyCredential["id"].toString(), id);
|
||||
|
||||
@ -414,7 +450,7 @@ void TestPasskeys::testGet()
|
||||
void TestPasskeys::testExtensions()
|
||||
{
|
||||
auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
|
||||
auto result = browserPasskeys()->buildExtensionData(extensions);
|
||||
auto result = passkeyUtils()->buildExtensionData(extensions);
|
||||
|
||||
BrowserCbor cbor;
|
||||
auto extensionJson = cbor.getJsonFromCborData(result);
|
||||
@ -425,8 +461,8 @@ void TestPasskeys::testExtensions()
|
||||
|
||||
auto partial = QJsonObject({{"props", true}, {"uvm", true}});
|
||||
auto faulty = QJsonObject({{"uvx", true}});
|
||||
auto partialData = browserPasskeys()->buildExtensionData(partial);
|
||||
auto faultyData = browserPasskeys()->buildExtensionData(faulty);
|
||||
auto partialData = passkeyUtils()->buildExtensionData(partial);
|
||||
auto faultyData = passkeyUtils()->buildExtensionData(faulty);
|
||||
|
||||
auto partialJson = cbor.getJsonFromCborData(partialData);
|
||||
QCOMPARE(partialJson["uvm"].toArray().size(), 1);
|
||||
@ -496,3 +532,164 @@ void TestPasskeys::testEntry()
|
||||
|
||||
QVERIFY(entry->hasPasskey());
|
||||
}
|
||||
|
||||
void TestPasskeys::testIsDomain()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isDomain("test.example.com"));
|
||||
QVERIFY(passkeyUtils()->isDomain("example.com"));
|
||||
|
||||
QVERIFY(!passkeyUtils()->isDomain("exa[mple.org"));
|
||||
QVERIFY(!passkeyUtils()->isDomain("example.com."));
|
||||
QVERIFY(!passkeyUtils()->isDomain("127.0.0.1"));
|
||||
QVERIFY(!passkeyUtils()->isDomain("127.0.0.1."));
|
||||
}
|
||||
|
||||
// List from https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
|
||||
void TestPasskeys::testRegistrableDomainSuffix()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("example.com")));
|
||||
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("example.com.")));
|
||||
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.com."), QString("example.com")));
|
||||
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example.com"), QString("www.example.com")));
|
||||
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("com"), QString("example.com")));
|
||||
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("example"), QString("example")));
|
||||
QVERIFY(
|
||||
!passkeyUtils()->isRegistrableDomainSuffix(QString("s3.amazonaws.com"), QString("example.s3.amazonaws.com")));
|
||||
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("example.compute.amazonaws.com"),
|
||||
QString("www.example.compute.amazonaws.com")));
|
||||
QVERIFY(!passkeyUtils()->isRegistrableDomainSuffix(QString("amazonaws.com"),
|
||||
QString("www.example.compute.amazonaws.com")));
|
||||
QVERIFY(passkeyUtils()->isRegistrableDomainSuffix(QString("amazonaws.com"), QString("test.amazonaws.com")));
|
||||
}
|
||||
|
||||
void TestPasskeys::testRpIdValidation()
|
||||
{
|
||||
QString result;
|
||||
auto allowedIdentical = passkeyUtils()->validateRpId(QString("example.com"), QString("example.com"), &result);
|
||||
QCOMPARE(result, QString("example.com"));
|
||||
QVERIFY(allowedIdentical == 0);
|
||||
|
||||
result.clear();
|
||||
auto allowedSubdomain = passkeyUtils()->validateRpId(QString("example.com"), QString("www.example.com"), &result);
|
||||
QCOMPARE(result, QString("example.com"));
|
||||
QVERIFY(allowedSubdomain == 0);
|
||||
|
||||
result.clear();
|
||||
auto emptyRpId = passkeyUtils()->validateRpId({}, QString("example.com"), &result);
|
||||
QCOMPARE(result, QString(""));
|
||||
QVERIFY(emptyRpId == ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto ipRpId = passkeyUtils()->validateRpId(QString("127.0.0.1"), QString("example.com"), &result);
|
||||
QCOMPARE(result, QString(""));
|
||||
QVERIFY(ipRpId == ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto emptyOrigin = passkeyUtils()->validateRpId(QString("example.com"), QString(""), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(emptyOrigin, ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED);
|
||||
|
||||
result.clear();
|
||||
auto ipOrigin = passkeyUtils()->validateRpId(QString("example.com"), QString("127.0.0.1"), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(ipOrigin, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto invalidRpId = passkeyUtils()->validateRpId(QString(".com"), QString("example.com"), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(invalidRpId, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto malformedOrigin = passkeyUtils()->validateRpId(QString("example.com."), QString("example.com."), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(malformedOrigin, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto malformed = passkeyUtils()->validateRpId(QString("...com."), QString("example...com"), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(malformed, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
|
||||
result.clear();
|
||||
auto differentDomain = passkeyUtils()->validateRpId(QString("another.com"), QString("example.com"), &result);
|
||||
QVERIFY(result.isEmpty());
|
||||
QCOMPARE(differentDomain, ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH);
|
||||
}
|
||||
|
||||
void TestPasskeys::testParseAttestation()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("")) == QString("none"));
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("direct")) == QString("direct"));
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("none")) == QString("none"));
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("indirect")) == QString("none"));
|
||||
QVERIFY(passkeyUtils()->parseAttestation(QString("invalidvalue")) == QString("none"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testParseCredentialTypes()
|
||||
{
|
||||
const QJsonArray invalidPubKeyCredParams = {
|
||||
QJsonObject({{"type", "private-key"}, {"alg", -7}}),
|
||||
QJsonObject({{"type", "private-key"}, {"alg", -257}}),
|
||||
};
|
||||
|
||||
const QJsonArray partiallyInvalidPubKeyCredParams = {
|
||||
QJsonObject({{"type", "private-key"}, {"alg", -7}}),
|
||||
QJsonObject({{"type", "public-key"}, {"alg", -257}}),
|
||||
};
|
||||
|
||||
auto validResponse = passkeyUtils()->parseCredentialTypes(validPubKeyCredParams);
|
||||
QVERIFY(validResponse == validPubKeyCredParams);
|
||||
|
||||
auto invalidResponse = passkeyUtils()->parseCredentialTypes(invalidPubKeyCredParams);
|
||||
QVERIFY(invalidResponse.isEmpty());
|
||||
|
||||
auto partiallyInvalidResponse = passkeyUtils()->parseCredentialTypes(partiallyInvalidPubKeyCredParams);
|
||||
QVERIFY(partiallyInvalidResponse != validPubKeyCredParams);
|
||||
QVERIFY(partiallyInvalidResponse.size() == 1);
|
||||
QVERIFY(partiallyInvalidResponse.first()["type"].toString() == QString("public-key"));
|
||||
QVERIFY(partiallyInvalidResponse.first()["alg"].toInt() == -257);
|
||||
|
||||
auto emptyResponse = passkeyUtils()->parseCredentialTypes({});
|
||||
QVERIFY(emptyResponse == validPubKeyCredParams);
|
||||
|
||||
const auto publicKeyOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
auto responseFromPublicKey = passkeyUtils()->parseCredentialTypes(publicKeyOptions["pubKeyCredParams"].toArray());
|
||||
QVERIFY(responseFromPublicKey == validPubKeyCredParams);
|
||||
}
|
||||
|
||||
void TestPasskeys::testIsAuthenticatorSelectionValid()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid({}));
|
||||
QVERIFY(passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "platform"}})));
|
||||
QVERIFY(
|
||||
passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "cross-platform"}})));
|
||||
QVERIFY(!passkeyUtils()->isAuthenticatorSelectionValid(QJsonObject({{"authenticatorAttachment", "something"}})));
|
||||
}
|
||||
|
||||
void TestPasskeys::testIsResidentKeyRequired()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "required"}})));
|
||||
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "preferred"}})));
|
||||
QVERIFY(!passkeyUtils()->isResidentKeyRequired(QJsonObject({{"residentKey", "discouraged"}})));
|
||||
QVERIFY(passkeyUtils()->isResidentKeyRequired(QJsonObject({{"requireResidentKey", true}})));
|
||||
}
|
||||
|
||||
void TestPasskeys::testIsUserVerificationRequired()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "required"}})));
|
||||
QVERIFY(passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "preferred"}})));
|
||||
QVERIFY(!passkeyUtils()->isUserVerificationRequired(QJsonObject({{"userVerification", "discouraged"}})));
|
||||
}
|
||||
|
||||
void TestPasskeys::testAllowLocalhostWithPasskeys()
|
||||
{
|
||||
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(false, "https://example.com"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://example.com"));
|
||||
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "https://example.com"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://example.com"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://localhost"));
|
||||
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhost"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhosting"));
|
||||
QVERIFY(passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://test.localhost"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(false, "http://test.localhost"));
|
||||
QVERIFY(!passkeyUtils()->isOriginAllowedWithLocalhost(true, "http://localhost.example.com"));
|
||||
}
|
||||
|
@ -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
|
||||
@ -45,5 +45,14 @@ private slots:
|
||||
void testSetFlags();
|
||||
|
||||
void testEntry();
|
||||
void testIsDomain();
|
||||
void testRegistrableDomainSuffix();
|
||||
void testRpIdValidation();
|
||||
void testParseAttestation();
|
||||
void testParseCredentialTypes();
|
||||
void testIsAuthenticatorSelectionValid();
|
||||
void testIsResidentKeyRequired();
|
||||
void testIsUserVerificationRequired();
|
||||
void testAllowLocalhostWithPasskeys();
|
||||
};
|
||||
#endif // KEEPASSXC_TESTPASSKEYS_H
|
||||
|
@ -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
|
||||
@ -68,6 +68,14 @@ void TestTools::testIsBase64()
|
||||
QVERIFY(!Tools::isBase64(QByteArray("123")));
|
||||
}
|
||||
|
||||
void TestTools::testIsAsciiString()
|
||||
{
|
||||
QVERIFY(Tools::isAsciiString("abcd9876DEFGhijkMNO"));
|
||||
QVERIFY(Tools::isAsciiString("-!&5a?`~"));
|
||||
QVERIFY(!Tools::isAsciiString("Štest"));
|
||||
QVERIFY(!Tools::isAsciiString("Ãß"));
|
||||
}
|
||||
|
||||
void TestTools::testEnvSubstitute()
|
||||
{
|
||||
QProcessEnvironment environment;
|
||||
|
@ -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,6 +27,7 @@ private slots:
|
||||
void testHumanReadableFileSize();
|
||||
void testIsHex();
|
||||
void testIsBase64();
|
||||
void testIsAsciiString();
|
||||
void testEnvSubstitute();
|
||||
void testValidUuid();
|
||||
void testBackupFilePatternSubstitution_data();
|
||||
|
@ -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 @@ void TestUrlTools::testTopLevelDomain()
|
||||
QList<QPair<QString, QString>> tldUrls{
|
||||
{QString("https://another.example.co.uk"), QString("co.uk")},
|
||||
{QString("https://www.example.com"), QString("com")},
|
||||
{QString("https://example.com"), QString("com")},
|
||||
{QString("https://github.com"), QString("com")},
|
||||
{QString("http://test.net"), QString("net")},
|
||||
{QString("http://so.many.subdomains.co.jp"), QString("co.jp")},
|
||||
@ -81,6 +82,9 @@ void TestUrlTools::testIsIpAddress()
|
||||
auto host6 = "fe80::1ff:fe23:4567:890a";
|
||||
auto host7 = "2001:20::1";
|
||||
auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid
|
||||
auto host9 = "[::]";
|
||||
auto host10 = "::";
|
||||
auto host11 = "[2001:20::1]";
|
||||
|
||||
QVERIFY(!urlTools()->isIpAddress(host1));
|
||||
QVERIFY(urlTools()->isIpAddress(host2));
|
||||
@ -90,6 +94,9 @@ void TestUrlTools::testIsIpAddress()
|
||||
QVERIFY(urlTools()->isIpAddress(host6));
|
||||
QVERIFY(urlTools()->isIpAddress(host7));
|
||||
QVERIFY(!urlTools()->isIpAddress(host8));
|
||||
QVERIFY(urlTools()->isIpAddress(host9));
|
||||
QVERIFY(urlTools()->isIpAddress(host10));
|
||||
QVERIFY(urlTools()->isIpAddress(host11));
|
||||
}
|
||||
|
||||
void TestUrlTools::testIsUrlIdentical()
|
||||
@ -117,6 +124,7 @@ void TestUrlTools::testIsUrlValid()
|
||||
urls["//github.com"] = true;
|
||||
urls["github.com/{}<>"] = false;
|
||||
urls["http:/example.com"] = false;
|
||||
urls["http:/example.com."] = false;
|
||||
urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true;
|
||||
urls["file:///Users/testUser/Code/test.html"] = true;
|
||||
urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true;
|
||||
@ -127,3 +135,10 @@ void TestUrlTools::testIsUrlValid()
|
||||
QCOMPARE(urlTools()->isUrlValid(i.key()), i.value());
|
||||
}
|
||||
}
|
||||
|
||||
void TestUrlTools::testDomainHasIllegalCharacters()
|
||||
{
|
||||
QVERIFY(!urlTools()->domainHasIllegalCharacters("example.com"));
|
||||
QVERIFY(urlTools()->domainHasIllegalCharacters("domain has spaces.com"));
|
||||
QVERIFY(urlTools()->domainHasIllegalCharacters("example#|.com"));
|
||||
}
|
||||
|
@ -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
|
||||
@ -34,6 +34,7 @@ private slots:
|
||||
void testIsIpAddress();
|
||||
void testIsUrlIdentical();
|
||||
void testIsUrlValid();
|
||||
void testDomainHasIllegalCharacters();
|
||||
|
||||
private:
|
||||
QPointer<UrlTools> m_urlTools;
|
||||
|
Loading…
Reference in New Issue
Block a user