Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / services / fxaccounts / FxAccountsKeys.sys.mjs
blob9717f010c7325dcdbe5c0a65b3760c720995f101
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 { CommonUtils } from "resource://services-common/utils.sys.mjs";
7 import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs";
9 import {
10   SCOPE_OLD_SYNC,
11   DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
12   FX_OAUTH_CLIENT_ID,
13   log,
14   logPII,
15 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
17 // The following top-level fields have since been deprecated and exist here purely
18 // to be removed from the account state when seen. After a reasonable period of time
19 // has passed, where users have been migrated away from those keys they should be safe to be removed
20 const DEPRECATED_DERIVED_KEYS_NAMES = [
21   "kSync",
22   "kXCS",
23   "kExtSync",
24   "kExtKbHash",
25   "ecosystemUserId",
26   "ecosystemAnonId",
29 // This scope and its associated key material were used by the old Kinto webextension
30 // storage backend, but has since been decommissioned. It's here entirely so that we
31 // remove the corresponding key from storage if present. We should be safe to remove it
32 // after some sensible period of time has elapsed to allow most clients to update.
33 const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage";
35 // These are the scopes that correspond to new storage for the `LEGACY_DERIVED_KEYS_NAMES`.
36 // We will, if necessary, migrate storage for those keys so that it's associated with
37 // these scopes.
38 const LEGACY_DERIVED_KEY_SCOPES = [SCOPE_OLD_SYNC];
40 // These are scopes that we used to store, but are no longer using,
41 // and hence should be deleted from storage if present.
42 const DEPRECATED_KEY_SCOPES = [
43   DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
44   DEPRECATED_SCOPE_WEBEXT_SYNC,
47 /**
48  * Utilities for working with key material linked to the user's account.
49  *
50  * Each Firefox Account has 32 bytes of root key material called `kB` which is
51  * linked to the user's password, and which is used to derive purpose-specific
52  * subkeys for things like encrypting the user's sync data. This class provides
53  * the interface for working with such key material.
54  *
55  * Most recent FxA clients obtain appropriate key material directly as part of
56  * their sign-in flow, using a special extension of the OAuth2.0 protocol to
57  * securely deliver the derived keys without revealing `kB`. Keys obtained in
58  * in this way are called "scoped keys" since each corresponds to a particular
59  * OAuth scope, and this class provides a `getKeyForScope` method that is the
60  * preferred method for consumers to work with such keys.
61  *
62  * However, since the FxA integration in Firefox Desktop pre-dates the use of
63  * OAuth2.0, we also have a lot of code for fetching keys via an older flow.
64  * This flow uses a special `keyFetchToken` to obtain `kB` and then derive various
65  * sub-keys from it. Consumers should consider this an internal implementation
66  * detail of the `FxAccountsKeys` class and should prefer `getKeyForScope` where
67  * possible.  We intend to remove support for Firefox ever directly handling `kB`
68  * at some point in the future.
69  */
70 export class FxAccountsKeys {
71   constructor(fxAccountsInternal) {
72     this._fxai = fxAccountsInternal;
73   }
75   /**
76    * Checks if we currently have the key for a given scope, or if we have enough to
77    * be able to successfully fetch and unwrap it for the signed-in-user.
78    *
79    * Unlike `getKeyForScope`, this will not hit the network to fetch wrapped keys if
80    * they aren't available locally.
81    */
82   canGetKeyForScope(scope) {
83     return this._fxai.withCurrentAccountState(async currentState => {
84       let userData = await currentState.getUserAccountData();
85       if (!userData) {
86         throw new Error("Can't possibly get keys; User is not signed in");
87       }
88       if (!userData.verified) {
89         log.info("Can't get keys; user is not verified");
90         return false;
91       }
93       if (userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope)) {
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    * Validates if the given scoped keys are valid keys
136    *
137    * @param { Object } scopedKeys: The scopedKeys bundle
138    *
139    * @return { Boolean }: true if the scopedKeys bundle is valid, false otherwise
140    */
141   validScopedKeys(scopedKeys) {
142     for (const expectedScope of Object.keys(scopedKeys)) {
143       const key = scopedKeys[expectedScope];
144       if (
145         !key.hasOwnProperty("scope") ||
146         !key.hasOwnProperty("kid") ||
147         !key.hasOwnProperty("kty") ||
148         !key.hasOwnProperty("k")
149       ) {
150         return false;
151       }
152       const { scope, kid, kty, k } = key;
153       if (scope != expectedScope || kty != "oct") {
154         return false;
155       }
156       // We verify the format of the key id is `timestamp-fingerprint`
157       if (!kid.includes("-")) {
158         return false;
159       }
160       const [keyRotationTimestamp, fingerprint] = kid.split("-");
161       // We then verify that the timestamp is a valid timestamp
162       const keyRotationTimestampNum = Number(keyRotationTimestamp);
163       // If the value we got back is falsy it's not a valid timestamp
164       // note that we treat a 0 timestamp as invalid
165       if (!keyRotationTimestampNum) {
166         return false;
167       }
168       // For extra safety, we validate that the timestamp can be converted into a valid
169       // Date object
170       const date = new Date(keyRotationTimestampNum);
171       if (isNaN(date.getTime()) || date.getTime() <= 0) {
172         return false;
173       }
175       // Finally, we validate that the fingerprint and the key itself are valid base64 values
176       // Note that we can't verify the fingerprint is correct here because we don't have kb
177       const validB64String = b64String => {
178         let decoded;
179         try {
180           decoded = ChromeUtils.base64URLDecode(b64String, {
181             padding: "reject",
182           });
183         } catch (e) {
184           return false;
185         }
186         return !!decoded;
187       };
188       if (!validB64String(fingerprint) || !validB64String(k)) {
189         return false;
190       }
191     }
192     return true;
193   }
195   /**
196    * Format a JWK kid as hex rather than base64.
197    *
198    * This is a backwards-compatibility helper for code that needs a raw key fingerprint
199    * for use as a key identifier, rather than the timestamp+fingerprint format used by
200    * FxA scoped keys.
201    *
202    * @param {Object} jwk The JWK from which to extract the `kid` field as hex.
203    */
204   kidAsHex(jwk) {
205     // The kid format is "{timestamp}-{b64url(fingerprint)}", but we have to be careful
206     // because the fingerprint component may contain "-" as well, and we want to ensure
207     // the timestamp component was non-empty.
208     const idx = jwk.kid.indexOf("-") + 1;
209     if (idx <= 1) {
210       throw new Error(`Invalid kid: ${jwk.kid}`);
211     }
212     return CommonUtils.base64urlToHex(jwk.kid.slice(idx));
213   }
215   /**
216    * Fetch encryption keys for the signed-in-user from the FxA API server.
217    *
218    * Not for user consumption.  Exists to cause the keys to be fetched.
219    *
220    * Returns user data so that it can be chained with other methods.
221    *
222    * @return Promise
223    *        The promise resolves to the credentials object of the signed-in user:
224    *        {
225    *          email: The user's email address
226    *          uid: The user's unique id
227    *          sessionToken: Session for the FxA server
228    *          scopedKeys: Object mapping OAuth scopes to corresponding derived keys
229    *          verified: email verification status
230    *        }
231    * @throws If there is no user signed in.
232    */
233   async _loadOrFetchKeys() {
234     return this._fxai.withCurrentAccountState(async currentState => {
235       try {
236         let userData = await currentState.getUserAccountData();
237         if (!userData) {
238           throw new Error("Can't get keys; User is not signed in");
239         }
240         // If we have all the keys in latest storage location, we're good.
241         if (userData.scopedKeys) {
242           if (
243             LEGACY_DERIVED_KEY_SCOPES.every(scope =>
244               userData.scopedKeys.hasOwnProperty(scope)
245             ) &&
246             !DEPRECATED_KEY_SCOPES.some(scope =>
247               userData.scopedKeys.hasOwnProperty(scope)
248             ) &&
249             !DEPRECATED_DERIVED_KEYS_NAMES.some(keyName =>
250               userData.hasOwnProperty(keyName)
251             )
252           ) {
253             return userData;
254           }
255         }
256         // If not, we've got work to do, and we debounce to avoid duplicating it.
257         if (!currentState.whenKeysReadyDeferred) {
258           currentState.whenKeysReadyDeferred = Promise.withResolvers();
259           // N.B. we deliberately don't `await` here, and instead use the promise
260           // to resolve `whenKeysReadyDeferred` (which we then `await` below).
261           this._migrateOrFetchKeys(currentState, userData).then(
262             dataWithKeys => {
263               currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
264               currentState.whenKeysReadyDeferred = null;
265             },
266             err => {
267               currentState.whenKeysReadyDeferred.reject(err);
268               currentState.whenKeysReadyDeferred = null;
269             }
270           );
271         }
272         return await currentState.whenKeysReadyDeferred.promise;
273       } catch (err) {
274         return this._fxai._handleTokenError(err);
275       }
276     });
277   }
279   /**
280    * Set externally derived scoped keys in internal storage
281    * @param { Object } scopedKeys: The scoped keys object derived by the oauth flow
282    *
283    * @return { Promise }: A promise that resolves if the keys were successfully stored,
284    *    or rejects if we failed to persist the keys, or if the user is not signed in already
285    */
286   async setScopedKeys(scopedKeys) {
287     return this._fxai.withCurrentAccountState(async currentState => {
288       const userData = await currentState.getUserAccountData();
289       if (!userData) {
290         throw new Error("Cannot persist keys, no user signed in");
291       }
292       await currentState.updateUserAccountData({
293         scopedKeys,
294       });
295     });
296   }
298   /**
299    * Key storage migration or fetching logic.
300    *
301    * This method contains the doing-expensive-operations part of the logic of
302    * _loadOrFetchKeys(), factored out into a separate method so we can debounce it.
303    *
304    */
305   async _migrateOrFetchKeys(currentState, userData) {
306     // If the required scopes are present in `scopedKeys`, then we know that we've
307     // previously applied all earlier migrations
308     // so we are safe to delete deprecated fields that older migrations
309     // might have depended on.
310     if (
311       userData.scopedKeys &&
312       LEGACY_DERIVED_KEY_SCOPES.every(scope =>
313         userData.scopedKeys.hasOwnProperty(scope)
314       )
315     ) {
316       return this._removeDeprecatedKeys(currentState, userData);
317     }
319     // Otherwise, we need to fetch from the network and unwrap.
320     if (!userData.sessionToken) {
321       throw new Error("No sessionToken");
322     }
323     if (!userData.keyFetchToken) {
324       throw new Error("No keyFetchToken");
325     }
326     return this._fetchAndUnwrapAndDeriveKeys(
327       currentState,
328       userData.sessionToken,
329       userData.keyFetchToken
330     );
331   }
333   /**
334    * Removes deprecated keys from storage and returns an
335    * updated user data object
336    */
337   async _removeDeprecatedKeys(currentState, userData) {
338     // Bug 1838708: Delete any deprecated high level keys from storage
339     const keysToRemove = DEPRECATED_DERIVED_KEYS_NAMES.filter(keyName =>
340       userData.hasOwnProperty(keyName)
341     );
342     if (keysToRemove.length) {
343       const removedKeys = {};
344       for (const keyName of keysToRemove) {
345         removedKeys[keyName] = null;
346       }
347       await currentState.updateUserAccountData({
348         ...removedKeys,
349       });
350       userData = await currentState.getUserAccountData();
351     }
352     // Bug 1697596 - delete any deprecated scoped keys from storage.
353     const scopesToRemove = DEPRECATED_KEY_SCOPES.filter(scope =>
354       userData.scopedKeys.hasOwnProperty(scope)
355     );
356     if (scopesToRemove.length) {
357       const updatedScopedKeys = {
358         ...userData.scopedKeys,
359       };
360       for (const scope of scopesToRemove) {
361         delete updatedScopedKeys[scope];
362       }
363       await currentState.updateUserAccountData({
364         scopedKeys: updatedScopedKeys,
365       });
366       userData = await currentState.getUserAccountData();
367     }
368     return userData;
369   }
371   /**
372    * Fetch keys from the server, unwrap them, and derive required sub-keys.
373    *
374    * Once the user's email is verified, we can resquest the root key `kB` from the
375    * FxA server, unwrap it using the client-side secret `unwrapBKey`, and then
376    * derive all the sub-keys required for operation of the browser.
377    */
378   async _fetchAndUnwrapAndDeriveKeys(
379     currentState,
380     sessionToken,
381     keyFetchToken
382   ) {
383     if (logPII()) {
384       log.debug(
385         `fetchAndUnwrapKeys: sessionToken: ${sessionToken}, keyFetchToken: ${keyFetchToken}`
386       );
387     }
389     // Sign out if we don't have the necessary tokens.
390     if (!sessionToken || !keyFetchToken) {
391       // this seems really bad and we should remove this - bug 1572313.
392       log.warn("improper _fetchAndUnwrapKeys() call: token missing");
393       await this._fxai.signOut();
394       return null;
395     }
397     // Deriving OAuth scoped keys requires additional metadata from the server.
398     // We fetch this first, before fetching the actual key material, because the
399     // keyFetchToken is single-use and we don't want to do a potentially-fallible
400     // operation after consuming it.
401     const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
402       sessionToken
403     );
405     // Fetch the wrapped keys.
406     // It would be nice to be able to fetch this in a single operation with fetching
407     // the metadata above, but that requires server-side changes in FxA.
408     let { wrapKB } = await this._fetchKeys(keyFetchToken);
410     let data = await currentState.getUserAccountData();
412     // Sanity check that the user hasn't changed out from under us (which should
413     // be impossible given this is called within _withCurrentAccountState, but...)
414     if (data.keyFetchToken !== keyFetchToken) {
415       throw new Error("Signed in user changed while fetching keys!");
416     }
418     let kBbytes = CryptoUtils.xor(
419       CommonUtils.hexToBytes(data.unwrapBKey),
420       wrapKB
421     );
423     if (logPII()) {
424       log.debug("kBbytes: " + kBbytes);
425     }
427     let updateData = {
428       ...(await this._deriveKeys(data.uid, kBbytes, scopedKeysMetadata)),
429       keyFetchToken: null, // null values cause the item to be removed.
430       unwrapBKey: null,
431     };
433     if (logPII()) {
434       log.debug(`Keys Obtained: ${updateData.scopedKeys}`);
435     } else {
436       log.debug(
437         "Keys Obtained: " + Object.keys(updateData.scopedKeys).join(", ")
438       );
439     }
441     // Just double-check that scoped keys are there now
442     if (!updateData.scopedKeys) {
443       throw new Error(`user data missing: scopedKeys`);
444     }
446     await currentState.updateUserAccountData(updateData);
447     return currentState.getUserAccountData();
448   }
450   /**
451    * Fetch the wrapped root key `wrapKB` from the FxA server.
452    *
453    * This consumes the single-use `keyFetchToken`.
454    */
455   _fetchKeys(keyFetchToken) {
456     let client = this._fxai.fxAccountsClient;
457     log.debug(
458       `Fetching keys with token ${!!keyFetchToken} from ${client.host}`
459     );
460     if (logPII()) {
461       log.debug("fetchKeys - the token is " + keyFetchToken);
462     }
463     return client.accountKeys(keyFetchToken);
464   }
466   /**
467    * Fetch additional metadata required for deriving scoped keys.
468    *
469    * This includes timestamps and a server-provided secret to mix in to
470    * the derived value in order to support key rotation.
471    */
472   async _fetchScopedKeysMetadata(sessionToken) {
473     // Hard-coded list of scopes that we know about.
474     // This list will probably grow in future.
475     const scopes = [SCOPE_OLD_SYNC].join(" ");
476     const scopedKeysMetadata =
477       await this._fxai.fxAccountsClient.getScopedKeyData(
478         sessionToken,
479         FX_OAUTH_CLIENT_ID,
480         scopes
481       );
482     // The server may decline us permission for some of those scopes, although it really shouldn't.
483     // We can live without them...except for the OLDSYNC scope, whose absence would be catastrophic.
484     if (!scopedKeysMetadata.hasOwnProperty(SCOPE_OLD_SYNC)) {
485       log.warn(
486         "The FxA server did not grant Firefox the `oldsync` scope; this is most unexpected!" +
487           ` scopes were: ${Object.keys(scopedKeysMetadata)}`
488       );
489       throw new Error(
490         "The FxA server did not grant Firefox the `oldsync` scope"
491       );
492     }
493     return scopedKeysMetadata;
494   }
496   /**
497    * Derive purpose-specific keys from the root FxA key `kB`.
498    *
499    * Everything that uses an encryption key from FxA uses a purpose-specific derived
500    * key. For new uses this is derived in a structured way based on OAuth scopes,
501    * while for legacy uses (mainly Firefox Sync) it is derived in a more ad-hoc fashion.
502    * This method does all the derivations for the uses that we know about.
503    *
504    */
505   async _deriveKeys(uid, kBbytes, scopedKeysMetadata) {
506     const scopedKeys = await this._deriveScopedKeys(
507       uid,
508       kBbytes,
509       scopedKeysMetadata
510     );
511     return {
512       scopedKeys,
513     };
514   }
516   /**
517    * Derive various scoped keys from the root FxA key `kB`.
518    *
519    * The `scopedKeysMetadata` object is additional information fetched from the server that
520    * that gets mixed in to the key derivation, with each member of the object corresponding
521    * to an OAuth scope that keys its own scoped key.
522    *
523    * As a special case for backwards-compatibility, sync-related scopes get special
524    * treatment to use a legacy derivation algorithm.
525    *
526    */
527   async _deriveScopedKeys(uid, kBbytes, scopedKeysMetadata) {
528     const scopedKeys = {};
529     for (const scope in scopedKeysMetadata) {
530       if (LEGACY_DERIVED_KEY_SCOPES.includes(scope)) {
531         scopedKeys[scope] = await this._deriveLegacyScopedKey(
532           uid,
533           kBbytes,
534           scope,
535           scopedKeysMetadata[scope]
536         );
537       } else {
538         scopedKeys[scope] = await this._deriveScopedKey(
539           uid,
540           kBbytes,
541           scope,
542           scopedKeysMetadata[scope]
543         );
544       }
545     }
546     return scopedKeys;
547   }
549   /**
550    * Derive a scoped key for an individual OAuth scope.
551    *
552    * The derivation here uses HKDF to combine:
553    *   - the root key material kB
554    *   - a unique identifier for this scoped key
555    *   - a server-provided secret that allows for key rotation
556    *   - the account uid as an additional salt
557    *
558    * It produces 32 bytes of (secret) key material along with a (potentially public)
559    * key identifier, formatted as a JWK.
560    *
561    * The full details are in the technical docs at
562    * https://docs.google.com/document/d/1IvQJFEBFz0PnL4uVlIvt8fBS_IPwSK-avK0BRIHucxQ/
563    */
564   async _deriveScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
565     kBbytes = CommonUtils.byteStringToArrayBuffer(kBbytes);
567     const FINGERPRINT_LENGTH = 16;
568     const KEY_LENGTH = 32;
569     const VALID_UID = /^[0-9a-f]{32}$/i;
570     const VALID_ROTATION_SECRET = /^[0-9a-f]{64}$/i;
572     // Engage paranoia mode for input data.
573     if (!VALID_UID.test(uid)) {
574       throw new Error("uid must be a 32-character hex string");
575     }
576     if (kBbytes.length != 32) {
577       throw new Error("kBbytes must be exactly 32 bytes");
578     }
579     if (
580       typeof scopedKeyMetadata.identifier !== "string" ||
581       scopedKeyMetadata.identifier.length < 10
582     ) {
583       throw new Error("identifier must be a string of length >= 10");
584     }
585     if (typeof scopedKeyMetadata.keyRotationTimestamp !== "number") {
586       throw new Error("keyRotationTimestamp must be a number");
587     }
588     if (!VALID_ROTATION_SECRET.test(scopedKeyMetadata.keyRotationSecret)) {
589       throw new Error("keyRotationSecret must be a 64-character hex string");
590     }
592     // The server returns milliseconds, we want seconds as a string.
593     const keyRotationTimestamp =
594       "" + Math.round(scopedKeyMetadata.keyRotationTimestamp / 1000);
595     if (keyRotationTimestamp.length < 10) {
596       throw new Error("keyRotationTimestamp must round to a 10-digit number");
597     }
599     const keyRotationSecret = CommonUtils.hexToArrayBuffer(
600       scopedKeyMetadata.keyRotationSecret
601     );
602     const salt = CommonUtils.hexToArrayBuffer(uid);
603     const context = new TextEncoder().encode(
604       "identity.mozilla.com/picl/v1/scoped_key\n" + scopedKeyMetadata.identifier
605     );
607     const inputKey = new Uint8Array(64);
608     inputKey.set(kBbytes, 0);
609     inputKey.set(keyRotationSecret, 32);
611     const derivedKeyMaterial = await CryptoUtils.hkdf(
612       inputKey,
613       salt,
614       context,
615       FINGERPRINT_LENGTH + KEY_LENGTH
616     );
617     const fingerprint = derivedKeyMaterial.slice(0, FINGERPRINT_LENGTH);
618     const key = derivedKeyMaterial.slice(
619       FINGERPRINT_LENGTH,
620       FINGERPRINT_LENGTH + KEY_LENGTH
621     );
623     return {
624       kid:
625         keyRotationTimestamp +
626         "-" +
627         ChromeUtils.base64URLEncode(fingerprint, {
628           pad: false,
629         }),
630       k: ChromeUtils.base64URLEncode(key, {
631         pad: false,
632       }),
633       kty: "oct",
634     };
635   }
637   /**
638    * Derive the scoped key for the one of our legacy sync-related scopes.
639    *
640    * These uses a different key-derivation algoritm that incorporates less server-provided
641    * data, for backwards-compatibility reasons.
642    *
643    */
644   async _deriveLegacyScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
645     let kid, key;
646     if (scope == SCOPE_OLD_SYNC) {
647       kid = await this._deriveXClientState(kBbytes);
648       key = await this._deriveSyncKey(kBbytes);
649     } else {
650       throw new Error(`Unexpected legacy key-bearing scope: ${scope}`);
651     }
652     kid = CommonUtils.byteStringToArrayBuffer(kid);
653     key = CommonUtils.byteStringToArrayBuffer(key);
654     return this._formatLegacyScopedKey(kid, key, scope, scopedKeyMetadata);
655   }
657   /**
658    * Format key material for a legacy scyne-related scope as a JWK.
659    *
660    * @param {ArrayBuffer} kid bytes of the key hash to use in the key identifier
661    * @param {ArrayBuffer} key bytes of the derived sync key
662    * @param {String} scope the scope with which this key is associated
663    * @param {Number} keyRotationTimestamp server-provided timestamp of last key rotation
664    * @returns {Object} key material formatted as a JWK object
665    */
666   _formatLegacyScopedKey(kid, key, scope, { keyRotationTimestamp }) {
667     kid = ChromeUtils.base64URLEncode(kid, {
668       pad: false,
669     });
670     key = ChromeUtils.base64URLEncode(key, {
671       pad: false,
672     });
673     return {
674       kid: `${keyRotationTimestamp}-${kid}`,
675       k: key,
676       kty: "oct",
677     };
678   }
680   /**
681    * Derive the Sync Key given the byte string kB.
682    *
683    * @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)>
684    */
685   async _deriveSyncKey(kBbytes) {
686     return CryptoUtils.hkdfLegacy(
687       kBbytes,
688       undefined,
689       "identity.mozilla.com/picl/v1/oldsync",
690       2 * 32
691     );
692   }
694   /**
695    * Derive the X-Client-State header given the byte string kB.
696    *
697    * @returns Promise<SHA256(kB)[:16]>
698    */
699   async _deriveXClientState(kBbytes) {
700     return this._sha256(kBbytes).slice(0, 16);
701   }
703   _sha256(bytes) {
704     let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
705       Ci.nsICryptoHash
706     );
707     hasher.init(hasher.SHA256);
708     return CryptoUtils.digestBytes(bytes, hasher);
709   }