Bug 1494333 - index crons just like artifacts r=Callek
[gecko.git] / dom / u2f / U2F.cpp
blob904e12606aaa8da47bd7903271152f0f86109654
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 "nsIEffectiveTLDService.h"
15 #include "nsNetUtil.h"
16 #include "nsURLParsers.h"
18 using namespace mozilla::ipc;
20 // Forward decl because of nsHTMLDocument.h's complex dependency on /layout/style
21 class nsHTMLDocument {
22 public:
23 bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
24 const nsACString& aOrigHost);
27 namespace mozilla {
28 namespace dom {
30 static mozilla::LazyLogModule gU2FLog("u2fmanager");
32 NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment");
33 NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion");
35 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
36 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
37 "https://www.gstatic.com/securitykey/origins.json");
38 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2,
39 "https://www.gstatic.com/securitykey/a/google.com/origins.json");
41 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F)
42 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
43 NS_INTERFACE_MAP_ENTRY(nsISupports)
44 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
45 NS_INTERFACE_MAP_END
47 NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F)
48 NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F)
50 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F, mParent)
52 /***********************************************************************
53 * Utility Functions
54 **********************************************************************/
56 static ErrorCode
57 ConvertNSResultToErrorCode(const nsresult& aError)
59 if (aError == NS_ERROR_DOM_TIMEOUT_ERR) {
60 return ErrorCode::TIMEOUT;
62 /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
63 if (aError == NS_ERROR_DOM_INVALID_STATE_ERR) {
64 return ErrorCode::DEVICE_INELIGIBLE;
66 return ErrorCode::OTHER_ERROR;
69 static uint32_t
70 AdjustedTimeoutMillis(const Optional<Nullable<int32_t>>& opt_aSeconds)
72 uint32_t adjustedTimeoutMillis = 30000u;
73 if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) {
74 adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u;
75 adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis);
76 adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis);
78 return adjustedTimeoutMillis;
81 static nsresult
82 AssembleClientData(const nsAString& aOrigin, const nsAString& aTyp,
83 const nsAString& aChallenge,
84 /* 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;
96 return NS_OK;
99 static void
100 RegisteredKeysToScopedCredentialList(const nsAString& aAppId,
101 const nsTArray<RegisteredKey>& aKeys,
102 nsTArray<WebAuthnScopedCredential>& aList)
104 for (const RegisteredKey& key : aKeys) {
105 // Check for required attributes
106 if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() ||
107 key.mVersion.Value() != kRequiredU2FVersion) {
108 continue;
111 // If this key's mAppId doesn't match the invocation, we can't handle it.
112 if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) {
113 continue;
116 CryptoBuffer keyHandle;
117 nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value());
118 if (NS_WARN_IF(NS_FAILED(rv))) {
119 continue;
122 WebAuthnScopedCredential c;
123 c.id() = keyHandle;
124 aList.AppendElement(c);
128 /***********************************************************************
129 * U2F JavaScript API Implementation
130 **********************************************************************/
132 U2F::~U2F()
134 MOZ_ASSERT(NS_IsMainThread());
136 if (mTransaction.isSome()) {
137 RejectTransaction(NS_ERROR_ABORT);
140 if (mChild) {
141 RefPtr<WebAuthnTransactionChild> c;
142 mChild.swap(c);
143 c->Disconnect();
147 void
148 U2F::Init(ErrorResult& aRv)
150 MOZ_ASSERT(mParent);
152 nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
153 MOZ_ASSERT(doc);
154 if (!doc) {
155 aRv.Throw(NS_ERROR_FAILURE);
156 return;
159 nsIPrincipal* principal = doc->NodePrincipal();
160 aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
161 if (NS_WARN_IF(aRv.Failed())) {
162 return;
165 if (NS_WARN_IF(mOrigin.IsEmpty())) {
166 aRv.Throw(NS_ERROR_FAILURE);
167 return;
171 /* virtual */ JSObject*
172 U2F::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
174 return U2F_Binding::Wrap(aCx, this, aGivenProto);
177 template<typename T, typename C>
178 void
179 U2F::ExecuteCallback(T& aResp, nsMainThreadPtrHandle<C>& aCb)
181 MOZ_ASSERT(NS_IsMainThread());
182 MOZ_ASSERT(aCb);
184 // Assert that mTransaction was cleared before before we were called to allow
185 // reentrancy from microtask checkpoints.
186 MOZ_ASSERT(mTransaction.isNothing());
188 ErrorResult error;
189 aCb->Call(aResp, error);
190 NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed");
191 error.SuppressException(); // Useful exceptions already emitted
194 void
195 U2F::Register(const nsAString& aAppId,
196 const Sequence<RegisterRequest>& aRegisterRequests,
197 const Sequence<RegisteredKey>& aRegisteredKeys,
198 U2FRegisterCallback& aCallback,
199 const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
200 ErrorResult& aRv)
202 MOZ_ASSERT(NS_IsMainThread());
204 if (mTransaction.isSome()) {
205 CancelTransaction(NS_ERROR_ABORT);
208 nsMainThreadPtrHandle<U2FRegisterCallback> callback(
209 new nsMainThreadPtrHolder<U2FRegisterCallback>("U2F::Register::callback",
210 &aCallback));
212 // Ensure we have a callback.
213 if (NS_WARN_IF(!callback)) {
214 return;
217 // Evaluate the AppID
218 nsString adjustedAppId(aAppId);
219 if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Register, adjustedAppId)) {
220 RegisterResponse response;
221 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
222 ExecuteCallback(response, callback);
223 return;
226 nsAutoString clientDataJSON;
228 // Pick the first valid RegisterRequest; we can only work with one.
229 CryptoBuffer challenge;
230 for (const RegisterRequest& req : aRegisterRequests) {
231 if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() ||
232 req.mVersion.Value() != kRequiredU2FVersion) {
233 continue;
235 if (!challenge.Assign(NS_ConvertUTF16toUTF8(req.mChallenge.Value()))) {
236 continue;
239 nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment,
240 req.mChallenge.Value(), clientDataJSON);
241 if (NS_WARN_IF(NS_FAILED(rv))) {
242 continue;
246 // Did we not get a valid RegisterRequest? Abort.
247 if (clientDataJSON.IsEmpty()) {
248 RegisterResponse response;
249 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
250 ExecuteCallback(response, callback);
251 return;
254 // Build the exclusion list, if any
255 nsTArray<WebAuthnScopedCredential> excludeList;
256 RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
257 excludeList);
259 if (!MaybeCreateBackgroundActor()) {
260 RegisterResponse response;
261 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
262 ExecuteCallback(response, callback);
263 return;
266 ListenForVisibilityEvents();
268 NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
269 uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
271 WebAuthnMakeCredentialInfo info(mOrigin,
272 adjustedAppId,
273 challenge,
274 clientData,
275 adjustedTimeoutMillis,
276 excludeList,
277 null_t() /* no extra info for U2F */);
279 MOZ_ASSERT(mTransaction.isNothing());
280 mTransaction = Some(U2FTransaction(AsVariant(callback)));
281 mChild->SendRequestRegister(mTransaction.ref().mId, info);
284 void
285 U2F::FinishMakeCredential(const uint64_t& aTransactionId,
286 const WebAuthnMakeCredentialResult& aResult)
288 MOZ_ASSERT(NS_IsMainThread());
290 // Check for a valid transaction.
291 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
292 return;
295 if (NS_WARN_IF(!mTransaction.ref().HasRegisterCallback())) {
296 RejectTransaction(NS_ERROR_ABORT);
297 return;
300 // A CTAP2 response.
301 if (aResult.RegistrationData().Length() == 0) {
302 RejectTransaction(NS_ERROR_ABORT);
303 return;
306 CryptoBuffer clientDataBuf;
307 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
308 RejectTransaction(NS_ERROR_ABORT);
309 return;
312 CryptoBuffer regBuf;
313 if (NS_WARN_IF(!regBuf.Assign(aResult.RegistrationData()))) {
314 RejectTransaction(NS_ERROR_ABORT);
315 return;
318 nsString clientDataBase64;
319 nsString registrationDataBase64;
320 nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
321 nsresult rvRegistrationData = regBuf.ToJwkBase64(registrationDataBase64);
323 if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
324 NS_WARN_IF(NS_FAILED(rvRegistrationData))) {
325 RejectTransaction(NS_ERROR_ABORT);
326 return;
329 // Assemble a response object to return
330 RegisterResponse response;
331 response.mVersion.Construct(kRequiredU2FVersion);
332 response.mClientData.Construct(clientDataBase64);
333 response.mRegistrationData.Construct(registrationDataBase64);
334 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
336 // Keep the callback pointer alive.
337 nsMainThreadPtrHandle<U2FRegisterCallback> callback(
338 mTransaction.ref().GetRegisterCallback());
340 ClearTransaction();
341 ExecuteCallback(response, callback);
344 void
345 U2F::Sign(const nsAString& aAppId,
346 const nsAString& aChallenge,
347 const Sequence<RegisteredKey>& aRegisteredKeys,
348 U2FSignCallback& aCallback,
349 const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
350 ErrorResult& aRv)
352 MOZ_ASSERT(NS_IsMainThread());
354 if (mTransaction.isSome()) {
355 CancelTransaction(NS_ERROR_ABORT);
358 nsMainThreadPtrHandle<U2FSignCallback> callback(
359 new nsMainThreadPtrHolder<U2FSignCallback>("U2F::Sign::callback",
360 &aCallback));
362 // Ensure we have a callback.
363 if (NS_WARN_IF(!callback)) {
364 return;
367 // Evaluate the AppID
368 nsString adjustedAppId(aAppId);
369 if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, adjustedAppId)) {
370 SignResponse response;
371 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
372 ExecuteCallback(response, callback);
373 return;
376 // Produce the AppParam from the current AppID
377 nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);
379 nsAutoString clientDataJSON;
380 nsresult rv = AssembleClientData(mOrigin, kGetAssertion, aChallenge,
381 clientDataJSON);
382 if (NS_WARN_IF(NS_FAILED(rv))) {
383 SignResponse response;
384 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
385 ExecuteCallback(response, callback);
386 return;
389 CryptoBuffer challenge;
390 if (!challenge.Assign(NS_ConvertUTF16toUTF8(aChallenge))) {
391 SignResponse response;
392 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
393 ExecuteCallback(response, callback);
394 return;
397 // Build the key list, if any
398 nsTArray<WebAuthnScopedCredential> permittedList;
399 RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
400 permittedList);
402 if (!MaybeCreateBackgroundActor()) {
403 SignResponse response;
404 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
405 ExecuteCallback(response, callback);
406 return;
409 ListenForVisibilityEvents();
411 // Always blank for U2F
412 nsTArray<WebAuthnExtension> extensions;
414 NS_ConvertUTF16toUTF8 clientData(clientDataJSON);
415 uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);
417 WebAuthnGetAssertionInfo info(mOrigin,
418 adjustedAppId,
419 challenge,
420 clientData,
421 adjustedTimeoutMillis,
422 permittedList,
423 null_t() /* no extra info for U2F */);
425 MOZ_ASSERT(mTransaction.isNothing());
426 mTransaction = Some(U2FTransaction(AsVariant(callback)));
427 mChild->SendRequestSign(mTransaction.ref().mId, info);
430 void
431 U2F::FinishGetAssertion(const uint64_t& aTransactionId,
432 const WebAuthnGetAssertionResult& aResult)
434 MOZ_ASSERT(NS_IsMainThread());
436 // Check for a valid transaction.
437 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
438 return;
441 if (NS_WARN_IF(!mTransaction.ref().HasSignCallback())) {
442 RejectTransaction(NS_ERROR_ABORT);
443 return;
446 // A CTAP2 response.
447 if (aResult.SignatureData().Length() == 0) {
448 RejectTransaction(NS_ERROR_ABORT);
449 return;
452 CryptoBuffer clientDataBuf;
453 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
454 RejectTransaction(NS_ERROR_ABORT);
455 return;
458 CryptoBuffer credBuf;
459 if (NS_WARN_IF(!credBuf.Assign(aResult.KeyHandle()))) {
460 RejectTransaction(NS_ERROR_ABORT);
461 return;
464 CryptoBuffer sigBuf;
465 if (NS_WARN_IF(!sigBuf.Assign(aResult.SignatureData()))) {
466 RejectTransaction(NS_ERROR_ABORT);
467 return;
470 // Assemble a response object to return
471 nsString clientDataBase64;
472 nsString signatureDataBase64;
473 nsString keyHandleBase64;
474 nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
475 nsresult rvSignatureData = sigBuf.ToJwkBase64(signatureDataBase64);
476 nsresult rvKeyHandle = credBuf.ToJwkBase64(keyHandleBase64);
477 if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
478 NS_WARN_IF(NS_FAILED(rvSignatureData) ||
479 NS_WARN_IF(NS_FAILED(rvKeyHandle)))) {
480 RejectTransaction(NS_ERROR_ABORT);
481 return;
484 SignResponse response;
485 response.mKeyHandle.Construct(keyHandleBase64);
486 response.mClientData.Construct(clientDataBase64);
487 response.mSignatureData.Construct(signatureDataBase64);
488 response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));
490 // Keep the callback pointer alive.
491 nsMainThreadPtrHandle<U2FSignCallback> callback(
492 mTransaction.ref().GetSignCallback());
494 ClearTransaction();
495 ExecuteCallback(response, callback);
498 void
499 U2F::ClearTransaction()
501 if (!NS_WARN_IF(mTransaction.isNothing())) {
502 StopListeningForVisibilityEvents();
505 mTransaction.reset();
508 void
509 U2F::RejectTransaction(const nsresult& aError)
511 if (NS_WARN_IF(mTransaction.isNothing())) {
512 return;
515 StopListeningForVisibilityEvents();
517 // Clear out mTransaction before calling ExecuteCallback() below to allow
518 // reentrancy from microtask checkpoints.
519 Maybe<U2FTransaction> maybeTransaction(std::move(mTransaction));
520 MOZ_ASSERT(mTransaction.isNothing() && maybeTransaction.isSome());
522 U2FTransaction& transaction = maybeTransaction.ref();
523 ErrorCode code = ConvertNSResultToErrorCode(aError);
525 if (transaction.HasRegisterCallback()) {
526 RegisterResponse response;
527 response.mErrorCode.Construct(static_cast<uint32_t>(code));
528 ExecuteCallback(response, transaction.GetRegisterCallback());
531 if (transaction.HasSignCallback()) {
532 SignResponse response;
533 response.mErrorCode.Construct(static_cast<uint32_t>(code));
534 ExecuteCallback(response, transaction.GetSignCallback());
538 void
539 U2F::CancelTransaction(const nsresult& aError)
541 if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
542 mChild->SendRequestCancel(mTransaction.ref().mId);
545 RejectTransaction(aError);
548 void
549 U2F::RequestAborted(const uint64_t& aTransactionId, const nsresult& aError)
551 MOZ_ASSERT(NS_IsMainThread());
553 if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
554 RejectTransaction(aError);
558 } // namespace dom
559 } // namespace mozilla