Files
keepassxc/src/browser/BrowserPasskeysClient.cpp
Sami Vänttinen ac2b445db6 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.
2024-03-06 07:42:01 -05:00

174 lines
7.6 KiB
C++

/*
* 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;
}