--- a/browser/components/uitour/test/browser_fxa.js
+++ b/browser/components/uitour/test/browser_fxa.js
@@ -52,18 +52,20 @@ var tests = [
// interfere with the tests.
function setSignedInUser(data) {
if (!data) {
data = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
}
return fxAccounts.setSignedInUser(data);
}
function signOut() {
// we always want a "localOnly" signout here...
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/FxAccountsProfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://services-sync/util.js");
// All properties exposed by the public FxAccounts API.
var publicProperties = [
"accountStatus",
+ "canGetKeys",
"checkVerificationStatus",
"getAccountsClient",
"getAssertion",
"getDeviceId",
"getDeviceList",
"getKeys",
"getOAuthToken",
"getProfileCache",
@@ -514,18 +515,20 @@ FxAccountsInternal.prototype = {
* Get the user currently signed in to Firefox Accounts.
*
* @return Promise
* The promise resolves to the credentials object of the signed-in user:
* {
* email: The user's email address
* uid: The user's unique id
* sessionToken: Session for the FxA server
- * kA: An encryption key from the FxA server
- * kB: An encryption key derived from the user's FxA password
+ * 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
* verified: email verification status
* authAt: The time (seconds since epoch) that this record was
* authenticated
* }
* or null if no user is signed in.
*/
getSignedInUser: function getSignedInUser() {
let currentState = this.currentAccountState;
@@ -893,120 +896,209 @@ FxAccountsInternal.prototype = {
* the server.
*/
async hasLocalSession() {
let data = await this.getSignedInUser();
return data && data.sessionToken;
},
/**
+ * Checks if we currently have encryption keys or if we have enough to
+ * be able to successfully fetch them for the signed-in-user.
+ */
+ async canGetKeys() {
+ let currentState = this.currentAccountState;
+ let userData = await currentState.getUserAccountData();
+ if (!userData) {
+ throw new Error("Can't possibly get keys; User is not signed in");
+ }
+ // - 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);
+ },
+
+ /**
* 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.
*
* Returns user data so that it can be chained with other methods.
*
* @return Promise
* The promise resolves to the credentials object of the signed-in user:
* {
* email: The user's email address
* uid: The user's unique id
* sessionToken: Session for the FxA server
- * kA: An encryption key from the FxA server
- * kB: An encryption key derived from the user's FxA password
+ * 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
* verified: email verification status
* }
* or null if no user is signed in
*/
- getKeys() {
+ async getKeys() {
let currentState = this.currentAccountState;
- return currentState.getUserAccountData().then((userData) => {
+ try {
+ let userData = await currentState.getUserAccountData();
if (!userData) {
throw new Error("Can't get keys; User is not signed in");
}
- if (userData.kA && userData.kB) {
- return userData;
+ if (userData.kB) { // Bug 1426306 - Migrate from kB to derived keys.
+ log.info("Migrating kB to derived keys.");
+ const {uid, kB} = userData;
+ await this.updateUserAccountData({
+ uid,
+ ...this._deriveKeys(uid, CommonUtils.hexToBytes(kB)),
+ kA: null, // Remove kA and kB from storage.
+ kB: null
+ });
+ userData = await this.getUserAccountData();
+ }
+ if (DERIVED_KEYS_NAMES.every(k => userData[k])) {
+ return currentState.resolve(userData);
}
if (!currentState.whenKeysReadyDeferred) {
currentState.whenKeysReadyDeferred = PromiseUtils.defer();
if (userData.keyFetchToken) {
this.fetchAndUnwrapKeys(userData.keyFetchToken).then(
(dataWithKeys) => {
- if (!dataWithKeys.kA || !dataWithKeys.kB) {
+ if (DERIVED_KEYS_NAMES.some(k => !userData[k])) {
+ const missing = DERIVED_KEYS_NAMES.filter(k => !userData[k]);
currentState.whenKeysReadyDeferred.reject(
- new Error("user data missing kA or kB")
+ new Error(`user data missing: ${missing.join(", ")}`)
);
return;
}
currentState.whenKeysReadyDeferred.resolve(dataWithKeys);
},
(err) => {
currentState.whenKeysReadyDeferred.reject(err);
}
);
} else {
currentState.whenKeysReadyDeferred.reject("No keyFetchToken");
}
}
- return currentState.whenKeysReadyDeferred.promise;
- }).catch(err =>
- this._handleTokenError(err)
- ).then(result => currentState.resolve(result));
- },
+ return await currentState.resolve(currentState.whenKeysReadyDeferred.promise);
+ } catch (err) {
+ return currentState.resolve(this._handleTokenError(err));
+ }
+ },
async fetchAndUnwrapKeys(keyFetchToken) {
if (logPII) {
log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
}
let currentState = this.currentAccountState;
// Sign out if we don't have a key fetch token.
if (!keyFetchToken) {
log.warn("improper fetchAndUnwrapKeys() call: token missing");
await this.signOut();
return currentState.resolve(null);
}
- let {kA, wrapKB} = await this.fetchKeys(keyFetchToken);
+ let {wrapKB} = await this.fetchKeys(keyFetchToken);
let data = await currentState.getUserAccountData();
// Sanity check that the user hasn't changed out from under us
if (data.keyFetchToken !== keyFetchToken) {
throw new Error("Signed in user changed while fetching keys!");
}
// Next statements must be synchronous until we setUserAccountData
// so that we don't risk getting into a weird state.
- let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
- wrapKB);
+ let kBbytes = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
+ wrapKB);
if (logPII) {
- log.debug("kB_hex: " + kB_hex);
+ log.debug("kBbytes: " + kBbytes);
}
let updateData = {
- kA: CommonUtils.bytesAsHex(kA),
- kB: CommonUtils.bytesAsHex(kB_hex),
+ ...this._deriveKeys(data.uid, kBbytes),
keyFetchToken: null, // null values cause the item to be removed.
unwrapBKey: null,
};
- log.debug("Keys Obtained: kA=" + !!updateData.kA + ", kB=" + !!updateData.kB);
+ log.debug("Keys Obtained:" +
+ DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", "));
if (logPII) {
- log.debug("Keys Obtained: kA=" + updateData.kA + ", kB=" + updateData.kB);
+ log.debug("Keys Obtained:" +
+ DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", "));
}
await currentState.updateUserAccountData(updateData);
// We are now ready for business. This should only be invoked once
// per setSignedInUser(), regardless of whether we've rebooted since
// setSignedInUser() was called.
await this.notifyObservers(ONVERIFIED_NOTIFICATION);
data = await currentState.getUserAccountData();
return currentState.resolve(data);
},
+ _deriveKeys(uid, kBbytes) {
+ return {
+ kSync: CommonUtils.bytesAsHex(this._deriveSyncKey(kBbytes)),
+ kXCS: CommonUtils.bytesAsHex(this._deriveXClientState(kBbytes)),
+ kExtSync: CommonUtils.bytesAsHex(this._deriveWebExtSyncStoreKey(kBbytes)),
+ kExtKbHash: CommonUtils.bytesAsHex(this._deriveWebExtKbHash(uid, kBbytes)),
+ };
+ },
+
+ /**
+ * Derive the Sync Key given the byte string kB.
+ *
+ * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
+ */
+ _deriveSyncKey(kBbytes) {
+ return CryptoUtils.hkdf(kBbytes, undefined,
+ "identity.mozilla.com/picl/v1/oldsync", 2 * 32);
+ },
+
+ /**
+ * Derive the WebExtensions Sync Storage Key given the byte string kB.
+ *
+ * @returns HKDF(kB, undefined, "identity.mozilla.com/picl/v1/chrome.storage.sync", 64)
+ */
+ _deriveWebExtSyncStoreKey(kBbytes) {
+ return CryptoUtils.hkdf(kBbytes, undefined,
+ "identity.mozilla.com/picl/v1/chrome.storage.sync",
+ 2 * 32);
+ },
+
+ /**
+ * Derive the WebExtensions kbHash given the byte string kB.
+ *
+ * @returns SHA256(uid + kB)
+ */
+ _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);
+ hasher.init(hasher.SHA256);
+ return CryptoUtils.digestBytes(bytes, hasher);
+ },
+
async getAssertionFromCert(data, keyPair, cert, audience) {
log.debug("getAssertionFromCert");
let options = {
duration: ASSERTION_LIFETIME,
localtimeOffsetMsec: this.localtimeOffsetMsec,
now: this.now()
};
let currentState = this.currentAccountState;
@@ -1155,18 +1247,18 @@ FxAccountsInternal.prototype = {
if (logPII) {
log.debug("startVerifiedCheck with user data", data);
}
// Get us to the verified state, then get the keys. This returns a promise
// that will fire when we are completely ready.
//
// Login is truly complete once keys have been fetched, so once getKeys()
- // obtains and stores kA and kB, it will fire the onverified observer
- // notification.
+ // obtains and stores kSync kXCS kExtSync and kExtKbHash, it will fire the
+ // onverified observer notification.
// The callers of startVerifiedCheck never consume a returned promise (ie,
// this is simply kicking off a background fetch) so we must add a rejection
// handler to avoid runtime warnings about the rejection not being handled.
this.whenVerified(data).then(
() => this.getKeys(),
err => log.info("startVerifiedCheck promise was rejected: " + err)
);
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -200,30 +200,32 @@ exports.ERROR_INVALID_CONTENT_TYPE
exports.ERROR_NO_ACCOUNT = "NO_ACCOUNT";
exports.ERROR_AUTH_ERROR = "AUTH_ERROR";
exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER";
// Status code errors
exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405;
exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
+exports.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.
// In order to prevent new fields accidentally ending up in the "wrong" place,
// all fields stored are listed here.
// The fields we save in the plaintext JSON.
// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile",
"deviceId", "deviceRegistrationVersion", "profileCache"]);
// Fields we store in secure storage if it exists.
exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
- ["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
+ [...exports.DERIVED_KEYS_NAMES, "keyFetchToken", "unwrapBKey", "assertion"]);
// Fields we keep in memory and don't persist anywhere.
exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
["cert", "keyPair"]);
// A whitelist of fields that remain in storage when the user needs to
// reauthenticate. All other fields will be removed.
exports.FXA_PWDMGR_REAUTH_WHITELIST = new Set(
--- a/services/fxaccounts/FxAccountsStorage.jsm
+++ b/services/fxaccounts/FxAccountsStorage.jsm
@@ -209,31 +209,28 @@ this.FxAccountsStorageManager.prototype
throw new Error("No user is logged in");
}
if (!newFields || "uid" in newFields) {
throw new Error("Can't change uid");
}
log.debug("_updateAccountData with items", Object.keys(newFields));
// work out what bucket.
for (let [name, value] of Object.entries(newFields)) {
- if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
- if (value == null) {
- delete this.cachedMemory[name];
- } else {
- this.cachedMemory[name] = value;
- }
+ if (value == null) {
+ delete this.cachedMemory[name];
+ delete this.cachedPlain[name];
+ // no need to do the "delete on null" thing for this.cachedSecure -
+ // we need to keep it until we have managed to read so we can nuke
+ // it on write.
+ this.cachedSecure[name] = null;
+ } else if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
+ this.cachedMemory[name] = value;
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
- if (value == null) {
- delete this.cachedPlain[name];
- } else {
- this.cachedPlain[name] = value;
- }
+ this.cachedPlain[name] = value;
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
- // don't do the "delete on null" thing here - we need to keep it until
- // we have managed to read so we can nuke it on write.
this.cachedSecure[name] = value;
} else {
// Throwing seems reasonable here as some client code has explicitly
// specified the field name, so it's either confused or needs to update
// how this field is to be treated.
throw new Error("unexpected field '" + name + "'");
}
}
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -213,40 +213,48 @@ add_task(async function test_non_https_r
add_task(async function test_get_signed_in_user_initially_unset() {
_("Check getSignedInUser initially and after signout reports no user");
let account = MakeFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
let result = await account.getSignedInUser();
Assert.equal(result, null);
await account.setSignedInUser(credentials);
let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED");
Assert.equal(histogram.snapshot().sum, 1);
histogram.clear();
result = await account.getSignedInUser();
Assert.equal(result.email, credentials.email);
Assert.equal(result.assertion, credentials.assertion);
- Assert.equal(result.kB, credentials.kB);
+ Assert.equal(result.kSync, credentials.kSync);
+ Assert.equal(result.kXCS, credentials.kXCS);
+ Assert.equal(result.kExtSync, credentials.kExtSync);
+ Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
// Delete the memory cache and force the user
// to be read and parsed from storage (e.g. disk via JSONStorage).
delete account.internal.signedInUser;
result = await account.getSignedInUser();
Assert.equal(result.email, credentials.email);
Assert.equal(result.assertion, credentials.assertion);
- Assert.equal(result.kB, credentials.kB);
+ Assert.equal(result.kSync, credentials.kSync);
+ Assert.equal(result.kXCS, credentials.kXCS);
+ Assert.equal(result.kExtSync, credentials.kExtSync);
+ Assert.equal(result.kExtKbHash, credentials.kExtKbHash);
// sign out
let localOnly = true;
await account.signOut(localOnly);
// user should be undefined after sign out
result = await account.getSignedInUser();
Assert.equal(result, null);
@@ -256,18 +264,20 @@ add_task(async function test_set_signed_
_("Check setSignedInUser tries to delete a previous registered device");
let account = MakeFxAccounts();
let deleteDeviceRegistrationCalled = false;
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
await account.setSignedInUser(credentials);
account.internal.deleteDeviceRegistration = () => {
deleteDeviceRegistrationCalled = true;
return Promise.resolve(true);
};
@@ -279,18 +289,20 @@ add_task(async function test_set_signed_
add_task(async function test_update_account_data() {
_("Check updateUserAccountData does the right thing.");
let account = MakeFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
await account.setSignedInUser(credentials);
let newCreds = {
email: credentials.email,
uid: credentials.uid,
assertion: "new_assertion",
@@ -638,38 +650,64 @@ add_test(function test_getKeys() {
let user = getTestUser("eusebius");
// Once email has been verified, we will be able to get keys
user.verified = true;
fxa.setSignedInUser(user).then(() => {
fxa.getSignedInUser().then((user2) => {
// Before getKeys, we have no keys
- Assert.equal(!!user2.kA, false);
- Assert.equal(!!user2.kB, false);
+ Assert.equal(!!user2.kSync, false);
+ Assert.equal(!!user2.kXCS, false);
+ Assert.equal(!!user2.kExtSync, false);
+ Assert.equal(!!user2.kExtKbHash, false);
// And we still have a key-fetch token and unwrapBKey to use
Assert.equal(!!user2.keyFetchToken, true);
Assert.equal(!!user2.unwrapBKey, true);
fxa.internal.getKeys().then(() => {
fxa.getSignedInUser().then((user3) => {
// Now we should have keys
Assert.equal(fxa.internal.isUserEmailVerified(user3), true);
Assert.equal(!!user3.verified, true);
- Assert.equal(user3.kA, expandHex("11"));
- Assert.equal(user3.kB, expandHex("66"));
+ Assert.notEqual(null, user3.kSync);
+ Assert.notEqual(null, user3.kXCS);
+ Assert.notEqual(null, user3.kExtSync);
+ Assert.notEqual(null, user3.kExtKbHash);
Assert.equal(user3.keyFetchToken, undefined);
Assert.equal(user3.unwrapBKey, undefined);
run_next_test();
});
});
});
});
});
+add_task(async function test_getKeys_kb_migration() {
+ let fxa = new MockFxAccounts();
+ let user = getTestUser("eusebius");
+
+ user.verified = true;
+ // Set-up the deprecated set of keys.
+ user.kA = "e0245ab7f10e483470388e0a28f0a03379a3b417174fb2b42feab158b4ac2dbd";
+ user.kB = "eaf9570b7219a4187d3d6bf3cec2770c2e0719b7cc0dfbb38243d6f1881675e9";
+
+ await fxa.setSignedInUser(user);
+ await fxa.internal.getKeys();
+ let newUser = await fxa.getSignedInUser();
+ Assert.equal(newUser.kA, null);
+ Assert.equal(newUser.kB, null);
+ Assert.equal(newUser.kSync, "0d6fe59791b05fa489e463ea25502e3143f6b7a903aa152e95cd9c6eddbac5b4" +
+ "dc68a19097ef65dbd147010ee45222444e66b8b3d7c8a441ebb7dd3dce015a9e");
+ Assert.equal(newUser.kXCS, "22a42fe289dced5715135913424cb23b");
+ Assert.equal(newUser.kExtSync, "baded53eb3587d7900e604e8a68d860abf9de30b5c955d3c4d5dba63f26fd882" +
+ "65cd85923f6e9dcd16aef3b82bc88039a89c59ecd9e88de09a7418c7d94f90c9");
+ Assert.equal(newUser.kExtKbHash, "25ed0ab3ae2f1e5365d923c9402d4255770dbe6ce79b09ed49f516985c0aa0c1");
+});
+
add_task(async function test_getKeys_nonexistent_account() {
let fxa = new MockFxAccounts();
let bismarck = getTestUser("bismarck");
let client = fxa.internal.fxAccountsClient;
client.accountStatus = () => Promise.resolve(false);
client.accountKeys = () => {
return Promise.reject({
@@ -796,18 +834,20 @@ add_test(function test_overlapping_signi
add_task(async function test_getAssertion_invalid_token() {
let fxa = new MockFxAccounts();
let client = fxa.internal.fxAccountsClient;
client.accountStatus = () => Promise.resolve(true);
let creds = {
sessionToken: "sessionToken",
- kA: expandHex("11"),
- kB: expandHex("66"),
+ kSync: expandHex("11"),
+ kXCS: expandHex("66"),
+ kExtSync: expandHex("88"),
+ kExtKbHash: expandHex("22"),
verified: true,
email: "sonia@example.com",
};
await fxa.setSignedInUser(creds);
// we have what we still believe to be a valid session token, so we should
// consider that we have a local session.
Assert.ok(await fxa.hasLocalSession());
@@ -834,21 +874,23 @@ add_task(async function test_getAssertio
let fxa = new MockFxAccounts();
do_check_throws(async function() {
await fxa.getAssertion("nonaudience");
});
let creds = {
sessionToken: "sessionToken",
- kA: expandHex("11"),
- kB: expandHex("66"),
+ kSync: expandHex("11"),
+ kXCS: expandHex("66"),
+ kExtSync: expandHex("88"),
+ kExtKbHash: expandHex("22"),
verified: true
};
- // By putting kA/kB/verified in "creds", we skip ahead
+ // By putting kSync/kXCS/kExtSync/kExtKbHash/verified in "creds", we skip ahead
// to the "we're ready" stage.
await fxa.setSignedInUser(creds);
_("== ready to go\n");
// Start with a nice arbitrary but realistic date. Here we use a nice RFC
// 1123 date string like we would get from an HTTP header. Over the course of
// the test, we will update 'now', but leave 'start' where it is.
let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
@@ -1546,16 +1588,33 @@ add_task(async function test_checkVerifi
await fxa.checkVerificationStatus();
user = await fxa.internal.getUserAccountData();
Assert.equal(user.email, alice.email);
Assert.equal(user.sessionToken, null);
});
+add_test(function test_deriveKeys() {
+ let account = MakeFxAccounts();
+ let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
+ let kB = CommonUtils.hexToBytes(kBhex);
+ const uid = "1ad7f502-4cc7-4ec1-a209-071fd2fae348";
+
+ const {kSync, kXCS, kExtSync, kExtKbHash} = account.internal._deriveKeys(uid, kB);
+
+ Assert.equal(kSync, "ad501a50561be52b008878b2e0d8a73357778a712255f7722f497b5d4df14b05" +
+ "dc06afb836e1521e882f521eb34691d172337accdbf6e2a5b968b05a7bbb9885");
+ Assert.equal(kXCS, "6ae94683571c7a7c54dab4700aa3995f");
+ Assert.equal(kExtSync, "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
+ "5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395");
+ Assert.equal(kExtKbHash, "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273");
+ run_next_test();
+});
+
/*
* End of tests.
* Utility functions follow.
*/
function expandHex(two_hex) {
// Return a 64-character hex string, encoding 32 identical bytes.
let eight_hex = two_hex + two_hex + two_hex + two_hex;
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -58,58 +58,66 @@ function createFxAccounts() {
add_task(async function test_simple() {
let fxa = createFxAccounts();
let creds = {
uid: "abcd",
email: "test@example.com",
sessionToken: "sessionToken",
- kA: "the kA value",
- kB: "the kB value",
+ kSync: "the kSync value",
+ kXCS: "the kXCS value",
+ kExtSync: "the kExtSync value",
+ kExtKbHash: "the kExtKbHash value",
verified: true
};
await fxa.setSignedInUser(creds);
// This should have stored stuff in both the .json file in the profile
// dir, and the login dir.
let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
let data = await CommonUtils.readJSON(path);
Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
- Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
- Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
+ Assert.ok(!("kSync" in data.accountData), "kSync not stored in clear text");
+ Assert.ok(!("kXCS" in data.accountData), "kXCS not stored in clear text");
+ Assert.ok(!("kExtSync" in data.accountData), "kExtSync not stored in clear text");
+ Assert.ok(!("kExtKbHash" in data.accountData), "kExtKbHash not stored in clear text");
let login = getLoginMgrData();
Assert.strictEqual(login.username, creds.uid, "uid used for username");
let loginData = JSON.parse(login.password);
Assert.strictEqual(loginData.version, data.version, "same version flag in both places");
- Assert.strictEqual(loginData.accountData.kA, creds.kA, "correct kA in the login mgr");
- Assert.strictEqual(loginData.accountData.kB, creds.kB, "correct kB in the login mgr");
+ Assert.strictEqual(loginData.accountData.kSync, creds.kSync, "correct kSync in the login mgr");
+ Assert.strictEqual(loginData.accountData.kXCS, creds.kXCS, "correct kXCS in the login mgr");
+ Assert.strictEqual(loginData.accountData.kExtSync, creds.kExtSync, "correct kExtSync in the login mgr");
+ Assert.strictEqual(loginData.accountData.kExtKbHash, creds.kExtKbHash, "correct kExtKbHash in the login mgr");
Assert.ok(!("email" in loginData), "email not stored in the login mgr json");
Assert.ok(!("sessionToken" in loginData), "sessionToken not stored in the login mgr json");
Assert.ok(!("verified" in loginData), "verified not stored in the login mgr json");
await fxa.signOut(/* localOnly = */ true);
Assert.strictEqual(getLoginMgrData(), null, "login mgr data deleted on logout");
});
add_task(async function test_MPLocked() {
let fxa = createFxAccounts();
let creds = {
uid: "abcd",
email: "test@example.com",
sessionToken: "sessionToken",
- kA: "the kA value",
- kB: "the kB value",
+ kSync: "the kSync value",
+ kXCS: "the kXCS value",
+ kExtSync: "the kExtSync value",
+ kExtKbHash: "the kExtKbHash value",
verified: true
};
Assert.strictEqual(getLoginMgrData(), null, "no login mgr at the start");
// tell the storage that the MP is locked.
setLoginMgrLoggedInState(false);
await fxa.setSignedInUser(creds);
@@ -117,84 +125,90 @@ add_task(async function test_MPLocked()
// will not exist.
let path = OS.Path.join(OS.Constants.Path.profileDir, "signedInUser.json");
let data = await CommonUtils.readJSON(path);
Assert.strictEqual(data.accountData.email, creds.email, "correct email in the clear text");
Assert.strictEqual(data.accountData.sessionToken, creds.sessionToken, "correct sessionToken in the clear text");
Assert.strictEqual(data.accountData.verified, creds.verified, "correct verified flag");
- Assert.ok(!("kA" in data.accountData), "kA not stored in clear text");
- Assert.ok(!("kB" in data.accountData), "kB not stored in clear text");
+ Assert.ok(!("kSync" in data.accountData), "kSync not stored in clear text");
+ Assert.ok(!("kXCS" in data.accountData), "kXCS not stored in clear text");
+ Assert.ok(!("kExtSync" in data.accountData), "kExtSync not stored in clear text");
+ Assert.ok(!("kExtKbHash" in data.accountData), "kExtKbHash not stored in clear text");
Assert.strictEqual(getLoginMgrData(), null, "login mgr data doesn't exist");
await fxa.signOut(/* localOnly = */ true);
});
add_task(async function test_consistentWithMPEdgeCases() {
setLoginMgrLoggedInState(true);
let fxa = createFxAccounts();
let creds1 = {
uid: "uid1",
email: "test@example.com",
sessionToken: "sessionToken",
- kA: "the kA value",
- kB: "the kB value",
+ kSync: "the kSync value",
+ kXCS: "the kXCS value",
+ kExtSync: "the kExtSync value",
+ kExtKbHash: "the kExtKbHash value",
verified: true
};
let creds2 = {
uid: "uid2",
email: "test2@example.com",
sessionToken: "sessionToken2",
- kA: "the kA value2",
- kB: "the kB value2",
+ kSync: "the kSync value2",
+ kXCS: "the kXCS value2",
+ kExtSync: "the kExtSync value2",
+ kExtKbHash: "the kExtKbHash value2",
verified: false,
};
// Log a user in while MP is unlocked.
await fxa.setSignedInUser(creds1);
// tell the storage that the MP is locked - this will prevent logout from
// being able to clear the data.
setLoginMgrLoggedInState(false);
// now set the second credentials.
await fxa.setSignedInUser(creds2);
// We should still have creds1 data in the login manager.
let login = getLoginMgrData();
Assert.strictEqual(login.username, creds1.uid);
- // and that we do have the first kA in the login manager.
- Assert.strictEqual(JSON.parse(login.password).accountData.kA, creds1.kA,
+ // and that we do have the first kSync in the login manager.
+ Assert.strictEqual(JSON.parse(login.password).accountData.kSync, creds1.kSync,
"stale data still in login mgr");
// Make a new FxA instance (otherwise the values in memory will be used)
// and we want the login manager to be unlocked.
setLoginMgrLoggedInState(true);
fxa = createFxAccounts();
let accountData = await fxa.getSignedInUser();
Assert.strictEqual(accountData.email, creds2.email);
- // we should have no kA at all.
- Assert.strictEqual(accountData.kA, undefined, "stale kA wasn't used");
+ // we should have no kSync at all.
+ Assert.strictEqual(accountData.kSync, undefined, "stale kSync wasn't used");
await fxa.signOut(/* localOnly = */ true);
});
// A test for the fact we will accept either a UID or email when looking in
// the login manager.
add_task(async function test_uidMigration() {
setLoginMgrLoggedInState(true);
Assert.strictEqual(getLoginMgrData(), null, "expect no logins at the start");
// create the login entry using email as a key.
- let contents = {kA: "kA"};
+ let contents = {kSync: "kSync"};
let loginInfo = new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
let login = new loginInfo(FXA_PWDMGR_HOST,
null, // aFormSubmitURL,
FXA_PWDMGR_REALM, // aHttpRealm,
"foo@bar.com", // aUsername
JSON.stringify(contents), // aPassword
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -106,18 +106,20 @@ function MockFxAccounts(device = {}) {
async function createMockFxA() {
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
await fxa.setSignedInUser(credentials);
return fxa;
}
// The tests.
--- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -110,18 +110,20 @@ function MockFxAccounts(mockGrantClient)
async function createMockFxA(mockGrantClient) {
let fxa = new MockFxAccounts(mockGrantClient);
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
- kA: "beef",
- kB: "cafe",
+ kSync: "beef",
+ kXCS: "cafe",
+ kExtSync: "bacon",
+ kExtKbHash: "cheese",
verified: true
};
await fxa.setSignedInUser(credentials);
return fxa;
}
// The tests.
--- a/services/fxaccounts/tests/xpcshell/test_storage_manager.js
+++ b/services/fxaccounts/tests/xpcshell/test_storage_manager.js
@@ -87,48 +87,48 @@ function add_storage_task(testFunction)
// initialized without account data and there's nothing to read. Not logged in.
add_storage_task(async function checkInitializedEmpty(sm) {
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize();
Assert.strictEqual((await sm.getAccountData()), null);
- Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in");
+ Assert.rejects(sm.updateAccountData({kXCS: "kXCS"}), "No user is logged in");
});
// Initialized with account data (ie, simulating a new user being logged in).
// Should reflect the initial data and be written to storage.
add_storage_task(async function checkNewUser(sm) {
let initialAccountData = {
uid: "uid",
email: "someone@somewhere.com",
- kA: "kA",
+ kXCS: "kXCS",
deviceId: "device id"
};
sm.plainStorage = new MockedPlainStorage();
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
await sm.initialize(initialAccountData);
let accountData = await sm.getAccountData();
Assert.equal(accountData.uid, initialAccountData.uid);
Assert.equal(accountData.email, initialAccountData.email);
- Assert.equal(accountData.kA, initialAccountData.kA);
+ Assert.equal(accountData.kXCS, initialAccountData.kXCS);
Assert.equal(accountData.deviceId, initialAccountData.deviceId);
// and it should have been written to storage.
Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid);
Assert.equal(sm.plainStorage.data.accountData.email, initialAccountData.email);
Assert.equal(sm.plainStorage.data.accountData.deviceId, initialAccountData.deviceId);
// check secure
if (sm.secureStorage) {
- Assert.equal(sm.secureStorage.data.accountData.kA, initialAccountData.kA);
+ Assert.equal(sm.secureStorage.data.accountData.kXCS, initialAccountData.kXCS);
} else {
- Assert.equal(sm.plainStorage.data.accountData.kA, initialAccountData.kA);
+ Assert.equal(sm.plainStorage.data.accountData.kXCS, initialAccountData.kXCS);
}
});
// Initialized without account data but storage has it available.
add_storage_task(async function checkEverythingRead(sm) {
sm.plainStorage = new MockedPlainStorage({
uid: "uid",
email: "someone@somewhere.com",
@@ -144,73 +144,103 @@ add_storage_task(async function checkEve
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.equal(accountData.deviceId, "wibble");
Assert.equal(accountData.deviceRegistrationVersion, null);
// Update the data - we should be able to fetch it back and it should appear
// in our storage.
await sm.updateAccountData({
verified: true,
- kA: "kA",
- kB: "kB",
+ kSync: "kSync",
+ kXCS: "kXCS",
+ kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash",
deviceRegistrationVersion: DEVICE_REGISTRATION_VERSION
});
accountData = await sm.getAccountData();
- Assert.equal(accountData.kB, "kB");
- Assert.equal(accountData.kA, "kA");
+ Assert.equal(accountData.kSync, "kSync");
+ Assert.equal(accountData.kXCS, "kXCS");
+ Assert.equal(accountData.kExtSync, "kExtSync");
+ Assert.equal(accountData.kExtKbHash, "kExtKbHash");
Assert.equal(accountData.deviceId, "wibble");
Assert.equal(accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
// Check the new value was written to storage.
await sm._promiseStorageComplete; // storage is written in the background.
// "verified", "deviceId" and "deviceRegistrationVersion" are plain-text fields.
Assert.equal(sm.plainStorage.data.accountData.verified, true);
Assert.equal(sm.plainStorage.data.accountData.deviceId, "wibble");
Assert.equal(sm.plainStorage.data.accountData.deviceRegistrationVersion, DEVICE_REGISTRATION_VERSION);
- // "kA" and "foo" are secure
+ // derive keys are secure
if (sm.secureStorage) {
- Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
- Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
+ Assert.equal(sm.secureStorage.data.accountData.kExtKbHash, "kExtKbHash");
+ Assert.equal(sm.secureStorage.data.accountData.kExtSync, "kExtSync");
+ Assert.equal(sm.secureStorage.data.accountData.kXCS, "kXCS");
+ Assert.equal(sm.secureStorage.data.accountData.kSync, "kSync");
} else {
- Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
- Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
+ Assert.equal(sm.plainStorage.data.accountData.kExtKbHash, "kExtKbHash");
+ Assert.equal(sm.plainStorage.data.accountData.kExtSync, "kExtSync");
+ Assert.equal(sm.plainStorage.data.accountData.kXCS, "kXCS");
+ Assert.equal(sm.plainStorage.data.accountData.kSync, "kSync");
}
});
add_storage_task(function checkInvalidUpdates(sm) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
if (sm.secureStorage) {
sm.secureStorage = new MockedSecureStorage(null);
}
Assert.rejects(sm.updateAccountData({uid: "another"}), "Can't change");
Assert.rejects(sm.updateAccountData({email: "someoneelse"}), "Can't change");
});
add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+ sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash"});
} else {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
- kA: "kA", kB: "kB"});
+ kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash"});
}
await sm.initialize();
- await sm.updateAccountData({kA: null});
+ await sm.updateAccountData({kXCS: null});
+ let accountData = await sm.getAccountData();
+ Assert.ok(!accountData.kXCS);
+ Assert.equal(accountData.kSync, "kSync");
+});
+
+add_storage_task(async function checkNullRemovesUnlistedFields(sm) {
+ // kA and kB are not listed in FXA_PWDMGR_*_FIELDS, but we still want to
+ // be able to delete them (migration case).
+ if (sm.secureStorage) {
+ sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
+ sm.secureStorage = new MockedSecureStorage({kA: "kA", kb: "kB"});
+ } else {
+ sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
+ kA: "kA", kb: "kB"});
+ }
+ await sm.initialize();
+
+ await sm.updateAccountData({kA: null, kB: null});
let accountData = await sm.getAccountData();
Assert.ok(!accountData.kA);
- Assert.equal(accountData.kB, "kB");
+ Assert.ok(!accountData.kB);
});
add_storage_task(async function checkDelete(sm) {
if (sm.secureStorage) {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+ sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash"});
} else {
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com",
- kA: "kA", kB: "kB"});
+ kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash"});
}
await sm.initialize();
await sm.deleteAccountData();
// Storage should have been reset to null.
Assert.equal(sm.plainStorage.data, null);
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data, null);
@@ -218,132 +248,133 @@ add_storage_task(async function checkDel
// And everything should reflect no user.
Assert.equal((await sm.getAccountData()), null);
});
// Some tests only for the secure storage manager.
add_task(async function checkNullUpdatesRemovedLocked() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA", kB: "kB"});
+ sm.secureStorage = new MockedSecureStorage({kSync: "kSync", kXCS: "kXCS", kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash"});
sm.secureStorage.locked = true;
await sm.initialize();
- await sm.updateAccountData({kA: null});
+ await sm.updateAccountData({kSync: null});
let accountData = await sm.getAccountData();
- Assert.ok(!accountData.kA);
- // still no kB as we are locked.
- Assert.ok(!accountData.kB);
+ Assert.ok(!accountData.kSync);
+ // still no kXCS as we are locked.
+ Assert.ok(!accountData.kXCS);
- // now unlock - should still be no kA but kB should appear.
+ // now unlock - should still be no kSync but kXCS should appear.
sm.secureStorage.locked = false;
accountData = await sm.getAccountData();
- Assert.ok(!accountData.kA);
- Assert.equal(accountData.kB, "kB");
+ Assert.ok(!accountData.kSync);
+ Assert.equal(accountData.kXCS, "kXCS");
// And secure storage should have been written with our previously-cached
// data.
- Assert.strictEqual(sm.secureStorage.data.accountData.kA, undefined);
- Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
+ Assert.strictEqual(sm.secureStorage.data.accountData.kSync, undefined);
+ Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
});
add_task(async function checkEverythingReadSecure() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+ sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
await sm.initialize();
let accountData = await sm.getAccountData();
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
- Assert.equal(accountData.kA, "kA");
+ Assert.equal(accountData.kXCS, "kXCS");
});
add_task(async function checkMemoryFieldsNotReturnedByDefault() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+ sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
await sm.initialize();
// keyPair is a memory field.
await sm.updateAccountData({keyPair: "the keypair value"});
let accountData = await sm.getAccountData();
// Requesting everything should *not* return in memory fields.
Assert.strictEqual(accountData.keyPair, undefined);
// But requesting them specifically does get them.
accountData = await sm.getAccountData("keyPair");
Assert.strictEqual(accountData.keyPair, "the keypair value");
});
add_task(async function checkExplicitGet() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+ sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
await sm.initialize();
- let accountData = await sm.getAccountData(["uid", "kA"]);
+ let accountData = await sm.getAccountData(["uid", "kXCS"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
- Assert.equal(accountData.kA, "kA");
+ Assert.equal(accountData.kXCS, "kXCS");
// We didn't ask for email so shouldn't have got it.
Assert.strictEqual(accountData.email, undefined);
});
add_task(async function checkExplicitGetNoSecureRead() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+ sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
await sm.initialize();
Assert.equal(sm.secureStorage.fetchCount, 0);
// request 2 fields in secure storage - it should have caused a single fetch.
let accountData = await sm.getAccountData(["email", "uid"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
- Assert.strictEqual(accountData.kA, undefined);
+ Assert.strictEqual(accountData.kXCS, undefined);
Assert.equal(sm.secureStorage.fetchCount, 1);
});
add_task(async function checkLockedUpdates() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "old-kA", kB: "kB"});
+ sm.secureStorage = new MockedSecureStorage({kSync: "old-kSync", kXCS: "kXCS"});
sm.secureStorage.locked = true;
await sm.initialize();
let accountData = await sm.getAccountData();
- // requesting kA and kB will fail as storage is locked.
- Assert.ok(!accountData.kA);
- Assert.ok(!accountData.kB);
+ // requesting kSync and kXCS will fail as storage is locked.
+ Assert.ok(!accountData.kSync);
+ Assert.ok(!accountData.kXCS);
// While locked we can still update it and see the updated value.
- sm.updateAccountData({kA: "new-kA"});
+ sm.updateAccountData({kSync: "new-kSync"});
accountData = await sm.getAccountData();
- Assert.equal(accountData.kA, "new-kA");
+ Assert.equal(accountData.kSync, "new-kSync");
// unlock.
sm.secureStorage.locked = false;
accountData = await sm.getAccountData();
// should reflect the value we updated and the one we didn't.
- Assert.equal(accountData.kA, "new-kA");
- Assert.equal(accountData.kB, "kB");
+ Assert.equal(accountData.kSync, "new-kSync");
+ Assert.equal(accountData.kXCS, "kXCS");
// And storage should also reflect it.
- Assert.strictEqual(sm.secureStorage.data.accountData.kA, "new-kA");
- Assert.strictEqual(sm.secureStorage.data.accountData.kB, "kB");
+ Assert.strictEqual(sm.secureStorage.data.accountData.kSync, "new-kSync");
+ Assert.strictEqual(sm.secureStorage.data.accountData.kXCS, "kXCS");
});
// Some tests for the "storage queue" functionality.
// A helper for our queued tests. It creates a StorageManager and then queues
// an unresolved promise. The tests then do additional setup and checks, then
// resolves or rejects the blocked promise.
async function setupStorageManagerForQueueTest() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"});
- sm.secureStorage = new MockedSecureStorage({kA: "kA"});
+ sm.secureStorage = new MockedSecureStorage({kXCS: "kXCS"});
sm.secureStorage.locked = true;
await sm.initialize();
let resolveBlocked, rejectBlocked;
let blockedPromise = new Promise((resolve, reject) => {
resolveBlocked = resolve;
rejectBlocked = reject;
});
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -473,18 +473,20 @@ add_test(function test_helpers_open_sync
});
add_task(async function test_helpers_getFxAStatus_extra_engines() {
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
getSignedInUser() {
return Promise.resolve({
email: "testuser@testuser.com",
- kA: "kA",
- kb: "kB",
+ kSync: "kSync",
+ kXCS: "kXCS",
+ kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash",
sessionToken: "sessionToken",
uid: "uid",
verified: true
});
}
},
privateBrowsingUtils: {
isBrowserPrivate: () => true
@@ -508,18 +510,20 @@ add_task(async function test_helpers_get
};
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
getSignedInUser() {
wasCalled.getSignedInUser = true;
return Promise.resolve({
email: "testuser@testuser.com",
- kA: "kA",
- kb: "kB",
+ kSync: "kSync",
+ kXCS: "kXCS",
+ kExtSync: "kExtSync",
+ kExtKbHash: "kExtKbHash",
sessionToken: "sessionToken",
uid: "uid",
verified: true
});
}
}
});
@@ -542,18 +546,20 @@ add_task(async function test_helpers_get
Assert.equal(signedInUser.email, "testuser@testuser.com");
Assert.equal(signedInUser.sessionToken, "sessionToken");
Assert.equal(signedInUser.uid, "uid");
Assert.ok(signedInUser.verified);
// These properties are filtered and should not
// be returned to the requester.
- Assert.equal(false, "kA" in signedInUser);
- Assert.equal(false, "kB" in signedInUser);
+ Assert.equal(false, "kSync" in signedInUser);
+ Assert.equal(false, "kXCS" in signedInUser);
+ Assert.equal(false, "kExtSync" in signedInUser);
+ Assert.equal(false, "kExtKbHash" in signedInUser);
});
});
add_task(async function test_helpers_getFxaStatus_allowed_no_signedInUser() {
let wasCalled = {
getSignedInUser: false,
shouldAllowFxaStatus: false
};
@@ -764,17 +770,17 @@ add_task(async function test_helpers_cha
updateDeviceRegistration: false
};
let helpers = new FxAccountsWebChannelHelpers({
fxAccounts: {
updateUserAccountData(credentials) {
return new Promise(resolve => {
Assert.ok(credentials.hasOwnProperty("email"));
Assert.ok(credentials.hasOwnProperty("uid"));
- Assert.ok(credentials.hasOwnProperty("kA"));
+ Assert.ok(credentials.hasOwnProperty("unwrapBKey"));
Assert.ok(credentials.hasOwnProperty("deviceId"));
Assert.equal(null, credentials.deviceId);
// "foo" isn't a field known by storage, so should be dropped.
Assert.ok(!credentials.hasOwnProperty("foo"));
wasCalled.updateUserAccountData = true;
resolve();
});
@@ -782,17 +788,17 @@ add_task(async function test_helpers_cha
updateDeviceRegistration() {
Assert.equal(arguments.length, 0);
wasCalled.updateDeviceRegistration = true;
return Promise.resolve();
}
}
});
- await helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
+ await helpers.changePassword({ email: "email", uid: "uid", unwrapBKey: "unwrapBKey", foo: "foo" });
Assert.ok(wasCalled.updateUserAccountData);
Assert.ok(wasCalled.updateDeviceRegistration);
});
add_task(async function test_helpers_change_password_with_error() {
let wasCalled = {
updateUserAccountData: false,
updateDeviceRegistration: false
--- a/services/sync/modules-testing/utils.js
+++ b/services/sync/modules-testing/utils.js
@@ -112,19 +112,21 @@ this.makeIdentityConfig = function(overr
// first setup the defaults.
let result = {
// Username used in both fxaccount and sync identity configs.
username: "foo",
// fxaccount specific credentials.
fxaccount: {
user: {
assertion: "assertion",
- email: "email",
- kA: "kA",
- kB: "kB",
+ email: "foo",
+ kSync: "a".repeat(128),
+ kXCS: "a".repeat(32),
+ kExtSync: "a".repeat(128),
+ kExtKbHash: "a".repeat(32),
sessionToken: "sessionToken",
uid: "a".repeat(32),
verified: true,
},
token: {
endpoint: null,
duration: 300,
id: "id",
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -131,26 +131,16 @@ this.telemetryHelper = {
},
// hookable by tests.
nowInMinutes() {
return Math.floor(Date.now() / 1000 / 60);
},
};
-
-function deriveKeyBundle(kB) {
- let out = CryptoUtils.hkdf(kB, undefined,
- "identity.mozilla.com/picl/v1/oldsync", 2 * 32);
- let bundle = new BulkKeyBundle();
- // [encryptionKey, hmacKey]
- bundle.keyPair = [out.slice(0, 32), out.slice(32, 64)];
- return bundle;
-}
-
/*
General authentication error for abstracting authentication
errors from multiple sources (e.g., from FxAccounts, TokenServer).
details is additional details about the error - it might be a string, or
some other error object (which should do the right thing when toString() is
called on it)
*/
function AuthenticationError(details, source) {
@@ -250,17 +240,17 @@ this.BrowserIDManager.prototype = {
// After this is called, we can expect Service.identity != this.
for (let topic of OBSERVER_TOPICS) {
Services.obs.removeObserver(this, topic);
}
this.resetCredentials();
this._signedInUser = null;
},
- initializeWithCurrentIdentity(isInitialSync = false) {
+ async initializeWithCurrentIdentity(isInitialSync = false) {
// While this function returns a promise that resolves once we've started
// the auth process, that process is complete when
// this.whenReadyToAuthenticate.promise resolves.
this._log.trace("initializeWithCurrentIdentity");
// Reset the world before we do anything async.
this.whenReadyToAuthenticate = PromiseUtils.defer();
this.whenReadyToAuthenticate.promise.catch(err => {
@@ -269,66 +259,69 @@ this.BrowserIDManager.prototype = {
// initializeWithCurrentIdentity() can be called after the
// identity module was first initialized, e.g., after the
// user completes a force authentication, so we should make
// sure all credentials are reset before proceeding.
this.resetCredentials();
this._authFailureReason = null;
- return this._fxaService.getSignedInUser().then(accountData => {
+ try {
+ let accountData = await this._fxaService.getSignedInUser();
if (!accountData) {
this._log.info("initializeWithCurrentIdentity has no user logged in");
// and we are as ready as we can ever be for auth.
this._shouldHaveSyncKeyBundle = true;
this.whenReadyToAuthenticate.reject("no user is logged in");
return;
}
this.username = accountData.email;
this._updateSignedInUser(accountData);
// The user must be verified before we can do anything at all; we kick
// this and the rest of initialization off in the background (ie, we
// don't return the promise)
- this._log.info("Waiting for user to be verified.");
- if (!accountData.verified) {
- telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
- }
- this._fxaService.whenVerified(accountData).then(accountData => {
- this._updateSignedInUser(accountData);
+ CommonUtils.nextTick(async () => {
+ try {
+ this._log.info("Waiting for user to be verified.");
+ if (!accountData.verified) {
+ telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.NOTVERIFIED);
+ }
+ accountData = await this._fxaService.whenVerified(accountData);
+ this._updateSignedInUser(accountData);
- this._log.info("Starting fetch for key bundle.");
- return this._fetchTokenForUser();
- }).then(token => {
- this._token = token;
- if (token) {
- // We may not have a token if the master-password is locked - but we
- // still treat this as "success" so we don't prompt for re-authentication.
- this._hashedUID = token.hashed_fxa_uid; // see _ensureValidToken for why we do this...
+ this._log.info("Starting fetch for key bundle.");
+ let token = await this._fetchTokenForUser();
+ this._token = token;
+ if (token) {
+ // We may not have a token if the master-password is locked - but we
+ // still treat this as "success" so we don't prompt for re-authentication.
+ this._hashedUID = token.hashed_fxa_uid; // see _ensureValidToken for why we do this...
+ }
+ this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
+ this.whenReadyToAuthenticate.resolve();
+ this._log.info("Background fetch for key bundle done");
+ Weave.Status.login = LOGIN_SUCCEEDED;
+ if (isInitialSync) {
+ this._log.info("Doing initial sync actions");
+ Svc.Prefs.set("firstSync", "resetClient");
+ Services.obs.notifyObservers(null, "weave:service:setup-complete");
+ CommonUtils.nextTick(Weave.Service.sync, Weave.Service);
+ }
+ } catch (authErr) {
+ // report what failed...
+ this._log.error("Background fetch for key bundle failed", authErr);
+ this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
+ this.whenReadyToAuthenticate.reject(authErr);
}
- this._shouldHaveSyncKeyBundle = true; // and we should actually have one...
- this.whenReadyToAuthenticate.resolve();
- this._log.info("Background fetch for key bundle done");
- Weave.Status.login = LOGIN_SUCCEEDED;
- if (isInitialSync) {
- this._log.info("Doing initial sync actions");
- Svc.Prefs.set("firstSync", "resetClient");
- Services.obs.notifyObservers(null, "weave:service:setup-complete");
- CommonUtils.nextTick(Weave.Service.sync, Weave.Service);
- }
- }).catch(authErr => {
- // report what failed...
- this._log.error("Background fetch for key bundle failed", authErr);
- this._shouldHaveSyncKeyBundle = true; // but we probably don't have one...
- this.whenReadyToAuthenticate.reject(authErr);
+ // and we are done - the fetch continues on in the background...
});
- // and we are done - the fetch continues on in the background...
- }).catch(err => {
+ } catch (err) {
this._log.error("Processing logged in account", err);
- });
+ }
},
_updateSignedInUser(userData) {
// This object should only ever be used for a single user. It is an
// error to update the data if the user changes (but updates are still
// necessary, as each call may add more attributes to the user).
// We start with no user, so an initial update is always ok.
if (this._signedInUser && this._signedInUser.email != userData.email) {
@@ -397,35 +390,16 @@ this.BrowserIDManager.prototype = {
this.resetCredentials();
this._ensureValidToken().catch(err =>
this._log.error("Error while fetching a new token", err));
break;
}
},
/**
- * Compute the sha256 of the message bytes. Return bytes.
- */
- _sha256(message) {
- let hasher = Cc["@mozilla.org/security/hash;1"]
- .createInstance(Ci.nsICryptoHash);
- hasher.init(hasher.SHA256);
- return CryptoUtils.digestBytes(message, hasher);
- },
-
- /**
- * Compute the X-Client-State header given the byte string kB.
- *
- * Return string: hex(first16Bytes(sha256(kBbytes)))
- */
- _computeXClientState(kBbytes) {
- return CommonUtils.bytesAsHex(this._sha256(kBbytes).slice(0, 16), false);
- },
-
- /**
* Provide override point for testing token expiration.
*/
_now() {
return this._fxaService.now();
},
get _localtimeOffsetMsec() {
return this._fxaService.localtimeOffsetMsec;
@@ -532,60 +506,48 @@ this.BrowserIDManager.prototype = {
// username seems to make things fail fast so that's good.
if (!this.username) {
return LOGIN_FAILED_NO_USERNAME;
}
return STATUS_OK;
},
- // Do we currently have keys, or do we have enough that we should be able
- // to successfully fetch them?
- _canFetchKeys() {
- let userData = this._signedInUser;
- // a keyFetchToken means we can almost certainly grab them.
- // kA and kB means we already have them.
- return userData && (userData.keyFetchToken || (userData.kA && userData.kB));
- },
-
/**
* Verify the current auth state, unlocking the master-password if necessary.
*
* Returns a promise that resolves with the current auth state after
* attempting to unlock.
*/
- unlockAndVerifyAuthState() {
- if (this._canFetchKeys()) {
+ async unlockAndVerifyAuthState() {
+ if ((await this._fxaService.canGetKeys())) {
log.debug("unlockAndVerifyAuthState already has (or can fetch) sync keys");
- return Promise.resolve(STATUS_OK);
+ return STATUS_OK;
}
// so no keys - ensure MP unlocked.
if (!Utils.ensureMPUnlocked()) {
// user declined to unlock, so we don't know if they are stored there.
log.debug("unlockAndVerifyAuthState: user declined to unlock master-password");
- return Promise.resolve(MASTER_PASSWORD_LOCKED);
+ return MASTER_PASSWORD_LOCKED;
}
// now we are unlocked we must re-fetch the user data as we may now have
// the details that were previously locked away.
- return this._fxaService.getSignedInUser().then(
- accountData => {
- this._updateSignedInUser(accountData);
- // If we still can't get keys it probably means the user authenticated
- // 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 (this._canFetchKeys()) {
- result = STATUS_OK;
- } else {
- result = LOGIN_FAILED_LOGIN_REJECTED;
- }
- log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
- return result;
- }
- );
+ const accountData = await this._fxaService.getSignedInUser();
+ this._updateSignedInUser(accountData);
+ // If we still can't get keys it probably means the user authenticated
+ // 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.canGetKeys())) {
+ result = STATUS_OK;
+ } else {
+ result = LOGIN_FAILED_LOGIN_REJECTED;
+ }
+ log.debug("unlockAndVerifyAuthState re-fetched credentials and is returning", result);
+ return result;
},
/**
* Do we have a non-null, not yet expired token for the user currently
* signed in?
*/
hasValidToken() {
// If pref is set to ignore cached authentication credentials for debugging,
@@ -615,39 +577,34 @@ this.BrowserIDManager.prototype = {
url = url.slice(0, -1);
}
return url;
},
// Refresh the sync token for our user. Returns a promise that resolves
// with a token (which may be null in one sad edge-case), or rejects with an
// error.
- _fetchTokenForUser() {
+ async _fetchTokenForUser() {
// tokenServerURI is mis-named - convention is uri means nsISomething...
let tokenServerURI = this._tokenServerUrl;
let log = this._log;
let client = this._tokenServerClient;
let fxa = this._fxaService;
let userData = this._signedInUser;
- // We need kA and kB for things to work. If we don't have them, just
+ // 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 (!this._canFetchKeys()) {
+ if (!(await this._fxaService.canGetKeys())) {
log.info("Unable to fetch keys (master-password locked?), so aborting token fetch");
return Promise.resolve(null);
}
let maybeFetchKeys = () => {
- // This is called at login time and every time we need a new token - in
- // the latter case we already have kA and kB, so optimise that case.
- if (userData.kA && userData.kB) {
- return null;
- }
- log.info("Fetching new keys");
+ log.info("Getting keys");
return this._fxaService.getKeys().then(
newUserData => {
userData = newUserData;
this._updateSignedInUser(userData); // throws if the user changed.
}
);
};
@@ -657,18 +614,17 @@ this.BrowserIDManager.prototype = {
let cb = function(err, token) {
if (err) {
return deferred.reject(err);
}
log.debug("Successfully got a sync token");
return deferred.resolve(token);
};
- let kBbytes = CommonUtils.hexToBytes(userData.kB);
- let headers = {"X-Client-State": this._computeXClientState(kBbytes)};
+ let headers = {"X-Client-State": userData.kXCS};
client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers);
return deferred.promise;
};
let getAssertion = () => {
log.info("Getting an assertion from", tokenServerURI);
let audience = Services.io.newURI(tokenServerURI).prePath;
return fxa.getAssertion(audience);
@@ -692,18 +648,17 @@ this.BrowserIDManager.prototype = {
.then(assertion => getToken(assertion));
})
.then(token => {
// TODO: Make it be only 80% of the duration, so refresh the token
// before it actually expires. This is to avoid sync storage errors
// otherwise, we get a nasty notification bar briefly. Bug 966568.
token.expiration = this._now() + (token.duration * 1000) * 0.80;
if (!this._syncKeyBundle) {
- // We are given kA/kB as hex.
- this._syncKeyBundle = deriveKeyBundle(CommonUtils.hexToBytes(userData.kB));
+ this._syncKeyBundle = BulkKeyBundle.fromHexKey(userData.kSync);
}
telemetryHelper.maybeRecordLoginState(telemetryHelper.STATES.SUCCESS);
return token;
})
.catch(err => {
// TODO: unify these errors - we need to handle errors thrown by
// both tokenserverclient and hawkclient.
// A tokenserver error thrown based on a bad response.
@@ -868,17 +823,17 @@ BrowserIDClusterManager.prototype = {
this.service.clusterURL = cluster;
return true;
},
_findCluster() {
let endPointFromIdentityToken = () => {
// The only reason (in theory ;) that we can end up with a null token
- // is when this.identity._canFetchKeys() returned false. In turn, this
+ // is when this._fxaService.canGetKeys() returned false. In turn, this
// should only happen if the master-password is locked or the credentials
// storage is screwed, and in those cases we shouldn't have started
// syncing so shouldn't get here anyway.
// But better safe than sorry! To keep things clearer, throw an explicit
// exception - the message will appear in the logs and the error will be
// treated as transient.
if (!this.identity._token) {
throw new Error("Can't get a cluster URL as we can't fetch keys.");
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -125,16 +125,23 @@ KeyBundle.prototype = {
*/
this.BulkKeyBundle = function BulkKeyBundle(collection) {
let log = Log.repository.getLogger("Sync.BulkKeyBundle");
log.info("BulkKeyBundle being created for " + collection);
KeyBundle.call(this);
this._collection = collection;
};
+BulkKeyBundle.fromHexKey = function(hexKey) {
+ let key = CommonUtils.hexToBytes(hexKey);
+ let bundle = new BulkKeyBundle();
+ // [encryptionKey, hmacKey]
+ bundle.keyPair = [key.slice(0, 32), key.slice(32, 64)];
+ return bundle;
+};
BulkKeyBundle.prototype = {
__proto__: KeyBundle.prototype,
get collection() {
return this._collection;
},
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -124,27 +124,28 @@ add_task(async function test_initialiali
Assert.ok(signCertificateCalled);
Assert.ok(accountStatusCalled);
Assert.ok(!browseridManager._token);
Assert.ok(!browseridManager.hasValidToken());
Assert.deepEqual(getLoginTelemetryScalar(), {REJECTED: 1});
});
add_task(async function test_initialializeWithNoKeys() {
- _("Verify start after initializeWithCurrentIdentity without kA, kB or keyFetchToken");
+ _("Verify start after initializeWithCurrentIdentity without kSync, kXCS, kExtSync, kExtKbHash or keyFetchToken");
let identityConfig = makeIdentityConfig();
- delete identityConfig.fxaccount.user.kA;
- delete identityConfig.fxaccount.user.kB;
+ delete identityConfig.fxaccount.user.kSync;
+ delete identityConfig.fxaccount.user.kXCS;
+ delete identityConfig.fxaccount.user.kExtSync;
+ delete identityConfig.fxaccount.user.kExtKbHash;
// there's no keyFetchToken by default, so the initialize should fail.
configureFxAccountIdentity(globalBrowseridManager, identityConfig);
await globalBrowseridManager.initializeWithCurrentIdentity();
await globalBrowseridManager.whenReadyToAuthenticate.promise;
Assert.equal(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
- Assert.ok(!globalBrowseridManager._canFetchKeys(), "_canFetchKeys reflects lack of keys");
Assert.equal(globalBrowseridManager._token, null, "we don't have a token");
});
add_test(function test_getResourceAuthenticator() {
_("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
configureFxAccountIdentity(globalBrowseridManager);
let authenticator = globalBrowseridManager.getResourceAuthenticator();
Assert.ok(!!authenticator);
@@ -187,34 +188,23 @@ add_test(function test_resourceAuthentic
// Sanity check
Assert.equal(hawkClient.now(), now);
Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);
// Properly picked up by the client
Assert.equal(fxaClient.now(), now);
Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);
- let fxa = new MockFxAccounts();
- fxa.internal._now_is = now;
- fxa.internal.fxAccountsClient = fxaClient;
-
- // Picked up by the signed-in user module
- Assert.equal(fxa.internal.now(), now);
- Assert.equal(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec);
-
- Assert.equal(fxa.now(), now);
- Assert.equal(fxa.localtimeOffsetMsec, localtimeOffsetMsec);
+ let identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ fxaInternal._now_is = now;
+ fxaInternal.fxAccountsClient = fxaClient;
// Mocks within mocks...
- configureFxAccountIdentity(browseridManager, globalIdentityConfig);
-
- // Ensure the new FxAccounts mock has a signed-in user.
- fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
-
- browseridManager._fxaService = fxa;
+ configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);
Assert.equal(browseridManager._fxaService.internal.now(), now);
Assert.equal(browseridManager._fxaService.internal.localtimeOffsetMsec,
localtimeOffsetMsec);
Assert.equal(browseridManager._fxaService.now(), now);
Assert.equal(browseridManager._fxaService.localtimeOffsetMsec,
localtimeOffsetMsec);
@@ -248,26 +238,23 @@ add_test(function test_RESTResourceAuthe
return now;
};
// Imagine there's already been one fxa request and the hawk client has
// already detected skew vs the fxa auth server.
hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;
let fxaClient = new MockFxAccountsClient();
fxaClient.hawk = hawkClient;
- let fxa = new MockFxAccounts();
- fxa.internal._now_is = now;
- fxa.internal.fxAccountsClient = fxaClient;
- configureFxAccountIdentity(browseridManager, globalIdentityConfig);
+ let identityConfig = makeIdentityConfig();
+ let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+ fxaInternal._now_is = now;
+ fxaInternal.fxAccountsClient = fxaClient;
- // Ensure the new FxAccounts mock has a signed-in user.
- fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser;
-
- browseridManager._fxaService = fxa;
+ configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);
Assert.equal(browseridManager._fxaService.internal.now(), now);
let request = new Resource("https://example.net/i/like/pie/");
let authenticator = browseridManager.getResourceAuthenticator();
let output = authenticator(request, "GET");
dump("output" + JSON.stringify(output));
let authHeader = output.headers.authorization;
@@ -337,54 +324,16 @@ add_test(function test_tokenExpiration()
});
Assert.ok(bimExp._token.expiration < bimExp._now());
_("... means BrowserIDManager knows to re-fetch it on the next call.");
Assert.ok(!bimExp.hasValidToken());
run_next_test();
}
);
-add_test(function test_sha256() {
- // Test vectors from http://www.bichlmeier.info/sha256test.html
- let vectors = [
- ["",
- "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
- ["abc",
- "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
- ["message digest",
- "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"],
- ["secure hash algorithm",
- "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"],
- ["SHA256 is considered to be safe",
- "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"],
- ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
- "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
- ["For this sample, this 63-byte string will be used as input data",
- "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"],
- ["This is exactly 64 bytes long, not counting the terminating byte",
- "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"]
- ];
- let bidUser = new BrowserIDManager();
- for (let [input, output] of vectors) {
- Assert.equal(CommonUtils.bytesAsHex(bidUser._sha256(input)), output);
- }
- run_next_test();
-});
-
-add_test(function test_computeXClientStateHeader() {
- let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d";
- let kB = CommonUtils.hexToBytes(kBhex);
-
- let bidUser = new BrowserIDManager();
- let header = bidUser._computeXClientState(kB);
-
- Assert.equal(header, "6ae94683571c7a7c54dab4700aa3995f");
- run_next_test();
-});
-
add_task(async function test_getTokenErrors() {
_("BrowserIDManager correctly handles various failures to get a token.");
_("Arrange for a 401 - Sync should reflect an auth error.");
initializeIdentityWithTokenServerResponse({
status: 401,
headers: {"content-type": "application/json"},
body: JSON.stringify({}),
@@ -524,19 +473,21 @@ add_task(async function test_getKeysErro
_("Auth server (via hawk) sends an observer notification on backoff headers.");
// Set Sync's backoffInterval to zero - after we simulated the backoff header
// it should reflect the value we sent.
Status.backoffInterval = 0;
_("Arrange for a 503 with a X-Backoff header.");
let config = makeIdentityConfig();
- // We want no kA or kB so we attempt to fetch them.
- delete config.fxaccount.user.kA;
- delete config.fxaccount.user.kB;
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
config.fxaccount.user.keyFetchToken = "keyfetchtoken";
await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
Assert.equal(method, "get");
Assert.equal(uri, "http://mockedserver:9999/account/keys");
return {
status: 503,
headers: {"content-type": "application/json",
"x-backoff": "100"},
@@ -558,19 +509,21 @@ add_task(async function test_getKeysErro
_("Auth server (via hawk) sends an observer notification on retry headers.");
// Set Sync's backoffInterval to zero - after we simulated the backoff header
// it should reflect the value we sent.
Status.backoffInterval = 0;
_("Arrange for a 503 with a Retry-After header.");
let config = makeIdentityConfig();
- // We want no kA or kB so we attempt to fetch them.
- delete config.fxaccount.user.kA;
- delete config.fxaccount.user.kB;
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
config.fxaccount.user.keyFetchToken = "keyfetchtoken";
await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
Assert.equal(method, "get");
Assert.equal(uri, "http://mockedserver:9999/account/keys");
return {
status: 503,
headers: {"content-type": "application/json",
"retry-after": "100"},
@@ -621,19 +574,21 @@ add_task(async function test_getHAWKErro
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});
add_task(async function test_getGetKeysFailing401() {
_("BrowserIDManager correctly handles 401 responses fetching keys.");
_("Arrange for a 401 - Sync should reflect an auth error.");
let config = makeIdentityConfig();
- // We want no kA or kB so we attempt to fetch them.
- delete config.fxaccount.user.kA;
- delete config.fxaccount.user.kB;
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
config.fxaccount.user.keyFetchToken = "keyfetchtoken";
await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
Assert.equal(method, "get");
Assert.equal(uri, "http://mockedserver:9999/account/keys");
return {
status: 401,
headers: {"content-type": "application/json"},
body: "{}",
@@ -642,19 +597,21 @@ add_task(async function test_getGetKeysF
Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
});
add_task(async function test_getGetKeysFailing503() {
_("BrowserIDManager correctly handles 5XX responses fetching keys.");
_("Arrange for a 503 - Sync should reflect a network error.");
let config = makeIdentityConfig();
- // We want no kA or kB so we attempt to fetch them.
- delete config.fxaccount.user.kA;
- delete config.fxaccount.user.kB;
+ // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
+ delete config.fxaccount.user.kSync;
+ delete config.fxaccount.user.kXCS;
+ delete config.fxaccount.user.kExtSync;
+ delete config.fxaccount.user.kExtKbHash;
config.fxaccount.user.keyFetchToken = "keyfetchtoken";
await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
Assert.equal(method, "get");
Assert.equal(uri, "http://mockedserver:9999/account/keys");
return {
status: 503,
headers: {"content-type": "application/json"},
body: "{}",
@@ -663,20 +620,22 @@ add_task(async function test_getGetKeysF
Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
});
add_task(async function test_getKeysMissing() {
_("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");
let browseridManager = new BrowserIDManager();
let identityConfig = makeIdentityConfig();
- // our mock identity config already has kA and kB - remove them or we never
+ // our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
// try and fetch them.
- delete identityConfig.fxaccount.user.kA;
- delete identityConfig.fxaccount.user.kB;
+ 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 that returns no keys
let fxa = new FxAccounts({
fetchAndUnwrapKeys() {
return Promise.resolve({});
@@ -709,26 +668,28 @@ add_task(async function test_getKeysMiss
let ex;
try {
await browseridManager.whenReadyToAuthenticate.promise;
} catch (e) {
ex = e;
}
- Assert.ok(ex.message.indexOf("missing kA or kB") >= 0);
+ Assert.equal(ex.message, "user data missing: kSync, kXCS, kExtSync, kExtKbHash");
});
add_task(async function test_signedInUserMissing() {
_("BrowserIDManager detects getSignedInUser returning incomplete account data");
let browseridManager = new BrowserIDManager();
// Delete stored keys and the key fetch token.
- delete globalIdentityConfig.fxaccount.user.kA;
- delete globalIdentityConfig.fxaccount.user.kB;
+ delete globalIdentityConfig.fxaccount.user.kSync;
+ delete globalIdentityConfig.fxaccount.user.kXCS;
+ delete globalIdentityConfig.fxaccount.user.kExtSync;
+ delete globalIdentityConfig.fxaccount.user.kExtKbHash;
delete globalIdentityConfig.fxaccount.user.keyFetchToken;
configureFxAccountIdentity(browseridManager, globalIdentityConfig);
let fxa = new FxAccounts({
fetchAndUnwrapKeys() {
return Promise.resolve({});
},
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -1,11 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://services-sync/browserid_identity.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/status.js");
@@ -521,33 +522,37 @@ add_task(async function test_autoconnect
Utils.mpLocked = () => true;
let origEnsureMPUnlocked = Utils.ensureMPUnlocked;
Utils.ensureMPUnlocked = () => {
_("Faking Master Password entry cancelation.");
return false;
};
- let origCanFetchKeys = Service.identity._canFetchKeys;
- Service.identity._canFetchKeys = () => false;
+ let origFxA = Service.identity._fxaService;
+ Service.identity._fxaService = new FxAccounts({
+ canGetKeys() {
+ return false;
+ }
+ });
// A locked master password will still trigger a sync, but then we'll hit
// MASTER_PASSWORD_LOCKED and hence MASTER_PASSWORD_LOCKED_RETRY_INTERVAL.
let promiseObserved = promiseOneObserver("weave:service:login:error");
scheduler.delayedAutoConnect(0);
await promiseObserved;
await Async.promiseYield();
Assert.equal(Status.login, MASTER_PASSWORD_LOCKED);
Utils.mpLocked = origLocked;
Utils.ensureMPUnlocked = origEnsureMPUnlocked;
- Service.identity._canFetchKeys = origCanFetchKeys;
+ Service.identity._fxaService = origFxA;
await cleanUpAndGo(server);
});
add_task(async function test_no_autoconnect_during_wizard() {
let server = sync_httpd_setup();
await setUp(server);
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -135,25 +135,21 @@ function ciphertextHMAC(keyBundle, id, I
* @returns {string} sha256 of the user's kB as a hex string
*/
const getKBHash = async function(fxaService) {
const signedInUser = await fxaService.getSignedInUser();
if (!signedInUser) {
throw new Error("User isn't signed in!");
}
- if (!signedInUser.kB) {
- throw new Error("User doesn't have kB??");
+ if (!signedInUser.kExtKbHash) {
+ throw new Error("User doesn't have KbHash??");
}
- let kBbytes = CommonUtils.hexToBytes(signedInUser.kB);
- let hasher = Cc["@mozilla.org/security/hash;1"]
- .createInstance(Ci.nsICryptoHash);
- hasher.init(hasher.SHA256);
- return CommonUtils.bytesAsHex(CryptoUtils.digestBytes(signedInUser.uid + kBbytes, hasher));
+ return signedInUser.kExtKbHash;
};
/**
* A "remote transformer" that the Kinto library will use to
* encrypt/decrypt records when syncing.
*
* This is an "abstract base class". Subclass this and override
* getKeys() to use it.
@@ -265,29 +261,21 @@ class KeyRingEncryptionRemoteTransformer
return (async function() {
const user = await self._fxaService.getSignedInUser();
// FIXME: we should permit this if the user is self-hosting
// their storage
if (!user) {
throw new Error("user isn't signed in to FxA; can't sync");
}
- if (!user.kB) {
- throw new Error("user doesn't have kB");
+ if (!user.kExtSync) {
+ throw new Error("user doesn't have kExtSync");
}
- let kB = CommonUtils.hexToBytes(user.kB);
-
- let keyMaterial = CryptoUtils.hkdf(kB, undefined,
- "identity.mozilla.com/picl/v1/chrome.storage.sync",
- 2 * 32);
- let bundle = new BulkKeyBundle();
- // [encryptionKey, hmacKey]
- bundle.keyPair = [keyMaterial.slice(0, 32), keyMaterial.slice(32, 64)];
- return bundle;
+ return BulkKeyBundle.fromHexKey(user.kExtSync);
})();
}
// Pass through the kbHash field from the unencrypted record. If
// encryption fails, we can use this to try to detect whether we are
// being compromised or if the record here was encoded with a
// different kB.
async encode(record) {
const encoded = await super.encode(record);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -512,20 +512,22 @@ const assertExtensionRecord = async func
"decrypted data should have a key attribute corresponding to the extension data key");
return decoded;
};
// Tests using this ID will share keys in local storage, so be careful.
const defaultExtensionId = "{13bdde76-4dc7-11e6-9bdc-54ee758d6342}";
const defaultExtension = {id: defaultExtensionId};
-const BORING_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+const kExtSync = "63f9057577c04bbbb9f0c3fd85b5d4032b60e13edc1f8dd309bf4305d66f2cc312dde16ce46021a496f713950d0a6c566ce181521a44726e7be97cf577b31b31";
+const KB_HASH = "2350cba8fced5a2fbae3b1f180baf860f78f6542bef7be709fda96cd3e3dc800";
const loggedInUser = {
uid: "0123456789abcdef0123456789abcdef",
- kB: BORING_KB,
+ kExtSync,
+ kExtKbHash: KB_HASH,
oauthTokens: {
"sync:addon-storage": {
token: "some-access-token",
},
},
};
function uuid() {
@@ -866,17 +868,17 @@ add_task(async function ensureCanSync_ha
deepEqual(postBody.keys.collections[extensionId], extensionKey.keyPairB64,
`decrypted new post should have preserved the key for ${extensionId}`);
});
});
});
add_task(async function checkSyncKeyRing_reuploads_keys() {
// Verify that when keys are present, they are reuploaded with the
- // new kB when we call touchKeys().
+ // new kbHash when we call touchKeys().
const extensionId = uuid();
let extensionKey, extensionSalt;
await withContextAndServer(async function(context, server) {
await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
server.installCollection("storage-sync-crypto");
server.etag = 765;
await extensionStorageSync.cryptoCollection._clear();
@@ -887,70 +889,72 @@ add_task(async function checkSyncKeyRing
`ensureCanSync should return a keyring that has a key for ${extensionId}`);
extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
equal(server.getPosts().length, 1,
"generating a key that doesn't exist on the server should post it");
const body = await assertPostedEncryptedKeys(fxaService, server.getPosts()[0]);
extensionSalt = body.salts[extensionId];
});
- // The user changes their password. This is their new kB, with
- // the last f changed to an e.
- const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
- const newUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ // 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";
+ const newUser = Object.assign({}, loggedInUser, {kExtKbHash: NEW_KB_HASH, kExtSync: NEW_KEXT});
let postedKeys;
await withSignedInUser(newUser, async function(extensionStorageSync, fxaService) {
await extensionStorageSync.checkSyncKeyRing();
let posts = server.getPosts();
equal(posts.length, 2,
- "when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
+ "when kBHash changes, checkSyncKeyRing should post the keyring reencrypted with the new kBHash");
postedKeys = posts[1];
assertPostedUpdatedRecord(postedKeys, 765);
let body = await assertPostedEncryptedKeys(fxaService, postedKeys);
deepEqual(body.keys.collections[extensionId], extensionKey,
`the posted keyring should have the same key for ${extensionId} as the old one`);
deepEqual(body.salts[extensionId], extensionSalt,
`the posted keyring should have the same salt for ${extensionId} as the old one`);
});
- // Verify that with the old kB, we can't decrypt the record.
+ // Verify that with the old kBHash, we can't decrypt the record.
await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
let error;
try {
await new KeyRingEncryptionRemoteTransformer(fxaService).decode(postedKeys.body.data);
} catch (e) {
error = e;
}
- ok(error, "decrypting the keyring with the old kB should fail");
+ ok(error, "decrypting the keyring with the old kBHash should fail");
ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
- "decrypting the keyring with the old kB should throw an HMAC mismatch");
+ "decrypting the keyring with the old kBHash should throw an HMAC mismatch");
});
});
});
add_task(async function checkSyncKeyRing_overwrites_on_conflict() {
// If there is already a record on the server that was encrypted
- // with a different kB, we wipe the server, clear sync state, and
+ // with a different kbHash, we wipe the server, clear sync state, and
// overwrite it with our keys.
const extensionId = uuid();
let extensionKey;
await withSyncContext(async function(context) {
await withServer(async function(server) {
- // The old device has this kB, which is very similar to the
- // current kB but with the last f changed to an e.
- const NOVEL_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee";
- const oldUser = Object.assign({}, loggedInUser, {kB: NOVEL_KB});
+ // 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";
+ const oldUser = Object.assign({}, loggedInUser, {kExtKbHash: NEW_KB_HASH, kExtSync: NEW_KEXT});
server.installDeleteBucket();
await withSignedInUser(oldUser, async function(extensionStorageSync, fxaService) {
await server.installKeyRing(fxaService, {}, {}, 765);
});
- // Now we have this new user with a different kB.
+ // Now we have this new user with a different kbHash.
await withSignedInUser(loggedInUser, async function(extensionStorageSync, fxaService) {
await extensionStorageSync.cryptoCollection._clear();
// Do an `ensureCanSync` to generate some keys.
// This will try to sync, notice that the record is
// undecryptable, and clear the server.
let collectionKeys = await extensionStorageSync.ensureCanSync([extensionId]);
ok(collectionKeys.hasKeysFor([extensionId]),
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -72,17 +72,17 @@
"forms.jsm": ["FormData"],
"FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
"FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
"FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"],
"FrameScriptManager.jsm": ["getNewLoaderID"],
"fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
- "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
+ "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "DERIVED_KEYS_NAMES", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
"FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],
"FxAccountsProfileClient.jsm": ["FxAccountsProfileClient", "FxAccountsProfileClientError"],
"FxAccountsPush.js": ["FxAccountsPushService"],
"FxAccountsStorage.jsm": ["FxAccountsStorageManagerCanStoreField", "FxAccountsStorageManager"],
"FxAccountsWebChannel.jsm": ["EnsureFxAccountsWebChannel"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"gDevTools.jsm": ["gDevTools", "gDevToolsBrowser"],
"Geometry.jsm": ["Point", "Rect"],