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