keepassxc

Форк
0
/
PasskeyUtils.cpp 
407 строк · 14.5 Кб
1
/*
2
 *  Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
 *
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.
8
 *
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.
13
 *
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/>.
16
 */
17

18
#include "PasskeyUtils.h"
19
#include "BrowserMessageBuilder.h"
20
#include "BrowserPasskeys.h"
21
#include "core/Tools.h"
22
#include "core/UrlTools.h"
23

24
#include <QList>
25
#include <QUrl>
26

27
Q_GLOBAL_STATIC(PasskeyUtils, s_passkeyUtils);
28

29
PasskeyUtils* PasskeyUtils::instance()
30
{
31
    return s_passkeyUtils;
32
}
33

34
int PasskeyUtils::checkLimits(const QJsonObject& pkOptions) const
35
{
36
    const auto challenge = pkOptions["challenge"].toString();
37
    if (challenge.isEmpty() || challenge.length() < 16) {
38
        return ERROR_PASSKEYS_INVALID_CHALLENGE;
39
    }
40

41
    const auto userIdBase64 = pkOptions["user"]["id"].toString();
42
    const auto userId = browserMessageBuilder()->getArrayFromBase64(userIdBase64);
43
    if (userId.isEmpty() || (userId.length() < 1 || userId.length() > 64)) {
44
        return ERROR_PASSKEYS_INVALID_USER_ID;
45
    }
46

47
    return PASSKEYS_SUCCESS;
48
}
49

50
// Basic check for the object that it contains necessary variables in a correct form
51
bool PasskeyUtils::checkCredentialCreationOptions(const QJsonObject& credentialCreationOptions) const
52
{
53
    if (!credentialCreationOptions["attestation"].isString()
54
        || credentialCreationOptions["attestation"].toString().isEmpty()
55
        || !credentialCreationOptions["clientDataJSON"].isObject()
56
        || credentialCreationOptions["clientDataJSON"].toObject().isEmpty()
57
        || !credentialCreationOptions["rp"].isObject() || credentialCreationOptions["rp"].toObject().isEmpty()
58
        || !credentialCreationOptions["user"].isObject() || credentialCreationOptions["user"].toObject().isEmpty()
59
        || !credentialCreationOptions["residentKey"].isBool() || credentialCreationOptions["residentKey"].isUndefined()
60
        || !credentialCreationOptions["userPresence"].isBool()
61
        || credentialCreationOptions["userPresence"].isUndefined()
62
        || !credentialCreationOptions["userVerification"].isBool()
63
        || credentialCreationOptions["userVerification"].isUndefined()
64
        || !credentialCreationOptions["credTypesAndPubKeyAlgs"].isArray()
65
        || credentialCreationOptions["credTypesAndPubKeyAlgs"].toArray().isEmpty()
66
        || !credentialCreationOptions["excludeCredentials"].isArray()
67
        || credentialCreationOptions["excludeCredentials"].isUndefined()) {
68
        return false;
69
    }
70

71
    return true;
72
}
73

74
// Basic check for the object that it contains necessary variables in a correct form
75
bool PasskeyUtils::checkCredentialAssertionOptions(const QJsonObject& assertionOptions) const
76
{
77
    if (!assertionOptions["clientDataJson"].isObject() || assertionOptions["clientDataJson"].toObject().isEmpty()
78
        || !assertionOptions["rpId"].isString() || assertionOptions["rpId"].toString().isEmpty()
79
        || !assertionOptions["userPresence"].isBool() || assertionOptions["userPresence"].isUndefined()
80
        || !assertionOptions["userVerification"].isBool() || assertionOptions["userVerification"].isUndefined()) {
81
        return false;
82
    }
83

84
    return true;
85
}
86

87
int PasskeyUtils::getEffectiveDomain(const QString& origin, QString* result) const
88
{
89
    if (!result) {
90
        return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
91
    }
92

93
    if (origin.isEmpty()) {
94
        return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
95
    }
96

97
    const auto effectiveDomain = QUrl::fromUserInput(origin).host();
98
    if (!isDomain(effectiveDomain)) {
99
        return ERROR_PASSKEYS_DOMAIN_IS_NOT_VALID;
100
    }
101

102
    *result = effectiveDomain;
103
    return PASSKEYS_SUCCESS;
104
}
105

