2
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 3 of the License, or
7
* (at your option) any later version.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "BrowserPasskeysClient.h"
19
#include "BrowserMessageBuilder.h"
20
#include "BrowserPasskeys.h"
21
#include "PasskeyUtils.h"
23
#include <QJsonDocument>
25
Q_GLOBAL_STATIC(BrowserPasskeysClient, s_browserPasskeysClient);
27
BrowserPasskeysClient* BrowserPasskeysClient::instance()
29
return s_browserPasskeysClient;
32
// Constructs CredentialCreationOptions from the original PublicKeyCredential
33
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#createCredential
34
int BrowserPasskeysClient::getCredentialCreationOptions(const QJsonObject& publicKeyOptions,
35
const QString& origin,
36
QJsonObject* result) const
38
if (!result || publicKeyOptions.isEmpty()) {
39
return ERROR_PASSKEYS_EMPTY_PUBLIC_KEY;
42
// Check validity of some basic values
43
const auto checkResultError = passkeyUtils()->checkLimits(publicKeyOptions);
44
if (checkResultError > 0) {
45
return checkResultError;
48
// Get effective domain
49
QString effectiveDomain;
50
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
51
if (effectiveDomainResponse > 0) {
52
return effectiveDomainResponse;
57
const auto rpName = publicKeyOptions["rp"]["name"].toString();
58
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rp"]["id"], effectiveDomain, &rpId);
59
if (rpIdResponse > 0) {
63
// Check PublicKeyCredentialTypes
64
const auto pubKeyCredParams = passkeyUtils()->parseCredentialTypes(publicKeyOptions["pubKeyCredParams"].toArray());
65
if (pubKeyCredParams.isEmpty() && !publicKeyOptions["pubKeyCredParams"].toArray().isEmpty()) {
66
return ERROR_PASSKEYS_NO_SUPPORTED_ALGORITHMS;
70
const auto attestation = passkeyUtils()->parseAttestation(publicKeyOptions["attestation"].toString());
72
// Check validity of AuthenticatorSelection
73
auto authenticatorSelection = publicKeyOptions["authenticatorSelection"].toObject();
74
const bool isAuthenticatorSelectionValid = passkeyUtils()->isAuthenticatorSelectionValid(authenticatorSelection);
75
if (!isAuthenticatorSelectionValid) {
76
return ERROR_PASSKEYS_WAIT_FOR_LIFETIMER;
79
// Add default values for compatibility
80
if (authenticatorSelection.isEmpty()) {
81
authenticatorSelection = QJsonObject({{"userVerification", BrowserPasskeys::REQUIREMENT_PREFERRED}});
82
} else if (authenticatorSelection["userVerification"].toString().isEmpty()) {
83
authenticatorSelection["userVerification"] = BrowserPasskeys::REQUIREMENT_PREFERRED;
86
auto authenticatorAttachment = authenticatorSelection["authenticatorAttachment"].toString();
87
if (authenticatorAttachment.isEmpty()) {
88
authenticatorAttachment = BrowserPasskeys::ATTACHMENT_PLATFORM;
91
// Unknown values are ignored, but a warning will be still shown just in case
92
const auto userVerification = authenticatorSelection["userVerification"].toString();
93
if (!passkeyUtils()->isUserVerificationValid(userVerification)) {
94
qWarning() << browserMessageBuilder()->getErrorMessage(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
97
// Parse requireResidentKey and userVerification
98
const auto isResidentKeyRequired = passkeyUtils()->isResidentKeyRequired(authenticatorSelection);
99
const auto isUserVerificationRequired = passkeyUtils()->isUserVerificationRequired(authenticatorSelection);
102
auto extensionObject = publicKeyOptions["extensions"].toObject();
103
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
104
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData.extensionData);
106
// Construct the final object
107
QJsonObject credentialCreationOptions;
108
credentialCreationOptions["attestation"] = attestation; // Set this, even if only "none" is supported
109
credentialCreationOptions["authenticatorAttachment"] = authenticatorAttachment;
110
credentialCreationOptions["clientDataJSON"] = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, false);
111
credentialCreationOptions["clientExtensionResults"] = extensionData.extensionObject;
112
credentialCreationOptions["credTypesAndPubKeyAlgs"] = pubKeyCredParams;
113
credentialCreationOptions["excludeCredentials"] = publicKeyOptions["excludeCredentials"];
114
credentialCreationOptions["extensions"] = extensions;
115
credentialCreationOptions["residentKey"] = isResidentKeyRequired;
116
credentialCreationOptions["rp"] = QJsonObject({{"id", rpId}, {"name", rpName}});
117
credentialCreationOptions["user"] = publicKeyOptions["user"];
118
credentialCreationOptions["userPresence"] = !isUserVerificationRequired;
119
credentialCreationOptions["userVerification"] = isUserVerificationRequired;
121
*result = credentialCreationOptions;
125
// Use an existing credential
126
// https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#getAssertion
127
int BrowserPasskeysClient::getAssertionOptions(const QJsonObject& publicKeyOptions,
128
const QString& origin,
129
QJsonObject* result) const
131
if (!result || publicKeyOptions.isEmpty()) {
132
return ERROR_PASSKEYS_EMPTY_PUBLIC_KEY;
135
// Get effective domain
136
QString effectiveDomain;
137
const auto effectiveDomainResponse = passkeyUtils()->getEffectiveDomain(origin, &effectiveDomain);
138
if (effectiveDomainResponse > 0) {
139
return effectiveDomainResponse;
144
const auto rpIdResponse = passkeyUtils()->validateRpId(publicKeyOptions["rpId"], effectiveDomain, &rpId);
145
if (rpIdResponse > 0) {
150
auto extensionObject = publicKeyOptions["extensions"].toObject();
151
const auto extensionData = passkeyUtils()->buildExtensionData(extensionObject);
152
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData.extensionData);
155
const auto clientDataJson = passkeyUtils()->buildClientDataJson(publicKeyOptions, origin, true);
157
// Unknown values are ignored, but a warning will be still shown just in case
158
const auto userVerification = publicKeyOptions["userVerification"].toString();
159
if (!passkeyUtils()->isUserVerificationValid(userVerification)) {
160
qWarning() << browserMessageBuilder()->getErrorMessage(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
162
const auto isUserVerificationRequired = passkeyUtils()->isUserVerificationRequired(publicKeyOptions);
164
QJsonObject assertionOptions;
165
assertionOptions["allowCredentials"] = publicKeyOptions["allowCredentials"];
166
assertionOptions["clientDataJson"] = clientDataJson;
167
assertionOptions["clientExtensionResults"] = extensionData.extensionObject;
168
assertionOptions["extensions"] = extensions;
169
assertionOptions["rpId"] = rpId;
170
assertionOptions["userPresence"] = true;
171
assertionOptions["userVerification"] = isUserVerificationRequired;
173
*result = assertionOptions;