mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 08:15:31 +00:00
Bug 1661407 - refactor FxA key handling to use "scoped keys". r=markh
Differential Revision: https://phabricator.services.mozilla.com/D90361
This commit is contained in:
parent
05303de70a
commit
08d2c540ec
@ -269,6 +269,12 @@ var CommonUtils = {
|
||||
return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
|
||||
},
|
||||
|
||||
base64urlToHex(b64str) {
|
||||
return CommonUtils.bufferToHex(
|
||||
new Uint8Array(ChromeUtils.base64URLDecode(b64str, { padding: "reject" }))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Base32 encode (RFC 4648) a string
|
||||
*/
|
||||
|
@ -176,7 +176,7 @@ AccountState.prototype = {
|
||||
}
|
||||
if (this.whenKeysReadyDeferred) {
|
||||
this.whenKeysReadyDeferred.reject(
|
||||
new Error("Verification aborted; Another user signing in")
|
||||
new Error("Key fetching aborted; Another user signing in")
|
||||
);
|
||||
this.whenKeysReadyDeferred = null;
|
||||
}
|
||||
@ -492,57 +492,15 @@ class FxAccounts {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an OAuth authorization code.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param options.client_id
|
||||
* @param options.state
|
||||
* @param options.scope
|
||||
* @param options.access_type
|
||||
* @param options.code_challenge_method
|
||||
* @param options.code_challenge
|
||||
* @param [options.keys_jwe]
|
||||
* @returns {Promise<Object>} Object containing "code" and "state" properties.
|
||||
*/
|
||||
authorizeOAuthCode(options) {
|
||||
return this._withVerifiedAccountState(async state => {
|
||||
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
|
||||
const params = { ...options };
|
||||
if (params.keys_jwk) {
|
||||
const jwk = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
ChromeUtils.base64URLDecode(params.keys_jwk, { padding: "reject" })
|
||||
)
|
||||
);
|
||||
params.keys_jwe = await this._internal.createKeysJWE(
|
||||
params.client_id,
|
||||
params.scope,
|
||||
jwk
|
||||
);
|
||||
delete params.keys_jwk;
|
||||
}
|
||||
try {
|
||||
return await this._internal.fxAccountsClient.oauthAuthorize(
|
||||
sessionToken,
|
||||
params
|
||||
);
|
||||
} catch (err) {
|
||||
throw this._internal._errorToErrorClass(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an OAuth token for the user.
|
||||
* If you need a corresponding scoped key to go along with this
|
||||
* token, consider using the new 'getAccessToken' method instead.
|
||||
*
|
||||
* @param options
|
||||
* {
|
||||
* scope: (string/array) the oauth scope(s) being requested. As a
|
||||
* convenience, you may pass a string if only one scope is
|
||||
* required, or an array of strings if multiple are needed.
|
||||
* ttl: (number) OAuth token TTL in seconds.
|
||||
* }
|
||||
*
|
||||
* @return Promise.<string | Error>
|
||||
@ -563,48 +521,6 @@ class FxAccounts {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an OAuth token based on the given scope
|
||||
* and its key in a single operation.
|
||||
*
|
||||
* @param scope {String} the requested OAuth scope
|
||||
* @param ttl {Number} OAuth token TTL
|
||||
* @returns {Promise<Object>} Object containing "scope", "token"
|
||||
* and "key" properties.
|
||||
*/
|
||||
async getAccessToken(scope, ttl) {
|
||||
log.debug("getAccessToken enter");
|
||||
const token = await this._internal.getOAuthToken({ scope, ttl });
|
||||
const ACCT_DATA_FIELDS = ["scopedKeys"];
|
||||
|
||||
return this._withCurrentAccountState(async currentState => {
|
||||
const data = await currentState.getUserAccountData(ACCT_DATA_FIELDS);
|
||||
const scopedKeys = data.scopedKeys || {};
|
||||
let key;
|
||||
|
||||
if (!scopedKeys.hasOwnProperty(scope)) {
|
||||
log.debug(`Fetching scopedKeys data for ${scope}`);
|
||||
const newKeyData = await this._internal.keys.getScopedKeys(
|
||||
scope,
|
||||
FX_OAUTH_CLIENT_ID
|
||||
);
|
||||
|
||||
scopedKeys[scope] = newKeyData[scope] || null;
|
||||
await currentState.updateUserAccountData({ scopedKeys });
|
||||
} else {
|
||||
log.debug(`Using cached scopedKeys data for ${scope}`);
|
||||
}
|
||||
|
||||
key = scopedKeys[scope];
|
||||
|
||||
return {
|
||||
scope,
|
||||
token,
|
||||
key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an OAuth token from the token cache. Callers should call this
|
||||
* after they determine a token is invalid, so a new token will be fetched
|
||||
@ -1772,18 +1688,6 @@ FxAccountsInternal.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} clientId
|
||||
* @param {String} scope Space separated requested scopes
|
||||
* @param {Object} jwk
|
||||
*/
|
||||
async createKeysJWE(clientId, scope, jwk) {
|
||||
let scopedKeys = await this.keys.getScopedKeys(scope, clientId);
|
||||
scopedKeys = new TextEncoder().encode(JSON.stringify(scopedKeys));
|
||||
return jwcrypto.generateJWE(jwk, scopedKeys);
|
||||
},
|
||||
|
||||
async _getVerifiedAccountOrReject() {
|
||||
let data = await this.currentAccountState.getUserAccountData();
|
||||
if (!data) {
|
||||
|
@ -4,9 +4,12 @@
|
||||
|
||||
const EXPORTED_SYMBOLS = ["SendTab", "FxAccountsCommands"];
|
||||
|
||||
const { COMMAND_SENDTAB, COMMAND_SENDTAB_TAIL, log } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsCommon.js"
|
||||
);
|
||||
const {
|
||||
COMMAND_SENDTAB,
|
||||
COMMAND_SENDTAB_TAIL,
|
||||
SCOPE_OLD_SYNC,
|
||||
log,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PushCrypto",
|
||||
@ -234,9 +237,9 @@ class FxAccountsCommands {
|
||||
/**
|
||||
* Send Tab is built on top of FxA commands.
|
||||
*
|
||||
* Devices exchange keys wrapped in kSync between themselves (getEncryptedKey)
|
||||
* Devices exchange keys wrapped in the oldsync key between themselves (getEncryptedKey)
|
||||
* during the device registration flow. The FxA server can theorically never
|
||||
* retrieve the send tab keys since it doesn't know kSync.
|
||||
* retrieve the send tab keys since it doesn't know the oldsync key.
|
||||
*/
|
||||
class SendTab {
|
||||
constructor(commands, fxAccountsInternal) {
|
||||
@ -329,7 +332,9 @@ class SendTab {
|
||||
if (!bundle) {
|
||||
throw new Error(`Device ${device.id} does not have send tab keys.`);
|
||||
}
|
||||
const { kSync, kXCS: ourKid } = await this._fxai.keys.getKeys();
|
||||
const oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
// Older clients expect this to be hex, due to pre-JWK sync key ids :-(
|
||||
const ourKid = this._fxai.keys.kidAsHex(oldsyncKey);
|
||||
const { kid: theirKid } = JSON.parse(
|
||||
device.availableCommands[COMMAND_SENDTAB]
|
||||
);
|
||||
@ -339,7 +344,7 @@ class SendTab {
|
||||
const json = JSON.parse(bundle);
|
||||
const wrapper = new CryptoWrapper();
|
||||
wrapper.deserialize({ payload: json });
|
||||
const syncKeyBundle = BulkKeyBundle.fromHexKey(kSync);
|
||||
const syncKeyBundle = BulkKeyBundle.fromJWK(oldsyncKey);
|
||||
let { publicKey, authSecret } = await wrapper.decrypt(syncKeyBundle);
|
||||
authSecret = urlsafeBase64Decode(authSecret);
|
||||
publicKey = urlsafeBase64Decode(publicKey);
|
||||
@ -406,33 +411,29 @@ class SendTab {
|
||||
};
|
||||
// getEncryptedKey() will be called as part of device registration, which
|
||||
// happens immediately after signup/signin, so there's a good chance we
|
||||
// don't yet have kSync et. al. Unverified users will be unable to fetch
|
||||
// don't yet have the sync keys. Unverified users will be unable to fetch
|
||||
// keys, meaning they will end up registering the device twice (once without
|
||||
// sendtab support, then once with sendtab support when they verify), but
|
||||
// that's OK.
|
||||
if (!(await this._fxai.keys.canGetKeys())) {
|
||||
// TODO: this will fail if master password is locked; should we prompt to unlock it here?
|
||||
if (!(await this._fxai.keys.canGetKeyForScope(SCOPE_OLD_SYNC))) {
|
||||
log.info("Can't fetch keys, so unable to determine sendtab keys");
|
||||
return null;
|
||||
}
|
||||
let kSync, kXCS;
|
||||
let oldsyncKey;
|
||||
try {
|
||||
({ kSync, kXCS } = await this._fxai.keys.getKeys());
|
||||
if (!kSync || !kXCS) {
|
||||
log.warn(
|
||||
"Fetched the keys but didn't get any, so unable to determine sendtab keys"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
} catch (ex) {
|
||||
log.warn("Failed to fetch keys, so unable to determine sendtab keys", ex);
|
||||
return null;
|
||||
}
|
||||
const wrapper = new CryptoWrapper();
|
||||
wrapper.cleartext = keyToEncrypt;
|
||||
const keyBundle = BulkKeyBundle.fromHexKey(kSync);
|
||||
const keyBundle = BulkKeyBundle.fromJWK(oldsyncKey);
|
||||
await wrapper.encrypt(keyBundle);
|
||||
return JSON.stringify({
|
||||
kid: kXCS,
|
||||
// Older clients expect this to be hex, due to pre-JWK sync key ids :-(
|
||||
kid: this._fxai.keys.kidAsHex(oldsyncKey),
|
||||
IV: wrapper.IV,
|
||||
hmac: wrapper.hmac,
|
||||
ciphertext: wrapper.ciphertext,
|
||||
|
@ -113,6 +113,10 @@ exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
|
||||
exports.SCOPE_PROFILE = "profile";
|
||||
exports.SCOPE_PROFILE_WRITE = "profile:write";
|
||||
exports.SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync";
|
||||
// This scope and its associated key material are used by the old Kinto webextension
|
||||
// storage backend. We plan to remove that at some point (ref Bug 1637465) and when
|
||||
// we do, all uses of this legacy scope can be removed.
|
||||
exports.LEGACY_SCOPE_WEBEXT_SYNC = "sync:addon_storage";
|
||||
|
||||
// OAuth metadata for other Firefox-related services that we might need to know about
|
||||
// in order to provide an enhanced user experience.
|
||||
@ -265,7 +269,14 @@ exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER";
|
||||
exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405;
|
||||
exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
||||
|
||||
exports.DERIVED_KEYS_NAMES = ["kSync", "kXCS", "kExtSync", "kExtKbHash"];
|
||||
// When FxA support first landed in Firefox, it was only used for sync and
|
||||
// we stored the relevant encryption keys as top-level fields in the account state.
|
||||
// We've since grown a more elaborate scheme of derived keys linked to specific
|
||||
// OAuth scopes, which are stored in a map in the `scopedKeys` field.
|
||||
// These are the names of pre-scoped-keys key material, maintained for b/w
|
||||
// compatibility to code elsewhere in Firefox; once all consuming code is updated
|
||||
// to use scoped keys, these fields can be removed from the account userData.
|
||||
exports.LEGACY_DERIVED_KEYS_NAMES = ["kSync", "kXCS", "kExtSync", "kExtKbHash"];
|
||||
|
||||
// FxAccounts has the ability to "split" the credentials between a plain-text
|
||||
// JSON file in the profile dir and in the login manager.
|
||||
@ -289,7 +300,7 @@ exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set([
|
||||
|
||||
// Fields we store in secure storage if it exists.
|
||||
exports.FXA_PWDMGR_SECURE_FIELDS = new Set([
|
||||
...exports.DERIVED_KEYS_NAMES,
|
||||
...exports.LEGACY_DERIVED_KEYS_NAMES,
|
||||
"keyFetchToken",
|
||||
"unwrapBKey",
|
||||
"assertion",
|
||||
|
@ -384,6 +384,15 @@ class FxAccountsDevice {
|
||||
// you'll have to bump the DEVICE_REGISTRATION_VERSION number to force older
|
||||
// devices to re-register when Firefox updates.
|
||||
async _registerOrUpdateDevice(currentState, signedInUser) {
|
||||
// This method has the side-effect of setting some account-related prefs
|
||||
// (e.g. for caching the device name) so it's important we don't execute it
|
||||
// if the signed-in state has changed.
|
||||
if (!currentState.isCurrent) {
|
||||
throw new Error(
|
||||
"_registerOrUpdateDevice called after a different user has signed in"
|
||||
);
|
||||
}
|
||||
|
||||
const { sessionToken, device: currentDevice } = signedInUser;
|
||||
if (!sessionToken) {
|
||||
throw new Error("_registerOrUpdateDevice called without a session token");
|
||||
@ -550,7 +559,7 @@ class FxAccountsDevice {
|
||||
// registration will be retried.
|
||||
log.error("device registration failed", error);
|
||||
try {
|
||||
currentState.updateUserAccountData({
|
||||
await currentState.updateUserAccountData({
|
||||
device: null,
|
||||
});
|
||||
} catch (secondError) {
|
||||
|
@ -14,21 +14,57 @@ const { CryptoUtils } = ChromeUtils.import(
|
||||
"resource://services-crypto/utils.js"
|
||||
);
|
||||
|
||||
const { DERIVED_KEYS_NAMES, SCOPE_OLD_SYNC, log, logPII } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsCommon.js"
|
||||
);
|
||||
const {
|
||||
LEGACY_DERIVED_KEYS_NAMES,
|
||||
SCOPE_OLD_SYNC,
|
||||
LEGACY_SCOPE_WEBEXT_SYNC,
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
log,
|
||||
logPII,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
|
||||
// These are the scopes that correspond to new storage for the `LEGACY_DERIVED_KEYS_NAMES`.
|
||||
// We will, if necessary, migrate storage for those keys so that it's associated with
|
||||
// these scopes.
|
||||
const LEGACY_DERIVED_KEY_SCOPES = [SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC];
|
||||
|
||||
/**
|
||||
* Utilities for working with key material linked to the user's account.
|
||||
*
|
||||
* Each Firefox Account has 32 bytes of root key material called `kB` which is
|
||||
* linked to the user's password, and which is used to derive purpose-specific
|
||||
* subkeys for things like encrypting the user's sync data. This class provides
|
||||
* the interface for working with such key material.
|
||||
*
|
||||
* Most recent FxA clients obtain appropriate key material directly as part of
|
||||
* their sign-in flow, using a special extension of the OAuth2.0 protocol to
|
||||
* securely deliver the derived keys without revealing `kB`. Keys obtained in
|
||||
* in this way are called "scoped keys" since each corresponds to a particular
|
||||
* OAuth scope, and this class provides a `getKeyForScope` method that is the
|
||||
* preferred method for consumers to work with such keys.
|
||||
*
|
||||
* However, since the FxA integration in Firefox Desktop pre-dates the use of
|
||||
* OAuth2.0, we also have a lot of code for fetching keys via an older flow.
|
||||
* This flow uses a special `keyFetchToken` to obtain `kB` and then derive various
|
||||
* sub-keys from it. Consumers should consider this an internal implementation
|
||||
* detail of the `FxAccountsKeys` class and should prefer `getKeyForScope` where
|
||||
* possible. We intend to remove support for Firefox ever directly handling `kB`
|
||||
* at some point in the future.
|
||||
*/
|
||||
class FxAccountsKeys {
|
||||
constructor(fxAccountsInternal) {
|
||||
this._fxia = fxAccountsInternal;
|
||||
this._fxai = fxAccountsInternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we currently have encryption keys or if we have enough to
|
||||
* be able to successfully fetch them for the signed-in-user.
|
||||
* Checks if we currently have the key for a given scope, or if we have enough to
|
||||
* be able to successfully fetch and unwrap it for the signed-in-user.
|
||||
*
|
||||
* Unlike `getKeyForScope`, this will not hit the network to fetch wrapped keys if
|
||||
* they aren't available locally.
|
||||
*/
|
||||
canGetKeys() {
|
||||
return this._fxia.withCurrentAccountState(async currentState => {
|
||||
canGetKeyForScope(scope) {
|
||||
return this._fxai.withCurrentAccountState(async currentState => {
|
||||
let userData = await currentState.getUserAccountData();
|
||||
if (!userData) {
|
||||
throw new Error("Can't possibly get keys; User is not signed in");
|
||||
@ -37,22 +73,102 @@ class FxAccountsKeys {
|
||||
log.info("Can't get keys; user is not verified");
|
||||
return false;
|
||||
}
|
||||
// - keyFetchToken means we can almost certainly grab them.
|
||||
// - kSync, kXCS, kExtSync and kExtKbHash means we already have them.
|
||||
// - kB is deprecated but |getKeys| will help us migrate to kSync and friends.
|
||||
return (
|
||||
userData &&
|
||||
(userData.keyFetchToken ||
|
||||
DERIVED_KEYS_NAMES.every(k => userData[k]) ||
|
||||
userData.kB)
|
||||
);
|
||||
|
||||
if (userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For sync-related scopes, we might have stored the keys in a legacy format.
|
||||
if (scope == SCOPE_OLD_SYNC) {
|
||||
if (userData.kSync && userData.kXCS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
|
||||
if (userData.kExtSync && userData.kExtKbHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// `kB` is deprecated, but if we have it, we can use it to derive any scoped key.
|
||||
if (userData.kB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have a `keyFetchToken` we can fetch `kB`.
|
||||
if (userData.keyFetchToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
log.info("Can't get keys; no key material or tokens available");
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for a specified OAuth scope.
|
||||
*
|
||||
* @param {String} scope The OAuth scope whose key should be returned
|
||||
*
|
||||
* @return Promise<JWK>
|
||||
* If no key is available the promise resolves to `null`.
|
||||
* If a key is available for the given scope, th promise resolves to a JWK with fields:
|
||||
* {
|
||||
* scope: The requested scope
|
||||
* kid: Key identifier
|
||||
* k: Derived key material
|
||||
* kty: Always "oct" for scoped keys
|
||||
* }
|
||||
*
|
||||
*/
|
||||
async getKeyForScope(scope) {
|
||||
const { scopedKeys } = await this._loadOrFetchKeys();
|
||||
if (!scopedKeys.hasOwnProperty(scope)) {
|
||||
throw new Error(`Key not available for scope "${scope}"`);
|
||||
}
|
||||
return {
|
||||
scope,
|
||||
...scopedKeys[scope],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a JWK key material as hex rather than base64.
|
||||
*
|
||||
* This is a backwards-compatibility helper for code that needs raw key bytes rather
|
||||
* than the JWK format offered by FxA scopes keys.
|
||||
*
|
||||
* @param {Object} jwk The JWK from which to extract the `k` field as hex.
|
||||
*
|
||||
*/
|
||||
keyAsHex(jwk) {
|
||||
return CommonUtils.base64urlToHex(jwk.k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a JWK kid as hex rather than base64.
|
||||
*
|
||||
* This is a backwards-compatibility helper for code that needs a raw key fingerprint
|
||||
* for use as a key identifier, rather than the timestamp+fingerprint format used by
|
||||
* FxA scoped keys.
|
||||
*
|
||||
* @param {Object} jwk The JWK from which to extract the `kid` field as hex.
|
||||
*/
|
||||
kidAsHex(jwk) {
|
||||
// The kid format is "{timestamp}-{b64url(fingerprint)}", but we have to be careful
|
||||
// because the fingerprint component may contain "-" as well, and we want to ensure
|
||||
// the timestamp component was non-empty.
|
||||
const idx = jwk.kid.indexOf("-") + 1;
|
||||
if (idx <= 1) {
|
||||
throw new Error(`Invalid kid: ${jwk.kid}`);
|
||||
}
|
||||
return CommonUtils.base64urlToHex(jwk.kid.slice(idx));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch encryption keys for the signed-in-user from the FxA API server.
|
||||
*
|
||||
* Not for user consumption. Exists to cause the keys to be fetch.
|
||||
* Not for user consumption. Exists to cause the keys to be fetched.
|
||||
*
|
||||
* Returns user data so that it can be chained with other methods.
|
||||
*
|
||||
@ -62,72 +178,202 @@ class FxAccountsKeys {
|
||||
* email: The user's email address
|
||||
* uid: The user's unique id
|
||||
* sessionToken: Session for the FxA server
|
||||
* scopedKeys: Object mapping OAuth scopes to corresponding derived keys
|
||||
* kSync: An encryption key for Sync
|
||||
* kXCS: A key hash of kB for the X-Client-State header
|
||||
* kExtSync: An encryption key for WebExtensions syncing
|
||||
* kExtKbHash: A key hash of kB for WebExtensions syncing
|
||||
* ecosystemUserId: A derived key used for Account EcosystemTelemetry
|
||||
* verified: email verification status
|
||||
* }
|
||||
* or null if no user is signed in
|
||||
* @throws If there is no user signed in.
|
||||
*/
|
||||
async getKeys() {
|
||||
return this._fxia.withCurrentAccountState(async currentState => {
|
||||
async _loadOrFetchKeys() {
|
||||
return this._fxai.withCurrentAccountState(async currentState => {
|
||||
try {
|
||||
let userData = await currentState.getUserAccountData();
|
||||
if (!userData) {
|
||||
throw new Error("Can't get keys; User is not signed in");
|
||||
}
|
||||
if (userData.kB) {
|
||||
// Bug 1426306 - Migrate from kB to derived keys.
|
||||
log.info("Migrating kB to derived keys.");
|
||||
const { uid, kB } = userData;
|
||||
await currentState.updateUserAccountData({
|
||||
uid,
|
||||
...(await this._deriveKeys(uid, CommonUtils.hexToBytes(kB))),
|
||||
kA: null, // Remove kA and kB from storage.
|
||||
kB: null,
|
||||
});
|
||||
userData = await currentState.getUserAccountData();
|
||||
}
|
||||
if (DERIVED_KEYS_NAMES.every(k => !!userData[k])) {
|
||||
return userData;
|
||||
// If we have all the keys in latest storage location, we're good.
|
||||
if (userData.scopedKeys) {
|
||||
if (
|
||||
LEGACY_DERIVED_KEY_SCOPES.every(scope =>
|
||||
userData.scopedKeys.hasOwnProperty(scope)
|
||||
)
|
||||
) {
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
// If not, we've got work to do, and we debounce to avoid duplicating it.
|
||||
if (!currentState.whenKeysReadyDeferred) {
|
||||
currentState.whenKeysReadyDeferred = PromiseUtils.defer();
|
||||
if (userData.keyFetchToken) {
|
||||
this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
|
||||
dataWithKeys => {
|
||||
if (DERIVED_KEYS_NAMES.some(k => !dataWithKeys[k])) {
|
||||
const missing = DERIVED_KEYS_NAMES.filter(
|
||||
k => !dataWithKeys[k]
|
||||
);
|
||||
currentState.whenKeysReadyDeferred.reject(
|
||||
new Error(`user data missing: ${missing.join(", ")}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
|
||||
},
|
||||
err => {
|
||||
currentState.whenKeysReadyDeferred.reject(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
currentState.whenKeysReadyDeferred.reject("No keyFetchToken");
|
||||
}
|
||||
// N.B. we deliberately don't `await` here, and instead use the promise
|
||||
// to resolve `whenKeysReadyDeferred` (which we then `await` below).
|
||||
this._migrateOrFetchKeys(currentState, userData).then(
|
||||
dataWithKeys => {
|
||||
currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
|
||||
currentState.whenKeysReadyDeferred = null;
|
||||
},
|
||||
err => {
|
||||
currentState.whenKeysReadyDeferred.reject(err);
|
||||
currentState.whenKeysReadyDeferred = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
return await currentState.whenKeysReadyDeferred.promise;
|
||||
} catch (err) {
|
||||
return this._fxia._handleTokenError(err);
|
||||
return this._fxai._handleTokenError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the user's email is verified, we can request the keys
|
||||
* Key storage migration or fetching logic.
|
||||
*
|
||||
* This method contains the doing-expensive-operations part of the logic of
|
||||
* _loadOrFetchKeys(), factored out into a separate method so we can debounce it.
|
||||
*
|
||||
*/
|
||||
fetchKeys(keyFetchToken) {
|
||||
let client = this._fxia.fxAccountsClient;
|
||||
async _migrateOrFetchKeys(currentState, userData) {
|
||||
// Bug 1661407 - migrate from legacy storage of keys as top-level account
|
||||
// data fields, to storing them as scoped keys in the `scopedKeys` object.
|
||||
if (
|
||||
LEGACY_DERIVED_KEYS_NAMES.every(name => userData.hasOwnProperty(name))
|
||||
) {
|
||||
log.info("Migrating from legacy key fields to scopedKeys.");
|
||||
const scopedKeys = userData.scopedKeys || {};
|
||||
await currentState.updateUserAccountData({
|
||||
scopedKeys: {
|
||||
...scopedKeys,
|
||||
...(await this._deriveScopedKeysFromAccountData(userData)),
|
||||
},
|
||||
});
|
||||
userData = await currentState.getUserAccountData();
|
||||
return userData;
|
||||
}
|
||||
// Bug 1426306 - Migrate from kB to derived keys.
|
||||
if (userData.kB) {
|
||||
log.info("Migrating kB to derived keys.");
|
||||
const { uid, kB, sessionToken } = userData;
|
||||
const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
|
||||
sessionToken
|
||||
);
|
||||
await currentState.updateUserAccountData({
|
||||
uid,
|
||||
...(await this._deriveKeys(
|
||||
uid,
|
||||
CommonUtils.hexToBytes(kB),
|
||||
scopedKeysMetadata
|
||||
)),
|
||||
kA: null, // Remove kA and kB from storage.
|
||||
kB: null,
|
||||
});
|
||||
userData = await currentState.getUserAccountData();
|
||||
return userData;
|
||||
}
|
||||
// Otherwise, we need to fetch from the network and unwrap.
|
||||
if (!userData.sessionToken) {
|
||||
throw new Error("No sessionToken");
|
||||
}
|
||||
if (!userData.keyFetchToken) {
|
||||
throw new Error("No keyFetchToken");
|
||||
}
|
||||
return this._fetchAndUnwrapAndDeriveKeys(
|
||||
currentState,
|
||||
userData.sessionToken,
|
||||
userData.keyFetchToken
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch keys from the server, unwrap them, and derive required sub-keys.
|
||||
*
|
||||
* Once the user's email is verified, we can request the root key `kB` from the
|
||||
* FxA server, unwrap it using the client-side secret `unwrapBKey`, and then
|
||||
* derive all the sub-keys required for operation of the browser.
|
||||
*/
|
||||
async _fetchAndUnwrapAndDeriveKeys(
|
||||
currentState,
|
||||
sessionToken,
|
||||
keyFetchToken
|
||||
) {
|
||||
if (logPII) {
|
||||
log.debug(
|
||||
`fetchAndUnwrapKeys: sessionToken: ${sessionToken}, keyFetchToken: ${keyFetchToken}`
|
||||
);
|
||||
}
|
||||
|
||||
// Sign out if we don't have the necessary tokens.
|
||||
if (!sessionToken || !keyFetchToken) {
|
||||
// this seems really bad and we should remove this - bug 1572313.
|
||||
log.warn("improper _fetchAndUnwrapKeys() call: token missing");
|
||||
await this._fxai.signOut();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Deriving OAuth scoped keys requires additional metadata from the server.
|
||||
// We fetch this first, before fetching the actual key material, because the
|
||||
// keyFetchToken is single-use and we don't want to do a potentially-fallible
|
||||
// operation after consuming it.
|
||||
const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
|
||||
sessionToken
|
||||
);
|
||||
|
||||
// Fetch the wrapped keys.
|
||||
// It would be nice to be able to fetch this in a single operation with fetching
|
||||
// the metadata above, but that requires server-side changes in FxA.
|
||||
let { wrapKB } = await this._fetchKeys(keyFetchToken);
|
||||
|
||||
let data = await currentState.getUserAccountData();
|
||||
|
||||
// Sanity check that the user hasn't changed out from under us (which should
|
||||
// be impossible given this is called within _withCurrentAccountState, but...)
|
||||
if (data.keyFetchToken !== keyFetchToken) {
|
||||
throw new Error("Signed in user changed while fetching keys!");
|
||||
}
|
||||
|
||||
let kBbytes = CryptoUtils.xor(
|
||||
CommonUtils.hexToBytes(data.unwrapBKey),
|
||||
wrapKB
|
||||
);
|
||||
|
||||
if (logPII) {
|
||||
log.debug("kBbytes: " + kBbytes);
|
||||
}
|
||||
|
||||
let updateData = {
|
||||
...(await this._deriveKeys(data.uid, kBbytes, scopedKeysMetadata)),
|
||||
keyFetchToken: null, // null values cause the item to be removed.
|
||||
unwrapBKey: null,
|
||||
};
|
||||
|
||||
if (logPII) {
|
||||
log.debug(`Keys Obtained: ${updateData.scopedKeys}`);
|
||||
} else {
|
||||
log.debug(
|
||||
"Keys Obtained: " + Object.keys(updateData.scopedKeys).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
// Just double-check that we derived all the right stuff.
|
||||
const EXPECTED_FIELDS = LEGACY_DERIVED_KEYS_NAMES.concat(["scopedKeys"]);
|
||||
if (EXPECTED_FIELDS.some(k => !updateData[k])) {
|
||||
const missing = EXPECTED_FIELDS.filter(k => !updateData[k]);
|
||||
throw new Error(`user data missing: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
await currentState.updateUserAccountData(updateData);
|
||||
return currentState.getUserAccountData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the wrapped root key `wrapKB` from the FxA server.
|
||||
*
|
||||
* This consumes the single-use `keyFetchToken`.
|
||||
*/
|
||||
_fetchKeys(keyFetchToken) {
|
||||
let client = this._fxai.fxAccountsClient;
|
||||
log.debug(
|
||||
`Fetching keys with token ${!!keyFetchToken} from ${client.host}`
|
||||
);
|
||||
@ -137,114 +383,204 @@ class FxAccountsKeys {
|
||||
return client.accountKeys(keyFetchToken);
|
||||
}
|
||||
|
||||
fetchAndUnwrapKeys(keyFetchToken) {
|
||||
return this._fxia.withCurrentAccountState(async currentState => {
|
||||
if (logPII) {
|
||||
log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
|
||||
}
|
||||
// Sign out if we don't have a key fetch token.
|
||||
if (!keyFetchToken) {
|
||||
// this seems really bad and we should remove this - bug 1572313.
|
||||
log.warn("improper fetchAndUnwrapKeys() call: token missing");
|
||||
await this._fxia.signOut();
|
||||
return null;
|
||||
}
|
||||
|
||||
let { wrapKB } = await this.fetchKeys(keyFetchToken);
|
||||
|
||||
let data = await currentState.getUserAccountData();
|
||||
|
||||
// Sanity check that the user hasn't changed out from under us (which
|
||||
// should be impossible given our _withCurrentAccountState, but...)
|
||||
if (data.keyFetchToken !== keyFetchToken) {
|
||||
throw new Error("Signed in user changed while fetching keys!");
|
||||
}
|
||||
|
||||
let kBbytes = CryptoUtils.xor(
|
||||
CommonUtils.hexToBytes(data.unwrapBKey),
|
||||
wrapKB
|
||||
/**
|
||||
* Fetch additional metadata required for deriving scoped keys.
|
||||
*
|
||||
* This includes timestamps and a server-provided secret to mix in to
|
||||
* the derived value in order to support key rotation.
|
||||
*/
|
||||
async _fetchScopedKeysMetadata(sessionToken) {
|
||||
// Hard-coded list of scopes that we know about.
|
||||
// This list will probably grow in future.
|
||||
// Note that SCOPE_OLD_SYNC_WEBEXT is not in this list, it get special-case handling below.
|
||||
const scopes = [SCOPE_OLD_SYNC].join(" ");
|
||||
const scopedKeysMetadata = await this._fxai.fxAccountsClient.getScopedKeyData(
|
||||
sessionToken,
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
scopes
|
||||
);
|
||||
if (!scopedKeysMetadata.hasOwnProperty(SCOPE_OLD_SYNC)) {
|
||||
log.warn(
|
||||
"The FxA server did not grant Firefox the `oldsync` scope; this is most unexpected!" +
|
||||
` scopes were: ${Object.keys(scopedKeysMetadata)}`
|
||||
);
|
||||
|
||||
if (logPII) {
|
||||
log.debug("kBbytes: " + kBbytes);
|
||||
}
|
||||
let updateData = {
|
||||
...(await this._deriveKeys(data.uid, kBbytes)),
|
||||
keyFetchToken: null, // null values cause the item to be removed.
|
||||
unwrapBKey: null,
|
||||
};
|
||||
|
||||
log.debug(
|
||||
"Keys Obtained:" +
|
||||
DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", ")
|
||||
throw new Error(
|
||||
"The FxA server did not grant Firefox the `oldsync` scope"
|
||||
);
|
||||
if (logPII) {
|
||||
log.debug(
|
||||
"Keys Obtained:" +
|
||||
DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
await currentState.updateUserAccountData(updateData);
|
||||
data = await currentState.getUserAccountData();
|
||||
return data;
|
||||
});
|
||||
}
|
||||
// Firefox Desktop invented its own special scope for legacy webextension syncing,
|
||||
// with its own special key. Rather than teach the rest of FxA about this scope
|
||||
// that will never be used anywhere else, just give it the same metadata as
|
||||
// the main sync scope. This can go away once legacy webext sync is removed.
|
||||
// (ref Bug 1637465 for tracking that removal)
|
||||
scopedKeysMetadata[LEGACY_SCOPE_WEBEXT_SYNC] = {
|
||||
...scopedKeysMetadata[SCOPE_OLD_SYNC],
|
||||
identifier: LEGACY_SCOPE_WEBEXT_SYNC,
|
||||
};
|
||||
return scopedKeysMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} scope Single key bearing scope
|
||||
* Derive purpose-specific keys from the root FxA key `kB`.
|
||||
*
|
||||
* Everything that uses an encryption key from FxA uses a purpose-specific derived
|
||||
* key. For new uses this is derived in a structured way based on OAuth scopes,
|
||||
* while for legacy uses (mainly Firefox Sync) it is derived in a more ad-hoc fashion.
|
||||
* This method does all the derivations for the uses that we know about.
|
||||
*
|
||||
*/
|
||||
async getKeyForScope(scope, { keyRotationTimestamp }) {
|
||||
if (scope !== SCOPE_OLD_SYNC) {
|
||||
throw new Error(`Unavailable key material for ${scope}`);
|
||||
}
|
||||
let { kSync, kXCS } = await this.getKeys();
|
||||
if (!kSync || !kXCS) {
|
||||
throw new Error("Could not find requested key.");
|
||||
}
|
||||
kXCS = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kXCS), {
|
||||
pad: false,
|
||||
});
|
||||
kSync = ChromeUtils.base64URLEncode(CommonUtils.hexToArrayBuffer(kSync), {
|
||||
pad: false,
|
||||
});
|
||||
const kid = `${keyRotationTimestamp}-${kXCS}`;
|
||||
async _deriveKeys(uid, kBbytes, scopedKeysMetadata) {
|
||||
const scopedKeys = await this._deriveScopedKeys(
|
||||
uid,
|
||||
kBbytes,
|
||||
scopedKeysMetadata
|
||||
);
|
||||
return {
|
||||
scope,
|
||||
kid,
|
||||
k: kSync,
|
||||
kty: "oct",
|
||||
scopedKeys,
|
||||
// Existing browser code might expect sync keys to be available as top-level account data.
|
||||
// For b/w compat we can derive these even if they're not in our list of scoped keys for
|
||||
// some reason (since the derivation doesn't depend on server-provided data).
|
||||
kSync: scopedKeys[SCOPE_OLD_SYNC]
|
||||
? this.keyAsHex(scopedKeys[SCOPE_OLD_SYNC])
|
||||
: CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)),
|
||||
kXCS: scopedKeys[SCOPE_OLD_SYNC]
|
||||
? this.kidAsHex(scopedKeys[SCOPE_OLD_SYNC])
|
||||
: CommonUtils.bytesAsHex(await this._deriveXClientState(kBbytes)),
|
||||
kExtSync: scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC]
|
||||
? this.keyAsHex(scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC])
|
||||
: CommonUtils.bytesAsHex(await this._deriveWebExtSyncStoreKey(kBbytes)),
|
||||
kExtKbHash: scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC]
|
||||
? this.kidAsHex(scopedKeys[LEGACY_SCOPE_WEBEXT_SYNC])
|
||||
: CommonUtils.bytesAsHex(await this._deriveWebExtKbHash(uid, kBbytes)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} scopes Space separated requested scopes
|
||||
* @param {String} clientId oauth client id
|
||||
* Derive various scoped keys from the root FxA key `kB`.
|
||||
*
|
||||
* The `scopedKeysMetadata` object is additional information fetched from the server that
|
||||
* that gets mixed in to the key derivation, with each member of the object corresponding
|
||||
* to an OAuth scope that keys its own scoped key.
|
||||
*
|
||||
* As a special case for backwards-compatibility, sync-related scopes get special
|
||||
* treatment to use a legacy derivation algorithm.
|
||||
*
|
||||
*/
|
||||
async getScopedKeys(scopes, clientId) {
|
||||
const { sessionToken } = await this._fxia._getVerifiedAccountOrReject();
|
||||
const keyData = await this._fxia.fxAccountsClient.getScopedKeyData(
|
||||
sessionToken,
|
||||
clientId,
|
||||
scopes
|
||||
);
|
||||
async _deriveScopedKeys(uid, kBbytes, scopedKeysMetadata) {
|
||||
const scopedKeys = {};
|
||||
for (const [scope, data] of Object.entries(keyData)) {
|
||||
scopedKeys[scope] = await this.getKeyForScope(scope, data);
|
||||
for (const scope in scopedKeysMetadata) {
|
||||
if (LEGACY_DERIVED_KEY_SCOPES.includes(scope)) {
|
||||
scopedKeys[scope] = await this._deriveLegacyScopedKey(
|
||||
uid,
|
||||
kBbytes,
|
||||
scope,
|
||||
scopedKeysMetadata[scope]
|
||||
);
|
||||
} else {
|
||||
scopedKeys[scope] = await this._deriveScopedKey(
|
||||
uid,
|
||||
kBbytes,
|
||||
scope,
|
||||
scopedKeysMetadata[scope]
|
||||
);
|
||||
}
|
||||
}
|
||||
return scopedKeys;
|
||||
}
|
||||
|
||||
async _deriveKeys(uid, kBbytes) {
|
||||
/**
|
||||
* Derive the `scopedKeys` data field based on current account data.
|
||||
*
|
||||
* This is a backwards-compatibility convenience for users who are already signed in to Firefox
|
||||
* and have legacy fields like `kSync` and `kXCS` in their top-level account data, but do not have
|
||||
* the newer `scopedKeys` field. We populate it with the scoped keys for sync and webext-sync.
|
||||
*
|
||||
*/
|
||||
async _deriveScopedKeysFromAccountData(userData) {
|
||||
const scopedKeysMetadata = await this._fetchScopedKeysMetadata(
|
||||
userData.sessionToken
|
||||
);
|
||||
const scopedKeys = userData.scopedKeys || {};
|
||||
for (const scope of LEGACY_DERIVED_KEY_SCOPES) {
|
||||
if (scopedKeysMetadata.hasOwnProperty(scope)) {
|
||||
let kid, key;
|
||||
if (scope == SCOPE_OLD_SYNC) {
|
||||
({ kXCS: kid, kSync: key } = userData);
|
||||
} else if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
|
||||
({ kExtKbHash: kid, kExtSync: key } = userData);
|
||||
} else {
|
||||
// Should never happen, but a nice internal consistency check.
|
||||
throw new Error(`Unexpected legacy key-bearing scope: ${scope}`);
|
||||
}
|
||||
if (!kid || !key) {
|
||||
throw new Error(
|
||||
`Account is missing legacy key fields for scope: ${scope}`
|
||||
);
|
||||
}
|
||||
scopedKeys[scope] = await this._formatLegacyScopedKey(
|
||||
CommonUtils.hexToArrayBuffer(kid),
|
||||
CommonUtils.hexToArrayBuffer(key),
|
||||
scope,
|
||||
scopedKeysMetadata[scope]
|
||||
);
|
||||
}
|
||||
}
|
||||
return scopedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a scoped key for an individual OAuth scope.
|
||||
*
|
||||
*/
|
||||
async _deriveScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
|
||||
// N.B. when we come to implement this, remember that `kBbytes` will be a string
|
||||
// with bytes in it, not an `ArrayBuffer`.
|
||||
throw new Error("Only legacy scoped keys are currently implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the scoped key for the one of our legacy sync-related scopes.
|
||||
*
|
||||
* These uses a different key-derivation algoritm that incorporates less server-provided
|
||||
* data, for backwards-compatibility reasons.
|
||||
*
|
||||
*/
|
||||
async _deriveLegacyScopedKey(uid, kBbytes, scope, scopedKeyMetadata) {
|
||||
let kid, key;
|
||||
if (scope == SCOPE_OLD_SYNC) {
|
||||
kid = await this._deriveXClientState(kBbytes);
|
||||
key = await this._deriveSyncKey(kBbytes);
|
||||
} else if (scope == LEGACY_SCOPE_WEBEXT_SYNC) {
|
||||
kid = await this._deriveWebExtKbHash(uid, kBbytes);
|
||||
key = await this._deriveWebExtSyncStoreKey(kBbytes);
|
||||
} else {
|
||||
throw new Error(`Unexpected legacy key-bearing scope: ${scope}`);
|
||||
}
|
||||
kid = CommonUtils.byteStringToArrayBuffer(kid);
|
||||
key = CommonUtils.byteStringToArrayBuffer(key);
|
||||
return this._formatLegacyScopedKey(kid, key, scope, scopedKeyMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format key material for a legacy scyne-related scope as a JWK.
|
||||
*
|
||||
* @param {ArrayBuffer} kid bytes of the key hash to use in the key identifier
|
||||
* @param {ArrayBuffer} key bytes of the derived sync key
|
||||
* @param {String} scope the scope with which this key is associated
|
||||
* @param {Number} keyRotationTimestamp server-provided timestamp of last key rotation
|
||||
* @returns {Object} key material formatted as a JWK object
|
||||
*/
|
||||
_formatLegacyScopedKey(kid, key, scope, { keyRotationTimestamp }) {
|
||||
kid = ChromeUtils.base64URLEncode(kid, {
|
||||
pad: false,
|
||||
});
|
||||
key = ChromeUtils.base64URLEncode(key, {
|
||||
pad: false,
|
||||
});
|
||||
return {
|
||||
kSync: CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)),
|
||||
kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
|
||||
kExtSync: CommonUtils.bytesAsHex(
|
||||
await this._deriveWebExtSyncStoreKey(kBbytes)
|
||||
),
|
||||
kExtKbHash: CommonUtils.bytesAsHex(
|
||||
this._deriveWebExtKbHash(uid, kBbytes)
|
||||
),
|
||||
kid: `${keyRotationTimestamp}-${kid}`,
|
||||
k: key,
|
||||
kty: "oct",
|
||||
};
|
||||
}
|
||||
|
||||
@ -253,7 +589,7 @@ class FxAccountsKeys {
|
||||
*
|
||||
* @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)>
|
||||
*/
|
||||
_deriveSyncKey(kBbytes) {
|
||||
async _deriveSyncKey(kBbytes) {
|
||||
return CryptoUtils.hkdfLegacy(
|
||||
kBbytes,
|
||||
undefined,
|
||||
@ -262,12 +598,21 @@ class FxAccountsKeys {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the X-Client-State header given the byte string kB.
|
||||
*
|
||||
* @returns Promise<SHA256(kB)[:16]>
|
||||
*/
|
||||
async _deriveXClientState(kBbytes) {
|
||||
return this._sha256(kBbytes).slice(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the WebExtensions Sync Storage Key given the byte string kB.
|
||||
*
|
||||
* @returns Promise<HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)>
|
||||
*/
|
||||
_deriveWebExtSyncStoreKey(kBbytes) {
|
||||
async _deriveWebExtSyncStoreKey(kBbytes) {
|
||||
return CryptoUtils.hkdfLegacy(
|
||||
kBbytes,
|
||||
undefined,
|
||||
@ -279,21 +624,12 @@ class FxAccountsKeys {
|
||||
/**
|
||||
* Derive the WebExtensions kbHash given the byte string kB.
|
||||
*
|
||||
* @returns SHA256(uid + kB)
|
||||
* @returns Promise<SHA256(uid + kB)>
|
||||
*/
|
||||
_deriveWebExtKbHash(uid, kBbytes) {
|
||||
async _deriveWebExtKbHash(uid, kBbytes) {
|
||||
return this._sha256(uid + kBbytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the X-Client-State header given the byte string kB.
|
||||
*
|
||||
* @returns SHA256(kB)[:16]
|
||||
*/
|
||||
_deriveXClientState(kBbytes) {
|
||||
return this._sha256(kBbytes).slice(0, 16);
|
||||
}
|
||||
|
||||
_sha256(bytes) {
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
|
||||
Ci.nsICryptoHash
|
||||
|
@ -26,6 +26,11 @@ ChromeUtils.defineModuleGetter(
|
||||
"Weave",
|
||||
"resource://services-sync/main.js"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"jwcrypto",
|
||||
"resource://services-crypto/jwcrypto.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"FxAccountsPairingChannel",
|
||||
@ -214,6 +219,7 @@ this.FxAccountsPairingFlow = class FxAccountsPairingFlow {
|
||||
this._pairingChannel = options.pairingChannel;
|
||||
this._emitter = options.emitter;
|
||||
this._fxa = options.fxa;
|
||||
this._fxai = options.fxai || this._fxa._internal;
|
||||
this._fxaConfig = options.fxaConfig;
|
||||
this._weave = options.weave;
|
||||
this._stateMachine = new PairingStateMachine(this._emitter);
|
||||
@ -312,9 +318,7 @@ this.FxAccountsPairingFlow = class FxAccountsPairingFlow {
|
||||
code_challenge_method,
|
||||
keys_jwk,
|
||||
};
|
||||
const codeAndState = await this._fxa.authorizeOAuthCode(
|
||||
authorizeParams
|
||||
);
|
||||
const codeAndState = await this._authorizeOAuthCode(authorizeParams);
|
||||
if (codeAndState.state != state) {
|
||||
throw new Error(`OAuth state mismatch`);
|
||||
}
|
||||
@ -433,6 +437,86 @@ this.FxAccountsPairingFlow = class FxAccountsPairingFlow {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant an OAuth authorization code for the connecting client.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param options.client_id
|
||||
* @param options.state
|
||||
* @param options.scope
|
||||
* @param options.access_type
|
||||
* @param options.code_challenge_method
|
||||
* @param options.code_challenge
|
||||
* @param [options.keys_jwe]
|
||||
* @returns {Promise<Object>} Object containing "code" and "state" properties.
|
||||
*/
|
||||
_authorizeOAuthCode(options) {
|
||||
return this._fxa._withVerifiedAccountState(async state => {
|
||||
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
|
||||
const params = { ...options };
|
||||
if (params.keys_jwk) {
|
||||
const jwk = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
ChromeUtils.base64URLDecode(params.keys_jwk, { padding: "reject" })
|
||||
)
|
||||
);
|
||||
params.keys_jwe = await this._createKeysJWE(
|
||||
sessionToken,
|
||||
params.client_id,
|
||||
params.scope,
|
||||
jwk
|
||||
);
|
||||
delete params.keys_jwk;
|
||||
}
|
||||
try {
|
||||
return await this._fxai.fxAccountsClient.oauthAuthorize(
|
||||
sessionToken,
|
||||
params
|
||||
);
|
||||
} catch (err) {
|
||||
throw this._fxai._errorToErrorClass(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWE to deliver keys to another client via the OAuth scoped-keys flow.
|
||||
*
|
||||
* This method is used to transfer key material to another client, by providing
|
||||
* an appropriately-encrypted value for the `keys_jwe` OAuth response parameter.
|
||||
* Since we're transferring keys from one client to another, two things must be
|
||||
* true:
|
||||
*
|
||||
* * This client must actually have the key.
|
||||
* * The other client must be allowed to request that key.
|
||||
*
|
||||
* @param {String} sessionToken the sessionToken to use when fetching key metadata
|
||||
* @param {String} clientId the client requesting access to our keys
|
||||
* @param {String} scopes Space separated requested scopes being requested
|
||||
* @param {Object} jwk Ephemeral JWK provided by the client for secure key transfer
|
||||
*/
|
||||
async _createKeysJWE(sessionToken, clientId, scopes, jwk) {
|
||||
// This checks with the FxA server about what scopes the client is allowed.
|
||||
// Note that we pass the requesting client_id here, not our own client_id.
|
||||
const clientKeyData = await this._fxai.fxAccountsClient.getScopedKeyData(
|
||||
sessionToken,
|
||||
clientId,
|
||||
scopes
|
||||
);
|
||||
const scopedKeys = {};
|
||||
for (const scope of Object.keys(clientKeyData)) {
|
||||
const key = await this._fxai.keys.getKeyForScope(scope);
|
||||
if (!key) {
|
||||
throw new Error(`Key not available for scope "${scope}"`);
|
||||
}
|
||||
scopedKeys[scope] = key;
|
||||
}
|
||||
return jwcrypto.generateJWE(
|
||||
jwk,
|
||||
new TextEncoder().encode(JSON.stringify(scopedKeys))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const EXPORTED_SYMBOLS = ["FxAccountsPairingFlow"];
|
||||
|
@ -33,6 +33,7 @@ const {
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
ON_PROFILE_CHANGE_NOTIFICATION,
|
||||
PREF_LAST_FXA_USER,
|
||||
SCOPE_OLD_SYNC,
|
||||
WEBCHANNEL_ID,
|
||||
log,
|
||||
logPII,
|
||||
@ -624,9 +625,9 @@ FxAccountsWebChannelHelpers.prototype = {
|
||||
// in updateDeviceRegistration (but it's not clear we really do need to
|
||||
// force keys here - see bug 1580398 for more)
|
||||
try {
|
||||
await this._fxAccounts.keys.getKeys();
|
||||
await this._fxAccounts.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
} catch (e) {
|
||||
log.error("getKeys errored", e);
|
||||
log.error("getKeyForScope errored", e);
|
||||
}
|
||||
await this._fxAccounts._internal.updateDeviceRegistration();
|
||||
},
|
||||
|
@ -6,11 +6,39 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
var { XPCOMUtils } = ChromeUtils.import(
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
var { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
const { SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsCommon.js"
|
||||
);
|
||||
|
||||
// Some mock key data, in both scoped-key and legacy field formats.
|
||||
const MOCK_ACCOUNT_KEYS = {
|
||||
scopedKeys: {
|
||||
[SCOPE_OLD_SYNC]: {
|
||||
kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw",
|
||||
k:
|
||||
"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg",
|
||||
kty: "oct",
|
||||
},
|
||||
[LEGACY_SCOPE_WEBEXT_SYNC]: {
|
||||
kid: "1234567890123-3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0",
|
||||
k:
|
||||
"zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzA",
|
||||
kty: "oct",
|
||||
},
|
||||
},
|
||||
kSync:
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
kXCS: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
kExtSync:
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
kExtKbHash:
|
||||
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd ",
|
||||
};
|
||||
|
||||
(function initFxAccountsTestingInfrastructure() {
|
||||
do_get_profile();
|
||||
|
@ -15,11 +15,11 @@ const {
|
||||
ERRNO_INVALID_AUTH_TOKEN,
|
||||
ERROR_NETWORK,
|
||||
ERROR_NO_ACCOUNT,
|
||||
FX_OAUTH_CLIENT_ID,
|
||||
KEY_LIFETIME,
|
||||
ONLOGIN_NOTIFICATION,
|
||||
ONLOGOUT_NOTIFICATION,
|
||||
ONVERIFIED_NOTIFICATION,
|
||||
SCOPE_OLD_SYNC,
|
||||
} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
const { PromiseUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/PromiseUtils.jsm"
|
||||
@ -153,6 +153,24 @@ function MockFxAccountsClient() {
|
||||
});
|
||||
};
|
||||
|
||||
this.getScopedKeyData = function(sessionToken, client_id, scopes) {
|
||||
Assert.ok(sessionToken);
|
||||
Assert.equal(client_id, FX_OAUTH_CLIENT_ID);
|
||||
Assert.equal(scopes, SCOPE_OLD_SYNC);
|
||||
return new Promise(resolve => {
|
||||
do_timeout(50, () => {
|
||||
resolve({
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
identifier: "https://identity.mozilla.com/apps/oldsync",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1234567890123,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.resendVerificationEmail = function(sessionToken) {
|
||||
// Return the session token to show that we received it in the first place
|
||||
return Promise.resolve(sessionToken);
|
||||
@ -263,11 +281,8 @@ add_task(async function test_get_signed_in_user_initially_unset() {
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kSync: "beef",
|
||||
kXCS: "cafe",
|
||||
kExtSync: "bacon",
|
||||
kExtKbHash: "cheese",
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
let result = await account.getSignedInUser();
|
||||
Assert.equal(result, null);
|
||||
@ -278,6 +293,7 @@ add_task(async function test_get_signed_in_user_initially_unset() {
|
||||
result = await account.getSignedInUser();
|
||||
Assert.deepEqual(result.email, credentials.email);
|
||||
Assert.deepEqual(result.assertion, undefined);
|
||||
Assert.deepEqual(result.scopedKeys, undefined);
|
||||
Assert.deepEqual(result.kSync, undefined);
|
||||
Assert.deepEqual(result.kXCS, undefined);
|
||||
Assert.deepEqual(result.kExtSync, undefined);
|
||||
@ -286,20 +302,11 @@ add_task(async function test_get_signed_in_user_initially_unset() {
|
||||
result = await account._internal.currentAccountState.getUserAccountData();
|
||||
Assert.deepEqual(result.email, credentials.email);
|
||||
Assert.deepEqual(result.assertion, credentials.assertion);
|
||||
Assert.deepEqual(result.kSync, credentials.kSync);
|
||||
Assert.deepEqual(result.kXCS, credentials.kXCS);
|
||||
Assert.deepEqual(result.kExtSync, credentials.kExtSync);
|
||||
Assert.deepEqual(result.kExtKbHash, credentials.kExtKbHash);
|
||||
|
||||
// Delete the memory cache and force the user
|
||||
// to be read and parsed from storage (e.g. disk via JSONStorage).
|
||||
result = await account._internal.currentAccountState.getUserAccountData();
|
||||
Assert.equal(result.email, credentials.email);
|
||||
Assert.equal(result.assertion, credentials.assertion);
|
||||
Assert.equal(result.kSync, credentials.kSync);
|
||||
Assert.equal(result.kXCS, credentials.kXCS);
|
||||
Assert.equal(result.kExtSync, credentials.kExtSync);
|
||||
Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
|
||||
Assert.deepEqual(result.scopedKeys, credentials.scopedKeys);
|
||||
Assert.ok(result.kSync);
|
||||
Assert.ok(result.kXCS);
|
||||
Assert.ok(result.kExtSync);
|
||||
Assert.ok(result.kExtKbHash);
|
||||
|
||||
// sign out
|
||||
let localOnly = true;
|
||||
@ -318,11 +325,8 @@ add_task(async function test_set_signed_in_user_signs_out_previous_account() {
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kSync: "beef",
|
||||
kXCS: "cafe",
|
||||
kExtSync: "bacon",
|
||||
kExtKbHash: "cheese",
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
let account = await MakeFxAccounts({ credentials });
|
||||
|
||||
@ -342,11 +346,8 @@ add_task(async function test_update_account_data() {
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kSync: "beef",
|
||||
kXCS: "cafe",
|
||||
kExtSync: "bacon",
|
||||
kExtKbHash: "cheese",
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
let account = await MakeFxAccounts({ credentials });
|
||||
|
||||
@ -806,7 +807,7 @@ add_test(function test_pollEmailStatus_push() {
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_getKeys() {
|
||||
add_test(function test_getKeyForScope() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("eusebius");
|
||||
|
||||
@ -815,7 +816,8 @@ add_test(function test_getKeys() {
|
||||
|
||||
fxa.setSignedInUser(user).then(() => {
|
||||
fxa._internal.getUserAccountData().then(user2 => {
|
||||
// Before getKeys, we have no keys
|
||||
// Before getKeyForScope, we have no keys
|
||||
Assert.equal(!!user2.scopedKeys, false);
|
||||
Assert.equal(!!user2.kSync, false);
|
||||
Assert.equal(!!user2.kXCS, false);
|
||||
Assert.equal(!!user2.kExtSync, false);
|
||||
@ -824,11 +826,12 @@ add_test(function test_getKeys() {
|
||||
Assert.equal(!!user2.keyFetchToken, true);
|
||||
Assert.equal(!!user2.unwrapBKey, true);
|
||||
|
||||
fxa.keys.getKeys().then(() => {
|
||||
fxa.keys.getKeyForScope(SCOPE_OLD_SYNC).then(() => {
|
||||
fxa._internal.getUserAccountData().then(user3 => {
|
||||
// Now we should have keys
|
||||
Assert.equal(fxa._internal.isUserEmailVerified(user3), true);
|
||||
Assert.equal(!!user3.verified, true);
|
||||
Assert.notEqual(null, user3.scopedKeys);
|
||||
Assert.notEqual(null, user3.kSync);
|
||||
Assert.notEqual(null, user3.kXCS);
|
||||
Assert.notEqual(null, user3.kExtSync);
|
||||
@ -842,7 +845,7 @@ add_test(function test_getKeys() {
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_getKeys_kb_migration() {
|
||||
add_task(async function test_getKeyForScope_kb_migration() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("eusebius");
|
||||
|
||||
@ -852,10 +855,25 @@ add_task(async function test_getKeys_kb_migration() {
|
||||
user.kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9";
|
||||
|
||||
await fxa.setSignedInUser(user);
|
||||
await fxa.keys.getKeys();
|
||||
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
let newUser = await fxa._internal.getUserAccountData();
|
||||
Assert.equal(newUser.kA, null);
|
||||
Assert.equal(newUser.kB, null);
|
||||
Assert.deepEqual(newUser.scopedKeys, {
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
kid: "1234567890123-IqQv4onc7VcVE1kTQkyyOw",
|
||||
k:
|
||||
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
|
||||
kty: "oct",
|
||||
},
|
||||
"sync:addon_storage": {
|
||||
kid: "1234567890123-Je0Ks64vHlNl2SPJQC1CVXcNvmznmwntSfUWmFwKoME",
|
||||
k:
|
||||
"ut7VPrNYfXkA5gTopo2GCr-d4wtclV08TV26Y_Jv2IJlzYWSP26dzRau87gryIA5qJxZ7NnojeCadBjH2U-QyQ",
|
||||
kty: "oct",
|
||||
},
|
||||
});
|
||||
// These hex values were manually confirmed to be equivalent to the b64 values above.
|
||||
Assert.equal(
|
||||
newUser.kSync,
|
||||
"0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4" +
|
||||
@ -873,7 +891,33 @@ add_task(async function test_getKeys_kb_migration() {
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getKeys_nonexistent_account() {
|
||||
add_task(async function test_getKeyForScope_scopedKeys_migration() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("eusebius");
|
||||
|
||||
user.verified = true;
|
||||
// Set-up the keys in deprecated fields.
|
||||
user.kSync = MOCK_ACCOUNT_KEYS.kSync;
|
||||
user.kXCS = MOCK_ACCOUNT_KEYS.kXCS;
|
||||
user.kExtSync = MOCK_ACCOUNT_KEYS.kExtSync;
|
||||
user.kExtKbHash = MOCK_ACCOUNT_KEYS.kExtKbHash;
|
||||
Assert.equal(user.scopedKeys, null);
|
||||
|
||||
await fxa.setSignedInUser(user);
|
||||
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
let newUser = await fxa._internal.getUserAccountData();
|
||||
Assert.equal(newUser.kA, null);
|
||||
Assert.equal(newUser.kB, null);
|
||||
// It should have correctly formatted the corresponding scoped keys.
|
||||
Assert.deepEqual(newUser.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys);
|
||||
// And left the existing key fields unchanged.
|
||||
Assert.equal(newUser.kSync, user.kSync);
|
||||
Assert.equal(newUser.kXCS, user.kXCS);
|
||||
Assert.equal(newUser.kExtSync, user.kExtSync);
|
||||
Assert.equal(newUser.kExtKbHash, user.kExtKbHash);
|
||||
});
|
||||
|
||||
add_task(async function test_getKeyForScope_nonexistent_account() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let bismarck = getTestUser("bismarck");
|
||||
|
||||
@ -891,13 +935,16 @@ add_task(async function test_getKeys_nonexistent_account() {
|
||||
|
||||
let promiseLogout = new Promise(resolve => {
|
||||
makeObserver(ONLOGOUT_NOTIFICATION, function() {
|
||||
log.debug("test_getKeys_nonexistent_account observed logout");
|
||||
log.debug("test_getKeyForScope_nonexistent_account observed logout");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// XXX - the exception message here isn't ideal, but doesn't really matter...
|
||||
await Assert.rejects(fxa.keys.getKeys(), /A different user signed in/);
|
||||
await Assert.rejects(
|
||||
fxa.keys.getKeyForScope(SCOPE_OLD_SYNC),
|
||||
/A different user signed in/
|
||||
);
|
||||
|
||||
await promiseLogout;
|
||||
|
||||
@ -905,8 +952,8 @@ add_task(async function test_getKeys_nonexistent_account() {
|
||||
Assert.equal(user, null);
|
||||
});
|
||||
|
||||
// getKeys with invalid keyFetchToken should delete keyFetchToken from storage
|
||||
add_task(async function test_getKeys_invalid_token() {
|
||||
// getKeyForScope with invalid keyFetchToken should delete keyFetchToken from storage
|
||||
add_task(async function test_getKeyForScope_invalid_token() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let yusuf = getTestUser("yusuf");
|
||||
|
||||
@ -923,7 +970,7 @@ add_task(async function test_getKeys_invalid_token() {
|
||||
await fxa.setSignedInUser(yusuf);
|
||||
|
||||
try {
|
||||
await fxa.keys.getKeys();
|
||||
await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
Assert.ok(false);
|
||||
} catch (err) {
|
||||
Assert.equal(err.code, 401);
|
||||
@ -938,7 +985,7 @@ add_task(async function test_getKeys_invalid_token() {
|
||||
|
||||
// This is the exact same test vectors as
|
||||
// https://github.com/mozilla/fxa-crypto-relier/blob/f94f441159029a645a474d4b6439c38308da0bb0/test/deriver/ScopedKeys.js#L58
|
||||
add_task(async function test_getScopedKeys_oldsync() {
|
||||
add_task(async function test_getKeyForScope_oldsync() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let client = fxa._internal.fxAccountsClient;
|
||||
client.getScopedKeyData = () =>
|
||||
@ -953,44 +1000,42 @@ add_task(async function test_getScopedKeys_oldsync() {
|
||||
let user = {
|
||||
...getTestUser("eusebius"),
|
||||
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
||||
kB: "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9",
|
||||
verified: true,
|
||||
kSync:
|
||||
"0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e",
|
||||
kXCS: "22a42fe289dced5715135913424cb23b",
|
||||
kExtSync:
|
||||
"baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd88265cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9",
|
||||
kExtKbHash:
|
||||
"b776a89db29f22daedd154b44ff88397d0b210223fb956f5a749521dd8de8ddf",
|
||||
};
|
||||
await fxa.setSignedInUser(user);
|
||||
const keys = await fxa.keys.getScopedKeys(
|
||||
`${SCOPE_OLD_SYNC} profile`,
|
||||
"123456789a"
|
||||
);
|
||||
Assert.deepEqual(keys, {
|
||||
[SCOPE_OLD_SYNC]: {
|
||||
scope: SCOPE_OLD_SYNC,
|
||||
kid: "1510726317123-IqQv4onc7VcVE1kTQkyyOw",
|
||||
k:
|
||||
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
|
||||
kty: "oct",
|
||||
},
|
||||
const key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
Assert.deepEqual(key, {
|
||||
scope: SCOPE_OLD_SYNC,
|
||||
kid: "1510726317123-IqQv4onc7VcVE1kTQkyyOw",
|
||||
k:
|
||||
"DW_ll5GwX6SJ5GPqJVAuMUP2t6kDqhUulc2cbt26xbTcaKGQl-9l29FHAQ7kUiJETma4s9fIpEHrt909zgFang",
|
||||
kty: "oct",
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_getScopedKeys_unavailable_key() {
|
||||
add_task(async function test_getScopedKeys_unavailable_scope() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = {
|
||||
...getTestUser("eusebius"),
|
||||
uid: "aeaa1725c7a24ff983c6295725d5fc9b",
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
await fxa.setSignedInUser(user);
|
||||
await Assert.rejects(
|
||||
fxa.keys.getKeyForScope("otherkeybearingscope"),
|
||||
/Key not available for scope/
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getScopedKeys_misconfigured_fxa_server() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let client = fxa._internal.fxAccountsClient;
|
||||
client.getScopedKeyData = () =>
|
||||
Promise.resolve({
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
identifier: "https://identity.mozilla.com/apps/oldsync",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1510726317123,
|
||||
},
|
||||
otherkeybearingscope: {
|
||||
identifier: "otherkeybearingscope",
|
||||
wrongscope: {
|
||||
identifier: "wrongscope",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1510726331712,
|
||||
@ -1010,17 +1055,14 @@ add_task(async function test_getScopedKeys_unavailable_key() {
|
||||
};
|
||||
await fxa.setSignedInUser(user);
|
||||
await Assert.rejects(
|
||||
fxa.keys.getScopedKeys(
|
||||
`${SCOPE_OLD_SYNC} otherkeybearingscope profile`,
|
||||
"123456789a"
|
||||
),
|
||||
/Unavailable key material for otherkeybearingscope/
|
||||
fxa.keys.getKeyForScope(SCOPE_OLD_SYNC),
|
||||
/The FxA server did not grant Firefox the `oldsync` scope/
|
||||
);
|
||||
});
|
||||
|
||||
// fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
|
||||
// _fetchAndUnwrapAndDeriveKeys with no keyFetchToken should trigger signOut
|
||||
// XXX - actually, it probably shouldn't - bug 1572313.
|
||||
add_test(function test_fetchAndUnwrapKeys_no_token() {
|
||||
add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("lettuce.protheroe");
|
||||
delete user.keyFetchToken;
|
||||
@ -1035,7 +1077,7 @@ add_test(function test_fetchAndUnwrapKeys_no_token() {
|
||||
fxa
|
||||
.setSignedInUser(user)
|
||||
.then(user2 => {
|
||||
return fxa.keys.fetchAndUnwrapKeys();
|
||||
return fxa.keys._fetchAndUnwrapAndDeriveKeys();
|
||||
})
|
||||
.catch(error => {
|
||||
log.info("setSignedInUser correctly rejected");
|
||||
@ -1090,12 +1132,9 @@ add_task(async function test_getAssertion_invalid_token() {
|
||||
|
||||
let creds = {
|
||||
sessionToken: "sessionToken",
|
||||
kSync: expandHex("11"),
|
||||
kXCS: expandHex("66"),
|
||||
kExtSync: expandHex("88"),
|
||||
kExtKbHash: expandHex("22"),
|
||||
verified: true,
|
||||
email: "sonia@example.com",
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
await fxa.setSignedInUser(creds);
|
||||
// we have what we still believe to be a valid session token, so we should
|
||||
@ -1126,14 +1165,10 @@ add_task(async function test_getAssertion() {
|
||||
|
||||
let creds = {
|
||||
sessionToken: "sessionToken",
|
||||
kSync: expandHex("11"),
|
||||
kXCS: expandHex("66"),
|
||||
kExtSync: expandHex("88"),
|
||||
kExtKbHash: expandHex("22"),
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
// By putting kSync/kXCS/kExtSync/kExtKbHash/verified in "creds", we skip ahead
|
||||
// to the "we're ready" stage.
|
||||
// By putting scopedKeys in "creds", we skip ahead to the "we're ready" stage.
|
||||
await fxa.setSignedInUser(creds);
|
||||
|
||||
_("== ready to go\n");
|
||||
@ -1695,100 +1730,6 @@ add_task(async function test_getOAuthToken_authErrorRefreshesCertificate() {
|
||||
Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
|
||||
});
|
||||
|
||||
add_test(async function test_getAccessToken() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let alice = getTestUser("alice");
|
||||
alice.verified = true;
|
||||
let oauthTokenCalled = false;
|
||||
const TTL = 100;
|
||||
const SCOPE = "https://identity.mozilla.com/apps/oldsync";
|
||||
|
||||
fxa._internal._d_signCertificate.resolve("cert1");
|
||||
|
||||
let client = fxa._internal.fxAccountsOAuthGrantClient;
|
||||
client.getTokenFromAssertion = (assertion, scopeString, ttl) => {
|
||||
Assert.ok(assertion);
|
||||
Assert.equal(scopeString, SCOPE);
|
||||
Assert.equal(ttl, TTL);
|
||||
oauthTokenCalled = true;
|
||||
return Promise.resolve({ access_token: "token" });
|
||||
};
|
||||
|
||||
const KEY_DATA = {
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
k:
|
||||
"3TVYx0exDTbrc5SGMkNg_C_eoNfjV0elHClP7npHrAtrlJu-esNyTUQaR6UcJBVYilPr8-T4BqWlIp4TOpKavA",
|
||||
kid: "1569964308879-5y6waestOxDDM-Ia4_2u1Q",
|
||||
kty: "oct",
|
||||
scope: "https://identity.mozilla.com/apps/oldsync",
|
||||
},
|
||||
};
|
||||
|
||||
fxa._internal.keys.getScopedKeys = () => Promise.resolve(KEY_DATA);
|
||||
|
||||
await fxa.setSignedInUser(alice);
|
||||
let result = await fxa.getAccessToken(SCOPE, TTL);
|
||||
Assert.ok(oauthTokenCalled);
|
||||
Assert.equal(result.scope, SCOPE);
|
||||
Assert.equal(result.key, KEY_DATA[SCOPE]);
|
||||
Assert.equal(result.token, "token");
|
||||
// Test that the scoped key cache works
|
||||
fxa._internal.keys.getScopedKeys = () => {
|
||||
throw new Error("Should not be called");
|
||||
};
|
||||
result = await fxa.getAccessToken(SCOPE, TTL);
|
||||
Assert.ok(oauthTokenCalled);
|
||||
Assert.equal(result.scope, SCOPE);
|
||||
Assert.equal(result.key, KEY_DATA[SCOPE]);
|
||||
Assert.equal(result.token, "token");
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_getAccessToken_error_bad_scope() {
|
||||
let fxa = new MockFxAccounts();
|
||||
fxa.getAccessToken().catch(err => {
|
||||
Assert.equal(err.details, "Missing or invalid 'scope' option");
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(async function test_getAccessToken_no_scoped_keys() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let alice = getTestUser("alice");
|
||||
alice.verified = true;
|
||||
let oauthTokenCalled = false;
|
||||
const TTL = 100;
|
||||
const SCOPE = "profile";
|
||||
|
||||
fxa._internal._d_signCertificate.resolve("cert1");
|
||||
|
||||
let client = fxa._internal.fxAccountsOAuthGrantClient;
|
||||
client.getTokenFromAssertion = () => {
|
||||
oauthTokenCalled = true;
|
||||
return Promise.resolve({ access_token: "token" });
|
||||
};
|
||||
|
||||
fxa._internal.keys.getScopedKeys = () => Promise.resolve({});
|
||||
|
||||
await fxa.setSignedInUser(alice);
|
||||
let result = await fxa.getAccessToken(SCOPE, TTL);
|
||||
Assert.ok(oauthTokenCalled);
|
||||
Assert.equal(result.scope, SCOPE);
|
||||
Assert.equal(result.key, undefined);
|
||||
Assert.equal(result.token, "token");
|
||||
|
||||
// Test that the scoped key cache works
|
||||
fxa._internal.keys.getScopedKeys = () => {
|
||||
throw new Error("Should not be called");
|
||||
};
|
||||
result = await fxa.getAccessToken(SCOPE, TTL);
|
||||
Assert.ok(oauthTokenCalled);
|
||||
Assert.equal(result.scope, SCOPE);
|
||||
Assert.equal(result.key, undefined);
|
||||
Assert.equal(result.token, "token");
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(async function test_listAttachedOAuthClients() {
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
const ONE_DAY = 24 * ONE_HOUR;
|
||||
|
@ -921,8 +921,7 @@ function getTestUser(name) {
|
||||
email: name + "@example.com",
|
||||
uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
|
||||
sessionToken: name + "'s session token",
|
||||
keyFetchToken: name + "'s keyfetch token",
|
||||
unwrapBKey: expandHex("44"),
|
||||
verified: false,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
}
|
||||
|
@ -76,12 +76,15 @@ add_task(async function test_reset() {
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kSync: "beef",
|
||||
kXCS: "cafe",
|
||||
kExtSync: "bacon",
|
||||
kExtKbHash: "cheese",
|
||||
verified: true,
|
||||
...MOCK_ACCOUNT_KEYS,
|
||||
};
|
||||
// FxA will try to register its device record in the background after signin.
|
||||
const registerDevice = sinon
|
||||
.stub(fxAccounts._internal.fxAccountsClient, "registerDevice")
|
||||
.callsFake(async () => {
|
||||
return { id: "foo" };
|
||||
});
|
||||
await fxAccounts._internal.setSignedInUser(credentials);
|
||||
ok(!Services.prefs.prefHasUserValue(testPref));
|
||||
// signing the user out should reset the name pref.
|
||||
@ -89,6 +92,7 @@ add_task(async function test_reset() {
|
||||
ok(Services.prefs.prefHasUserValue(namePref));
|
||||
await fxAccounts.signOut(/* localOnly = */ true);
|
||||
ok(!Services.prefs.prefHasUserValue(namePref));
|
||||
registerDevice.restore();
|
||||
});
|
||||
|
||||
add_task(async function test_name_sanitization() {
|
||||
|
@ -43,6 +43,7 @@ const DEVICE_NAME = "Foo's computer";
|
||||
const PAIR_URI = "https://foo.bar/pair";
|
||||
const OAUTH_URI = "https://foo.bar/oauth";
|
||||
const KSYNC = "myksync";
|
||||
const SESSION = "mysession";
|
||||
const fxaConfig = {
|
||||
promisePairingURI() {
|
||||
return PAIR_URI;
|
||||
@ -52,20 +53,6 @@ const fxaConfig = {
|
||||
},
|
||||
};
|
||||
const fxAccounts = {
|
||||
keys: {
|
||||
getScopedKeys(scope) {
|
||||
return {
|
||||
[scope]: {
|
||||
kid: "123456",
|
||||
k: KSYNC,
|
||||
kty: "oct",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
authorizeOAuthCode() {
|
||||
return { code: "mycode", state: "mystate" };
|
||||
},
|
||||
getSignedInUser() {
|
||||
return {
|
||||
uid: UID,
|
||||
@ -74,6 +61,39 @@ const fxAccounts = {
|
||||
displayName: DISPLAY_NAME,
|
||||
};
|
||||
},
|
||||
async _withVerifiedAccountState(cb) {
|
||||
return cb({
|
||||
async getUserAccountData() {
|
||||
return {
|
||||
sessionToken: SESSION,
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
_internal: {
|
||||
keys: {
|
||||
getKeyForScope(scope) {
|
||||
return {
|
||||
kid: "123456",
|
||||
k: KSYNC,
|
||||
kty: "oct",
|
||||
};
|
||||
},
|
||||
},
|
||||
fxAccountsClient: {
|
||||
async getScopedKeyData() {
|
||||
return {
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
identifier: "https://identity.mozilla.com/apps/oldsync",
|
||||
keyRotationTimestamp: 12345678,
|
||||
},
|
||||
};
|
||||
},
|
||||
async oauthAuthorize() {
|
||||
return { code: "mycode", state: "mystate" };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const weave = {
|
||||
Service: { clientsEngine: { localName: DEVICE_NAME } },
|
||||
@ -132,6 +152,7 @@ add_task(async function testFullFlow() {
|
||||
const promiseSwitchToWebContent = emitter.once("view:SwitchToWebContent");
|
||||
const promiseMetadataSent = promiseOutgoingMessage(pairingChannel);
|
||||
const epk = await generateEphemeralKeypair();
|
||||
|
||||
pairingChannel.simulateIncoming({
|
||||
message: "pair:supp:request",
|
||||
data: {
|
||||
@ -177,17 +198,32 @@ add_task(async function testFullFlow() {
|
||||
pairSuppMetadata
|
||||
);
|
||||
|
||||
const authorizeOAuthCode = sinon.spy(fxAccounts, "authorizeOAuthCode");
|
||||
const generateJWE = sinon.spy(jwcrypto, "generateJWE");
|
||||
const oauthAuthorize = sinon.spy(
|
||||
fxAccounts._internal.fxAccountsClient,
|
||||
"oauthAuthorize"
|
||||
);
|
||||
const promiseOAuthParamsMsg = promiseOutgoingMessage(pairingChannel);
|
||||
await simulateIncomingWebChannel(flow, "fxaccounts:pair_authorize");
|
||||
Assert.ok(authorizeOAuthCode.calledOnce);
|
||||
const oauthCodeArgs = authorizeOAuthCode.firstCall.args[0];
|
||||
Assert.equal(
|
||||
oauthCodeArgs.keys_jwk,
|
||||
ChromeUtils.base64URLEncode(
|
||||
new TextEncoder().encode(JSON.stringify(epk.publicJWK)),
|
||||
{ pad: false }
|
||||
)
|
||||
// We should have generated the expected JWE.
|
||||
Assert.ok(generateJWE.calledOnce);
|
||||
const generateArgs = generateJWE.firstCall.args;
|
||||
Assert.deepEqual(generateArgs[0], epk.publicJWK);
|
||||
Assert.deepEqual(JSON.parse(new TextDecoder().decode(generateArgs[1])), {
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
kid: "123456",
|
||||
k: KSYNC,
|
||||
kty: "oct",
|
||||
},
|
||||
});
|
||||
// We should have authorized an oauth code with expected parameters.
|
||||
Assert.ok(oauthAuthorize.calledOnce);
|
||||
const oauthCodeArgs = oauthAuthorize.firstCall.args[1];
|
||||
console.log(oauthCodeArgs);
|
||||
Assert.ok(!oauthCodeArgs.keys_jwk);
|
||||
Assert.deepEqual(
|
||||
oauthCodeArgs.keys_jwe,
|
||||
await generateJWE.firstCall.returnValue
|
||||
);
|
||||
Assert.equal(oauthCodeArgs.client_id, "client_id_1");
|
||||
Assert.equal(oauthCodeArgs.access_type, "offline");
|
||||
@ -198,29 +234,35 @@ add_task(async function testFullFlow() {
|
||||
);
|
||||
Assert.equal(oauthCodeArgs.code_challenge, "chal");
|
||||
Assert.equal(oauthCodeArgs.code_challenge_method, "S256");
|
||||
|
||||
const oAuthParams = await promiseOAuthParamsMsg;
|
||||
Assert.deepEqual(oAuthParams, {
|
||||
message: "pair:auth:authorize",
|
||||
data: { code: "mycode", state: "mystate" },
|
||||
});
|
||||
|
||||
let heartbeat = await simulateIncomingWebChannel(
|
||||
flow,
|
||||
"fxaccounts:pair_heartbeat"
|
||||
);
|
||||
Assert.ok(!heartbeat.suppAuthorized);
|
||||
|
||||
await pairingChannel.simulateIncoming({
|
||||
message: "pair:supp:authorize",
|
||||
});
|
||||
|
||||
heartbeat = await simulateIncomingWebChannel(
|
||||
flow,
|
||||
"fxaccounts:pair_heartbeat"
|
||||
);
|
||||
Assert.ok(heartbeat.suppAuthorized);
|
||||
|
||||
await simulateIncomingWebChannel(flow, "fxaccounts:pair_complete");
|
||||
// The flow should have been destroyed!
|
||||
Assert.ok(!FxAccountsPairingFlow.get(CHANNEL_ID));
|
||||
Assert.ok(pairingChannel.closed);
|
||||
fxAccounts.authorizeOAuthCode.restore();
|
||||
generateJWE.restore();
|
||||
oauthAuthorize.restore();
|
||||
});
|
||||
|
||||
add_task(async function testUnknownPairingMessage() {
|
||||
|
@ -44,6 +44,9 @@ const { FxAccounts } = ChromeUtils.import(
|
||||
const { FxAccountsClient } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsClient.jsm"
|
||||
);
|
||||
const { SCOPE_OLD_SYNC, LEGACY_SCOPE_WEBEXT_SYNC } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsCommon.js"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// and grab non-exported stuff via a backstage pass.
|
||||
@ -150,9 +153,23 @@ var makeIdentityConfig = function(overrides) {
|
||||
assertion: "assertion",
|
||||
email: "foo",
|
||||
kSync: "a".repeat(128),
|
||||
kXCS: "a".repeat(32),
|
||||
kExtSync: "a".repeat(128),
|
||||
kExtKbHash: "a".repeat(32),
|
||||
kXCS: "b".repeat(32),
|
||||
kExtSync: "c".repeat(128),
|
||||
kExtKbHash: "d".repeat(64),
|
||||
scopedKeys: {
|
||||
[SCOPE_OLD_SYNC]: {
|
||||
kid: "1234567890123-u7u7u7u7u7u7u7u7u7u7uw",
|
||||
k:
|
||||
"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg",
|
||||
kty: "oct",
|
||||
},
|
||||
[LEGACY_SCOPE_WEBEXT_SYNC]: {
|
||||
kid: "1234567890123-3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d0",
|
||||
k:
|
||||
"zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzA",
|
||||
kty: "oct",
|
||||
},
|
||||
},
|
||||
sessionToken: "sessionToken",
|
||||
uid: "a".repeat(32),
|
||||
verified: true,
|
||||
|
@ -50,6 +50,12 @@ ChromeUtils.defineModuleGetter(
|
||||
"resource://gre/modules/FxAccounts.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"CommonUtils",
|
||||
"resource://services-common/utils.js"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", function() {
|
||||
let log = Log.repository.getLogger("Sync.BrowserIDManager");
|
||||
log.manageLevelFromPref("services.sync.log.logger.identity");
|
||||
@ -75,6 +81,8 @@ ChromeUtils.import(
|
||||
fxAccountsCommon
|
||||
);
|
||||
|
||||
const SCOPE_OLD_SYNC = fxAccountsCommon.SCOPE_OLD_SYNC;
|
||||
|
||||
const OBSERVER_TOPICS = [
|
||||
fxAccountsCommon.ONLOGIN_NOTIFICATION,
|
||||
fxAccountsCommon.ONVERIFIED_NOTIFICATION,
|
||||
@ -306,6 +314,7 @@ this.BrowserIDManager.prototype = {
|
||||
*/
|
||||
async unlockAndVerifyAuthState() {
|
||||
let data = await this.getSignedInUser();
|
||||
const fxa = this._fxaService;
|
||||
if (!data) {
|
||||
log.debug("unlockAndVerifyAuthState has no FxA user");
|
||||
return LOGIN_FAILED_NO_USERNAME;
|
||||
@ -320,7 +329,7 @@ this.BrowserIDManager.prototype = {
|
||||
log.debug("unlockAndVerifyAuthState has an unverified user");
|
||||
return LOGIN_FAILED_LOGIN_REJECTED;
|
||||
}
|
||||
if (await this._fxaService.keys.canGetKeys()) {
|
||||
if (await fxa.keys.canGetKeyForScope(SCOPE_OLD_SYNC)) {
|
||||
log.debug(
|
||||
"unlockAndVerifyAuthState already has (or can fetch) sync keys"
|
||||
);
|
||||
@ -338,7 +347,7 @@ this.BrowserIDManager.prototype = {
|
||||
// without unlocking the MP or cleared the saved logins, so we've now
|
||||
// lost them - the user will need to reauth before continuing.
|
||||
let result;
|
||||
if (await this._fxaService.keys.canGetKeys()) {
|
||||
if (await fxa.keys.canGetKeyForScope(SCOPE_OLD_SYNC)) {
|
||||
result = STATUS_OK;
|
||||
} else {
|
||||
result = LOGIN_FAILED_LOGIN_REJECTED;
|
||||
@ -392,7 +401,7 @@ this.BrowserIDManager.prototype = {
|
||||
// We need keys for things to work. If we don't have them, just
|
||||
// return null for the token - sync calling unlockAndVerifyAuthState()
|
||||
// before actually syncing will setup the error states if necessary.
|
||||
if (!(await fxa.keys.canGetKeys())) {
|
||||
if (!(await fxa.keys.canGetKeyForScope(SCOPE_OLD_SYNC))) {
|
||||
this._log.info(
|
||||
"Unable to fetch keys (master-password locked?), so aborting token fetch"
|
||||
);
|
||||
@ -400,47 +409,38 @@ this.BrowserIDManager.prototype = {
|
||||
}
|
||||
|
||||
// Do the assertion/certificate/token dance, with a retry.
|
||||
let getToken = async keys => {
|
||||
this._log.info("Getting an assertion from", this._tokenServerUrl);
|
||||
let getToken = async key => {
|
||||
this._log.info("Getting a sync token from", this._tokenServerUrl);
|
||||
let token;
|
||||
|
||||
if (USE_OAUTH_FOR_SYNC_TOKEN) {
|
||||
token = await this._fetchTokenUsingOAuth();
|
||||
token = await this._fetchTokenUsingOAuth(key);
|
||||
} else {
|
||||
const audience = Services.io.newURI(this._tokenServerUrl).prePath;
|
||||
const assertion = await fxa._internal.getAssertion(audience);
|
||||
this._log.debug("Getting a token using an Assertion");
|
||||
const headers = { "X-Client-State": keys.kXCS };
|
||||
token = await this._tokenServerClient.getTokenFromBrowserIDAssertion(
|
||||
this._tokenServerUrl,
|
||||
assertion,
|
||||
headers
|
||||
);
|
||||
token = await this._fetchTokenUsingBrowserID(key);
|
||||
}
|
||||
|
||||
this._log.trace("Successfully got a token");
|
||||
return token;
|
||||
};
|
||||
|
||||
try {
|
||||
let token;
|
||||
let keys;
|
||||
let token, key;
|
||||
try {
|
||||
this._log.info("Getting keys");
|
||||
keys = await fxa.keys.getKeys(); // throws if the user changed.
|
||||
token = await getToken(keys);
|
||||
this._log.info("Getting sync key");
|
||||
key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
if (!key) {
|
||||
throw new Error("browser does not have the sync key, cannot sync");
|
||||
}
|
||||
token = await getToken(key);
|
||||
} catch (err) {
|
||||
// If we get a 401 fetching the token it may be that our certificate
|
||||
// needs to be regenerated.
|
||||
// If we get a 401 fetching the token it may be that our auth tokens needed
|
||||
// to be regenerated; retry exactly once.
|
||||
if (!err.response || err.response.status !== 401) {
|
||||
throw err;
|
||||
}
|
||||
this._log.warn(
|
||||
"Token server returned 401, refreshing certificate and retrying token fetch"
|
||||
"Token server returned 401, retrying token fetch with fresh credentials"
|
||||
);
|
||||
await fxa._internal.invalidateCertificate();
|
||||
keys = await fxa.keys.getKeys();
|
||||
token = await getToken(keys);
|
||||
key = await fxa.keys.getKeyForScope(SCOPE_OLD_SYNC);
|
||||
token = await getToken(key);
|
||||
}
|
||||
// TODO: Make it be only 80% of the duration, so refresh the token
|
||||
// before it actually expires. This is to avoid sync storage errors
|
||||
@ -448,7 +448,7 @@ this.BrowserIDManager.prototype = {
|
||||
// (XXX - the above may no longer be true - someone should check ;)
|
||||
token.expiration = this._now() + token.duration * 1000 * 0.8;
|
||||
if (!this._syncKeyBundle) {
|
||||
this._syncKeyBundle = BulkKeyBundle.fromHexKey(keys.kSync);
|
||||
this._syncKeyBundle = BulkKeyBundle.fromJWK(key);
|
||||
}
|
||||
Weave.Status.login = LOGIN_SUCCEEDED;
|
||||
this._token = token;
|
||||
@ -486,30 +486,62 @@ this.BrowserIDManager.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches an OAuth token using the OLD_SYNC scope and later exchanges it
|
||||
* Fetches an OAuth token using the OLD_SYNC scope and exchanges it
|
||||
* for a TokenServer token.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _fetchTokenUsingOAuth() {
|
||||
async _fetchTokenUsingOAuth(key) {
|
||||
this._log.debug("Getting a token using OAuth");
|
||||
const fxa = this._fxaService;
|
||||
const scope = fxAccountsCommon.SCOPE_OLD_SYNC;
|
||||
const ttl = fxAccountsCommon.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS;
|
||||
const { token, key } = await fxa.getAccessToken(scope, ttl);
|
||||
const accessToken = await fxa.getOAuthToken({ scope: SCOPE_OLD_SYNC, ttl });
|
||||
const headers = {
|
||||
"X-KeyId": key.kid,
|
||||
};
|
||||
|
||||
return this._tokenServerClient
|
||||
.getTokenFromOAuthToken(this._tokenServerUrl, token, headers)
|
||||
.getTokenFromOAuthToken(this._tokenServerUrl, accessToken, headers)
|
||||
.catch(async err => {
|
||||
if (err.response || err.response.status === 401) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
// remove the cached token if we cannot authorize with it.
|
||||
// we have to do this here because we know which `token` to remove
|
||||
// from cache.
|
||||
await fxa.removeCachedOAuthToken({ token });
|
||||
console.log("REMOVE CACHED", accessToken);
|
||||
await fxa.removeCachedOAuthToken({ token: accessToken });
|
||||
}
|
||||
|
||||
// continue the error chain, so other handlers can deal with the error.
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Exchanges a BrowserID assertion for a TokenServer token.
|
||||
*
|
||||
* This is a legacy access method that we're in the process of deprecating;
|
||||
* if you have a choice you should use `_fetchTokenUsingOAuth` above.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _fetchTokenUsingBrowserID(key) {
|
||||
this._log.debug("Getting a token using BrowserID");
|
||||
const fxa = this._fxaService;
|
||||
const audience = Services.io.newURI(this._tokenServerUrl).prePath;
|
||||
const assertion = await fxa._internal.getAssertion(audience);
|
||||
const headers = {
|
||||
"X-Client-State": fxa._internal.keys.kidAsHex(key),
|
||||
};
|
||||
return this._tokenServerClient
|
||||
.getTokenFromBrowserIDAssertion(this._tokenServerUrl, assertion, headers)
|
||||
.catch(async err => {
|
||||
if (err.response && err.response.status === 401) {
|
||||
this._log.warn(
|
||||
"Token server returned 401, refreshing certificate and retrying token fetch"
|
||||
);
|
||||
await fxa._internal.invalidateCertificate();
|
||||
}
|
||||
|
||||
// continue the error chain, so other handlers can deal with the error.
|
||||
|
@ -129,6 +129,7 @@ function BulkKeyBundle(collection) {
|
||||
|
||||
this._collection = collection;
|
||||
}
|
||||
|
||||
BulkKeyBundle.fromHexKey = function(hexKey) {
|
||||
let key = CommonUtils.hexToBytes(hexKey);
|
||||
let bundle = new BulkKeyBundle();
|
||||
@ -137,6 +138,13 @@ BulkKeyBundle.fromHexKey = function(hexKey) {
|
||||
return bundle;
|
||||
};
|
||||
|
||||
BulkKeyBundle.fromJWK = function(jwk) {
|
||||
if (!jwk || !jwk.k || jwk.kty !== "oct") {
|
||||
throw new Error("Invalid JWK provided to BulkKeyBundle.fromJWK");
|
||||
}
|
||||
return BulkKeyBundle.fromHexKey(CommonUtils.base64urlToHex(jwk.k));
|
||||
};
|
||||
|
||||
BulkKeyBundle.prototype = {
|
||||
__proto__: KeyBundle.prototype,
|
||||
|
||||
|
@ -41,11 +41,8 @@ const MOCK_SCOPED_KEY = {
|
||||
scope: "https://identity.mozilla.com/apps/oldsync",
|
||||
};
|
||||
|
||||
const MOCK_ACCESS_TOKEN_RESPONSE = {
|
||||
token: "e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba",
|
||||
key: MOCK_SCOPED_KEY,
|
||||
scope: "https://identity.mozilla.com/apps/oldsync",
|
||||
};
|
||||
const MOCK_ACCESS_TOKEN =
|
||||
"e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba";
|
||||
|
||||
var globalIdentityConfig = makeIdentityConfig();
|
||||
var globalBrowseridManager = new BrowserIDManager();
|
||||
@ -65,6 +62,16 @@ MockFxAccountsClient.prototype = {
|
||||
accountStatus() {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
getScopedKeyData() {
|
||||
return Promise.resolve({
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
identifier: "https://identity.mozilla.com/apps/oldsync",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1234567890123,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
add_test(function test_initial_state() {
|
||||
@ -90,8 +97,8 @@ add_task(async function test_initialialize_via_oauth_token() {
|
||||
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
|
||||
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
|
||||
browseridManager._fxaService._internal.initialize();
|
||||
browseridManager._fxaService.getAccessToken = () =>
|
||||
Promise.resolve(MOCK_ACCESS_TOKEN_RESPONSE);
|
||||
browseridManager._fxaService.getOAuthToken = () =>
|
||||
Promise.resolve(MOCK_ACCESS_TOKEN);
|
||||
|
||||
await browseridManager._ensureValidToken();
|
||||
Assert.ok(!!browseridManager._token);
|
||||
@ -108,9 +115,9 @@ add_task(async function test_refreshOAuthTokenOn401() {
|
||||
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
|
||||
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
|
||||
browseridManager._fxaService._internal.initialize();
|
||||
browseridManager._fxaService.getAccessToken = () => {
|
||||
browseridManager._fxaService.getOAuthToken = () => {
|
||||
++getTokenCount;
|
||||
return Promise.resolve(MOCK_ACCESS_TOKEN_RESPONSE);
|
||||
return Promise.resolve(MOCK_ACCESS_TOKEN);
|
||||
};
|
||||
|
||||
let didReturn401 = false;
|
||||
@ -601,6 +608,7 @@ add_task(async function test_getKeysErrorWithBackoff() {
|
||||
|
||||
let config = makeIdentityConfig();
|
||||
// We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
|
||||
delete config.fxaccount.user.scopedKeys;
|
||||
delete config.fxaccount.user.kSync;
|
||||
delete config.fxaccount.user.kXCS;
|
||||
delete config.fxaccount.user.kExtSync;
|
||||
@ -643,6 +651,7 @@ add_task(async function test_getKeysErrorWithRetry() {
|
||||
|
||||
let config = makeIdentityConfig();
|
||||
// We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
|
||||
delete config.fxaccount.user.scopedKeys;
|
||||
delete config.fxaccount.user.kSync;
|
||||
delete config.fxaccount.user.kXCS;
|
||||
delete config.fxaccount.user.kExtSync;
|
||||
@ -728,6 +737,7 @@ add_task(async function test_getGetKeysFailing401() {
|
||||
_("Arrange for a 401 - Sync should reflect an auth error.");
|
||||
let config = makeIdentityConfig();
|
||||
// We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
|
||||
delete config.fxaccount.user.scopedKeys;
|
||||
delete config.fxaccount.user.kSync;
|
||||
delete config.fxaccount.user.kXCS;
|
||||
delete config.fxaccount.user.kExtSync;
|
||||
@ -755,6 +765,7 @@ add_task(async function test_getGetKeysFailing503() {
|
||||
_("Arrange for a 503 - Sync should reflect a network error.");
|
||||
let config = makeIdentityConfig();
|
||||
// We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
|
||||
delete config.fxaccount.user.scopedKeys;
|
||||
delete config.fxaccount.user.kSync;
|
||||
delete config.fxaccount.user.kXCS;
|
||||
delete config.fxaccount.user.kExtSync;
|
||||
@ -782,13 +793,14 @@ add_task(async function test_getGetKeysFailing503() {
|
||||
|
||||
add_task(async function test_getKeysMissing() {
|
||||
_(
|
||||
"BrowserIDManager correctly handles getKeys succeeding but not returning keys."
|
||||
"BrowserIDManager correctly handles getKeyForScope succeeding but not returning the key."
|
||||
);
|
||||
|
||||
let browseridManager = new BrowserIDManager();
|
||||
let identityConfig = makeIdentityConfig();
|
||||
// our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
|
||||
// try and fetch them.
|
||||
delete identityConfig.fxaccount.user.scopedKeys;
|
||||
delete identityConfig.fxaccount.user.kSync;
|
||||
delete identityConfig.fxaccount.user.kXCS;
|
||||
delete identityConfig.fxaccount.user.kExtSync;
|
||||
@ -812,30 +824,64 @@ add_task(async function test_getKeysMissing() {
|
||||
},
|
||||
// And the keys object with a mock that returns no keys.
|
||||
keys: {
|
||||
fetchAndUnwrapKeys() {
|
||||
return Promise.resolve({});
|
||||
getKeyForScope() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add a mock to the currentAccountState object.
|
||||
fxa._internal.currentAccountState.getCertificate = function(
|
||||
data,
|
||||
keyPair,
|
||||
mustBeValidUntil
|
||||
) {
|
||||
this.cert = {
|
||||
validUntil: fxa._internal.now() + CERT_LIFETIME,
|
||||
cert: "certificate",
|
||||
};
|
||||
return Promise.resolve(this.cert.cert);
|
||||
};
|
||||
|
||||
browseridManager._fxaService = fxa;
|
||||
|
||||
await Assert.rejects(
|
||||
browseridManager._ensureValidToken(),
|
||||
/user data missing: kSync, kXCS, kExtSync, kExtKbHash/
|
||||
/browser does not have the sync key, cannot sync/
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_getKeysUnexpecedError() {
|
||||
_(
|
||||
"BrowserIDManager correctly handles getKeyForScope throwing an unexpected error."
|
||||
);
|
||||
|
||||
let browseridManager = new BrowserIDManager();
|
||||
let identityConfig = makeIdentityConfig();
|
||||
// our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
|
||||
// try and fetch them.
|
||||
delete identityConfig.fxaccount.user.scopedKeys;
|
||||
delete identityConfig.fxaccount.user.kSync;
|
||||
delete identityConfig.fxaccount.user.kXCS;
|
||||
delete identityConfig.fxaccount.user.kExtSync;
|
||||
delete identityConfig.fxaccount.user.kExtKbHash;
|
||||
identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";
|
||||
|
||||
configureFxAccountIdentity(browseridManager, identityConfig);
|
||||
|
||||
// Mock a fxAccounts object
|
||||
let fxa = new FxAccounts({
|
||||
fxAccountsClient: new MockFxAccountsClient(),
|
||||
newAccountState(credentials) {
|
||||
// We only expect this to be called with null indicating the (mock)
|
||||
// storage should be read.
|
||||
if (credentials) {
|
||||
throw new Error("Not expecting to have credentials passed");
|
||||
}
|
||||
let storageManager = new MockFxaStorageManager();
|
||||
storageManager.initialize(identityConfig.fxaccount.user);
|
||||
return new AccountState(storageManager);
|
||||
},
|
||||
// And the keys object with a mock that returns no keys.
|
||||
keys: {
|
||||
async getKeyForScope() {
|
||||
throw new Error("well that was unexpected");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
browseridManager._fxaService = fxa;
|
||||
|
||||
await Assert.rejects(
|
||||
browseridManager._ensureValidToken(),
|
||||
/well that was unexpected/
|
||||
);
|
||||
});
|
||||
|
||||
@ -846,6 +892,7 @@ add_task(async function test_signedInUserMissing() {
|
||||
|
||||
let browseridManager = new BrowserIDManager();
|
||||
// Delete stored keys and the key fetch token.
|
||||
delete globalIdentityConfig.fxaccount.user.scopedKeys;
|
||||
delete globalIdentityConfig.fxaccount.user.kSync;
|
||||
delete globalIdentityConfig.fxaccount.user.kXCS;
|
||||
delete globalIdentityConfig.fxaccount.user.kExtSync;
|
||||
|
@ -622,7 +622,7 @@ add_task(async function test_autoconnect_mp_locked() {
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
canGetKeys() {
|
||||
canGetKeyForScope() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
@ -143,7 +143,8 @@ function ciphertextHMAC(keyBundle, id, IV, ciphertext) {
|
||||
* @returns {string} sha256 of the user's kB as a hex string
|
||||
*/
|
||||
const getKBHash = async function(fxaService) {
|
||||
return (await fxaService.keys.getKeys()).kExtKbHash;
|
||||
const key = await fxaService.keys.getKeyForScope(STORAGE_SYNC_SCOPE);
|
||||
return fxaService.keys.kidAsHex(key);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -272,12 +273,8 @@ class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
|
||||
throwIfNoFxA(this._fxaService, "encrypting chrome.storage.sync records");
|
||||
const self = this;
|
||||
return (async function() {
|
||||
let keys = await self._fxaService.keys.getKeys();
|
||||
if (!keys.kExtSync) {
|
||||
throw new Error("user doesn't have kExtSync");
|
||||
}
|
||||
|
||||
return BulkKeyBundle.fromHexKey(keys.kExtSync);
|
||||
let key = await self._fxaService.keys.getKeyForScope(STORAGE_SYNC_SCOPE);
|
||||
return BulkKeyBundle.fromJWK(key);
|
||||
})();
|
||||
}
|
||||
// Pass through the kbHash field from the unencrypted record. If
|
||||
|
@ -27,6 +27,9 @@ const {
|
||||
const { BulkKeyBundle } = ChromeUtils.import(
|
||||
"resource://services-sync/keys.js"
|
||||
);
|
||||
const { FxAccountsKeys } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsKeys.jsm"
|
||||
);
|
||||
const { Utils } = ChromeUtils.import("resource://services-sync/util.js");
|
||||
|
||||
const { createAppInfo, promiseStartupManager } = AddonTestUtils;
|
||||
@ -500,11 +503,11 @@ async function withSignedInUser(user, f) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
keys: {
|
||||
getKeys() {
|
||||
return Promise.resolve({
|
||||
kExtSync: user.kExtSync,
|
||||
kExtKbHash: user.kExtKbHash,
|
||||
});
|
||||
getKeyForScope(scope) {
|
||||
return Promise.resolve({ ...user.scopedKeys[scope] });
|
||||
},
|
||||
kidAsHex(jwk) {
|
||||
return new FxAccountsKeys({}).kidAsHex(jwk);
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -628,16 +631,18 @@ const assertExtensionRecord = async function(fxaService, post, extension, key) {
|
||||
const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
|
||||
const defaultExtension = { id: defaultExtensionId };
|
||||
|
||||
const kExtSync =
|
||||
"63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b31";
|
||||
const KB_HASH =
|
||||
"2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc800";
|
||||
const loggedInUser = {
|
||||
uid: "0123456789abcdef0123456789abcdef",
|
||||
kExtSync,
|
||||
kExtKbHash: KB_HASH,
|
||||
scopedKeys: {
|
||||
"sync:addon_storage": {
|
||||
kid: "1234567890123-I1DLqPztWi-647HxgLr4YPePZUK-975wn9qWzT49yAA",
|
||||
k:
|
||||
"Y_kFdXfAS7u58MP9hbXUAytg4T7cH43TCb9DBdZvLMMS3eFs5GAhpJb3E5UNCmxWbOGBUhpEcm576Xz1d7MbMQ",
|
||||
kty: "oct",
|
||||
},
|
||||
},
|
||||
oauthTokens: {
|
||||
"sync:addon-storage": {
|
||||
"sync:addon_storage": {
|
||||
token: "some-access-token",
|
||||
},
|
||||
},
|
||||
@ -1316,14 +1321,16 @@ add_task(async function checkSyncKeyRing_reuploads_keys() {
|
||||
});
|
||||
|
||||
// The user changes their password. This is their new kbHash, with
|
||||
// the last 0 changed to a 1.
|
||||
const NEW_KB_HASH =
|
||||
"2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc801";
|
||||
const NEW_KEXT =
|
||||
"63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b30";
|
||||
// the last character changed.
|
||||
const newUser = Object.assign({}, loggedInUser, {
|
||||
kExtKbHash: NEW_KB_HASH,
|
||||
kExtSync: NEW_KEXT,
|
||||
scopedKeys: {
|
||||
"sync:addon_storage": {
|
||||
kid: "1234567890123-I1DLqPztWi-647HxgLr4YPePZUK-975wn9qWzT49yAE",
|
||||
k:
|
||||
"Y_kFdXfAS7u58MP9hbXUAytg4T7cH43TCb9DBdZvLMMS3eFs5GAhpJb3E5UNCmxWbOGBUhpEcm576Xz1d7MbMA",
|
||||
kty: "oct",
|
||||
},
|
||||
},
|
||||
});
|
||||
let postedKeys;
|
||||
await withSignedInUser(newUser, async function(
|
||||
@ -1386,14 +1393,16 @@ add_task(async function checkSyncKeyRing_overwrites_on_conflict() {
|
||||
await withSyncContext(async function(context) {
|
||||
await withServer(async function(server) {
|
||||
// The old device has this kbHash, which is very similar to the
|
||||
// current kbHash but with the last 0 changed to a 1.
|
||||
const NEW_KB_HASH =
|
||||
"2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc801";
|
||||
const NEW_KEXT =
|
||||
"63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b30";
|
||||
// current kbHash but with the last character changed.
|
||||
const oldUser = Object.assign({}, loggedInUser, {
|
||||
kExtKbHash: NEW_KB_HASH,
|
||||
kExtSync: NEW_KEXT,
|
||||
scopedKeys: {
|
||||
"sync:addon_storage": {
|
||||
kid: "1234567890123-I1DLqPztWi-647HxgLr4YPePZUK-975wn9qWzT49yAE",
|
||||
k:
|
||||
"Y_kFdXfAS7u58MP9hbXUAytg4T7cH43TCb9DBdZvLMMS3eFs5GAhpJb3E5UNCmxWbOGBUhpEcm576Xz1d7MbMA",
|
||||
kty: "oct",
|
||||
},
|
||||
},
|
||||
});
|
||||
server.installDeleteBucket();
|
||||
await withSignedInUser(oldUser, async function(
|
||||
|
Loading…
Reference in New Issue
Block a user