106
int PasskeyUtils::validateRpId(const QJsonValue& rpIdValue, const QString& effectiveDomain, QString* result) const
107
{
108
    if (!result) {
109
        return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH;
110
    }
111

112
    if (effectiveDomain.isEmpty()) {
113
        return ERROR_PASSKEYS_ORIGIN_NOT_ALLOWED;
114
    }
115

116
    //  The RP ID defaults to being the caller's origin's effective domain unless the caller has explicitly set
117
    //  options.rp.id
118
    if (rpIdValue.isUndefined() || rpIdValue.isNull()) {
119
        *result = effectiveDomain;
120
        return PASSKEYS_SUCCESS;
121
    }
122

123
    const auto rpId = rpIdValue.toString();
124
    if (!isRegistrableDomainSuffix(rpId, effectiveDomain)) {
125
        return ERROR_PASSKEYS_DOMAIN_RPID_MISMATCH;
126
    }
127

128
    if (rpId == effectiveDomain) {
129
        *result = effectiveDomain;
130
        return PASSKEYS_SUCCESS;
131
    }
132

133
    *result = rpId;
134
    return PASSKEYS_SUCCESS;
135
}
136

137
// https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dom-publickeycredentialcreationoptions-attestation
138
QString PasskeyUtils::parseAttestation(const QString& attestation) const
139
{
140
    return attestation == BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT ? BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT
141
                                                                       : BrowserPasskeys::PASSKEYS_ATTESTATION_NONE;
142
}
143

144
QJsonArray PasskeyUtils::parseCredentialTypes(const QJsonArray& credentialTypes) const
145
{
146
    QJsonArray credTypesAndPubKeyAlgs;
147

148
    if (credentialTypes.isEmpty()) {
149
        // Set default values
150
        credTypesAndPubKeyAlgs.push_back(QJsonObject({
151
            {"type", BrowserPasskeys::PUBLIC_KEY},
152
            {"alg", WebAuthnAlgorithms::ES256},
153
        }));
154
        credTypesAndPubKeyAlgs.push_back(QJsonObject({
155
            {"type", BrowserPasskeys::PUBLIC_KEY},
156
            {"alg", WebAuthnAlgorithms::RS256},
157
        }));
158
    } else {
159
        for (const auto current : credentialTypes) {
160
            if (current["type"] != BrowserPasskeys::PUBLIC_KEY || current["alg"].isUndefined()) {
161
                continue;
162
            }
163

164
            const auto currentAlg = current["alg"].toInt();
165
            if (currentAlg != WebAuthnAlgorithms::ES256 && currentAlg != WebAuthnAlgorithms::RS256
166
                && currentAlg != WebAuthnAlgorithms::EDDSA) {
167
                continue;
168
            }
169

170
            credTypesAndPubKeyAlgs.push_back(QJsonObject({
171
                {"type", current["type"]},
172
                {"alg", currentAlg},
173
            }));
174
        }
175
    }
176

177
    return credTypesAndPubKeyAlgs;
178
}
179

180
bool PasskeyUtils::isAuthenticatorSelectionValid(const QJsonObject& authenticatorSelection) const
181
{
182
    const auto authenticatorAttachment = authenticatorSelection["authenticatorAttachment"].toString();
183
    if (!authenticatorAttachment.isEmpty() && authenticatorAttachment != BrowserPasskeys::ATTACHMENT_PLATFORM
184
        && authenticatorAttachment != BrowserPasskeys::ATTACHMENT_CROSS_PLATFORM) {
185
        return false;
186
    }
187

188
    const auto requireResidentKey = authenticatorSelection["requireResidentKey"].toBool();
189
    if (requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
190
        return false;
191
    }
192

193
    const auto residentKey = authenticatorSelection["residentKey"].toString();
194
    if (residentKey == "required" && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
195
        return false;
196
    }
197

198
    if (residentKey.isEmpty() && requireResidentKey && !BrowserPasskeys::SUPPORT_RESIDENT_KEYS) {
199
        return false;
200
    }
201

202
    const auto userVerification = authenticatorSelection["userVerification"].toBool();
203
    if (userVerification && !BrowserPasskeys::SUPPORT_USER_VERIFICATION) {
204
        return false;
205
    }
206

207
    return true;
208
}
209

