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/Document.h"
12 #include "mozilla/dom/WebAuthnTransactionChild.h"
13 #include "mozilla/dom/WebAuthnUtil.h"
14 #include "nsContentUtils.h"
15 #include "nsNetUtil.h"
16 #include "nsURLParsers.h"
19 # include "WinWebAuthnManager.h"
22 using namespace mozilla::ipc
;
26 // Forward decl because of nsHTMLDocument.h's complex dependency on
28 class nsHTMLDocument
{
30 bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString
& aHostSuffixString
,
31 const nsACString
& aOrigHost
);
34 namespace mozilla::dom
{
36 constexpr auto kFinishEnrollment
= u
"navigator.id.finishEnrollment"_ns
;
37 constexpr auto kGetAssertion
= u
"navigator.id.getAssertion"_ns
;
39 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F
)
40 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
41 NS_INTERFACE_MAP_END_INHERITING(WebAuthnManagerBase
)
43 NS_IMPL_ADDREF_INHERITED(U2F
, WebAuthnManagerBase
)
44 NS_IMPL_RELEASE_INHERITED(U2F
, WebAuthnManagerBase
)
46 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(U2F
)
47 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(U2F
, WebAuthnManagerBase
)
48 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction
)
49 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
50 tmp
->mTransaction
.reset();
51 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
52 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(U2F
, WebAuthnManagerBase
)
53 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction
)
54 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
56 /***********************************************************************
58 **********************************************************************/
60 static ErrorCode
ConvertNSResultToErrorCode(const nsresult
& aError
) {
61 if (aError
== NS_ERROR_DOM_TIMEOUT_ERR
) {
62 return ErrorCode::TIMEOUT
;
64 /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
65 if (aError
== NS_ERROR_DOM_INVALID_STATE_ERR
) {
66 return ErrorCode::DEVICE_INELIGIBLE
;
68 return ErrorCode::OTHER_ERROR
;
71 static uint32_t AdjustedTimeoutMillis(
72 const Optional
<Nullable
<int32_t>>& opt_aSeconds
) {
73 uint32_t adjustedTimeoutMillis
= 30000u;
74 if (opt_aSeconds
.WasPassed() && !opt_aSeconds
.Value().IsNull()) {
75 adjustedTimeoutMillis
= opt_aSeconds
.Value().Value() * 1000u;
76 adjustedTimeoutMillis
= std::max(15000u, adjustedTimeoutMillis
);
77 adjustedTimeoutMillis
= std::min(120000u, adjustedTimeoutMillis
);
79 return adjustedTimeoutMillis
;
82 static nsresult
AssembleClientData(const nsAString
& aOrigin
,
83 const nsAString
& aTyp
,
84 const nsAString
& aChallenge
,
85 /* out */ nsString
& aClientData
) {
86 MOZ_ASSERT(NS_IsMainThread());
87 U2FClientData clientDataObject
;
88 clientDataObject
.mTyp
.Construct(aTyp
); // "Typ" from the U2F specification
89 clientDataObject
.mChallenge
.Construct(aChallenge
);
90 clientDataObject
.mOrigin
.Construct(aOrigin
);
92 if (NS_WARN_IF(!clientDataObject
.ToJSON(aClientData
))) {
93 return NS_ERROR_FAILURE
;
99 static void RegisteredKeysToScopedCredentialList(
100 const nsAString
& aAppId
, const nsTArray
<RegisteredKey
>& aKeys
,
101 nsTArray
<WebAuthnScopedCredential
>& aList
) {
102 for (const RegisteredKey
& key
: aKeys
) {
103 // Check for required attributes
104 if (!key
.mVersion
.WasPassed() || !key
.mKeyHandle
.WasPassed() ||
105 key
.mVersion
.Value() != kRequiredU2FVersion
) {
109 // If this key's mAppId doesn't match the invocation, we can't handle it.
110 if (key
.mAppId
.WasPassed() && !key
.mAppId
.Value().Equals(aAppId
)) {
114 CryptoBuffer keyHandle
;
115 nsresult rv
= keyHandle
.FromJwkBase64(key
.mKeyHandle
.Value());
116 if (NS_WARN_IF(NS_FAILED(rv
))) {
120 WebAuthnScopedCredential c
;
122 aList
.AppendElement(c
);
126 /***********************************************************************
127 * U2F JavaScript API Implementation
128 **********************************************************************/
131 MOZ_ASSERT(NS_IsMainThread());
133 if (mTransaction
.isSome()) {
138 RefPtr
<WebAuthnTransactionChild
> c
;
144 void U2F::Init(ErrorResult
& aRv
) {
147 nsCOMPtr
<Document
> doc
= mParent
->GetDoc();
150 aRv
.Throw(NS_ERROR_FAILURE
);
154 nsIPrincipal
* principal
= doc
->NodePrincipal();
155 aRv
= nsContentUtils::GetUTFOrigin(principal
, mOrigin
);
156 if (NS_WARN_IF(aRv
.Failed())) {
160 if (NS_WARN_IF(mOrigin
.IsEmpty())) {
161 aRv
.Throw(NS_ERROR_FAILURE
);
167 JSObject
* U2F::WrapObject(JSContext
* aCx
, JS::Handle
<JSObject
*> aGivenProto
) {
168 return U2F_Binding::Wrap(aCx
, this, aGivenProto
);
171 template <typename T
, typename C
>
172 void U2F::ExecuteCallback(T
& aResp
, nsMainThreadPtrHandle
<C
>& aCb
) {
173 MOZ_ASSERT(NS_IsMainThread());
177 RefPtr
<C
> temp
= aCb
.get(); // Make sure it stays alive
178 temp
->Call(aResp
, error
);
179 NS_WARNING_ASSERTION(!error
.Failed(), "dom::U2F::Promise callback failed");
180 error
.SuppressException(); // Useful exceptions already emitted
183 void U2F::Register(const nsAString
& aAppId
,
184 const Sequence
<RegisterRequest
>& aRegisterRequests
,
185 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
186 U2FRegisterCallback
& aCallback
,
187 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
189 MOZ_ASSERT(NS_IsMainThread());
191 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
192 new nsMainThreadPtrHolder
<U2FRegisterCallback
>("U2F::Register::callback",
195 // Ensure we have a callback.
196 if (NS_WARN_IF(!callback
)) {
200 if (mTransaction
.isSome()) {
201 // If there hasn't been a visibility change during the current
202 // transaction, then let's let that one complete rather than
203 // cancelling it on a subsequent call.
204 if (!mTransaction
.ref().mVisibilityChanged
) {
205 RegisterResponse response
;
206 response
.mErrorCode
.Construct(
207 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
208 ExecuteCallback(response
, callback
);
212 // Otherwise, the user may well have clicked away, so let's
213 // abort the old transaction and take over control from here.
214 CancelTransaction(NS_ERROR_ABORT
);
217 // Evaluate the AppID
218 nsString
adjustedAppId(aAppId
);
219 if (!EvaluateAppID(mParent
, mOrigin
, adjustedAppId
)) {
220 RegisterResponse response
;
221 response
.mErrorCode
.Construct(
222 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
223 ExecuteCallback(response
, callback
);
227 nsAutoString clientDataJSON
;
229 // Pick the first valid RegisterRequest; we can only work with one.
230 CryptoBuffer challenge
;
231 for (const RegisterRequest
& req
: aRegisterRequests
) {
232 if (!req
.mChallenge
.WasPassed() || !req
.mVersion
.WasPassed() ||
233 req
.mVersion
.Value() != kRequiredU2FVersion
) {
236 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(req
.mChallenge
.Value()))) {
240 nsresult rv
= AssembleClientData(mOrigin
, kFinishEnrollment
,
241 req
.mChallenge
.Value(), clientDataJSON
);
242 if (NS_WARN_IF(NS_FAILED(rv
))) {
247 // Did we not get a valid RegisterRequest? Abort.
248 if (clientDataJSON
.IsEmpty()) {
249 RegisterResponse response
;
250 response
.mErrorCode
.Construct(
251 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
252 ExecuteCallback(response
, callback
);
256 // Build the exclusion list, if any
257 nsTArray
<WebAuthnScopedCredential
> excludeList
;
258 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
261 if (!MaybeCreateBackgroundActor()) {
262 RegisterResponse response
;
263 response
.mErrorCode
.Construct(
264 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
265 ExecuteCallback(response
, callback
);
270 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
271 ListenForVisibilityEvents();
274 ListenForVisibilityEvents();
277 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
278 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
280 BrowsingContext
* context
= mParent
->GetBrowsingContext();
282 RegisterResponse response
;
283 response
.mErrorCode
.Construct(
284 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
285 ExecuteCallback(response
, callback
);
289 WebAuthnMakeCredentialInfo
info(mOrigin
, adjustedAppId
, challenge
, clientData
,
290 adjustedTimeoutMillis
, excludeList
,
291 Nothing(), /* no extra info for U2F */
294 MOZ_ASSERT(mTransaction
.isNothing());
295 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
296 mChild
->SendRequestRegister(mTransaction
.ref().mId
, info
);
299 using binding_detail::GenericMethod
;
300 using binding_detail::NormalThisPolicy
;
301 using binding_detail::ThrowExceptions
;
303 // register_impl_methodinfo is generated by bindings.
304 namespace U2F_Binding
{
305 extern const JSJitInfo register_impl_methodinfo
;
306 } // namespace U2F_Binding
308 // We have 4 non-optional args.
309 static const JSFunctionSpec register_spec
= JS_FNSPEC(
310 "register", (GenericMethod
<NormalThisPolicy
, ThrowExceptions
>),
311 &U2F_Binding::register_impl_methodinfo
, 4, JSPROP_ENUMERATE
, nullptr);
313 void U2F::GetRegister(JSContext
* aCx
,
314 JS::MutableHandle
<JSObject
*> aRegisterFunc
,
316 JSFunction
* fun
= JS::NewFunctionFromSpec(aCx
, ®ister_spec
);
318 aRv
.NoteJSContextException(aCx
);
322 aRegisterFunc
.set(JS_GetFunctionObject(fun
));
325 void U2F::FinishMakeCredential(const uint64_t& aTransactionId
,
326 const WebAuthnMakeCredentialResult
& aResult
) {
327 MOZ_ASSERT(NS_IsMainThread());
329 // Check for a valid transaction.
330 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
334 if (NS_WARN_IF(!mTransaction
.ref().HasRegisterCallback())) {
335 RejectTransaction(NS_ERROR_ABORT
);
340 if (aResult
.RegistrationData().Length() == 0) {
341 RejectTransaction(NS_ERROR_ABORT
);
345 CryptoBuffer clientDataBuf
;
346 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
347 RejectTransaction(NS_ERROR_ABORT
);
352 if (NS_WARN_IF(!regBuf
.Assign(aResult
.RegistrationData()))) {
353 RejectTransaction(NS_ERROR_ABORT
);
357 nsString clientDataBase64
;
358 nsString registrationDataBase64
;
359 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
360 nsresult rvRegistrationData
= regBuf
.ToJwkBase64(registrationDataBase64
);
362 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
363 NS_WARN_IF(NS_FAILED(rvRegistrationData
))) {
364 RejectTransaction(NS_ERROR_ABORT
);
368 // Assemble a response object to return
369 RegisterResponse response
;
370 response
.mVersion
.Construct(kRequiredU2FVersion
);
371 response
.mClientData
.Construct(clientDataBase64
);
372 response
.mRegistrationData
.Construct(registrationDataBase64
);
373 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
375 // Keep the callback pointer alive.
376 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
377 mTransaction
.ref().GetRegisterCallback());
380 ExecuteCallback(response
, callback
);
383 void U2F::Sign(const nsAString
& aAppId
, const nsAString
& aChallenge
,
384 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
385 U2FSignCallback
& aCallback
,
386 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
388 MOZ_ASSERT(NS_IsMainThread());
390 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
391 new nsMainThreadPtrHolder
<U2FSignCallback
>("U2F::Sign::callback",
394 // Ensure we have a callback.
395 if (NS_WARN_IF(!callback
)) {
399 if (mTransaction
.isSome()) {
400 // If there hasn't been a visibility change during the current
401 // transaction, then let's let that one complete rather than
402 // cancelling it on a subsequent call.
403 if (!mTransaction
.ref().mVisibilityChanged
) {
404 SignResponse response
;
405 response
.mErrorCode
.Construct(
406 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
407 ExecuteCallback(response
, callback
);
411 // Otherwise, the user may well have clicked away, so let's
412 // abort the old transaction and take over control from here.
413 CancelTransaction(NS_ERROR_ABORT
);
416 // Evaluate the AppID
417 nsString
adjustedAppId(aAppId
);
418 if (!EvaluateAppID(mParent
, mOrigin
, adjustedAppId
)) {
419 SignResponse response
;
420 response
.mErrorCode
.Construct(
421 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
422 ExecuteCallback(response
, callback
);
426 // Produce the AppParam from the current AppID
427 nsCString cAppId
= NS_ConvertUTF16toUTF8(adjustedAppId
);
429 nsAutoString clientDataJSON
;
431 AssembleClientData(mOrigin
, kGetAssertion
, aChallenge
, clientDataJSON
);
432 if (NS_WARN_IF(NS_FAILED(rv
))) {
433 SignResponse response
;
434 response
.mErrorCode
.Construct(
435 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
436 ExecuteCallback(response
, callback
);
440 CryptoBuffer challenge
;
441 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(aChallenge
))) {
442 SignResponse response
;
443 response
.mErrorCode
.Construct(
444 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
445 ExecuteCallback(response
, callback
);
449 // Build the key list, if any
450 nsTArray
<WebAuthnScopedCredential
> permittedList
;
451 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
454 if (!MaybeCreateBackgroundActor()) {
455 SignResponse response
;
456 response
.mErrorCode
.Construct(
457 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
458 ExecuteCallback(response
, callback
);
463 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
464 ListenForVisibilityEvents();
467 ListenForVisibilityEvents();
470 // Always blank for U2F
471 nsTArray
<WebAuthnExtension
> extensions
;
473 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
474 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
476 BrowsingContext
* context
= mParent
->GetBrowsingContext();
478 SignResponse response
;
479 response
.mErrorCode
.Construct(
480 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
481 ExecuteCallback(response
, callback
);
485 WebAuthnGetAssertionInfo
info(mOrigin
, adjustedAppId
, challenge
, clientData
,
486 adjustedTimeoutMillis
, permittedList
,
487 Nothing(), /* no extra info for U2F */
490 MOZ_ASSERT(mTransaction
.isNothing());
491 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
492 mChild
->SendRequestSign(mTransaction
.ref().mId
, info
);
495 // sign_impl_methodinfo is generated by bindings.
496 namespace U2F_Binding
{
497 extern const JSJitInfo sign_impl_methodinfo
;
498 } // namespace U2F_Binding
500 // We have 4 non-optional args.
501 static const JSFunctionSpec sign_spec
=
502 JS_FNSPEC("sign", (GenericMethod
<NormalThisPolicy
, ThrowExceptions
>),
503 &U2F_Binding::sign_impl_methodinfo
, 4, JSPROP_ENUMERATE
, nullptr);
505 void U2F::GetSign(JSContext
* aCx
, JS::MutableHandle
<JSObject
*> aSignFunc
,
507 JSFunction
* fun
= JS::NewFunctionFromSpec(aCx
, &sign_spec
);
509 aRv
.NoteJSContextException(aCx
);
513 aSignFunc
.set(JS_GetFunctionObject(fun
));
516 void U2F::FinishGetAssertion(const uint64_t& aTransactionId
,
517 const WebAuthnGetAssertionResult
& aResult
) {
518 MOZ_ASSERT(NS_IsMainThread());
520 // Check for a valid transaction.
521 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
525 if (NS_WARN_IF(!mTransaction
.ref().HasSignCallback())) {
526 RejectTransaction(NS_ERROR_ABORT
);
531 if (aResult
.SignatureData().Length() == 0) {
532 RejectTransaction(NS_ERROR_ABORT
);
536 CryptoBuffer clientDataBuf
;
537 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
538 RejectTransaction(NS_ERROR_ABORT
);
542 CryptoBuffer credBuf
;
543 if (NS_WARN_IF(!credBuf
.Assign(aResult
.KeyHandle()))) {
544 RejectTransaction(NS_ERROR_ABORT
);
549 if (NS_WARN_IF(!sigBuf
.Assign(aResult
.SignatureData()))) {
550 RejectTransaction(NS_ERROR_ABORT
);
554 // Assemble a response object to return
555 nsString clientDataBase64
;
556 nsString signatureDataBase64
;
557 nsString keyHandleBase64
;
558 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
559 nsresult rvSignatureData
= sigBuf
.ToJwkBase64(signatureDataBase64
);
560 nsresult rvKeyHandle
= credBuf
.ToJwkBase64(keyHandleBase64
);
561 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
562 NS_WARN_IF(NS_FAILED(rvSignatureData
) ||
563 NS_WARN_IF(NS_FAILED(rvKeyHandle
)))) {
564 RejectTransaction(NS_ERROR_ABORT
);
568 SignResponse response
;
569 response
.mKeyHandle
.Construct(keyHandleBase64
);
570 response
.mClientData
.Construct(clientDataBase64
);
571 response
.mSignatureData
.Construct(signatureDataBase64
);
572 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
574 // Keep the callback pointer alive.
575 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
576 mTransaction
.ref().GetSignCallback());
579 ExecuteCallback(response
, callback
);
582 void U2F::ClearTransaction() {
583 if (!mTransaction
.isNothing()) {
584 StopListeningForVisibilityEvents();
587 mTransaction
.reset();
590 void U2F::RejectTransaction(const nsresult
& aError
) {
591 if (NS_WARN_IF(mTransaction
.isNothing())) {
595 StopListeningForVisibilityEvents();
597 // Clear out mTransaction before calling ExecuteCallback() below to allow
598 // reentrancy from microtask checkpoints.
599 Maybe
<U2FTransaction
> maybeTransaction(std::move(mTransaction
));
600 MOZ_ASSERT(mTransaction
.isNothing() && maybeTransaction
.isSome());
602 U2FTransaction
& transaction
= maybeTransaction
.ref();
603 ErrorCode code
= ConvertNSResultToErrorCode(aError
);
605 if (transaction
.HasRegisterCallback()) {
606 RegisterResponse response
;
607 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
608 // MOZ_KnownLive because "transaction" lives on the stack.
609 ExecuteCallback(response
, MOZ_KnownLive(transaction
.GetRegisterCallback()));
612 if (transaction
.HasSignCallback()) {
613 SignResponse response
;
614 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
615 // MOZ_KnownLive because "transaction" lives on the stack.
616 ExecuteCallback(response
, MOZ_KnownLive(transaction
.GetSignCallback()));
620 void U2F::CancelTransaction(const nsresult
& aError
) {
621 if (!NS_WARN_IF(!mChild
|| mTransaction
.isNothing())) {
622 mChild
->SendRequestCancel(mTransaction
.ref().mId
);
625 RejectTransaction(aError
);
628 void U2F::RequestAborted(const uint64_t& aTransactionId
,
629 const nsresult
& aError
) {
630 MOZ_ASSERT(NS_IsMainThread());
632 if (mTransaction
.isSome() && mTransaction
.ref().mId
== aTransactionId
) {
633 RejectTransaction(aError
);
637 void U2F::HandleVisibilityChange() {
638 if (mTransaction
.isSome()) {
639 mTransaction
.ref().mVisibilityChanged
= true;
643 } // namespace mozilla::dom