Bug 1728059 [wpt PR 30223] - Add Subresource Web Bundle tests for non utf-8 encoding...
[gecko.git] / services / fxaccounts / FxAccountsKeys.jsm
blobe09bc162a6e8f8c44601396374c67ac7fa956b88
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 const { PromiseUtils } = ChromeUtils.import(
7   "resource://gre/modules/PromiseUtils.jsm"
8 );
9 const { CommonUtils } = ChromeUtils.import(
10   "resource://services-common/utils.js"
13 const { CryptoUtils } = ChromeUtils.import(
14   "resource://services-crypto/utils.js"
17 const {
18   LEGACY_DERIVED_KEYS_NAMES,
19   SCOPE_OLD_SYNC,
20   LEGACY_SCOPE_WEBEXT_SYNC,
21   DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
22   FX_OAUTH_CLIENT_ID,
23   log,
24   logPII,
25 } = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
27 // These are the scopes that correspond to new storage for the `LEGACY_DERIVED_KEYS_NAMES`.
28 // We will, if necessary, migrate storage for those keys so that it's associated with
29 // these scopes.
30 const LEGACY_DERIVED_KEY_SCOPES = [SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC];
32 // These are scopes that we used to store, but are no longer using,
33 // and hence should be deleted from storage if present.
34 const DEPRECATED_KEY_SCOPES = [DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY];
36 /**
37  * Utilities for working with key material linked to the user's account.
38  *
39  * Each Firefox Account has 32 bytes of root key material called `kB` which is
40  * linked to the user's password, and which is used to derive purpose-specific
41  * subkeys for things like encrypting the user's sync data. This class provides
42  * the interface for working with such key material.
43  *
44  * Most recent FxA clients obtain appropriate key material directly as part of
45  * their sign-in flow, using a special extension of the OAuth2.0 protocol to
46  * securely deliver the derived keys without revealing `kB`. Keys obtained in
47  * in this way are called "scoped keys" since each corresponds to a particular
48  * OAuth scope, and this class provides a `getKeyForScope` method that is the
49  * preferred method for consumers to work with such keys.
50  *
51  * However, since the FxA integration in Firefox Desktop pre-dates the use of
52  * OAuth2.0, we also have a lot of code for fetching keys via an older flow.
53  * This flow uses a special `keyFetchToken` to obtain `kB` and then derive various
54  * sub-keys from it. Consumers should consider this an internal implementation
55  * detail of the `FxAccountsKeys` class and should prefer `getKeyForScope` where
56  * possible.  We intend to remove support for Firefox ever directly handling `kB`
57  * at some point in the future.
58  */
59 class FxAccountsKeys {
60   constructor(fxAccountsInternal) {
61     this._fxai = fxAccountsInternal;
62   }
64   /**
65    * Checks if we currently have the key for a given scope, or if we have enough to
66    * be able to successfully fetch and unwrap it for the signed-in-user.
67    *
68    * Unlike `getKeyForScope`, this will not hit the network to fetch wrapped keys if
69    * they aren't available locally.
70    */
71   canGetKeyForScope(scope) {
72     return this._fxai.withCurrentAccountState(async currentState => {
73       let userData = await currentState.getUserAccountData();
74       if (!userData) {
75         throw new Error("Can't possibly get keys; User is not signed in");
76       }
77       if (!userData.verified) {
78         log.info("Can't get keys; user is not verified");
79         return false;
80       }
82       if (userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope)) {
83         return true;
84       }
86       // For sync-related scopes, we might have stored the keys in a legacy format.
87       if (scope == SCOPE_OLD_SYNC) {
88         if (userData.kSync && userData.kXCS) {
89           return true;
90         }
91       }
92       if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
93         if (userData.kExtSync && userData.kExtKbHash) {
94           return true;
95         }
96       }
98       // `kB` is deprecated, but if we have it, we can use it to derive any scoped key.
99       if (userData.kB) {
100         return true;
101       }
103       // If we have a `keyFetchToken` we can fetch `kB`.
104       if (userData.keyFetchToken) {
105         return true;
106       }
108       log.info("Can't get keys; no key material or tokens available");
109       return false;
110     });
111   }
113   /**
114    * Get the key for a specified OAuth scope.
115    *
116    * @param {String} scope The OAuth scope whose key should be returned
117    *
118    * @return Promise<JWK>
119    *        If no key is available the promise resolves to `null`.
120    *        If a key is available for the given scope, th promise resolves to a JWK with fields:
121    *        {
122    *          scope: The requested scope
123    *          kid: Key identifier
124    *          k: Derived key material
125    *          kty: Always "oct" for scoped keys
126    *        }
127    *
128    */
129   async getKeyForScope(scope) {
130     const { scopedKeys } = await this._loadOrFetchKeys();
131     if (!scopedKeys.hasOwnProperty(scope)) {
132       throw new Error(`Key not available for scope "${scope}"`);
133     }
134     return {
135       scope,
136       ...scopedKeys[scope],
137     };
138   }
140   /**
141    * Format a JWK key material as hex rather than base64.
142    *
143    * This is a backwards-compatibility helper for code that needs raw key bytes rather
144    * than the JWK format offered by FxA scopes keys.
145    *
146    * @param {Object} jwk The JWK from which to extract the `k` field as hex.
147    *
148    */
149   keyAsHex(jwk) {
150     return CommonUtils.base64urlToHex(jwk.k);
151   }
153   /**
154    * Format a JWK kid as hex rather than base64.
155    *
156    * This is a backwards-compatibility helper for code that needs a raw key fingerprint
157    * for use as a key identifier, rather than the timestamp+fingerprint format used by
158    * FxA scoped keys.
159    *
160    * @param {Object} jwk The JWK from which to extract the `kid` field as hex.
161    */
162   kidAsHex(jwk) {
163     // The kid format is "{timestamp}-{b64url(fingerprint)}", but we have to be careful
164     // because the fingerprint component may contain "-" as well, and we want to ensure
165     // the timestamp component was non-empty.
166     const idx = jwk.kid.indexOf("-") + 1;
167     if (idx <= 1) {
168       throw new Error(`Invalid kid: ${jwk.kid}`);
169     }
170     return CommonUtils.base64urlToHex(jwk.kid.slice(idx));
171   }
173   /**
174    * Fetch encryption keys for the signed-in-user from the FxA API server.
175    *
176    * Not for user consumption.  Exists to cause the keys to be fetched.
177    *
178    * Returns user data so that it can be chained with other methods.
179    *
180    * @return Promise
181    *        The promise resolves to the credentials object of the signed-in user:
182    *        {
183    *          email: The user's email address
184    *          uid: The user's unique id
185    *          sessionToken: Session for the FxA server
186    *          scopedKeys: Object mapping OAuth scopes to corresponding derived keys
187    *          kSync: An encryption key for Sync
188    *          kXCS: A key hash of kB for the X-Client-State header
189    *          kExtSync: An encryption key for WebExtensions syncing
190    *          kExtKbHash: A key hash of kB for WebExtensions syncing
191    *          verified: email verification status
192    *        }
193    * @throws If there is no user signed in.
194    */
195   async _loadOrFetchKeys() {
196     return this._fxai.withCurrentAccountState(async currentState => {
197       try {
198         let userData = await currentState.getUserAccountData();
199         if (!userData) {
200           throw new Error("Can't get keys; User is not signed in");
201         }
202         // If we have all the keys in latest storage location, we're good.
203         if (userData.scopedKeys) {
204           if (
205             LEGACY_DERIVED_KEY_SCOPES.every(scope =>
206               userData.scopedKeys.hasOwnProperty(scope)
207             ) &&
208             !DEPRECATED_KEY_SCOPES.some(scope =>
209               userData.scopedKeys.hasOwnProperty(scope)
210             )
211           ) {
212             return userData;
213           }
214         }
215         // If not, we've got work to do, and we debounce to avoid duplicating it.
216         if (!currentState.whenKeysReadyDeferred) {
217           currentState.whenKeysReadyDeferred = PromiseUtils.defer();
218           // N.B. we deliberately don't `await` here, and instead use the promise
219           // to resolve `whenKeysReadyDeferred` (which we then `await` below).
220           this._migrateOrFetchKeys(currentState, userData).then(
221             dataWithKeys => {
222               currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
223               currentState.whenKeysReadyDeferred = null;
224             },
225             err => {
226               currentState.whenKeysReadyDeferred.reject(err);
227               currentState.whenKeysReadyDeferred = null;
228             }
229           );
230         }
231         return await currentState.whenKeysReadyDeferred.promise;
232       } catch (err) {
233         return this._fxai._handleTokenError(err);
234       }
235     });
236   }
238   /**
239    * Key storage migration or fetching logic.
240    *
241    * This method contains the doing-expensive-operations part of the logic of
242    * _loadOrFetchKeys(), factored out into a separate method so we can debounce it.
243    *
244    */
245   async _migrateOrFetchKeys(currentState, userData) {
246     // Bug 1697596 - delete any deprecated scoped keys from storage.
247     // If any of the deprecated keys are present, then we know that we've
248     // previously applied all the other migrations below, otherwise there
249     // would not be any `scopedKeys` field.
250     if (userData.scopedKeys) {
251       const toRemove = DEPRECATED_KEY_SCOPES.filter(scope =>
252         userData.scopedKeys.hasOwnProperty(scope)
253       );
254       if (toRemove.length > 0) {
255         for (const scope of toRemove) {
256           delete userData.scopedKeys[scope];
257         }
258         await currentState.updateUserAccountData({
259           scopedKeys: userData.scopedKeys,
260           // Prior to deprecating SCOPE_ECOSYSTEM_TELEMETRY, this file had some
261           // special code to store it as a top-level user data field. So, this
262           // file also gets to delete it as part of the deprecation.
263           ecosystemUserId: null,
264           ecosystemAnonId: null,
265         });
266         userData = await currentState.getUserAccountData();
267         return userData;
268       }
269     }
270     // Bug 1661407 - migrate from legacy storage of keys as top-level account
271     // data fields, to storing them as scoped keys in the `scopedKeys` object.
272     if (
273       LEGACY_DERIVED_KEYS_NAMES.every(name => userData.hasOwnProperty(name))
274     ) {
275       log.info("Migrating from legacy key fields to scopedKeys.");
276       const scopedKeys = userData.scopedKeys || {};
277       await currentState.updateUserAccountData({
278         scopedKeys: {
279           ...scopedKeys,
280           ...(await this._deriveScopedKeysFromAccountData(userData)),
281         },
282       });
283       userData = await currentState.getUserAccountData();
284       return userData;
285     }
286     // Bug 1426306 - Migrate from kB to derived keys.
287     if (userData.kB) {
288       log.info("Migrating kB to derived keys.");
289       const { uid, kB, sessionToken } = userData;
290       const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
291         sessionToken
292       );
293       await currentState.updateUserAccountData({
294         uid,
295         ...(await this._deriveKeys(
296           uid,
297           CommonUtils.hexToBytes(kB),
298           scopedKeysMetadata
299         )),
300         kA: null, // Remove kA and kB from storage.
301         kB: null,
302       });
303       userData = await currentState.getUserAccountData();
304       return userData;
305     }
306     // Otherwise, we need to fetch from the network and unwrap.
307     if (!userData.sessionToken) {
308       throw new Error("No sessionToken");
309     }
310     if (!userData.keyFetchToken) {
311       throw new Error("No keyFetchToken");
312     }
313     return this._fetchAndUnwrapAndDeriveKeys(
314       currentState,
315       userData.sessionToken,
316       userData.keyFetchToken
317     );
318   }
320   /**
321    * Fetch keys from the server, unwrap them, and derive required sub-keys.
322    *
323    * Once the user's email is verified, we can resquest the root key `kB` from the
324    * FxA server, unwrap it using the client-side secret `unwrapBKey`, and then
325    * derive all the sub-keys required for operation of the browser.
326    */
327   async _fetchAndUnwrapAndDeriveKeys(
328     currentState,
329     sessionToken,
330     keyFetchToken
331   ) {
332     if (logPII) {
333       log.debug(
334         `fetchAndUnwrapKeys: sessionToken: ${sessionToken}, keyFetchToken: ${keyFetchToken}`
335       );
336     }
338     // Sign out if we don't have the necessary tokens.
339     if (!sessionToken || !keyFetchToken) {
340       // this seems really bad and we should remove this - bug 1572313.
341       log.warn("improper _fetchAndUnwrapKeys() call: token missing");
342       await this._fxai.signOut();
343       return null;
344     }
346     // Deriving OAuth scoped keys requires additional metadata from the server.
347     // We fetch this first, before fetching the actual key material, because the
348     // keyFetchToken is single-use and we don't want to do a potentially-fallible
349     // operation after consuming it.
350     const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
351       sessionToken
352     );
354     // Fetch the wrapped keys.
355     // It would be nice to be able to fetch this in a single operation with fetching
356     // the metadata above, but that requires server-side changes in FxA.
357     let { wrapKB } = await this._fetchKeys(keyFetchToken);
359     let data = await currentState.getUserAccountData();
361     // Sanity check that the user hasn't changed out from under us (which should
362     // be impossible given this is called within _withCurrentAccountState, but...)
363     if (data.keyFetchToken !== keyFetchToken) {
364       throw new Error("Signed in user changed while fetching keys!");
365     }
367     let kBbytes = CryptoUtils.xor(
368       CommonUtils.hexToBytes(data.unwrapBKey),
369       wrapKB
370     );
372     if (logPII) {
373       log.debug("kBbytes: " + kBbytes);
374     }
376     let updateData = {
377       ...(await this._deriveKeys(data.uid, kBbytes, scopedKeysMetadata)),
378       keyFetchToken: null, // null values cause the item to be removed.
379       unwrapBKey: null,
380     };
382     if (logPII) {
383       log.debug(`Keys Obtained: ${updateData.scopedKeys}`);
384     } else {
385       log.debug(
386         "Keys Obtained: " + Object.keys(updateData.scopedKeys).join(", ")
387       );
388     }
390     // Just double-check that we derived all the right stuff.
391     const EXPECTED_FIELDS = LEGACY_DERIVED_KEYS_NAMES.concat(["scopedKeys"]);
392     if (EXPECTED_FIELDS.some(k => !updateData[k])) {
393       const missing = EXPECTED_FIELDS.filter(k => !updateData[k]);
394       throw new Error(`user data missing: ${missing.join(", ")}`);
395     }
397     await currentState.updateUserAccountData(updateData);
398     return currentState.getUserAccountData();
399   }
401   /**
402    * Fetch the wrapped root key `wrapKB` from the FxA server.
403    *
404    * This consumes the single-use `keyFetchToken`.
405    */
406   _fetchKeys(keyFetchToken) {
407     let client = this._fxai.fxAccountsClient;
408     log.debug(
409       `Fetching keys with token ${!!keyFetchToken} from ${client.host}`
410     );
411     if (logPII) {
412       log.debug("fetchKeys - the token is " + keyFetchToken);
413     }
414     return client.accountKeys(keyFetchToken);
415   }
417   /**
418    * Fetch additional metadata required for deriving scoped keys.
419    *
420    * This includes timestamps and a server-provided secret to mix in to
421    * the derived value in order to support key rotation.
422    */
423   async _fetchScopedKeysMetadata(sessionToken) {
424     // Hard-coded list of scopes that we know about.
425     // This list will probably grow in future.
426     // Note that LEGACY_SCOPE_WEBEXT_SYNC is not in this list, it gets special-case handling below.
427     const scopes = [SCOPE_OLD_SYNC].join(" ");
428     const scopedKeysMetadata = await this._fxai.fxAccountsClient.getScopedKeyData(
429       sessionToken,
430       FX_OAUTH_CLIENT_ID,
431       scopes
432     );
433     // The server may decline us permission for some of those scopes, although it really shouldn't.
434     // We can live without them...except for the OLDSYNC scope, whose absence would be catastrophic.
435     if (!scopedKeysMetadata.hasOwnProperty(SCOPE_OLD_SYNC)) {
436       log.warn(
437         "The FxA server did not grant Firefox the `oldsync` scope; this is most unexpected!" +
438           ` scopes were: ${Object.keys(scopedKeysMetadata)}`
439       );
440       throw new Error(
441         "The FxA server did not grant Firefox the `oldsync` scope"
442       );
443     }
444     // Firefox Desktop invented its own special scope for legacy webextension syncing,
445     // with its own special key. Rather than teach the rest of FxA about this scope
446     // that will never be used anywhere else, just give it the same metadata as
447     // the main sync scope. This can go away once legacy webext sync is removed.
448     // (ref Bug 1637465 for tracking that removal)
449     scopedKeysMetadata[LEGACY_SCOPE_WEBEXT_SYNC] = {
450       ...scopedKeysMetadata[SCOPE_OLD_SYNC],
451       identifier: LEGACY_SCOPE_WEBEXT_SYNC,
452     };
453     return scopedKeysMetadata;
454   }
456   /**
457    * Derive purpose-specific keys from the root FxA key `kB`.
458    *
459    * Everything that uses an encryption key from FxA uses a purpose-specific derived
460    * key. For new uses this is derived in a structured way based on OAuth scopes,
461    * while for legacy uses (mainly Firefox Sync) it is derived in a more ad-hoc fashion.
462    * This method does all the derivations for the uses that we know about.
463    *
464    */
465   async _deriveKeys(uid, kBbytes, scopedKeysMetadata) {
466     const scopedKeys = await this._deriveScopedKeys(
467       uid,
468       kBbytes,
469       scopedKeysMetadata
470     );
471     return {
472       scopedKeys,
473       // Existing browser code might expect sync keys to be available as top-level account data.
474       // For b/w compat we can derive these even if they're not in our list of scoped keys for
475       // some reason (since the derivation doesn't depend on server-provided data).
476       kSync: scopedKeys[SCOPE_OLD_SYNC]
477         ? this.keyAsHex(scopedKeys[SCOPE_OLD_SYNC])
478         : CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)),
479       kXCS: scopedKeys[SCOPE_OLD_SYNC]
480         ? this.kidAsHex(scopedKeys[SCOPE_OLD_SYNC])
481         : CommonUtils.bytesAsHex(await this._deriveXClientState(kBbytes)),
482       kExtSync: scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC]
483         ? this.keyAsHex(scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC])
484         : CommonUtils.bytesAsHex(await this._deriveWebExtSyncStoreKey(kBbytes)),
485       kExtKbHash: scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC]
486         ? this.kidAsHex(scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC])
487         : CommonUtils.bytesAsHex(await this._deriveWebExtKbHash(uid, kBbytes)),
488     };
489   }
491   /**
492    * Derive various scoped keys from the root FxA key `kB`.
493    *
494    * The `scopedKeysMetadata` object is additional information fetched from the server that
495    * that gets mixed in to the key derivation, with each member of the object corresponding
496    * to an OAuth scope that keys its own scoped key.
497    *
498    * As a special case for backwards-compatibility, sync-related scopes get special
499    * treatment to use a legacy derivation algorithm.
500    *
501    */
502   async _deriveScopedKeys(uid, kBbytes, scopedKeysMetadata) {
503     const scopedKeys = {};
504     for (const scope in scopedKeysMetadata) {
505       if (LEGACY_DERIVED_KEY_SCOPES.includes(scope)) {
506         scopedKeys[scope] = await this._deriveLegacyScopedKey(
507           uid,
508           kBbytes,
509           scope,
510           scopedKeysMetadata[scope]
511         );
512       } else {
513         scopedKeys[scope] = await this._deriveScopedKey(
514           uid,
515           kBbytes,
516           scope,
517           scopedKeysMetadata[scope]
518         );
519       }
520     }
521     return scopedKeys;
522   }
524   /**
525    * Derive the `scopedKeys` data field based on current account data.
526    *
527    * This is a backwards-compatibility convenience for users who are already signed in to Firefox
528    * and have legacy fields like `kSync` and `kXCS` in their top-level account data, but do not have
529    * the newer `scopedKeys` field. We populate it with the scoped keys for sync and webext-sync.
530    *
531    */
532   async _deriveScopedKeysFromAccountData(userData) {
533     const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
534       userData.sessionToken
535     );
536     const scopedKeys = userData.scopedKeys || {};
537     for (const scope of LEGACY_DERIVED_KEY_SCOPES) {
538       if (scopedKeysMetadata.hasOwnProperty(scope)) {
539         let kid, key;
540         if (scope == SCOPE_OLD_SYNC) {
541           ({ kXCS: kid, kSync: key } = userData);
542         } else if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
543           ({ kExtKbHash: kid, kExtSync: key } = userData);
544         } else {
545           // Should never happen, but a nice internal consistency check.
546           throw new Error(`Unexpected legacy key-bearing scope: ${scope}`);
547         }
548         if (!kid || !key) {
549           throw new Error(
550             `Account is missing legacy key fields for scope: ${scope}`
551           );
552         }
553         scopedKeys[scope] = await this._formatLegacyScopedKey(
554           CommonUtils.hexToArrayBuffer(kid),
555           CommonUtils.hexToArrayBuffer(key),
556           scope,
557           scopedKeysMetadata[scope]
558         );
559       }
560     }
561     return scopedKeys;
562   }
564   /**
565    * Derive a scoped key for an individual OAuth scope.
566    *
567    * The derivation here uses HKDF to combine:
568    *   - the root key material kB
569    *   - a unique identifier for this scoped key
570    *   - a server-provided secret that allows for key rotation
571    *   - the account uid as an additional salt
572    *
573    * It produces 32 bytes of (secret) key material along with a (potentially public)
574    * key identifier, formatted as a JWK.
575    *
576    * The full details are in the technical docs at
577    * https://docs.google.com/document/d/1IvQJFEBFz0PnL4uVlIvt8fBS_IPwSK-avK0BRIHucxQ/
578    */
579   async _deriveScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
580     kBbytes = CommonUtils.byteStringToArrayBuffer(kBbytes);
582     const FINGERPRINT_LENGTH = 16;
583     const KEY_LENGTH = 32;
584     const VALID_UID = /^[0-9a-f]{32}$/i;
585     const VALID_ROTATION_SECRET = /^[0-9a-f]{64}$/i;
587     // Engage paranoia mode for input data.
588     if (!VALID_UID.test(uid)) {
589       throw new Error("uid must be a 32-character hex string");
590     }
591     if (kBbytes.length != 32) {
592       throw new Error("kBbytes must be exactly 32 bytes");
593     }
594     if (
595       typeof scopedKeyMetadata.identifier !== "string" ||
596       scopedKeyMetadata.identifier.length < 10
597     ) {
598       throw new Error("identifier must be a string of length >= 10");
599     }
600     if (typeof scopedKeyMetadata.keyRotationTimestamp !== "number") {
601       throw new Error("keyRotationTimestamp must be a number");
602     }
603     if (!VALID_ROTATION_SECRET.test(scopedKeyMetadata.keyRotationSecret)) {
604       throw new Error("keyRotationSecret must be a 64-character hex string");
605     }
607     // The server returns milliseconds, we want seconds as a string.
608     const keyRotationTimestamp =
609       "" + Math.round(scopedKeyMetadata.keyRotationTimestamp / 1000);
610     if (keyRotationTimestamp.length < 10) {
611       throw new Error("keyRotationTimestamp must round to a 10-digit number");
612     }
614     const keyRotationSecret = CommonUtils.hexToArrayBuffer(
615       scopedKeyMetadata.keyRotationSecret
616     );
617     const salt = CommonUtils.hexToArrayBuffer(uid);
618     const context = new TextEncoder("utf8").encode(
619       "identity.mozilla.com/picl/v1/scoped_key\n" + scopedKeyMetadata.identifier
620     );
622     const inputKey = new Uint8Array(64);
623     inputKey.set(kBbytes, 0);
624     inputKey.set(keyRotationSecret, 32);
626     const derivedKeyMaterial = await CryptoUtils.hkdf(
627       inputKey,
628       salt,
629       context,
630       FINGERPRINT_LENGTH + KEY_LENGTH
631     );
632     const fingerprint = derivedKeyMaterial.slice(0, FINGERPRINT_LENGTH);
633     const key = derivedKeyMaterial.slice(
634       FINGERPRINT_LENGTH,
635       FINGERPRINT_LENGTH + KEY_LENGTH
636     );
638     return {
639       kid:
640         keyRotationTimestamp +
641         "-" +
642         ChromeUtils.base64URLEncode(fingerprint, {
643           pad: false,
644         }),
645       k: ChromeUtils.base64URLEncode(key, {
646         pad: false,
647       }),
648       kty: "oct",
649     };
650   }
652   /**
653    * Derive the scoped key for the one of our legacy sync-related scopes.
654    *
655    * These uses a different key-derivation algoritm that incorporates less server-provided
656    * data, for backwards-compatibility reasons.
657    *
658    */
659   async _deriveLegacyScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
660     let kid, key;
661     if (scope == SCOPE_OLD_SYNC) {
662       kid = await this._deriveXClientState(kBbytes);
663       key = await this._deriveSyncKey(kBbytes);
664     } else if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
665       kid = await this._deriveWebExtKbHash(uid, kBbytes);
666       key = await this._deriveWebExtSyncStoreKey(kBbytes);
667     } else {
668       throw new Error(`Unexpected legacy key-bearing scope: ${scope}`);
669     }
670     kid = CommonUtils.byteStringToArrayBuffer(kid);
671     key = CommonUtils.byteStringToArrayBuffer(key);
672     return this._formatLegacyScopedKey(kid, key, scope, scopedKeyMetadata);
673   }
675   /**
676    * Format key material for a legacy scyne-related scope as a JWK.
677    *
678    * @param {ArrayBuffer} kid bytes of the key hash to use in the key identifier
679    * @param {ArrayBuffer} key bytes of the derived sync key
680    * @param {String} scope the scope with which this key is associated
681    * @param {Number} keyRotationTimestamp server-provided timestamp of last key rotation
682    * @returns {Object} key material formatted as a JWK object
683    */
684   _formatLegacyScopedKey(kid, key, scope, { keyRotationTimestamp }) {
685     kid = ChromeUtils.base64URLEncode(kid, {
686       pad: false,
687     });
688     key = ChromeUtils.base64URLEncode(key, {
689       pad: false,
690     });
691     return {
692       kid: `${keyRotationTimestamp}-${kid}`,
693       k: key,
694       kty: "oct",
695     };
696   }
698   /**
699    * Derive the Sync Key given the byte string kB.
700    *
701    * @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)>
702    */
703   async _deriveSyncKey(kBbytes) {
704     return CryptoUtils.hkdfLegacy(
705       kBbytes,
706       undefined,
707       "identity.mozilla.com/picl/v1/oldsync",
708       2 * 32
709     );
710   }
712   /**
713    * Derive the X-Client-State header given the byte string kB.
714    *
715    * @returns Promise<SHA256(kB)[:16]>
716    */
717   async _deriveXClientState(kBbytes) {
718     return this._sha256(kBbytes).slice(0, 16);
719   }
721   /**
722    * Derive the WebExtensions Sync Storage Key given the byte string kB.
723    *
724    * @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)>
725    */
726   async _deriveWebExtSyncStoreKey(kBbytes) {
727     return CryptoUtils.hkdfLegacy(
728       kBbytes,
729       undefined,
730       "identity.mozilla.com/picl/v1/chrome.storage.sync",
731       2 * 32
732     );
733   }
735   /**
736    * Derive the WebExtensions kbHash given the byte string kB.
737    *
738    * @returns Promise<SHA256(uid + kB)>
739    */
740   async _deriveWebExtKbHash(uid, kBbytes) {
741     return this._sha256(uid + kBbytes);
742   }
744   _sha256(bytes) {
745     let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
746       Ci.nsICryptoHash
747     );
748     hasher.init(hasher.SHA256);
749     return CryptoUtils.digestBytes(bytes, hasher);
750   }
753 var EXPORTED_SYMBOLS = ["FxAccountsKeys"];