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