210
bool PasskeyUtils::isRegistrableDomainSuffix(const QString& hostSuffixString, const QString& originalHost) const
211
{
212
    if (hostSuffixString.isEmpty()) {
213
        return false;
214
    }
215

216
    if (!isDomain(originalHost)) {
217
        return false;
218
    }
219

220
    const auto hostSuffix = QUrl::fromUserInput(hostSuffixString).host();
221
    if (hostSuffix == originalHost) {
222
        return true;
223
    }
224

225
    if (!isDomain(hostSuffix)) {
226
        return false;
227
    }
228

229
    const auto prefixedHostSuffix = QString(".%1").arg(hostSuffix);
230
    if (!originalHost.endsWith(prefixedHostSuffix)) {
231
        return false;
232
    }
233

234
    if (hostSuffix == urlTools()->getTopLevelDomainFromUrl(hostSuffix)) {
235
        return false;
236
    }
237

238
    const auto originalPublicSuffix = urlTools()->getTopLevelDomainFromUrl(originalHost);
239
    if (originalPublicSuffix.isEmpty()) {
240
        return false;
241
    }
242

243
    if (originalPublicSuffix.endsWith(prefixedHostSuffix)) {
244
        return false;
245
    }
246

247
    if (!hostSuffix.endsWith(QString(".%1").arg(originalPublicSuffix))) {
248
        return false;
249
    }
250

251
    return true;
252
}
253

254
bool PasskeyUtils::isDomain(const QString& hostName) const
255
{
256
    const auto domain = QUrl::fromUserInput(hostName).host();
257
    return !domain.isEmpty() && !domain.endsWith('.') && Tools::isAsciiString(domain)
258
           && !urlTools()->domainHasIllegalCharacters(domain) && !urlTools()->isIpAddress(hostName);
259
}
260

261
bool PasskeyUtils::isUserVerificationValid(const QString& userVerification) const
262
{
263
    return QStringList({BrowserPasskeys::REQUIREMENT_PREFERRED,
264
                        BrowserPasskeys::REQUIREMENT_REQUIRED,
265
                        BrowserPasskeys::REQUIREMENT_DISCOURAGED})
266
        .contains(userVerification);
267
}
268

269
bool PasskeyUtils::isOriginAllowedWithLocalhost(bool allowLocalhostWithPasskeys, const QString& origin) const
270
{
271
    if (origin.startsWith("https://") || (allowLocalhostWithPasskeys && origin.startsWith("file://"))) {
272
        return true;
273
    }
274

275
    if (!allowLocalhostWithPasskeys) {
276
        return false;
277
    }
278

279
    const auto host = QUrl::fromUserInput(origin).host();
280
    return host == "localhost" || host == "localhost." || host.endsWith(".localhost") || host.endsWith(".localhost.");
281
}
282

283
bool PasskeyUtils::isResidentKeyRequired(const QJsonObject& authenticatorSelection) const
284
{
285
    if (authenticatorSelection.isEmpty()) {
286
        return false;
287
    }
288

289
    const auto residentKey = authenticatorSelection["residentKey"].toString();
290
    if (residentKey == BrowserPasskeys::REQUIREMENT_REQUIRED
291
        || (BrowserPasskeys::SUPPORT_RESIDENT_KEYS && residentKey == BrowserPasskeys::REQUIREMENT_PREFERRED)) {
292
        return true;
293
    } else if (residentKey == BrowserPasskeys::REQUIREMENT_DISCOURAGED) {
294
        return false;
295
    }
296

297
    return authenticatorSelection["requireResidentKey"].toBool();
298
}
299

