Bug 1614879 [wpt PR 21750] - Set request mode for beacon request with non-cors-safeli...
[gecko.git] / dom / u2f / U2F.cpp
blobd21f480b14e2c008b53ffac9dcc0945c7c0006d8
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/. */
7 #include "mozilla/dom/U2F.h"
8 #include "mozilla/dom/WebCryptoCommon.h"
9 #include "mozilla/ipc/PBackgroundChild.h"
10 #include "mozilla/ipc/BackgroundChild.h"
11 #include "mozilla/dom/WebAuthnTransactionChild.h"
12 #include "mozilla/dom/WebAuthnUtil.h"
13 #include "nsContentUtils.h"
14 #include "nsNetUtil.h"
15 #include "nsURLParsers.h"
17 #ifdef OS_WIN
18 # include "WinWebAuthnManager.h"
19 #endif
21 using namespace mozilla::ipc;
23 // Forward decl because of nsHTMLDocument.h's complex dependency on
24 // /layout/style
25 class nsHTMLDocument {
26 public:
27 bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
28 const nsACString& aOrigHost);
31 namespace mozilla {
32 namespace dom {
34 NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment");
35 NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion");
37 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
38 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
39 "https://www.gstatic.com/securitykey/origins.json");
40 NS_NAMED_LITERAL_STRING(
41 kGoogleAccountsAppId2,
42 "https://www.gstatic.com/securitykey/a/google.com/origins.json");
44 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F)
45 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
46 NS_INTERFACE_MAP_END_INHERITING(WebAuthnManagerBase)
48 NS_IMPL_ADDREF_INHERITED(U2F, WebAuthnManagerBase)
49 NS_IMPL_RELEASE_INHERITED(U2F, WebAuthnManagerBase)
51 NS_IMPL_CYCLE_COLLECTION_CLASS(U2F)
52 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(U2F, WebAuthnManagerBase)
53 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
54 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
55 tmp->mTransaction.reset();
56 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(U2F, WebAuthnManagerBase)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
60 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(U2F)
62 /***********************************************************************
63 * Utility Functions
64 **********************************************************************/
66 static ErrorCode ConvertNSResultToErrorCode(const nsresult& aError) {
67 if (aError == NS_ERROR_DOM_TIMEOUT_ERR) {
68 return ErrorCode::TIMEOUT;
70 /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
71 if (aError == NS_ERROR_DOM_INVALID_STATE_ERR) {
72 return ErrorCode::DEVICE_INELIGIBLE;
74 return ErrorCode::OTHER_ERROR;
77 static uint32_t AdjustedTimeoutMillis(
78 const Optional<Nullable<int32_t>>& opt_aSeconds) {
79 uint32_t adjustedTimeoutMillis = 30000u;
80 if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) {
81 adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u;
82 adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis);
83 adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis);
85 return adjustedTimeoutMillis;
88 static nsresult AssembleClientData(const nsAString& aOrigin,
89 const nsAString& aTyp,
90 const nsAString& aChallenge,
91 /* out */ nsString& aClientData) {
92 MOZ_ASSERT(NS_IsMainThread());
93 U2FClientData clientDataObject;
94 clientDataObject.mTyp.Construct(aTyp); // "Typ" from the U2F specification
95 clientDataObject.mChallenge.Construct(aChallenge);
96 clientDataObject.mOrigin.Construct(aOrigin);
98 if (NS_WARN_IF(!clientDataObject.ToJSON(aClientData))) {
99 return NS_ERROR_FAILURE;
102 return NS_OK;
105 static void RegisteredKeysToScopedCredentialList(
106 const nsAString& aAppId, const nsTArray<RegisteredKey>& aKeys,
107 nsTArray<WebAuthnScopedCredential>& aList) {
108 for (const RegisteredKey& key : aKeys) {
109 // Check for required attributes
110 if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() ||
111 key.mVersion.Value() != kRequiredU2FVersion) {
112 continue;
115 // If this key's mAppId doesn't match the invocation, we can't handle it.
116 if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) {
117 continue;
120 CryptoBuffer keyHandle;
121 nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value());
122 if (NS_WARN_IF(NS_FAILED(rv))) {
123 continue;
126 WebAuthnScopedCredential c;
127 c.id() = keyHandle;
128 aList.AppendElement(c);
132 /***********************************************************************
133 * U2F JavaScript API Implementation
134 **********************************************************************/
136 U2F::~U2F() {
137 MOZ_ASSERT(NS_IsMainThread());
139 if (mTransaction.isSome()) {
140 ClearTransaction();
143 if (mChild) {
144 RefPtr<WebAuthnTransactionChild> c;
145 mChild.swap(c);
146 c->Disconnect();
150 void U2F::Init(ErrorResult& aRv) {
151 MOZ_ASSERT(mParent);
153 nsCOMPtr<Document> doc = mParent->GetDoc();
154 MOZ_ASSERT(doc);
155 if (!doc) {
156 aRv.Throw(NS_ERROR_FAILURE);
157 return;
160 nsIPrincipal* principal = doc->NodePrincipal();
161 aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
162 if (NS_WARN_IF(aRv.Failed())) {
163 return;
166 if (NS_WARN_IF(mOrigin.IsEmpty())) {
167 aRv.Throw(NS_ERROR_FAILURE);
168 return;
172 /* virtual */
173 JSObject* U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
174 return U2F_Binding::Wrap(aCx, this, aGivenProto);
177 template <typename T, typename C>
178 void U2F::ExecuteCallback(T& aResp, nsMainThreadPtrHandle<C>& aCb) {
179 MOZ_ASSERT(NS_IsMainThread());
180 MOZ_ASSERT(aCb);
182 ErrorResult error;
183 aCb->Call(aResp, error);
184 NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed");
185 error.SuppressException(); // Useful exceptions already emitted
188 void U2F::Register(const nsAString& aAppId,
189 const Sequence<RegisterRequest>& aRegisterRequests,
190 const Sequence<RegisteredKey>& aRegisteredKeys,
191 U2FRegisterCallback& aCallback,
192 const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
193 ErrorResult& aRv) {
194 MOZ_ASSERT(NS_IsMainThread());
196 nsMainThreadPtrHandle<U2FRegisterCallback> callback(
197 new nsMainThreadPtrHolder<U2FRegisterCallback>("U2F::Register::callback",
198 &aCallback));
200 // Ensure we have a callback.
201 if (NS_WARN_IF(!callback)) {
202 return;
205 if (mTransaction.isSome()) {
206 // If there hasn't been a visibility change during the current
207 // transaction, then let's let that one complete rather than
208 // cancelling it on a subsequent call.
209 if (!mTransaction.ref().mVisibilityChanged) {
210 RegisterResponse response;
211 response.mErrorCode.Construct(
212 static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
213 ExecuteCallback(response, callback);
214 return;
217 // Otherwise, the user may well have clicked away, so let's
218 // abort the old transaction and take over control from here.
219 CancelTransaction(NS_ERROR_ABORT);
222 // Evaluate the AppID
223 nsString adjustedAppId(aAppId);
224 if (!EvaluateAppID(mParent, mOrigin, adjustedAppId)) {
225 RegisterResponse response;
226 response.mErrorCode.Construct(
227 static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
228 ExecuteCallback(response, callback);
229 return;
232 nsAutoString clientDataJSON;
234 // Pick the first valid RegisterRequest; we can only work with one.
235 CryptoBuffer challenge;
236 for (const RegisterRequest& req : aRegisterRequests) {
237 if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() ||
238 req.mVersion.Value() != kRequiredU2FVersion) {
239 continue;
241 if (!challenge.Assign(NS_ConvertUTF16toUTF8(req.mChallenge.Value()))) {
242 continue;
245 nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment,
246 req.mChallenge.Value(), clientDataJSON);
247 if (NS_WARN_IF(NS_FAILED(rv))) {
248 continue;
252 // Did we not get a valid RegisterRequest? Abort.
253 if (clientDataJSON.IsEmpty()) {
254 RegisterResponse response;
255 response.mErrorCode.Construct(
256 static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
257 ExecuteCallback(response, callback);
258 return;
261 // Build the exclusion list, if any
262 nsTArray<WebAuthnScopedCredential> excludeList;
263 RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
264 excludeList);
266 if (!MaybeCreateBackgroundActor()) {
267 RegisterResponse response;
268 response.mErrorCode.Construct(
269 static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
270 ExecuteCallback(response, callback);
271 return;
274 #ifdef OS_WIN
275 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
276 ListenForVisibilityEvents();
278 #else
279 ListenForVisibilityEvents();
280 #endif
282 NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
283 uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
285 WebAuthnMakeCredentialInfo info(mOrigin, adjustedAppId, challenge, clientData,
286 adjustedTimeoutMillis, excludeList,
287 Nothing() /* no extra info for U2F */);
289 MOZ_ASSERT(mTransaction.isNothing());
290 mTransaction = Some(U2FTransaction(AsVariant(callback)));
291 mChild->SendRequestRegister(mTransaction.ref().mId, info);
294 using binding_detail::GenericMethod;
295 using binding_detail::NormalThisPolicy;
296 using binding_detail::ThrowExceptions;
298 // register_impl_methodinfo is generated by bindings.
299 namespace U2F_Binding {
300 extern const JSJitInfo register_impl_methodinfo;
301 } // namespace U2F_Binding
303 // We have 4 non-optional args.
304 static const JSFunctionSpec register_spec = JS_FNSPEC(
305 "register", (GenericMethod<NormalThisPolicy, ThrowExceptions>),
306 &U2F_Binding::register_impl_methodinfo, 4, JSPROP_ENUMERATE, nullptr);
308 void U2F::GetRegister(JSContext* aCx,
309 JS::MutableHandle<JSObject*> aRegisterFunc,
310 ErrorResult& aRv) {
311 JSFunction* fun = JS::NewFunctionFromSpec(aCx, &register_spec);
312 if (!fun) {
313 aRv.NoteJSContextException(aCx);
314 return;
317 aRegisterFunc.set(JS_GetFunctionObject(fun));
320 void U2F::FinishMakeCredential(const uint64_t& aTransactionId,
321 const WebAuthnMakeCredentialResult& aResult) {
322 MOZ_ASSERT(NS_IsMainThread());
324 // Check for a valid transaction.
325 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
326 return;
329 if (NS_WARN_IF(!mTransaction.ref().HasRegisterCallback())) {
330 RejectTransaction(NS_ERROR_ABORT);
331 return;
334 // A CTAP2 response.
335 if (aResult.RegistrationData().Length() == 0) {
336 RejectTransaction(NS_ERROR_ABORT);
337 return;
340 CryptoBuffer clientDataBuf;
341 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
342 RejectTransaction(NS_ERROR_ABORT);
343 return;
346 CryptoBuffer regBuf;
347 if (NS_WARN_IF(!regBuf.Assign(aResult.RegistrationData()))) {
348 RejectTransaction(NS_ERROR_ABORT);
349 return;
352 nsString clientDataBase64;
353 nsString registrationDataBase64;
354 nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
355 nsresult rvRegistrationData = regBuf.ToJwkBase64(registrationDataBase64);
357 if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
358 NS_WARN_IF(NS_FAILED(rvRegistrationData))) {
359 RejectTransaction(NS_ERROR_ABORT);
360 return;
363 // Assemble a response object to return
364 RegisterResponse response;
365 response.mVersion.Construct(kRequiredU2FVersion);
366 response.mClientData.Construct(clientDataBase64);
367 response.mRegistrationData.Construct(registrationDataBase64);
368 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
370 // Keep the callback pointer alive.
371 nsMainThreadPtrHandle<U2FRegisterCallback> callback(
372 mTransaction.ref().GetRegisterCallback());
374 ClearTransaction();
375 ExecuteCallback(response, callback);
378 void U2F::Sign(const nsAString& aAppId, const nsAString& aChallenge,
379 const Sequence<RegisteredKey>& aRegisteredKeys,
380 U2FSignCallback& aCallback,
381 const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
382 ErrorResult& aRv) {
383 MOZ_ASSERT(NS_IsMainThread());
385 nsMainThreadPtrHandle<U2FSignCallback> callback(
386 new nsMainThreadPtrHolder<U2FSignCallback>("U2F::Sign::callback",
387 &aCallback));
389 // Ensure we have a callback.
390 if (NS_WARN_IF(!callback)) {
391 return;
394 if (mTransaction.isSome()) {
395 // If there hasn't been a visibility change during the current
396 // transaction, then let's let that one complete rather than
397 // cancelling it on a subsequent call.
398 if (!mTransaction.ref().mVisibilityChanged) {
399 SignResponse response;
400 response.mErrorCode.Construct(
401 static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
402 ExecuteCallback(response, callback);
403 return;
406 // Otherwise, the user may well have clicked away, so let's
407 // abort the old transaction and take over control from here.
408 CancelTransaction(NS_ERROR_ABORT);
411 // Evaluate the AppID
412 nsString adjustedAppId(aAppId);
413 if (!EvaluateAppID(mParent, mOrigin, adjustedAppId)) {
414 SignResponse response;
415 response.mErrorCode.Construct(
416 static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
417 ExecuteCallback(response, callback);
418 return;
421 // Produce the AppParam from the current AppID
422 nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);
424 nsAutoString clientDataJSON;
425 nsresult rv =
426 AssembleClientData(mOrigin, kGetAssertion, aChallenge, clientDataJSON);
427 if (NS_WARN_IF(NS_FAILED(rv))) {
428 SignResponse response;
429 response.mErrorCode.Construct(
430 static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
431 ExecuteCallback(response, callback);
432 return;
435 CryptoBuffer challenge;
436 if (!challenge.Assign(NS_ConvertUTF16toUTF8(aChallenge))) {
437 SignResponse response;
438 response.mErrorCode.Construct(
439 static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
440 ExecuteCallback(response, callback);
441 return;
444 // Build the key list, if any
445 nsTArray<WebAuthnScopedCredential> permittedList;
446 RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
447 permittedList);
449 if (!MaybeCreateBackgroundActor()) {
450 SignResponse response;
451 response.mErrorCode.Construct(
452 static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
453 ExecuteCallback(response, callback);
454 return;
457 #ifdef OS_WIN
458 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
459 ListenForVisibilityEvents();
461 #else
462 ListenForVisibilityEvents();
463 #endif
465 // Always blank for U2F
466 nsTArray<WebAuthnExtension> extensions;
468 NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
469 uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
471 WebAuthnGetAssertionInfo info(mOrigin, adjustedAppId, challenge, clientData,
472 adjustedTimeoutMillis, permittedList,
473 Nothing() /* no extra info for U2F */);
475 MOZ_ASSERT(mTransaction.isNothing());
476 mTransaction = Some(U2FTransaction(AsVariant(callback)));
477 mChild->SendRequestSign(mTransaction.ref().mId, info);
480 // sign_impl_methodinfo is generated by bindings.
481 namespace U2F_Binding {
482 extern const JSJitInfo sign_impl_methodinfo;
483 } // namespace U2F_Binding
485 // We have 4 non-optional args.
486 static const JSFunctionSpec sign_spec =
487 JS_FNSPEC("sign", (GenericMethod<NormalThisPolicy, ThrowExceptions>),
488 &U2F_Binding::sign_impl_methodinfo, 4, JSPROP_ENUMERATE, nullptr);
490 void U2F::GetSign(JSContext* aCx, JS::MutableHandle<JSObject*> aSignFunc,
491 ErrorResult& aRv) {
492 JSFunction* fun = JS::NewFunctionFromSpec(aCx, &sign_spec);
493 if (!fun) {
494 aRv.NoteJSContextException(aCx);
495 return;
498 aSignFunc.set(JS_GetFunctionObject(fun));
501 void U2F::FinishGetAssertion(const uint64_t& aTransactionId,
502 const WebAuthnGetAssertionResult& aResult) {
503 MOZ_ASSERT(NS_IsMainThread());
505 // Check for a valid transaction.
506 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
507 return;
510 if (NS_WARN_IF(!mTransaction.ref().HasSignCallback())) {
511 RejectTransaction(NS_ERROR_ABORT);
512 return;
515 // A CTAP2 response.
516 if (aResult.SignatureData().Length() == 0) {
517 RejectTransaction(NS_ERROR_ABORT);
518 return;
521 CryptoBuffer clientDataBuf;
522 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
523 RejectTransaction(NS_ERROR_ABORT);
524 return;
527 CryptoBuffer credBuf;
528 if (NS_WARN_IF(!credBuf.Assign(aResult.KeyHandle()))) {
529 RejectTransaction(NS_ERROR_ABORT);
530 return;
533 CryptoBuffer sigBuf;
534 if (NS_WARN_IF(!sigBuf.Assign(aResult.SignatureData()))) {
535 RejectTransaction(NS_ERROR_ABORT);
536 return;
539 // Assemble a response object to return
540 nsString clientDataBase64;
541 nsString signatureDataBase64;
542 nsString keyHandleBase64;
543 nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
544 nsresult rvSignatureData = sigBuf.ToJwkBase64(signatureDataBase64);
545 nsresult rvKeyHandle = credBuf.ToJwkBase64(keyHandleBase64);
546 if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
547 NS_WARN_IF(NS_FAILED(rvSignatureData) ||
548 NS_WARN_IF(NS_FAILED(rvKeyHandle)))) {
549 RejectTransaction(NS_ERROR_ABORT);
550 return;
553 SignResponse response;
554 response.mKeyHandle.Construct(keyHandleBase64);
555 response.mClientData.Construct(clientDataBase64);
556 response.mSignatureData.Construct(signatureDataBase64);
557 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
559 // Keep the callback pointer alive.
560 nsMainThreadPtrHandle<U2FSignCallback> callback(
561 mTransaction.ref().GetSignCallback());
563 ClearTransaction();
564 ExecuteCallback(response, callback);
567 void U2F::ClearTransaction() {
568 if (!mTransaction.isNothing()) {
569 StopListeningForVisibilityEvents();
572 mTransaction.reset();
575 void U2F::RejectTransaction(const nsresult& aError) {
576 if (NS_WARN_IF(mTransaction.isNothing())) {
577 return;
580 StopListeningForVisibilityEvents();
582 // Clear out mTransaction before calling ExecuteCallback() below to allow
583 // reentrancy from microtask checkpoints.
584 Maybe<U2FTransaction> maybeTransaction(std::move(mTransaction));
585 MOZ_ASSERT(mTransaction.isNothing() && maybeTransaction.isSome());
587 U2FTransaction& transaction = maybeTransaction.ref();
588 ErrorCode code = ConvertNSResultToErrorCode(aError);
590 if (transaction.HasRegisterCallback()) {
591 RegisterResponse response;
592 response.mErrorCode.Construct(static_cast<uint32_t>(code));
593 // MOZ_KnownLive because "transaction" lives on the stack.
594 ExecuteCallback(response, MOZ_KnownLive(transaction.GetRegisterCallback()));
597 if (transaction.HasSignCallback()) {
598 SignResponse response;
599 response.mErrorCode.Construct(static_cast<uint32_t>(code));
600 // MOZ_KnownLive because "transaction" lives on the stack.
601 ExecuteCallback(response, MOZ_KnownLive(transaction.GetSignCallback()));
605 void U2F::CancelTransaction(const nsresult& aError) {
606 if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
607 mChild->SendRequestCancel(mTransaction.ref().mId);
610 RejectTransaction(aError);
613 void U2F::RequestAborted(const uint64_t& aTransactionId,
614 const nsresult& aError) {
615 MOZ_ASSERT(NS_IsMainThread());
617 if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
618 RejectTransaction(aError);
622 void U2F::HandleVisibilityChange() {
623 if (mTransaction.isSome()) {
624 mTransaction.ref().mVisibilityChanged = true;
628 } // namespace dom
629 } // namespace mozilla