FxAccountsKeys.sys.mjs (25430B)
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 5 import { CommonUtils } from "resource://services-common/utils.sys.mjs"; 6 7 import { CryptoUtils } from "moz-src:///services/crypto/modules/utils.sys.mjs"; 8 9 import { 10 SCOPE_APP_SYNC, 11 DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY, 12 OAUTH_CLIENT_ID, 13 log, 14 logPII, 15 } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 16 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", 27 ]; 28 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"; 34 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_APP_SYNC]; 39 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, 45 ]; 46 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 * Note that Desktop is now slowly moving to these newer oauth flows - so all this 71 * key fetching and use of the keyFetchToken should be considered deprecated, and 72 * must not be used when the OAuth is in use. This code remains behind just for 73 * this transition and should be removed once we are committed to never rolling 74 * the flows back to the pre-oauth days. 75 */ 76 export class FxAccountsKeys { 77 constructor(fxAccountsInternal) { 78 this._fxai = fxAccountsInternal; 79 } 80 81 /** 82 * Checks if we currently have the key for a given scope, or if we have enough to 83 * be able to successfully fetch and unwrap it for the signed-in-user. 84 * 85 * Unlike `getKeyForScope`, this will not hit the network to fetch wrapped keys if 86 * they aren't available locally. 87 */ 88 canGetKeyForScope(scope) { 89 return this._fxai.withCurrentAccountState(async currentState => { 90 let userData = await currentState.getUserAccountData(); 91 if (!userData) { 92 throw new Error("Can't possibly get keys; User is not signed in"); 93 } 94 if (!userData.verified) { 95 log.info("Can't get keys; user is not verified"); 96 return false; 97 } 98 return userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope); 99 }); 100 } 101 102 /** 103 * Checks if we currently have the key for a given scope locally available. 104 * 105 * This method only checks if the keys exist in local storage. With OAuth-based 106 * authentication, keys cannot be fetched on demand - if they don't exist locally, 107 * there is no way to obtain them. 108 * 109 * @param {string} scope The OAuth scope whose key should be checked 110 * 111 * @return Promise<boolean> 112 * Resolves to true if the key exists locally, false otherwise. 113 */ 114 hasKeysForScope(scope) { 115 return this._fxai.withCurrentAccountState(async currentState => { 116 let userData = await currentState.getUserAccountData(); 117 if (!userData) { 118 return false; 119 } 120 return !!( 121 userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope) 122 ); 123 }); 124 } 125 126 /** 127 * Get the key for a specified OAuth scope. 128 * 129 * @param {string} scope The OAuth scope whose key should be returned 130 * 131 * @return Promise<JWK> 132 * If no key is available the promise resolves to `null`. 133 * If a key is available for the given scope, th promise resolves to a JWK with fields: 134 * { 135 * scope: The requested scope 136 * kid: Key identifier 137 * k: Derived key material 138 * kty: Always "oct" for scoped keys 139 * } 140 */ 141 async getKeyForScope(scope) { 142 const { scopedKeys } = await this._loadOrFetchKeys(); 143 if (!scopedKeys.hasOwnProperty(scope)) { 144 throw new Error(`Key not available for scope "${scope}"`); 145 } 146 return { 147 scope, 148 ...scopedKeys[scope], 149 }; 150 } 151 152 /** 153 * Validates if the given scoped keys are valid keys 154 * 155 * @param {object} scopedKeys: The scopedKeys bundle 156 * 157 * @return {boolean}: true if the scopedKeys bundle is valid, false otherwise 158 */ 159 validScopedKeys(scopedKeys) { 160 for (const expectedScope of Object.keys(scopedKeys)) { 161 const key = scopedKeys[expectedScope]; 162 if ( 163 !key.hasOwnProperty("scope") || 164 !key.hasOwnProperty("kid") || 165 !key.hasOwnProperty("kty") || 166 !key.hasOwnProperty("k") 167 ) { 168 return false; 169 } 170 const { scope, kid, kty, k } = key; 171 if (scope != expectedScope || kty != "oct") { 172 return false; 173 } 174 // We verify the format of the key id is `timestamp-fingerprint` 175 if (!kid.includes("-")) { 176 return false; 177 } 178 const dashIndex = kid.indexOf("-"); 179 const keyRotationTimestamp = kid.substring(0, dashIndex); 180 const fingerprint = kid.substring(dashIndex + 1); 181 // We then verify that the timestamp is a valid timestamp 182 const keyRotationTimestampNum = Number(keyRotationTimestamp); 183 // If the value we got back is falsy it's not a valid timestamp 184 // note that we treat a 0 timestamp as invalid 185 if (!keyRotationTimestampNum) { 186 return false; 187 } 188 // For extra safety, we validate that the timestamp can be converted into a valid 189 // Date object 190 const date = new Date(keyRotationTimestampNum); 191 if (isNaN(date.getTime()) || date.getTime() <= 0) { 192 return false; 193 } 194 195 // Finally, we validate that the fingerprint and the key itself are valid base64 values 196 // Note that we can't verify the fingerprint is correct here because we don't have kb 197 const validB64String = b64String => { 198 let decoded; 199 try { 200 decoded = ChromeUtils.base64URLDecode(b64String, { 201 padding: "reject", 202 }); 203 } catch (e) { 204 return false; 205 } 206 return !!decoded; 207 }; 208 if (!validB64String(fingerprint) || !validB64String(k)) { 209 return false; 210 } 211 } 212 return true; 213 } 214 215 /** 216 * Format a JWK kid as hex rather than base64. 217 * 218 * This is a backwards-compatibility helper for code that needs a raw key fingerprint 219 * for use as a key identifier, rather than the timestamp+fingerprint format used by 220 * FxA scoped keys. 221 * 222 * @param {object} jwk The JWK from which to extract the `kid` field as hex. 223 */ 224 kidAsHex(jwk) { 225 // The kid format is "{timestamp}-{b64url(fingerprint)}", but we have to be careful 226 // because the fingerprint component may contain "-" as well, and we want to ensure 227 // the timestamp component was non-empty. 228 const idx = jwk.kid.indexOf("-") + 1; 229 if (idx <= 1) { 230 throw new Error(`Invalid kid: ${jwk.kid}`); 231 } 232 return CommonUtils.base64urlToHex(jwk.kid.slice(idx)); 233 } 234 235 /** 236 * Fetch encryption keys for the signed-in-user from the FxA API server. 237 * 238 * Not for user consumption. Exists to cause the keys to be fetched. 239 * 240 * Returns user data so that it can be chained with other methods. 241 * 242 * @return Promise 243 * The promise resolves to the credentials object of the signed-in user: 244 * { 245 * email: The user's email address 246 * uid: The user's unique id 247 * sessionToken: Session for the FxA server 248 * scopedKeys: Object mapping OAuth scopes to corresponding derived keys 249 * verified: email verification status 250 * } 251 * @throws If there is no user signed in. 252 */ 253 async _loadOrFetchKeys() { 254 return this._fxai.withCurrentAccountState(async currentState => { 255 try { 256 let userData = await currentState.getUserAccountData(); 257 if (!userData) { 258 throw new Error("Can't get keys; User is not signed in"); 259 } 260 // If we have all the keys in latest storage location, we're good. 261 if (userData.scopedKeys) { 262 if ( 263 LEGACY_DERIVED_KEY_SCOPES.every(scope => 264 userData.scopedKeys.hasOwnProperty(scope) 265 ) && 266 !DEPRECATED_KEY_SCOPES.some(scope => 267 userData.scopedKeys.hasOwnProperty(scope) 268 ) && 269 !DEPRECATED_DERIVED_KEYS_NAMES.some(keyName => 270 userData.hasOwnProperty(keyName) 271 ) 272 ) { 273 return userData; 274 } 275 } 276 // If not, we've got work to do, and we debounce to avoid duplicating it. 277 if (!currentState.whenKeysReadyDeferred) { 278 currentState.whenKeysReadyDeferred = Promise.withResolvers(); 279 // N.B. we deliberately don't `await` here, and instead use the promise 280 // to resolve `whenKeysReadyDeferred` (which we then `await` below). 281 this._migrateOrFetchKeys(currentState, userData).then( 282 dataWithKeys => { 283 currentState.whenKeysReadyDeferred.resolve(dataWithKeys); 284 currentState.whenKeysReadyDeferred = null; 285 }, 286 err => { 287 currentState.whenKeysReadyDeferred.reject(err); 288 currentState.whenKeysReadyDeferred = null; 289 } 290 ); 291 } 292 return await currentState.whenKeysReadyDeferred.promise; 293 } catch (err) { 294 return this._fxai._handleTokenError(err); 295 } 296 }); 297 } 298 299 /** 300 * Set externally derived scoped keys in internal storage 301 * 302 * @param {object} scopedKeys: The scoped keys object derived by the oauth flow 303 * 304 * @return { Promise }: A promise that resolves if the keys were successfully stored, 305 * or rejects if we failed to persist the keys, or if the user is not signed in already 306 */ 307 async setScopedKeys(scopedKeys) { 308 return this._fxai.withCurrentAccountState(async currentState => { 309 const userData = await currentState.getUserAccountData(); 310 if (!userData) { 311 throw new Error("Cannot persist keys, no user signed in"); 312 } 313 await currentState.updateUserAccountData({ 314 scopedKeys, 315 }); 316 }); 317 } 318 319 /** 320 * Key storage migration or fetching logic. 321 * 322 * This method contains the doing-expensive-operations part of the logic of 323 * _loadOrFetchKeys(), factored out into a separate method so we can debounce it. 324 * 325 */ 326 async _migrateOrFetchKeys(currentState, userData) { 327 // If the required scopes are present in `scopedKeys`, then we know that we've 328 // previously applied all earlier migrations 329 // so we are safe to delete deprecated fields that older migrations 330 // might have depended on. 331 if ( 332 userData.scopedKeys && 333 LEGACY_DERIVED_KEY_SCOPES.every(scope => 334 userData.scopedKeys.hasOwnProperty(scope) 335 ) 336 ) { 337 return this._removeDeprecatedKeys(currentState, userData); 338 } 339 340 // Otherwise, we need to fetch from the network and unwrap. 341 if (!userData.sessionToken) { 342 throw new Error("No sessionToken"); 343 } 344 if (!userData.keyFetchToken) { 345 throw new Error("No keyFetchToken"); 346 } 347 return this._fetchAndUnwrapAndDeriveKeys( 348 currentState, 349 userData.sessionToken, 350 userData.keyFetchToken 351 ); 352 } 353 354 /** 355 * Removes deprecated keys from storage and returns an 356 * updated user data object 357 */ 358 async _removeDeprecatedKeys(currentState, userData) { 359 // Bug 1838708: Delete any deprecated high level keys from storage 360 const keysToRemove = DEPRECATED_DERIVED_KEYS_NAMES.filter(keyName => 361 userData.hasOwnProperty(keyName) 362 ); 363 if (keysToRemove.length) { 364 const removedKeys = {}; 365 for (const keyName of keysToRemove) { 366 removedKeys[keyName] = null; 367 } 368 await currentState.updateUserAccountData({ 369 ...removedKeys, 370 }); 371 userData = await currentState.getUserAccountData(); 372 } 373 // Bug 1697596 - delete any deprecated scoped keys from storage. 374 const scopesToRemove = DEPRECATED_KEY_SCOPES.filter(scope => 375 userData.scopedKeys.hasOwnProperty(scope) 376 ); 377 if (scopesToRemove.length) { 378 const updatedScopedKeys = { 379 ...userData.scopedKeys, 380 }; 381 for (const scope of scopesToRemove) { 382 delete updatedScopedKeys[scope]; 383 } 384 await currentState.updateUserAccountData({ 385 scopedKeys: updatedScopedKeys, 386 }); 387 userData = await currentState.getUserAccountData(); 388 } 389 return userData; 390 } 391 392 /** 393 * Fetch keys from the server, unwrap them, and derive required sub-keys. 394 * 395 * Once the user's email is verified, we can resquest the root key `kB` from the 396 * FxA server, unwrap it using the client-side secret `unwrapBKey`, and then 397 * derive all the sub-keys required for operation of the browser. 398 */ 399 async _fetchAndUnwrapAndDeriveKeys( 400 currentState, 401 sessionToken, 402 keyFetchToken 403 ) { 404 if (logPII()) { 405 log.debug( 406 `fetchAndUnwrapKeys: sessionToken: ${sessionToken}, keyFetchToken: ${keyFetchToken}` 407 ); 408 } 409 410 // Sign out if we don't have the necessary tokens. 411 if (!sessionToken || !keyFetchToken) { 412 // this seems really bad and we should remove this - bug 1572313. 413 log.warn("improper _fetchAndUnwrapKeys() call: token missing"); 414 await this._fxai.signOut(); 415 return null; 416 } 417 418 // Deriving OAuth scoped keys requires additional metadata from the server. 419 // We fetch this first, before fetching the actual key material, because the 420 // keyFetchToken is single-use and we don't want to do a potentially-fallible 421 // operation after consuming it. 422 const scopedKeysMetadata = 423 await this._fetchScopedKeysMetadata(sessionToken); 424 425 // Fetch the wrapped keys. 426 // It would be nice to be able to fetch this in a single operation with fetching 427 // the metadata above, but that requires server-side changes in FxA. 428 let { wrapKB } = await this._fetchKeys(keyFetchToken); 429 430 let data = await currentState.getUserAccountData(); 431 432 // Sanity check that the user hasn't changed out from under us (which should 433 // be impossible given this is called within _withCurrentAccountState, but...) 434 if (data.keyFetchToken !== keyFetchToken) { 435 throw new Error("Signed in user changed while fetching keys!"); 436 } 437 438 let kBbytes = CryptoUtils.xor( 439 CommonUtils.hexToBytes(data.unwrapBKey), 440 wrapKB 441 ); 442 443 if (logPII()) { 444 log.debug("kBbytes: " + kBbytes); 445 } 446 447 let updateData = { 448 ...(await this._deriveKeys(data.uid, kBbytes, scopedKeysMetadata)), 449 keyFetchToken: null, // null values cause the item to be removed. 450 unwrapBKey: null, 451 }; 452 453 if (logPII()) { 454 log.debug(`Keys Obtained: ${updateData.scopedKeys}`); 455 } else { 456 log.debug( 457 "Keys Obtained: " + Object.keys(updateData.scopedKeys).join(", ") 458 ); 459 } 460 461 // Just double-check that scoped keys are there now 462 if (!updateData.scopedKeys) { 463 throw new Error(`user data missing: scopedKeys`); 464 } 465 466 await currentState.updateUserAccountData(updateData); 467 return currentState.getUserAccountData(); 468 } 469 470 /** 471 * Fetch the wrapped root key `wrapKB` from the FxA server. 472 * 473 * This consumes the single-use `keyFetchToken`. 474 */ 475 _fetchKeys(keyFetchToken) { 476 let client = this._fxai.fxAccountsClient; 477 log.debug( 478 `Fetching keys with token ${!!keyFetchToken} from ${client.host}` 479 ); 480 if (logPII()) { 481 log.debug("fetchKeys - the token is " + keyFetchToken); 482 } 483 return client.accountKeys(keyFetchToken); 484 } 485 486 /** 487 * Fetch additional metadata required for deriving scoped keys. 488 * 489 * This includes timestamps and a server-provided secret to mix in to 490 * the derived value in order to support key rotation. 491 */ 492 async _fetchScopedKeysMetadata(sessionToken) { 493 // Hard-coded list of scopes that we know about. 494 // This list will probably grow in future. 495 const scopes = [SCOPE_APP_SYNC].join(" "); 496 const scopedKeysMetadata = 497 await this._fxai.fxAccountsClient.getScopedKeyData( 498 sessionToken, 499 OAUTH_CLIENT_ID, 500 scopes 501 ); 502 // The server may decline us permission for some of those scopes, although it really shouldn't. 503 // We can live without them...except for the sync scope, whose absence would be catastrophic. 504 if (!scopedKeysMetadata.hasOwnProperty(SCOPE_APP_SYNC)) { 505 log.warn( 506 "The FxA server did not grant Firefox the sync scope; this is most unexpected!" + 507 ` scopes were: ${Object.keys(scopedKeysMetadata)}` 508 ); 509 throw new Error("The FxA server did not grant Firefox the sync scope"); 510 } 511 return scopedKeysMetadata; 512 } 513 514 /** 515 * Derive purpose-specific keys from the root FxA key `kB`. 516 * 517 * Everything that uses an encryption key from FxA uses a purpose-specific derived 518 * key. For new uses this is derived in a structured way based on OAuth scopes, 519 * while for legacy uses (mainly Firefox Sync) it is derived in a more ad-hoc fashion. 520 * This method does all the derivations for the uses that we know about. 521 * 522 */ 523 async _deriveKeys(uid, kBbytes, scopedKeysMetadata) { 524 const scopedKeys = await this._deriveScopedKeys( 525 uid, 526 kBbytes, 527 scopedKeysMetadata 528 ); 529 return { 530 scopedKeys, 531 }; 532 } 533 534 /** 535 * Derive various scoped keys from the root FxA key `kB`. 536 * 537 * The `scopedKeysMetadata` object is additional information fetched from the server that 538 * that gets mixed in to the key derivation, with each member of the object corresponding 539 * to an OAuth scope that keys its own scoped key. 540 * 541 * As a special case for backwards-compatibility, sync-related scopes get special 542 * treatment to use a legacy derivation algorithm. 543 * 544 */ 545 async _deriveScopedKeys(uid, kBbytes, scopedKeysMetadata) { 546 const scopedKeys = {}; 547 for (const scope in scopedKeysMetadata) { 548 if (LEGACY_DERIVED_KEY_SCOPES.includes(scope)) { 549 scopedKeys[scope] = await this._deriveLegacyScopedKey( 550 uid, 551 kBbytes, 552 scope, 553 scopedKeysMetadata[scope] 554 ); 555 } else { 556 scopedKeys[scope] = await this._deriveScopedKey( 557 uid, 558 kBbytes, 559 scope, 560 scopedKeysMetadata[scope] 561 ); 562 } 563 } 564 return scopedKeys; 565 } 566 567 /** 568 * Derive a scoped key for an individual OAuth scope. 569 * 570 * The derivation here uses HKDF to combine: 571 * - the root key material kB 572 * - a unique identifier for this scoped key 573 * - a server-provided secret that allows for key rotation 574 * - the account uid as an additional salt 575 * 576 * It produces 32 bytes of (secret) key material along with a (potentially public) 577 * key identifier, formatted as a JWK. 578 * 579 * The full details are in the technical docs at 580 * https://docs.google.com/document/d/1IvQJFEBFz0PnL4uVlIvt8fBS_IPwSK-avK0BRIHucxQ/ 581 */ 582 async _deriveScopedKey(uid, kBbytes, scope, scopedKeyMetadata) { 583 kBbytes = CommonUtils.byteStringToArrayBuffer(kBbytes); 584 585 const FINGERPRINT_LENGTH = 16; 586 const KEY_LENGTH = 32; 587 const VALID_UID = /^[0-9a-f]{32}$/i; 588 const VALID_ROTATION_SECRET = /^[0-9a-f]{64}$/i; 589 590 // Engage paranoia mode for input data. 591 if (!VALID_UID.test(uid)) { 592 throw new Error("uid must be a 32-character hex string"); 593 } 594 if (kBbytes.length != 32) { 595 throw new Error("kBbytes must be exactly 32 bytes"); 596 } 597 if ( 598 typeof scopedKeyMetadata.identifier !== "string" || 599 scopedKeyMetadata.identifier.length < 10 600 ) { 601 throw new Error("identifier must be a string of length >= 10"); 602 } 603 if (typeof scopedKeyMetadata.keyRotationTimestamp !== "number") { 604 throw new Error("keyRotationTimestamp must be a number"); 605 } 606 if (!VALID_ROTATION_SECRET.test(scopedKeyMetadata.keyRotationSecret)) { 607 throw new Error("keyRotationSecret must be a 64-character hex string"); 608 } 609 610 // The server returns milliseconds, we want seconds as a string. 611 const keyRotationTimestamp = 612 "" + Math.round(scopedKeyMetadata.keyRotationTimestamp / 1000); 613 if (keyRotationTimestamp.length < 10) { 614 throw new Error("keyRotationTimestamp must round to a 10-digit number"); 615 } 616 617 const keyRotationSecret = CommonUtils.hexToArrayBuffer( 618 scopedKeyMetadata.keyRotationSecret 619 ); 620 const salt = CommonUtils.hexToArrayBuffer(uid); 621 const context = new TextEncoder().encode( 622 "identity.mozilla.com/picl/v1/scoped_key\n" + scopedKeyMetadata.identifier 623 ); 624 625 const inputKey = new Uint8Array(64); 626 inputKey.set(kBbytes, 0); 627 inputKey.set(keyRotationSecret, 32); 628 629 const derivedKeyMaterial = await CryptoUtils.hkdf( 630 inputKey, 631 salt, 632 context, 633 FINGERPRINT_LENGTH + KEY_LENGTH 634 ); 635 const fingerprint = derivedKeyMaterial.slice(0, FINGERPRINT_LENGTH); 636 const key = derivedKeyMaterial.slice( 637 FINGERPRINT_LENGTH, 638 FINGERPRINT_LENGTH + KEY_LENGTH 639 ); 640 641 return { 642 kid: 643 keyRotationTimestamp + 644 "-" + 645 ChromeUtils.base64URLEncode(fingerprint, { 646 pad: false, 647 }), 648 k: ChromeUtils.base64URLEncode(key, { 649 pad: false, 650 }), 651 kty: "oct", 652 }; 653 } 654 655 /** 656 * Derive the scoped key for the one of our legacy sync-related scopes. 657 * 658 * These uses a different key-derivation algoritm that incorporates less server-provided 659 * data, for backwards-compatibility reasons. 660 * 661 */ 662 async _deriveLegacyScopedKey(uid, kBbytes, scope, scopedKeyMetadata) { 663 let kid, key; 664 if (scope == SCOPE_APP_SYNC) { 665 kid = await this._deriveXClientState(kBbytes); 666 key = await this._deriveSyncKey(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 } 674 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 } 697 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 } 711 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 } 720 721 _sha256(bytes) { 722 let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( 723 Ci.nsICryptoHash 724 ); 725 hasher.init(hasher.SHA256); 726 return CryptoUtils.digestBytes(bytes, hasher); 727 } 728 }