300
bool PasskeyUtils::isUserVerificationRequired(const QJsonObject& authenticatorSelection) const
301
{
302
    const auto userVerification = authenticatorSelection["userVerification"].toString();
303
    return userVerification == BrowserPasskeys::REQUIREMENT_REQUIRED
304
           || (userVerification == BrowserPasskeys::REQUIREMENT_PREFERRED
305
               && BrowserPasskeys::SUPPORT_USER_VERIFICATION);
306
}
307

308
ExtensionResult PasskeyUtils::buildExtensionData(QJsonObject& extensionObject) const
309
{
310
    const QStringList allowedKeys = {"credProps", "uvm"};
311

312
    // Remove unsupported keys
313
    for (const auto& key : extensionObject.keys()) {
314
        if (!allowedKeys.contains(key)) {
315
            extensionObject.remove(key);
316
        }
317
    }
318

319
    // Create response object
320
    QJsonObject extensionJSON;
321

322
    // https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
323
    if (extensionObject.contains("credProps") && extensionObject["credProps"].toBool()) {
324
        extensionJSON["credProps"] = QJsonObject({{"rk", true}});
325
    }
326

327
    // https://w3c.github.io/webauthn/#sctn-uvm-extension
328
    if (extensionObject.contains("uvm") && extensionObject["uvm"].toBool()) {
329
        QJsonArray uvmResponse;
330
        QJsonArray uvmArray = {
331
            1, // userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001)
332
            1, // keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
333
            1, // matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
334
        };
335
        uvmResponse.append(uvmArray);
336
        extensionJSON["uvm"] = uvmResponse;
337
    }
338

339
    if (extensionJSON.isEmpty()) {
340
        return {};
341
    }
342

343
    auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
344
    if (!extensionData.isEmpty()) {
345
        ExtensionResult result;
346
        result.extensionData = extensionData;
347
        result.extensionObject = extensionJSON;
348
        return result;
349
    }
350

351
    return {};
352
}
353

354
QJsonObject PasskeyUtils::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get) const
355
{
356
    QJsonObject clientData;
357
    clientData["challenge"] = publicKey["challenge"];
358
    clientData["crossOrigin"] = false;
359
    clientData["origin"] = origin;
360
    clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
361

362
    return clientData;
363
}
364

365
QStringList PasskeyUtils::getAllowedCredentialsFromAssertionOptions(const QJsonObject& assertionOptions) const
366
{
367
    QStringList allowedCredentials;
368
    for (const auto& credential : assertionOptions["allowCredentials"].toArray()) {
369
        const auto cred = credential.toObject();
370
        const auto id = cred["id"].toString();
371
        const auto transports = cred["transports"].toArray();
372
        const auto hasSupportedTransport = transports.isEmpty()
373
                                           || (transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_INTERNAL)
374
                                               || transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_NFC)
375
                                               || transports.contains(BrowserPasskeys::AUTHENTICATOR_TRANSPORT_USB));
376

377
        if (cred["type"].toString() == BrowserPasskeys::PUBLIC_KEY && hasSupportedTransport && !id.isEmpty()) {
378
            allowedCredentials << id;
379
        }
380
    }
381

382
    return allowedCredentials;
383
}
384

385
// For compatibility with StrongBox (and other possible clients in the future)
386
QString PasskeyUtils::getCredentialIdFromEntry(const Entry* entry) const
387
{
388
    if (!entry) {
389
        return {};
390
    }
391

392
    return entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
393
               ? entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID)
394
               : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID);
395
}
396

397
// For compatibility with StrongBox (and other possible clients in the future)
398
QString PasskeyUtils::getUsernameFromEntry(const Entry* entry) const
399
{
400
    if (!entry) {
401
        return {};
402
    }
403

404
    return entry->attributes()->hasKey(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
405
               ? entry->attributes()->value(BrowserPasskeys::KPXC_PASSKEY_USERNAME)
406
               : entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);
407
}
408

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.