1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "hasht.h"
8 #include "nsHTMLDocument.h"
9 #include "nsIURIMutator.h"
10 #include "nsThreadUtils.h"
11 #include "WebAuthnCoseIdentifiers.h"
12 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
13 #include "mozilla/dom/Promise.h"
14 #include "mozilla/dom/PWebAuthnTransaction.h"
15 #include "mozilla/dom/WebAuthnManager.h"
16 #include "mozilla/dom/WebAuthnTransactionChild.h"
17 #include "mozilla/dom/WebAuthnUtil.h"
18 #include "mozilla/ipc/BackgroundChild.h"
19 #include "mozilla/ipc/PBackgroundChild.h"
20
21 #ifdef OS_WIN
22 # include "WinWebAuthnManager.h"
23 #endif
24
25 using namespace mozilla::ipc;
26
27 namespace mozilla {
28 namespace dom {
29
30 /***********************************************************************
31 * Statics
32 **********************************************************************/
33
34 namespace {
35 static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
36 }
37
38 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager,
39 WebAuthnManagerBase)
40
41 NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager)
42 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager,
43 WebAuthnManagerBase)
44 NS_IMPL_CYCLE_COLLECTION_UNLINK(mFollowingSignal)
45 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
46 tmp->mTransaction.reset();
47 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,WebAuthnManagerBase)48 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,
49 WebAuthnManagerBase)
50 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFollowingSignal)
51 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
52 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
53
54 /***********************************************************************
55 * Utility Functions
56 **********************************************************************/
57
58 static nsresult AssembleClientData(
59 const nsAString& aOrigin, const CryptoBuffer& aChallenge,
60 const nsAString& aType,
61 const AuthenticationExtensionsClientInputs& aExtensions,
62 /* out */ nsACString& aJsonOut) {
63 MOZ_ASSERT(NS_IsMainThread());
64
65 nsString challengeBase64;
66 nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
67 if (NS_WARN_IF(NS_FAILED(rv))) {
68 return NS_ERROR_FAILURE;
69 }
70
71 CollectedClientData clientDataObject;
72 clientDataObject.mType.Assign(aType);
73 clientDataObject.mChallenge.Assign(challengeBase64);
74 clientDataObject.mOrigin.Assign(aOrigin);
75 clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256");
76 clientDataObject.mClientExtensions = aExtensions;
77
78 nsAutoString temp;
79 if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
80 return NS_ERROR_FAILURE;
81 }
82
83 aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
84 return NS_OK;
85 }
86
GetOrigin(nsPIDOMWindowInner * aParent,nsAString & aOrigin,nsACString & aHost)87 nsresult GetOrigin(nsPIDOMWindowInner* aParent,
88 /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) {
89 MOZ_ASSERT(aParent);
90 nsCOMPtr<Document> doc = aParent->GetDoc();
91 MOZ_ASSERT(doc);
92
93 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
94 nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
95 if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(aOrigin.IsEmpty())) {
96 return NS_ERROR_FAILURE;
97 }
98
99 if (principal->GetIsIpAddress()) {
100 return NS_ERROR_DOM_SECURITY_ERR;
101 }
102
103 if (aOrigin.EqualsLiteral("null")) {
104 // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
105 // DOMException whose name is "NotAllowedError", and terminate this
106 // algorithm
107 MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
108 ("Rejecting due to opaque origin"));
109 return NS_ERROR_DOM_NOT_ALLOWED_ERR;
110 }
111
112 nsCOMPtr<nsIURI> originUri;
113 auto* basePrin = BasePrincipal::Cast(principal);
114 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(originUri)))) {
115 return NS_ERROR_FAILURE;
116 }
117 if (NS_FAILED(originUri->GetAsciiHost(aHost))) {
118 return NS_ERROR_FAILURE;
119 }
120
121 return NS_OK;
122 }
123
RelaxSameOrigin(nsPIDOMWindowInner * aParent,const nsAString & aInputRpId,nsACString & aRelaxedRpId)124 nsresult RelaxSameOrigin(nsPIDOMWindowInner* aParent,
125 const nsAString& aInputRpId,
126 /* out */ nsACString& aRelaxedRpId) {
127 MOZ_ASSERT(aParent);
128 nsCOMPtr<Document> doc = aParent->GetDoc();
129 MOZ_ASSERT(doc);
130
131 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
132 auto* basePrin = BasePrincipal::Cast(principal);
133 nsCOMPtr<nsIURI> uri;
134
135 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) {
136 return NS_ERROR_FAILURE;
137 }
138 nsAutoCString originHost;
139 if (NS_FAILED(uri->GetAsciiHost(originHost))) {
140 return NS_ERROR_FAILURE;
141 }
142 nsCOMPtr<Document> document = aParent->GetDoc();
143 if (!document || !document->IsHTMLDocument()) {
144 return NS_ERROR_FAILURE;
145 }
146 nsHTMLDocument* html = document->AsHTMLDocument();
147 // See if the given RP ID is a valid domain string.
148 // (We use the document's URI here as a template so we don't have to come up
149 // with our own scheme, etc. If we can successfully set the host as the given
150 // RP ID, then it should be a valid domain string.)
151 nsCOMPtr<nsIURI> inputRpIdURI;
152 nsresult rv = NS_MutateURI(uri)
153 .SetHost(NS_ConvertUTF16toUTF8(aInputRpId))
154 .Finalize(inputRpIdURI);
155 if (NS_FAILED(rv)) {
156 return NS_ERROR_DOM_SECURITY_ERR;
157 }
158 nsAutoCString inputRpId;
159 if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) {
160 return NS_ERROR_FAILURE;
161 }
162 if (!html->IsRegistrableDomainSuffixOfOrEqualTo(
163 NS_ConvertUTF8toUTF16(inputRpId), originHost)) {
164 return NS_ERROR_DOM_SECURITY_ERR;
165 }
166
167 aRelaxedRpId.Assign(inputRpId);
168 return NS_OK;
169 }
170
171 /***********************************************************************
172 * WebAuthnManager Implementation
173 **********************************************************************/
174
ClearTransaction()175 void WebAuthnManager::ClearTransaction() {
176 if (!mTransaction.isNothing()) {
177 StopListeningForVisibilityEvents();
178 }
179
180 mTransaction.reset();
181 Unfollow();
182 }
183
RejectTransaction(const nsresult & aError)184 void WebAuthnManager::RejectTransaction(const nsresult& aError) {
185 if (!NS_WARN_IF(mTransaction.isNothing())) {
186 mTransaction.ref().mPromise->MaybeReject(aError);
187 }
188
189 ClearTransaction();
190 }
191
CancelTransaction(const nsresult & aError)192 void WebAuthnManager::CancelTransaction(const nsresult& aError) {
193 if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
194 mChild->SendRequestCancel(mTransaction.ref().mId);
195 }
196
197 RejectTransaction(aError);
198 }
199
HandleVisibilityChange()200 void WebAuthnManager::HandleVisibilityChange() {
201 if (mTransaction.isSome()) {
202 mTransaction.ref().mVisibilityChanged = true;
203 }
204 }
205
~WebAuthnManager()206 WebAuthnManager::~WebAuthnManager() {
207 MOZ_ASSERT(NS_IsMainThread());
208
209 if (mTransaction.isSome()) {
210 ClearTransaction();
211 }
212
213 if (mChild) {
214 RefPtr<WebAuthnTransactionChild> c;
215 mChild.swap(c);
216 c->Disconnect();
217 }
218 }
219
MakeCredential(const PublicKeyCredentialCreationOptions & aOptions,const Optional<OwningNonNull<AbortSignal>> & aSignal)220 already_AddRefed<Promise> WebAuthnManager::MakeCredential(
221 const PublicKeyCredentialCreationOptions& aOptions,
222 const Optional<OwningNonNull<AbortSignal>>& aSignal) {
223 MOZ_ASSERT(NS_IsMainThread());
224
225 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
226
227 ErrorResult rv;
228 RefPtr<Promise> promise = Promise::Create(global, rv);
229 if (rv.Failed()) {
230 return nullptr;
231 }
232
233 if (mTransaction.isSome()) {
234 // If there hasn't been a visibility change during the current
235 // transaction, then let's let that one complete rather than
236 // cancelling it on a subsequent call.
237 if (!mTransaction.ref().mVisibilityChanged) {
238 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
239 return promise.forget();
240 }
241
242 // Otherwise, the user may well have clicked away, so let's
243 // abort the old transaction and take over control from here.
244 CancelTransaction(NS_ERROR_ABORT);
245 }
246
247 // Abort the request if aborted flag is already set.
248 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
249 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
250 return promise.forget();
251 }
252
253 nsString origin;
254 nsCString rpId;
255 rv = GetOrigin(mParent, origin, rpId);
256 if (NS_WARN_IF(rv.Failed())) {
257 promise->MaybeReject(std::move(rv));
258 return promise.forget();
259 }
260
261 // Enforce 5.4.3 User Account Parameters for Credential Generation
262 // When we add UX, we'll want to do more with this value, but for now
263 // we just have to verify its correctness.
264
265 CryptoBuffer userId;
266 userId.Assign(aOptions.mUser.mId);
267 if (userId.Length() > 64) {
268 promise->MaybeRejectWithTypeError("user.id is too long");
269 return promise.forget();
270 }
271
272 // If timeoutSeconds was specified, check if its value lies within a
273 // reasonable range as defined by the platform and if not, correct it to the
274 // closest value lying within that range.
275
276 uint32_t adjustedTimeout = 30000;
277 if (aOptions.mTimeout.WasPassed()) {
278 adjustedTimeout = aOptions.mTimeout.Value();
279 adjustedTimeout = std::max(15000u, adjustedTimeout);
280 adjustedTimeout = std::min(120000u, adjustedTimeout);
281 }
282
283 if (aOptions.mRp.mId.WasPassed()) {
284 // If rpId is specified, then invoke the procedure used for relaxing the
285 // same-origin restriction by setting the document.domain attribute, using
286 // rpId as the given value but without changing the current document’s
287 // domain. If no errors are thrown, set rpId to the value of host as
288 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
289 // Otherwise, reject promise with a DOMException whose name is
290 // "SecurityError", and terminate this algorithm.
291
292 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) {
293 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
294 return promise.forget();
295 }
296 }
297
298 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
299 if (aOptions.mExtensions.mAppid.WasPassed()) {
300 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
301 return promise.forget();
302 }
303
304 // Process each element of mPubKeyCredParams using the following steps, to
305 // produce a new sequence coseAlgos.
306 nsTArray<CoseAlg> coseAlgos;
307 for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
308 // If current.type does not contain a PublicKeyCredentialType
309 // supported by this implementation, then stop processing current and move
310 // on to the next element in mPubKeyCredParams.
311 if (aOptions.mPubKeyCredParams[a].mType !=
312 PublicKeyCredentialType::Public_key) {
313 continue;
314 }
315
316 coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg);
317 }
318
319 // If there are algorithms specified, but none are Public_key algorithms,
320 // reject the promise.
321 if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
322 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
323 return promise.forget();
324 }
325
326 // If excludeList is undefined, set it to the empty list.
327 //
328 // If extensions was specified, process any extensions supported by this
329 // client platform, to produce the extension data that needs to be sent to the
330 // authenticator. If an error is encountered while processing an extension,
331 // skip that extension and do not produce any extension data for it. Call the
332 // result of this processing clientExtensions.
333 //
334 // Currently no extensions are supported
335 //
336 // Use attestationChallenge, callerOrigin and rpId, along with the token
337 // binding key associated with callerOrigin (if any), to create a ClientData
338 // structure representing this request. Choose a hash algorithm for hashAlg
339 // and compute the clientDataJSON and clientDataHash.
340
341 CryptoBuffer challenge;
342 if (!challenge.Assign(aOptions.mChallenge)) {
343 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
344 return promise.forget();
345 }
346
347 nsAutoCString clientDataJSON;
348 nsresult srv = AssembleClientData(origin, challenge,
349 NS_LITERAL_STRING("webauthn.create"),
350 aOptions.mExtensions, clientDataJSON);
351 if (NS_WARN_IF(NS_FAILED(srv))) {
352 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
353 return promise.forget();
354 }
355
356 nsTArray<WebAuthnScopedCredential> excludeList;
357 for (const auto& s : aOptions.mExcludeCredentials) {
358 WebAuthnScopedCredential c;
359 CryptoBuffer cb;
360 cb.Assign(s.mId);
361 c.id() = cb;
362 excludeList.AppendElement(c);
363 }
364
365 if (!MaybeCreateBackgroundActor()) {
366 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
367 return promise.forget();
368 }
369
370 // TODO: Add extension list building
371 nsTArray<WebAuthnExtension> extensions;
372
373 // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
374 if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
375 bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
376 if (hmacCreateSecret) {
377 extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
378 }
379 }
380
381 const auto& selection = aOptions.mAuthenticatorSelection;
382 const auto& attachment = selection.mAuthenticatorAttachment;
383 const AttestationConveyancePreference& attestation = aOptions.mAttestation;
384
385 // Attachment
386 Maybe<AuthenticatorAttachment> authenticatorAttachment;
387 if (attachment.WasPassed()) {
388 authenticatorAttachment.emplace(attachment.Value());
389 }
390
391 // Create and forward authenticator selection criteria.
392 WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey,
393 selection.mUserVerification,
394 authenticatorAttachment);
395
396 nsString rpIcon;
397 if (aOptions.mRp.mIcon.WasPassed()) {
398 rpIcon = aOptions.mRp.mIcon.Value();
399 }
400
401 nsString userIcon;
402 if (aOptions.mUser.mIcon.WasPassed()) {
403 userIcon = aOptions.mUser.mIcon.Value();
404 }
405
406 WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName, rpIcon);
407
408 WebAuthnMakeCredentialUserInfo userInfo(
409 userId, aOptions.mUser.mName, userIcon, aOptions.mUser.mDisplayName);
410
411 WebAuthnMakeCredentialExtraInfo extra(rpInfo, userInfo, coseAlgos, extensions,
412 authSelection, attestation);
413
414 BrowsingContext* context = mParent->GetBrowsingContext();
415 if (!context) {
416 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
417 return promise.forget();
418 }
419
420 WebAuthnMakeCredentialInfo info(origin, NS_ConvertUTF8toUTF16(rpId),
421 challenge, clientDataJSON, adjustedTimeout,
422 excludeList, Some(extra), context->Id());
423
424 #ifdef OS_WIN
425 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
426 ListenForVisibilityEvents();
427 }
428 #else
429 ListenForVisibilityEvents();
430 #endif
431
432 AbortSignal* signal = nullptr;
433 if (aSignal.WasPassed()) {
434 signal = &aSignal.Value();
435 Follow(signal);
436 }
437
438 MOZ_ASSERT(mTransaction.isNothing());
439 mTransaction = Some(WebAuthnTransaction(promise));
440 mChild->SendRequestRegister(mTransaction.ref().mId, info);
441
442 return promise.forget();
443 }
444
GetAssertion(const PublicKeyCredentialRequestOptions & aOptions,const Optional<OwningNonNull<AbortSignal>> & aSignal)445 already_AddRefed<Promise> WebAuthnManager::GetAssertion(
446 const PublicKeyCredentialRequestOptions& aOptions,
447 const Optional<OwningNonNull<AbortSignal>>& aSignal) {
448 MOZ_ASSERT(NS_IsMainThread());
449
450 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
451
452 ErrorResult rv;
453 RefPtr<Promise> promise = Promise::Create(global, rv);
454 if (rv.Failed()) {
455 return nullptr;
456 }
457
458 if (mTransaction.isSome()) {
459 // If there hasn't been a visibility change during the current
460 // transaction, then let's let that one complete rather than
461 // cancelling it on a subsequent call.
462 if (!mTransaction.ref().mVisibilityChanged) {
463 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
464 return promise.forget();
465 }
466
467 // Otherwise, the user may well have clicked away, so let's
468 // abort the old transaction and take over control from here.
469 CancelTransaction(NS_ERROR_ABORT);
470 }
471
472 // Abort the request if aborted flag is already set.
473 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
474 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
475 return promise.forget();
476 }
477
478 nsString origin;
479 nsCString rpId;
480 rv = GetOrigin(mParent, origin, rpId);
481 if (NS_WARN_IF(rv.Failed())) {
482 promise->MaybeReject(std::move(rv));
483 return promise.forget();
484 }
485
486 // If timeoutSeconds was specified, check if its value lies within a
487 // reasonable range as defined by the platform and if not, correct it to the
488 // closest value lying within that range.
489
490 uint32_t adjustedTimeout = 30000;
491 if (aOptions.mTimeout.WasPassed()) {
492 adjustedTimeout = aOptions.mTimeout.Value();
493 adjustedTimeout = std::max(15000u, adjustedTimeout);
494 adjustedTimeout = std::min(120000u, adjustedTimeout);
495 }
496
497 if (aOptions.mRpId.WasPassed()) {
498 // If rpId is specified, then invoke the procedure used for relaxing the
499 // same-origin restriction by setting the document.domain attribute, using
500 // rpId as the given value but without changing the current document’s
501 // domain. If no errors are thrown, set rpId to the value of host as
502 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
503 // Otherwise, reject promise with a DOMException whose name is
504 // "SecurityError", and terminate this algorithm.
505
506 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) {
507 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
508 return promise.forget();
509 }
510 }
511
512 CryptoBuffer rpIdHash;
513 if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
514 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
515 return promise.forget();
516 }
517
518 // Use assertionChallenge, callerOrigin and rpId, along with the token binding
519 // key associated with callerOrigin (if any), to create a ClientData structure
520 // representing this request. Choose a hash algorithm for hashAlg and compute
521 // the clientDataJSON and clientDataHash.
522 CryptoBuffer challenge;
523 if (!challenge.Assign(aOptions.mChallenge)) {
524 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
525 return promise.forget();
526 }
527
528 nsAutoCString clientDataJSON;
529 nsresult srv =
530 AssembleClientData(origin, challenge, NS_LITERAL_STRING("webauthn.get"),
531 aOptions.mExtensions, clientDataJSON);
532 if (NS_WARN_IF(NS_FAILED(srv))) {
533 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
534 return promise.forget();
535 }
536
537 nsTArray<WebAuthnScopedCredential> allowList;
538 for (const auto& s : aOptions.mAllowCredentials) {
539 if (s.mType == PublicKeyCredentialType::Public_key) {
540 WebAuthnScopedCredential c;
541 CryptoBuffer cb;
542 cb.Assign(s.mId);
543 c.id() = cb;
544
545 // Serialize transports.
546 if (s.mTransports.WasPassed()) {
547 uint8_t transports = 0;
548
549 // Transports is a string, but we match it to an enumeration so
550 // that we have forward-compatibility, ignoring unknown transports.
551 for (const nsAString& str : s.mTransports.Value()) {
552 NS_ConvertUTF16toUTF8 cStr(str);
553 int i = FindEnumStringIndexImpl(
554 cStr.get(), cStr.Length(), AuthenticatorTransportValues::strings);
555 if (i < 0) {
556 continue; // Unknown enum
557 }
558 AuthenticatorTransport t = static_cast<AuthenticatorTransport>(i);
559
560 if (t == AuthenticatorTransport::Usb) {
561 transports |= U2F_AUTHENTICATOR_TRANSPORT_USB;
562 }
563 if (t == AuthenticatorTransport::Nfc) {
564 transports |= U2F_AUTHENTICATOR_TRANSPORT_NFC;
565 }
566 if (t == AuthenticatorTransport::Ble) {
567 transports |= U2F_AUTHENTICATOR_TRANSPORT_BLE;
568 }
569 if (t == AuthenticatorTransport::Internal) {
570 transports |= CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL;
571 }
572 }
573 c.transports() = transports;
574 }
575
576 allowList.AppendElement(c);
577 }
578 }
579
580 if (!MaybeCreateBackgroundActor()) {
581 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
582 return promise.forget();
583 }
584
585 // If extensions were specified, process any extensions supported by this
586 // client platform, to produce the extension data that needs to be sent to the
587 // authenticator. If an error is encountered while processing an extension,
588 // skip that extension and do not produce any extension data for it. Call the
589 // result of this processing clientExtensions.
590 nsTArray<WebAuthnExtension> extensions;
591
592 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
593 if (aOptions.mExtensions.mAppid.WasPassed()) {
594 nsString appId(aOptions.mExtensions.mAppid.Value());
595
596 // Check that the appId value is allowed.
597 if (!EvaluateAppID(mParent, origin, appId)) {
598 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
599 return promise.forget();
600 }
601
602 CryptoBuffer appIdHash;
603 if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) {
604 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
605 return promise.forget();
606 }
607
608 // We need the SHA-256 hash of the appId.
609 srv = HashCString(NS_ConvertUTF16toUTF8(appId), appIdHash);
610 if (NS_WARN_IF(NS_FAILED(srv))) {
611 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
612 return promise.forget();
613 }
614
615 // Append the hash and send it to the backend.
616 extensions.AppendElement(WebAuthnExtensionAppId(appIdHash, appId));
617 }
618
619 WebAuthnGetAssertionExtraInfo extra(extensions, aOptions.mUserVerification);
620
621 BrowsingContext* context = mParent->GetBrowsingContext();
622 if (!context) {
623 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
624 return promise.forget();
625 }
626
627 WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge,
628 clientDataJSON, adjustedTimeout, allowList,
629 Some(extra), context->Id());
630
631 #ifdef OS_WIN
632 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
633 ListenForVisibilityEvents();
634 }
635 #else
636 ListenForVisibilityEvents();
637 #endif
638
639 AbortSignal* signal = nullptr;
640 if (aSignal.WasPassed()) {
641 signal = &aSignal.Value();
642 Follow(signal);
643 }
644
645 MOZ_ASSERT(mTransaction.isNothing());
646 mTransaction = Some(WebAuthnTransaction(promise));
647 mChild->SendRequestSign(mTransaction.ref().mId, info);
648
649 return promise.forget();
650 }
651
Store(const Credential & aCredential)652 already_AddRefed<Promise> WebAuthnManager::Store(
653 const Credential& aCredential) {
654 MOZ_ASSERT(NS_IsMainThread());
655
656 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
657
658 ErrorResult rv;
659 RefPtr<Promise> promise = Promise::Create(global, rv);
660 if (rv.Failed()) {
661 return nullptr;
662 }
663
664 if (mTransaction.isSome()) {
665 // If there hasn't been a visibility change during the current
666 // transaction, then let's let that one complete rather than
667 // cancelling it on a subsequent call.
668 if (!mTransaction.ref().mVisibilityChanged) {
669 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
670 return promise.forget();
671 }
672
673 // Otherwise, the user may well have clicked away, so let's
674 // abort the old transaction and take over control from here.
675 CancelTransaction(NS_ERROR_ABORT);
676 }
677
678 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
679 return promise.forget();
680 }
681
FinishMakeCredential(const uint64_t & aTransactionId,const WebAuthnMakeCredentialResult & aResult)682 void WebAuthnManager::FinishMakeCredential(
683 const uint64_t& aTransactionId,
684 const WebAuthnMakeCredentialResult& aResult) {
685 MOZ_ASSERT(NS_IsMainThread());
686
687 // Check for a valid transaction.
688 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
689 return;
690 }
691
692 CryptoBuffer clientDataBuf;
693 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
694 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
695 return;
696 }
697
698 CryptoBuffer attObjBuf;
699 if (NS_WARN_IF(!attObjBuf.Assign(aResult.AttestationObject()))) {
700 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
701 return;
702 }
703
704 CryptoBuffer keyHandleBuf;
705 if (NS_WARN_IF(!keyHandleBuf.Assign(aResult.KeyHandle()))) {
706 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
707 return;
708 }
709
710 nsAutoString keyHandleBase64Url;
711 nsresult rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
712 if (NS_WARN_IF(NS_FAILED(rv))) {
713 RejectTransaction(rv);
714 return;
715 }
716
717 // Create a new PublicKeyCredential object and populate its fields with the
718 // values returned from the authenticator as well as the clientDataJSON
719 // computed earlier.
720 RefPtr<AuthenticatorAttestationResponse> attestation =
721 new AuthenticatorAttestationResponse(mParent);
722 attestation->SetClientDataJSON(clientDataBuf);
723 attestation->SetAttestationObject(attObjBuf);
724
725 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
726 credential->SetId(keyHandleBase64Url);
727 credential->SetType(NS_LITERAL_STRING("public-key"));
728 credential->SetRawId(keyHandleBuf);
729 credential->SetResponse(attestation);
730
731 // Forward client extension results.
732 for (auto& ext : aResult.Extensions()) {
733 if (ext.type() ==
734 WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
735 bool hmacCreateSecret =
736 ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
737 credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
738 }
739 }
740
741 mTransaction.ref().mPromise->MaybeResolve(credential);
742 ClearTransaction();
743 }
744
FinishGetAssertion(const uint64_t & aTransactionId,const WebAuthnGetAssertionResult & aResult)745 void WebAuthnManager::FinishGetAssertion(
746 const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) {
747 MOZ_ASSERT(NS_IsMainThread());
748
749 // Check for a valid transaction.
750 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
751 return;
752 }
753
754 CryptoBuffer clientDataBuf;
755 if (!clientDataBuf.Assign(aResult.ClientDataJSON())) {
756 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
757 return;
758 }
759
760 CryptoBuffer credentialBuf;
761 if (!credentialBuf.Assign(aResult.KeyHandle())) {
762 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
763 return;
764 }
765
766 CryptoBuffer signatureBuf;
767 if (!signatureBuf.Assign(aResult.Signature())) {
768 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
769 return;
770 }
771
772 CryptoBuffer authenticatorDataBuf;
773 if (!authenticatorDataBuf.Assign(aResult.AuthenticatorData())) {
774 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
775 return;
776 }
777
778 nsAutoString credentialBase64Url;
779 nsresult rv = credentialBuf.ToJwkBase64(credentialBase64Url);
780 if (NS_WARN_IF(NS_FAILED(rv))) {
781 RejectTransaction(rv);
782 return;
783 }
784
785 CryptoBuffer userHandleBuf;
786 // U2FTokenManager don't return user handle.
787 // Best effort.
788 userHandleBuf.Assign(aResult.UserHandle());
789
790 // If any authenticator returns success:
791
792 // Create a new PublicKeyCredential object named value and populate its fields
793 // with the values returned from the authenticator as well as the
794 // clientDataJSON computed earlier.
795 RefPtr<AuthenticatorAssertionResponse> assertion =
796 new AuthenticatorAssertionResponse(mParent);
797 assertion->SetClientDataJSON(clientDataBuf);
798 assertion->SetAuthenticatorData(authenticatorDataBuf);
799 assertion->SetSignature(signatureBuf);
800 if (!userHandleBuf.IsEmpty()) {
801 assertion->SetUserHandle(userHandleBuf);
802 }
803
804 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
805 credential->SetId(credentialBase64Url);
806 credential->SetType(NS_LITERAL_STRING("public-key"));
807 credential->SetRawId(credentialBuf);
808 credential->SetResponse(assertion);
809
810 // Forward client extension results.
811 for (auto& ext : aResult.Extensions()) {
812 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
813 bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
814 credential->SetClientExtensionResultAppId(appid);
815 }
816 }
817
818 mTransaction.ref().mPromise->MaybeResolve(credential);
819 ClearTransaction();
820 }
821
RequestAborted(const uint64_t & aTransactionId,const nsresult & aError)822 void WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
823 const nsresult& aError) {
824 MOZ_ASSERT(NS_IsMainThread());
825
826 if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
827 RejectTransaction(aError);
828 }
829 }
830
Abort()831 void WebAuthnManager::Abort() { CancelTransaction(NS_ERROR_DOM_ABORT_ERR); }
832
833 } // namespace dom
834 } // namespace mozilla
835