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