--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -4,17 +4,17 @@
// TODO:
// * find out how the Chrome implementation deals with conflicts
"use strict";
/* exported extensionIdToCollectionId */
-this.EXPORTED_SYMBOLS = ["ExtensionStorageSync"];
+this.EXPORTED_SYMBOLS = ["ExtensionStorageSync", "extensionStorageSync"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
const global = this;
Cu.import("resource://gre/modules/AppConstants.jsm");
@@ -29,19 +29,18 @@ const STORAGE_SYNC_CRYPTO_KEYRING_RECORD
const STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES = 32;
const FXA_OAUTH_OPTIONS = {
scope: STORAGE_SYNC_SCOPE,
};
// Default is 5sec, which seems a bit aggressive on the open internet
const KINTO_REQUEST_TIMEOUT = 30000;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-const {
- runSafeSyncWithoutClone,
-} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BulkKeyBundle",
"resource://services-sync/keys.js");
XPCOMUtils.defineLazyModuleGetter(this, "CollectionKeyManager",
"resource://services-sync/record.js");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
@@ -70,40 +69,93 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://services-sync/util.js");
XPCOMUtils.defineLazyPreferenceGetter(this, "prefPermitsStorageSync",
STORAGE_SYNC_ENABLED_PREF, true);
XPCOMUtils.defineLazyPreferenceGetter(this, "prefStorageSyncServerURL",
STORAGE_SYNC_SERVER_URL_PREF,
KINTO_DEFAULT_SERVER_URL);
+const {
+ runSafeSyncWithoutClone,
+} = ExtensionUtils;
/* globals prefPermitsStorageSync, prefStorageSyncServerURL */
// Map of Extensions to Set<Contexts> to track contexts that are still
// "live" and use storage.sync.
const extensionContexts = new Map();
// Borrow logger from Sync.
const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
+// A global that is fxAccounts, or null if (as on android) fxAccounts
+// isn't available.
+let _fxaService = null;
+if (AppConstants.platform != "android") {
+ _fxaService = fxAccounts;
+}
+
+/**
+ * Check for FXA and throw an exception if we don't have access.
+ *
+ * @param {Object} fxAccounts The reference we were hoping to use to
+ * access FxA
+ * @param {string} action The thing we were doing when we decided to
+ * see if we had access to FxA
+ */
+function throwIfNoFxA(fxAccounts, action) {
+ if (!fxAccounts) {
+ throw new Error(`${action} is impossible because FXAccounts is not available; are you on Android?`);
+ }
+}
+
+// Global ExtensionStorageSync instance that extensions and Fx Sync use.
+// On Android, because there's no FXAccounts instance, any syncing
+// operations will fail.
+this.extensionStorageSync = null;
+
/**
* Utility function to enforce an order of fields when computing an HMAC.
*
* @param {KeyBundle} keyBundle The key bundle to use to compute the HMAC
* @param {string} id The record ID to use when computing the HMAC
* @param {string} IV The IV to use when computing the HMAC
* @param {string} ciphertext The ciphertext over which to compute the HMAC
* @returns {string} The computed HMAC
*/
function ciphertextHMAC(keyBundle, id, IV, ciphertext) {
const hasher = keyBundle.sha256HMACHasher;
return Utils.bytesAsHex(Utils.digestUTF8(id + IV + ciphertext, hasher));
}
/**
+ * Get the current user's hashed kB.
+ *
+ * @param {FXAccounts} fxaService The service to use to get the
+ * current user.
+ * @returns {string} sha256 of the user's kB as a hex string
+ */
+const getKBHash = Task.async(function* (fxaService) {
+ const signedInUser = yield fxaService.getSignedInUser();
+ if (!signedInUser) {
+ throw new Error("User isn't signed in!");
+ }
+
+ if (!signedInUser.kB) {
+ throw new Error("User doesn't have kB??");
+ }
+
+ 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));
+});
+
+/**
* 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.
*/
class EncryptionRemoteTransformer {
encode(record) {
@@ -197,28 +249,28 @@ class EncryptionRemoteTransformer {
* @returns {Promise<string>} The ID to use.
*/
getEncodedRecordId(record) {
return Promise.resolve(record.id);
}
}
global.EncryptionRemoteTransformer = EncryptionRemoteTransformer;
-// This is meant to be a hook for use during unit testing.
-EncryptionRemoteTransformer.prototype._fxaService = null;
-if (AppConstants.platform != "android") {
- EncryptionRemoteTransformer.prototype._fxaService = fxAccounts;
-}
-
/**
* An EncryptionRemoteTransformer that provides a keybundle derived
* from the user's kB, suitable for encrypting a keyring.
*/
class KeyRingEncryptionRemoteTransformer extends EncryptionRemoteTransformer {
+ constructor(fxaService) {
+ super();
+ this._fxaService = fxaService;
+ }
+
getKeys() {
+ throwIfNoFxA(this._fxaService, "encrypting chrome.storage.sync records");
const self = this;
return Task.spawn(function* () {
const user = yield 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");
}
@@ -245,33 +297,30 @@ class KeyRingEncryptionRemoteTransformer
const encodePromise = super.encode(record);
return Task.spawn(function* () {
const encoded = yield encodePromise;
encoded.kbHash = record.kbHash;
return encoded;
});
}
- decode(record) {
- const decodePromise = super.decode(record);
- return Task.spawn(function* () {
- try {
- return yield decodePromise;
- } catch (e) {
- if (Utils.isHMACMismatch(e)) {
- const currentKBHash = yield ExtensionStorageSync.getKBHash();
- if (record.kbHash != currentKBHash) {
- // Some other client encoded this with a kB that we don't
- // have access to.
- KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash);
- }
+ async decode(record) {
+ try {
+ return await super.decode(record);
+ } catch (e) {
+ if (Utils.isHMACMismatch(e)) {
+ const currentKBHash = await getKBHash(this._fxaService);
+ if (record.kbHash != currentKBHash) {
+ // Some other client encoded this with a kB that we don't
+ // have access to.
+ KeyRingEncryptionRemoteTransformer.throwOutdatedKB(currentKBHash, record.kbHash);
}
- throw e;
}
- });
+ throw e;
+ }
}
// Generator and discriminator for KB-is-outdated exceptions.
static throwOutdatedKB(shouldBe, is) {
throw new Error(`kB hash on record is outdated: should be ${shouldBe}, is ${is}`);
}
static isOutdatedKB(exc) {
@@ -379,34 +428,39 @@ const cryptoCollectionIdSchema = {
validate(id) {
return true;
},
};
/**
* Wrapper around the crypto collection providing some handy utilities.
*/
-let cryptoCollection = this.cryptoCollection = {
- getCollection: Task.async(function* () {
- const {kinto} = yield storageSyncInit;
+class CryptoCollection {
+ constructor(fxaService) {
+ this._fxaService = fxaService;
+ }
+
+ async getCollection() {
+ throwIfNoFxA(this._fxaService, "tried to access cryptoCollection");
+ const {kinto} = await storageSyncInit;
return kinto.collection(STORAGE_SYNC_CRYPTO_COLLECTION_NAME, {
idSchema: cryptoCollectionIdSchema,
- remoteTransformers: [new KeyRingEncryptionRemoteTransformer()],
+ remoteTransformers: [new KeyRingEncryptionRemoteTransformer(this._fxaService)],
});
- }),
+ }
/**
* Generate a new salt for use in hashing extension and record
* IDs.
*
* @returns {string} A base64-encoded string of the salt
*/
getNewSalt() {
return btoa(CryptoUtils.generateRandomBytes(STORAGE_SYNC_CRYPTO_SALT_LENGTH_BYTES));
- },
+ }
/**
* Retrieve the keyring record from the crypto collection.
*
* You can use this if you want to check metadata on the keyring
* record rather than use the keyring itself.
*
* The keyring record, if present, should have the structure:
@@ -416,46 +470,51 @@ let cryptoCollection = this.cryptoCollec
* - uuid: a record identifier. This will only change when we wipe
* the collection (due to kB getting reset).
* - keys: a "WBO" form of a CollectionKeyManager.
* - salts: a normal JS Object with keys being collection IDs and
* values being base64-encoded salts to use when hashing IDs
* for that collection.
* @returns {Promise<Object>}
*/
- getKeyRingRecord: Task.async(function* () {
- const collection = yield this.getCollection();
- const cryptoKeyRecord = yield collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
+ async getKeyRingRecord() {
+ const collection = await this.getCollection();
+ const cryptoKeyRecord = await collection.getAny(STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID);
let data = cryptoKeyRecord.data;
if (!data) {
// This is a new keyring. Invent an ID for this record. If this
// changes, it means a client replaced the keyring, so we need to
// reupload everything.
const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
const uuid = uuidgen.generateUUID().toString();
data = {uuid, id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID};
}
return data;
- }),
+ }
- getSalts: Task.async(function* () {
- const cryptoKeyRecord = yield this.getKeyRingRecord();
+ async getSalts() {
+ const cryptoKeyRecord = await this.getKeyRingRecord();
return cryptoKeyRecord && cryptoKeyRecord.salts;
- }),
+ }
/**
* Used for testing with a known salt.
+ *
+ * @param {string} extensionId The extension ID for which to set a
+ * salt.
+ * @param {string} salt The salt to use for this extension, as a
+ * base64-encoded salt.
*/
- _setSalt: Task.async(function* (extensionId, salt) {
- const cryptoKeyRecord = yield this.getKeyRingRecord();
+ async _setSalt(extensionId, salt) {
+ const cryptoKeyRecord = await this.getKeyRingRecord();
cryptoKeyRecord.salts = cryptoKeyRecord.salts || {};
cryptoKeyRecord.salts[extensionId] = salt;
- this.upsert(cryptoKeyRecord);
- }),
+ await this.upsert(cryptoKeyRecord);
+ }
/**
* Hash an extension ID for a given user so that an attacker can't
* identify the extensions a user has installed.
*
* The extension ID is assumed to be a string (i.e. series of
* code points), and its UTF8 encoding is prefixed with the salt
* for that collection and hashed.
@@ -468,129 +527,131 @@ let cryptoCollection = this.cryptoCollec
* (-) or an underscore (_), prefix it with "ext-".
*
* @param {string} extensionId The extension ID to obfuscate.
* @returns {Promise<bytestring>} A collection ID suitable for use to sync to.
*/
extensionIdToCollectionId(extensionId) {
return this.hashWithExtensionSalt(CommonUtils.encodeUTF8(extensionId), extensionId)
.then(hash => `ext-${hash}`);
- },
+ }
/**
* Hash some value with the salt for the given extension.
*
* The value should be a "bytestring", i.e. a string whose
* "characters" are values, each within [0, 255]. You can produce
* such a bytestring using e.g. CommonUtils.encodeUTF8.
*
* The returned value is a base64url-encoded string of the hash.
*
* @param {bytestring} value The value to be hashed.
* @param {string} extensionId The ID of the extension whose salt
* we should use.
* @returns {Promise<bytestring>} The hashed value.
*/
- hashWithExtensionSalt: Task.async(function* (value, extensionId) {
- const salts = yield this.getSalts();
+ async hashWithExtensionSalt(value, extensionId) {
+ const salts = await this.getSalts();
const saltBase64 = salts && salts[extensionId];
if (!saltBase64) {
// This should never happen; salts should be populated before
// we need them by ensureCanSync.
throw new Error(`no salt available for ${extensionId}; how did this happen?`);
}
const hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA256);
const salt = atob(saltBase64);
const message = `${salt}\x00${value}`;
const hash = CryptoUtils.digestBytes(message, hasher);
return CommonUtils.encodeBase64URL(hash, false);
- }),
+ }
/**
* Retrieve the actual keyring from the crypto collection.
*
* @returns {Promise<CollectionKeyManager>}
*/
- getKeyRing: Task.async(function* () {
- const cryptoKeyRecord = yield this.getKeyRingRecord();
+ async getKeyRing() {
+ const cryptoKeyRecord = await this.getKeyRingRecord();
const collectionKeys = new CollectionKeyManager();
if (cryptoKeyRecord.keys) {
collectionKeys.setContents(cryptoKeyRecord.keys, cryptoKeyRecord.last_modified);
} else {
// We never actually use the default key, so it's OK if we
// generate one multiple times.
collectionKeys.generateDefaultKey();
}
// Pass through uuid field so that we can save it if we need to.
collectionKeys.uuid = cryptoKeyRecord.uuid;
return collectionKeys;
- }),
+ }
- updateKBHash: Task.async(function* (kbHash) {
- const coll = yield this.getCollection();
- yield coll.update({id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
+ async updateKBHash(kbHash) {
+ const coll = await this.getCollection();
+ await coll.update({id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
kbHash: kbHash},
{patch: true});
- }),
+ }
- upsert: Task.async(function* (record) {
- const collection = yield this.getCollection();
- yield collection.upsert(record);
- }),
+ async upsert(record) {
+ const collection = await this.getCollection();
+ await collection.upsert(record);
+ }
- sync: Task.async(function* () {
- const collection = yield this.getCollection();
- return yield ExtensionStorageSync._syncCollection(collection, {
+ async sync(extensionStorageSync) {
+ const collection = await this.getCollection();
+ return await extensionStorageSync._syncCollection(collection, {
strategy: "server_wins",
});
- }),
+ }
/**
* Reset sync status for ALL collections by directly
* accessing the FirefoxAdapter.
*/
- resetSyncStatus: Task.async(function* () {
- const coll = yield this.getCollection();
- yield coll.db.resetSyncStatus();
- }),
+ async resetSyncStatus() {
+ const coll = await this.getCollection();
+ await coll.db.resetSyncStatus();
+ }
// Used only for testing.
- _clear: Task.async(function* () {
- const collection = yield this.getCollection();
- yield collection.clear();
- }),
-};
+ async _clear() {
+ const collection = await this.getCollection();
+ await collection.clear();
+ }
+}
+this.CryptoCollection = CryptoCollection;
/**
* An EncryptionRemoteTransformer for extension records.
*
* It uses the special "keys" record to find a key for a given
* extension, thus its name
* CollectionKeyEncryptionRemoteTransformer.
*
* Also, during encryption, it will replace the ID of the new record
* with a hashed ID, using the salt for this collection.
*
* @param {string} extensionId The extension ID for which to find a key.
*/
let CollectionKeyEncryptionRemoteTransformer = class extends EncryptionRemoteTransformer {
- constructor(extensionId) {
+ constructor(cryptoCollection, extensionId) {
super();
+ this.cryptoCollection = cryptoCollection;
this.extensionId = extensionId;
}
getKeys() {
const self = this;
return Task.spawn(function* () {
// FIXME: cache the crypto record for the duration of a sync cycle?
- const collectionKeys = yield cryptoCollection.getKeyRing();
+ const collectionKeys = yield self.cryptoCollection.getKeyRing();
if (!collectionKeys.hasKeysFor([self.extensionId])) {
// This should never happen. Keys should be created (and
// synced) at the beginning of the sync cycle.
throw new Error(`tried to encrypt records for ${this.extensionId}, but key is not present`);
}
return collectionKeys.keyForCollection(self.extensionId);
});
}
@@ -598,17 +659,17 @@ let CollectionKeyEncryptionRemoteTransfo
getEncodedRecordId(record) {
// It isn't really clear whether kinto.js record IDs are
// bytestrings or strings that happen to only contain ASCII
// characters, so encode them to be sure.
const id = CommonUtils.encodeUTF8(record.id);
// Like extensionIdToCollectionId, the rules about Kinto record
// IDs preclude equals signs or strings starting with a
// non-alphanumeric, so prefix all IDs with a constant "id-".
- return cryptoCollection.hashWithExtensionSalt(id, this.extensionId)
+ return this.cryptoCollection.hashWithExtensionSalt(id, this.extensionId)
.then(hash => `id-${hash}`);
}
};
global.CollectionKeyEncryptionRemoteTransformer = CollectionKeyEncryptionRemoteTransformer;
/**
* Clean up now that one context is no longer using this extension's collection.
@@ -637,68 +698,68 @@ function cleanUpForContext(extension, co
* The extension whose collection needs to
* be opened.
* @param {Context} context
* The context for this extension. The Collection
* will shut down automatically when all contexts
* close.
* @returns {Promise<Collection>}
*/
-const openCollection = Task.async(function* (extension, context) {
+const openCollection = Task.async(function* (cryptoCollection, extension, context) {
let collectionId = extension.id;
const {kinto} = yield storageSyncInit;
- const remoteTransformers = [new CollectionKeyEncryptionRemoteTransformer(extension.id)];
+ const remoteTransformers = [new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extension.id)];
const coll = kinto.collection(collectionId, {
idSchema: storageSyncIdSchema,
remoteTransformers,
});
return coll;
});
-// FIXME: This is kind of ugly. Probably we should have
-// ExtensionStorageSync not be a singleton, but a constructed object,
-// and this should be a constructor argument.
-let _fxaService = null;
-if (AppConstants.platform != "android") {
- _fxaService = fxAccounts;
-}
+class ExtensionStorageSync {
+ /**
+ * @param {FXAccounts} fxaService (Optional) If not
+ * present, trying to sync will fail.
+ */
+ constructor(fxaService) {
+ this._fxaService = fxaService;
+ this.cryptoCollection = new CryptoCollection(fxaService);
+ this.listeners = new WeakMap();
+ }
-this.ExtensionStorageSync = {
- _fxaService,
- listeners: new WeakMap(),
-
- syncAll: Task.async(function* () {
+ async syncAll() {
const extensions = extensionContexts.keys();
const extIds = Array.from(extensions, extension => extension.id);
log.debug(`Syncing extension settings for ${JSON.stringify(extIds)}`);
if (extIds.length == 0) {
// No extensions to sync. Get out.
return;
}
- yield this.ensureCanSync(extIds);
- yield this.checkSyncKeyRing();
+ await this.ensureCanSync(extIds);
+ await this.checkSyncKeyRing();
const promises = Array.from(extensionContexts.keys(), extension => {
- return openCollection(extension).then(coll => {
+ return openCollection(this.cryptoCollection, extension).then(coll => {
return this.sync(extension, coll);
});
});
- yield Promise.all(promises);
- }),
+ await Promise.all(promises);
+ }
- sync: Task.async(function* (extension, collection) {
- const signedInUser = yield this._fxaService.getSignedInUser();
+ async sync(extension, collection) {
+ throwIfNoFxA(this._fxaService, "syncing chrome.storage.sync");
+ const signedInUser = await this._fxaService.getSignedInUser();
if (!signedInUser) {
// FIXME: this should support syncing to self-hosted
log.info("User was not signed into FxA; cannot sync");
throw new Error("Not signed in to FxA");
}
- const collectionId = yield cryptoCollection.extensionIdToCollectionId(extension.id);
+ const collectionId = await this.cryptoCollection.extensionIdToCollectionId(extension.id);
let syncResults;
try {
- syncResults = yield this._syncCollection(collection, {
+ syncResults = await this._syncCollection(collection, {
strategy: "client_wins",
collection: collectionId,
});
} catch (err) {
log.warn("Syncing failed", err);
throw err;
}
@@ -730,91 +791,94 @@ this.ExtensionStorageSync = {
changes[conflict.remote.key] = {
oldValue: conflict.local.data,
newValue: conflict.remote.data,
};
}
if (Object.keys(changes).length > 0) {
this.notifyListeners(extension, changes);
}
- }),
+ }
/**
* Utility function that handles the common stuff about syncing all
* Kinto collections (including "meta" collections like the crypto
* one).
*
* @param {Collection} collection
* @param {Object} options
* Additional options to be passed to sync().
* @returns {Promise<SyncResultObject>}
*/
- _syncCollection: Task.async(function* (collection, options) {
+ async _syncCollection(collection, options) {
// FIXME: this should support syncing to self-hosted
- return yield this._requestWithToken(`Syncing ${collection.name}`, function* (token) {
+ return await this._requestWithToken(`Syncing ${collection.name}`, async function(token) {
const allOptions = Object.assign({}, {
remote: prefStorageSyncServerURL,
headers: {
Authorization: "Bearer " + token,
},
}, options);
- return yield collection.sync(allOptions);
+ return await collection.sync(allOptions);
});
- }),
+ }
// Make a Kinto request with a current FxA token.
// If the response indicates that the token might have expired,
// retry the request.
- _requestWithToken: Task.async(function* (description, f) {
- const fxaToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
+ async _requestWithToken(description, f) {
+ throwIfNoFxA(this._fxaService, "making remote requests from chrome.storage.sync");
+ const fxaToken = await this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
try {
- return yield f(fxaToken);
+ return await f(fxaToken);
} catch (e) {
log.error(`${description}: request failed`, e);
if (e && e.data && e.data.code == 401) {
// Our token might have expired. Refresh and retry.
log.info("Token might have expired");
- yield this._fxaService.removeCachedOAuthToken({token: fxaToken});
- const newToken = yield this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
+ await this._fxaService.removeCachedOAuthToken({token: fxaToken});
+ const newToken = await this._fxaService.getOAuthToken(FXA_OAUTH_OPTIONS);
// If this fails too, let it go.
- return yield f(newToken);
+ return await f(newToken);
}
// Otherwise, we don't know how to handle this error, so just reraise.
throw e;
}
- }),
+ }
/**
* Helper similar to _syncCollection, but for deleting the user's bucket.
+ *
+ * @returns {Promise<void>}
*/
- _deleteBucket: Task.async(function* () {
- return yield this._requestWithToken("Clearing server", function* (token) {
+ async _deleteBucket() {
+ return await this._requestWithToken("Clearing server", async function(token) {
const headers = {Authorization: "Bearer " + token};
const kintoHttp = new KintoHttpClient(prefStorageSyncServerURL, {
headers: headers,
timeout: KINTO_REQUEST_TIMEOUT,
});
- return yield kintoHttp.deleteBucket("default");
+ return await kintoHttp.deleteBucket("default");
});
- }),
+ }
- ensureSaltsFor: Task.async(function* (keysRecord, extIds) {
+ async ensureSaltsFor(keysRecord, extIds) {
const newSalts = Object.assign({}, keysRecord.salts);
for (let collectionId of extIds) {
if (newSalts[collectionId]) {
continue;
}
- newSalts[collectionId] = cryptoCollection.getNewSalt();
+ newSalts[collectionId] = this.cryptoCollection.getNewSalt();
}
return newSalts;
- }),
+ }
/**
* Check whether the keys record (provided) already has salts for
* all the extensions given in extIds.
*
* @param {Object} keysRecord A previously-retrieved keys record.
* @param {Array<string>} extIds The IDs of the extensions which
* need salts.
@@ -827,122 +891,102 @@ this.ExtensionStorageSync = {
for (let collectionId of extIds) {
if (!keysRecord.salts[collectionId]) {
return false;
}
}
return true;
- },
+ }
/**
* Recursive promise that terminates when our local collectionKeys,
* as well as that on the server, have keys for all the extensions
* in extIds.
*
* @param {Array<string>} extIds
* The IDs of the extensions which need keys.
* @returns {Promise<CollectionKeyManager>}
*/
- ensureCanSync: Task.async(function* (extIds) {
- const keysRecord = yield cryptoCollection.getKeyRingRecord();
- const collectionKeys = yield cryptoCollection.getKeyRing();
+ async ensureCanSync(extIds) {
+ const keysRecord = await this.cryptoCollection.getKeyRingRecord();
+ const collectionKeys = await this.cryptoCollection.getKeyRing();
if (collectionKeys.hasKeysFor(extIds) && this.hasSaltsFor(keysRecord, extIds)) {
return collectionKeys;
}
- const kbHash = yield this.getKBHash();
- const newKeys = yield collectionKeys.ensureKeysFor(extIds);
- const newSalts = yield this.ensureSaltsFor(keysRecord, extIds);
+ const kbHash = await getKBHash(this._fxaService);
+ const newKeys = await collectionKeys.ensureKeysFor(extIds);
+ const newSalts = await this.ensureSaltsFor(keysRecord, extIds);
const newRecord = {
id: STORAGE_SYNC_CRYPTO_KEYRING_RECORD_ID,
keys: newKeys.asWBO().cleartext,
salts: newSalts,
uuid: collectionKeys.uuid,
// Add a field for the current kB hash.
kbHash: kbHash,
};
- yield cryptoCollection.upsert(newRecord);
- const result = yield this._syncKeyRing(newRecord);
+ await this.cryptoCollection.upsert(newRecord);
+ const result = await this._syncKeyRing(newRecord);
if (result.resolved.length != 0) {
// We had a conflict which was automatically resolved. We now
// have a new keyring which might have keys for the
// collections. Recurse.
- return yield this.ensureCanSync(extIds);
+ return await this.ensureCanSync(extIds);
}
// No conflicts. We're good.
return newKeys;
- }),
-
- /**
- * Get the current user's hashed kB.
- *
- * @returns sha256 of the user's kB as a hex string
- */
- getKBHash: Task.async(function* () {
- const signedInUser = yield this._fxaService.getSignedInUser();
- if (!signedInUser) {
- throw new Error("User isn't signed in!");
- }
-
- if (!signedInUser.kB) {
- throw new Error("User doesn't have kB??");
- }
-
- 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));
- }),
+ }
/**
* Update the kB in the crypto record.
*/
- updateKeyRingKB: Task.async(function* () {
- const signedInUser = yield this._fxaService.getSignedInUser();
+ async updateKeyRingKB() {
+ throwIfNoFxA(this._fxaService, "use of chrome.storage.sync \"keyring\"");
+ const signedInUser = await this._fxaService.getSignedInUser();
if (!signedInUser) {
// Although this function is meant to be called on login,
// it's not unreasonable to check any time, even if we aren't
// logged in.
//
// If we aren't logged in, we don't have any information about
// the user's kB, so we can't be sure that the user changed
// their kB, so just return.
return;
}
- const thisKBHash = yield this.getKBHash();
- yield cryptoCollection.updateKBHash(thisKBHash);
- }),
+ const thisKBHash = await getKBHash(this._fxaService);
+ await this.cryptoCollection.updateKBHash(thisKBHash);
+ }
/**
* Make sure the keyring is up to date and synced.
*
* This is called on syncs to make sure that we don't sync anything
* to any collection unless the key for that collection is on the
* server.
*/
- checkSyncKeyRing: Task.async(function* () {
- yield this.updateKeyRingKB();
+ async checkSyncKeyRing() {
+ await this.updateKeyRingKB();
- const cryptoKeyRecord = yield cryptoCollection.getKeyRingRecord();
+ const cryptoKeyRecord = await this.cryptoCollection.getKeyRingRecord();
if (cryptoKeyRecord && cryptoKeyRecord._status !== "synced") {
// We haven't successfully synced the keyring since the last
// change. This could be because kB changed and we touched the
// keyring, or it could be because we failed to sync after
// adding a key. Either way, take this opportunity to sync the
// keyring.
- yield this._syncKeyRing(cryptoKeyRecord);
+ await this._syncKeyRing(cryptoKeyRecord);
}
- }),
+ }
- _syncKeyRing: Task.async(function* (cryptoKeyRecord) {
+ async _syncKeyRing(cryptoKeyRecord) {
+ throwIfNoFxA(this._fxaService, "syncing chrome.storage.sync \"keyring\"");
try {
// Try to sync using server_wins.
//
// We use server_wins here because whatever is on the server is
// at least consistent with itself -- the crypto in the keyring
// matches the crypto on the collection records. This is because
// we generate and upload keys just before syncing data.
//
@@ -960,46 +1004,46 @@ this.ExtensionStorageSync = {
// everything we have on our end too, so we detect this by
// adding a UUID to the keyring. UUIDs are preserved throughout
// the lifetime of a keyring, so the only time a keyring UUID
// changes is when a new keyring is uploaded, which only happens
// after a server wipe. So when we get a "conflict" (resolved by
// server_wins), we check whether the server version has a new
// UUID. If so, reset our sync status, so that we'll reupload
// everything.
- const result = yield cryptoCollection.sync();
+ const result = await this.cryptoCollection.sync(this);
if (result.resolved.length > 0) {
if (result.resolved[0].uuid != cryptoKeyRecord.uuid) {
log.info(`Detected a new UUID (${result.resolved[0].uuid}, was ${cryptoKeyRecord.uuid}). Reseting sync status for everything.`);
- yield cryptoCollection.resetSyncStatus();
+ await this.cryptoCollection.resetSyncStatus();
// Server version is now correct. Return that result.
return result;
}
}
// No conflicts, or conflict was just someone else adding keys.
return result;
} catch (e) {
if (KeyRingEncryptionRemoteTransformer.isOutdatedKB(e)) {
// Check if our token is still valid, or if we got locked out
// between starting the sync and talking to Kinto.
- const isSessionValid = yield this._fxaService.sessionStatus();
+ const isSessionValid = await this._fxaService.sessionStatus();
if (isSessionValid) {
- yield this._deleteBucket();
- yield cryptoCollection.resetSyncStatus();
+ await this._deleteBucket();
+ await this.cryptoCollection.resetSyncStatus();
// Reupload our keyring, which is the only new keyring.
// We don't want client_wins here because another device
// could have uploaded another keyring in the meantime.
- return yield cryptoCollection.sync();
+ return await this.cryptoCollection.sync(this);
}
}
throw e;
}
- }),
+ }
/**
* Get the collection for an extension, and register the extension
* as being "in use".
*
* @param {Extension} extension
* The extension for which we are seeking
* a collection.
@@ -1021,24 +1065,24 @@ this.ExtensionStorageSync = {
// New context. Register it and make sure it cleans itself up
// when it closes.
contexts.add(context);
context.callOnClose({
close: () => cleanUpForContext(extension, context),
});
}
- return openCollection(extension, context);
- },
+ return openCollection(this.cryptoCollection, extension, context);
+ }
- set: Task.async(function* (extension, items, context) {
- const coll = yield this.getCollection(extension, context);
+ async set(extension, items, context) {
+ const coll = await this.getCollection(extension, context);
const keys = Object.keys(items);
const ids = keys.map(keyToId);
- const changes = yield coll.execute(txn => {
+ const changes = await coll.execute(txn => {
let changes = {};
for (let [i, key] of keys.entries()) {
const id = ids[i];
let item = items[key];
let {oldRecord} = txn.upsert({
id,
key,
data: item,
@@ -1050,57 +1094,57 @@ this.ExtensionStorageSync = {
// Extract the "data" field from the old record, which
// represents the value part of the key-value store
changes[key].oldValue = oldRecord.data;
}
}
return changes;
}, {preloadIds: ids});
this.notifyListeners(extension, changes);
- }),
+ }
- remove: Task.async(function* (extension, keys, context) {
- const coll = yield this.getCollection(extension, context);
+ async remove(extension, keys, context) {
+ const coll = await this.getCollection(extension, context);
keys = [].concat(keys);
const ids = keys.map(keyToId);
let changes = {};
- yield coll.execute(txn => {
+ await coll.execute(txn => {
for (let [i, key] of keys.entries()) {
const id = ids[i];
const res = txn.deleteAny(id);
if (res.deleted) {
changes[key] = {
oldValue: res.data.data,
};
}
}
return changes;
}, {preloadIds: ids});
if (Object.keys(changes).length > 0) {
this.notifyListeners(extension, changes);
}
- }),
+ }
- clear: Task.async(function* (extension, context) {
+ async clear(extension, context) {
// We can't call Collection#clear here, because that just clears
// the local database. We have to explicitly delete everything so
// that the deletions can be synced as well.
- const coll = yield this.getCollection(extension, context);
- const res = yield coll.list();
+ const coll = await this.getCollection(extension, context);
+ const res = await coll.list();
const records = res.data;
const keys = records.map(record => record.key);
- yield this.remove(extension, keys, context);
- }),
+ await this.remove(extension, keys, context);
+ }
- get: Task.async(function* (extension, spec, context) {
- const coll = yield this.getCollection(extension, context);
+ async get(extension, spec, context) {
+ const coll = await this.getCollection(extension, context);
let keys, records;
if (spec === null) {
records = {};
- const res = yield coll.list();
+ const res = await coll.list();
for (let record of res.data) {
records[record.key] = record.data;
}
return records;
}
if (typeof spec === "string") {
keys = [spec];
records = {};
@@ -1108,44 +1152,46 @@ this.ExtensionStorageSync = {
keys = spec;
records = {};
} else {
keys = Object.keys(spec);
records = Cu.cloneInto(spec, global);
}
for (let key of keys) {
- const res = yield coll.getAny(keyToId(key));
+ const res = await coll.getAny(keyToId(key));
if (res.data && res.data._status != "deleted") {
records[res.data.key] = res.data.data;
}
}
return records;
- }),
+ }
addOnChangedListener(extension, listener, context) {
let listeners = this.listeners.get(extension) || new Set();
listeners.add(listener);
this.listeners.set(extension, listeners);
// Force opening the collection so that we will sync for this extension.
return this.getCollection(extension, context);
- },
+ }
removeOnChangedListener(extension, listener) {
let listeners = this.listeners.get(extension);
listeners.delete(listener);
if (listeners.size == 0) {
this.listeners.delete(extension);
}
- },
+ }
notifyListeners(extension, changes) {
Observers.notify("ext.storage.sync-changed");
let listeners = this.listeners.get(extension) || new Set();
if (listeners) {
for (let listener of listeners) {
runSafeSyncWithoutClone(listener, changes);
}
}
- },
-};
+ }
+}
+this.ExtensionStorageSync = ExtensionStorageSync;
+this.extensionStorageSync = new ExtensionStorageSync(_fxaService);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
@@ -5,18 +5,17 @@
do_get_profile(); // so we can use FxAccounts
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
const {
CollectionKeyEncryptionRemoteTransformer,
- cryptoCollection,
- EncryptionRemoteTransformer,
+ CryptoCollection,
ExtensionStorageSync,
idToKey,
KeyRingEncryptionRemoteTransformer,
keyToId,
} = Cu.import("resource://gre/modules/ExtensionStorageSync.jsm", {});
Cu.import("resource://services-sync/engines/extension-storage.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/util.js");
@@ -258,27 +257,27 @@ class KintoServer {
last_modified: 1475161309026,
id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
},
}));
});
}
// Utility function to install a keyring at the start of a test.
- installKeyRing(keysData, salts, etag, {conflict = false} = {}) {
+ installKeyRing(fxaService, keysData, salts, etag, {conflict = false} = {}) {
this.installCollection("storage-sync-crypto");
const keysRecord = {
"id": "keys",
"keys": keysData,
"salts": salts,
"last_modified": etag,
};
this.etag = etag;
const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
- this[methodName](new KeyRingEncryptionRemoteTransformer(),
+ this[methodName](new KeyRingEncryptionRemoteTransformer(fxaService),
"storage-sync-crypto", keysRecord);
}
// Add an already-encrypted record.
addRecord(collectionId, record) {
this.collections.get(collectionId).add(record);
}
@@ -352,37 +351,32 @@ function* withContextAndServer(f) {
yield* withSyncContext(function* (context) {
yield* withServer(function* (server) {
yield* f(context, server);
});
});
}
// Run a block of code with fxa mocked out to return a specific user.
+// Calls the given function with an ExtensionStorageSync instance that
+// was constructed using a mocked FxAccounts instance.
function* withSignedInUser(user, f) {
- const oldESSFxAccounts = ExtensionStorageSync._fxaService;
- const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService;
- ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = {
+ let fxaServiceMock = {
getSignedInUser() {
return Promise.resolve(user);
},
getOAuthToken() {
return Promise.resolve("some-access-token");
},
sessionStatus() {
return Promise.resolve(true);
},
};
-
- try {
- yield* f();
- } finally {
- ExtensionStorageSync._fxaService = oldESSFxAccounts;
- EncryptionRemoteTransformer.prototype._fxaService = oldERTFxAccounts;
- }
+ let extensionStorageSync = new ExtensionStorageSync(fxaServiceMock);
+ yield* f(extensionStorageSync, fxaServiceMock);
}
// Some assertions that make it easier to write tests about what was
// posted and when.
// Assert that the request was made with the correct access token.
// This should be true of all requests, so this is usually called from
// another assertion.
@@ -405,20 +399,20 @@ function assertPostedNewRecord(post) {
// Also calls assertAuthenticatedRequest(post).
function assertPostedUpdatedRecord(post, since) {
assertAuthenticatedRequest(post);
equal(post.headers["If-Match"], `"${since}"`);
}
// Assert that this post was an encrypted keyring, and produce the
// decrypted body. Sanity check the body while we're here.
-const assertPostedEncryptedKeys = Task.async(function* (post) {
+const assertPostedEncryptedKeys = Task.async(function* (fxaService, post) {
equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
- let body = yield new KeyRingEncryptionRemoteTransformer().decode(post.body.data);
+ let body = yield new KeyRingEncryptionRemoteTransformer(fxaService).decode(post.body.data);
ok(body.keys, `keys object should be present in decoded body`);
ok(body.keys.default, `keys object should have a default key`);
ok(body.salts, `salts object should be present in decoded body`);
return body;
});
// assertEqual, but for keyring[extensionId] == key.
function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
@@ -426,16 +420,31 @@ function assertKeyRingKey(keyRing, exten
message = `expected keyring's key for ${extensionId} to match ${expectedKey.keyPairB64}`;
}
ok(keyRing.hasKeysFor([extensionId]),
`expected keyring to have a key for ${extensionId}\n`);
deepEqual(keyRing.keyForCollection(extensionId).keyPairB64, expectedKey.keyPairB64,
message);
}
+// Assert that this post was posted for a given extension.
+const assertExtensionRecord = Task.async(function* (fxaService, post, extension, key) {
+ const extensionId = extension.id;
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const hashedId = "id-" + (yield cryptoCollection.hashWithExtensionSalt(keyToId(key), extensionId));
+ const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
+ const transformer = new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extensionId);
+ equal(post.path, `${collectionRecordsPath(collectionId)}/${hashedId}`,
+ "decrypted data should be posted to path corresponding to its key");
+ let decoded = yield transformer.decode(post.body.data);
+ equal(decoded.key, key,
+ "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 loggedInUser = {
uid: "0123456789abcdef0123456789abcdef",
kB: BORING_KB,
@@ -473,59 +482,65 @@ add_task(function* test_key_to_id() {
equal(idToKey("key-_HI_"), null);
equal(idToKey("key-"), "");
equal(idToKey("key-1"), "1");
equal(idToKey("key-_2D_"), "-");
});
add_task(function* test_extension_id_to_collection_id() {
const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
- // Fake a static keyring since the server doesn't exist.
- const salt = "Scgx8RJ8Y0rxMGFYArUiKeawlW+0zJyFmtTDvro9qPo=";
- yield cryptoCollection._setSalt(extensionId, salt);
+ // FIXME: this doesn't actually require the signed in user, but the
+ // extensionIdToCollectionId method exists on CryptoCollection,
+ // which needs an fxaService to be instantiated.
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ // Fake a static keyring since the server doesn't exist.
+ const salt = "Scgx8RJ8Y0rxMGFYArUiKeawlW+0zJyFmtTDvro9qPo=";
+ const cryptoCollection = new CryptoCollection(fxaService);
+ yield cryptoCollection._setSalt(extensionId, salt);
- equal(yield cryptoCollection.extensionIdToCollectionId(extensionId),
- "ext-0_QHA1P93_yJoj7ONisrR0lW6uN4PZ3Ii-rT-QOjtvo");
+ equal(yield cryptoCollection.extensionIdToCollectionId(extensionId),
+ "ext-0_QHA1P93_yJoj7ONisrR0lW6uN4PZ3Ii-rT-QOjtvo");
+ });
});
add_task(function* ensureCanSync_posts_new_keys() {
const extensionId = uuid();
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
server.installCollection("storage-sync-crypto");
server.etag = 1000;
- let newKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let newKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
ok(newKeys.hasKeysFor([extensionId]), `key isn't present for ${extensionId}`);
let posts = server.getPosts();
equal(posts.length, 1);
const post = posts[0];
assertPostedNewRecord(post);
- const body = yield assertPostedEncryptedKeys(post);
+ const body = yield assertPostedEncryptedKeys(fxaService, post);
const oldSalt = body.salts[extensionId];
ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
ok(oldSalt, `salts object should have a salt for ${extensionId}`);
// Try adding another key to make sure that the first post was
// OK, even on a new profile.
- yield cryptoCollection._clear();
+ yield extensionStorageSync.cryptoCollection._clear();
server.clearPosts();
// Restore the first posted keyring, but add a last_modified date
const firstPostedKeyring = Object.assign({}, post.body.data, {last_modified: server.etag});
server.addRecordInPast("storage-sync-crypto", firstPostedKeyring);
const extensionId2 = uuid();
- newKeys = yield ExtensionStorageSync.ensureCanSync([extensionId2]);
+ newKeys = yield extensionStorageSync.ensureCanSync([extensionId2]);
ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
posts = server.getPosts();
equal(posts.length, 1);
const newPost = posts[posts.length - 1];
- const newBody = yield assertPostedEncryptedKeys(newPost);
+ const newBody = yield assertPostedEncryptedKeys(fxaService, newPost);
ok(newBody.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
ok(newBody.keys.collections[extensionId2], `keys object should have a key for ${extensionId2}`);
ok(newBody.salts[extensionId], `salts object should have a key for ${extensionId}`);
ok(newBody.salts[extensionId2], `salts object should have a key for ${extensionId2}`);
equal(oldSalt, newBody.salts[extensionId], `old salt should be preserved in post`);
});
});
});
@@ -540,90 +555,92 @@ add_task(function* ensureCanSync_pulls_k
const extensionId = uuid();
const extensionId2 = uuid();
const extensionOnlyKey = uuid();
const extensionOnlySalt = uuid();
const DEFAULT_KEY = new BulkKeyBundle("[default]");
DEFAULT_KEY.generateRandom();
const RANDOM_KEY = new BulkKeyBundle(extensionId);
RANDOM_KEY.generateRandom();
- const RANDOM_SALT = cryptoCollection.getNewSalt();
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ // FIXME: generating a random salt probably shouldn't require a CryptoCollection?
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const RANDOM_SALT = cryptoCollection.getNewSalt();
const keysData = {
"default": DEFAULT_KEY.keyPairB64,
"collections": {
[extensionId]: RANDOM_KEY.keyPairB64,
},
};
const saltData = {
[extensionId]: RANDOM_SALT,
};
- server.installKeyRing(keysData, saltData, 999);
+ server.installKeyRing(fxaService, keysData, saltData, 999);
- let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let collectionKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
let posts = server.getPosts();
equal(posts.length, 0,
"ensureCanSync shouldn't push when the server keyring has the right key");
// Another client generates a key for extensionId2
const newKey = new BulkKeyBundle(extensionId2);
newKey.generateRandom();
keysData.collections[extensionId2] = newKey.keyPairB64;
saltData[extensionId2] = cryptoCollection.getNewSalt();
server.clearCollection("storage-sync-crypto");
- server.installKeyRing(keysData, saltData, 1000);
+ server.installKeyRing(fxaService, keysData, saltData, 1000);
- let newCollectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionId2]);
+ let newCollectionKeys = yield extensionStorageSync.ensureCanSync([extensionId, extensionId2]);
assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
`ensureCanSync shouldn't lose the old key for ${extensionId}`);
posts = server.getPosts();
equal(posts.length, 0, "ensureCanSync shouldn't push when updating keys");
// Another client generates a key, but not a salt, for extensionOnlyKey
const onlyKey = new BulkKeyBundle(extensionOnlyKey);
onlyKey.generateRandom();
keysData.collections[extensionOnlyKey] = onlyKey.keyPairB64;
server.clearCollection("storage-sync-crypto");
- server.installKeyRing(keysData, saltData, 1001);
+ server.installKeyRing(fxaService, keysData, saltData, 1001);
- let withNewKey = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionOnlyKey]);
+ let withNewKey = yield extensionStorageSync.ensureCanSync([extensionId, extensionOnlyKey]);
dump(`got ${JSON.stringify(withNewKey.asWBO().cleartext)}\n`);
assertKeyRingKey(withNewKey, extensionOnlyKey, onlyKey);
assertKeyRingKey(withNewKey, extensionId, RANDOM_KEY,
`ensureCanSync shouldn't lose the old key for ${extensionId}`);
posts = server.getPosts();
equal(posts.length, 1, "ensureCanSync should push when generating a new salt");
- const withNewKeyRecord = yield assertPostedEncryptedKeys(posts[0]);
+ const withNewKeyRecord = yield assertPostedEncryptedKeys(fxaService, posts[0]);
// We don't a priori know what the new salt is
dump(`${JSON.stringify(withNewKeyRecord)}\n`);
ok(withNewKeyRecord.salts[extensionOnlyKey],
`ensureCanSync should generate a salt for an extension that only had a key`);
// Another client generates a key, but not a salt, for extensionOnlyKey
const newSalt = cryptoCollection.getNewSalt();
saltData[extensionOnlySalt] = newSalt;
server.clearCollection("storage-sync-crypto");
- server.installKeyRing(keysData, saltData, 1002);
+ server.installKeyRing(fxaService, keysData, saltData, 1002);
- let withOnlySaltKey = yield ExtensionStorageSync.ensureCanSync([extensionId, extensionOnlySalt]);
+ let withOnlySaltKey = yield extensionStorageSync.ensureCanSync([extensionId, extensionOnlySalt]);
assertKeyRingKey(withOnlySaltKey, extensionId, RANDOM_KEY,
`ensureCanSync shouldn't lose the old key for ${extensionId}`);
// We don't a priori know what the new key is
ok(withOnlySaltKey.hasKeysFor([extensionOnlySalt]),
`ensureCanSync generated a key for an extension that only had a salt`);
posts = server.getPosts();
equal(posts.length, 2, "ensureCanSync should push when generating a new key");
- const withNewSaltRecord = yield assertPostedEncryptedKeys(posts[1]);
+ const withNewSaltRecord = yield assertPostedEncryptedKeys(fxaService, posts[1]);
equal(withNewSaltRecord.salts[extensionOnlySalt], newSalt,
"ensureCanSync should keep the existing salt when generating only a key");
});
});
});
add_task(function* ensureCanSync_handles_conflicts() {
// Syncing is done through a pull followed by a push of any merged
@@ -631,146 +648,148 @@ add_task(function* ensureCanSync_handles
// i.e. with the server rejecting a change -- is if
// someone pushes changes between our pull and our push. Ensure that
// if this happens, we still behave sensibly (keep the remote key).
const extensionId = uuid();
const DEFAULT_KEY = new BulkKeyBundle("[default]");
DEFAULT_KEY.generateRandom();
const RANDOM_KEY = new BulkKeyBundle(extensionId);
RANDOM_KEY.generateRandom();
- const RANDOM_SALT = cryptoCollection.getNewSalt();
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ // FIXME: generating salts probably shouldn't rely on a CryptoCollection
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const RANDOM_SALT = cryptoCollection.getNewSalt();
const keysData = {
"default": DEFAULT_KEY.keyPairB64,
"collections": {
[extensionId]: RANDOM_KEY.keyPairB64,
},
};
const saltData = {
[extensionId]: RANDOM_SALT,
};
- server.installKeyRing(keysData, saltData, 765, {conflict: true});
+ server.installKeyRing(fxaService, keysData, saltData, 765, {conflict: true});
- yield cryptoCollection._clear();
+ yield extensionStorageSync.cryptoCollection._clear();
- let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let collectionKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY,
`syncing keyring should keep the server key for ${extensionId}`);
let posts = server.getPosts();
equal(posts.length, 1,
"syncing keyring should have tried to post a keyring");
const failedPost = posts[0];
assertPostedNewRecord(failedPost);
- let body = yield assertPostedEncryptedKeys(failedPost);
+ let body = yield assertPostedEncryptedKeys(fxaService, failedPost);
// This key will be the one the client generated locally, so
// we don't know what its value will be
ok(body.keys.collections[extensionId],
`decrypted failed post should have a key for ${extensionId}`);
notEqual(body.keys.collections[extensionId], RANDOM_KEY.keyPairB64,
`decrypted failed post should have a randomly-generated key for ${extensionId}`);
});
});
});
add_task(function* checkSyncKeyRing_reuploads_keys() {
// Verify that when keys are present, they are reuploaded with the
// new kB when we call touchKeys().
const extensionId = uuid();
let extensionKey, extensionSalt;
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
server.installCollection("storage-sync-crypto");
server.etag = 765;
- yield cryptoCollection._clear();
+ yield extensionStorageSync.cryptoCollection._clear();
// Do an `ensureCanSync` to generate some keys.
- let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let collectionKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
ok(collectionKeys.hasKeysFor([extensionId]),
`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 = yield assertPostedEncryptedKeys(server.getPosts()[0]);
+ const body = yield 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});
let postedKeys;
- yield* withSignedInUser(newUser, function* () {
- yield ExtensionStorageSync.checkSyncKeyRing();
+ yield* withSignedInUser(newUser, function* (extensionStorageSync, fxaService) {
+ yield extensionStorageSync.checkSyncKeyRing();
let posts = server.getPosts();
equal(posts.length, 2,
"when kB changes, checkSyncKeyRing should post the keyring reencrypted with the new kB");
postedKeys = posts[1];
assertPostedUpdatedRecord(postedKeys, 765);
- let body = yield assertPostedEncryptedKeys(postedKeys);
+ let body = yield 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.
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
let error;
try {
- yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ yield new KeyRingEncryptionRemoteTransformer(fxaService).decode(postedKeys.body.data);
} catch (e) {
error = e;
}
ok(error, "decrypting the keyring with the old kB should fail");
ok(Utils.isHMACMismatch(error) || KeyRingEncryptionRemoteTransformer.isOutdatedKB(error),
"decrypting the keyring with the old kB should throw an HMAC mismatch");
});
});
});
add_task(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
// overwrite it with our keys.
const extensionId = uuid();
- const transformer = new KeyRingEncryptionRemoteTransformer();
let extensionKey;
yield* withSyncContext(function* (context) {
yield* withServer(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});
server.installCollection("storage-sync-crypto");
server.installDeleteBucket();
server.etag = 765;
- yield* withSignedInUser(oldUser, function* () {
+ yield* withSignedInUser(oldUser, function* (extensionStorageSync, fxaService) {
+ const transformer = new KeyRingEncryptionRemoteTransformer(fxaService);
const FAKE_KEYRING = {
id: "keys",
keys: {},
salts: {},
uuid: "abcd",
kbHash: "abcd",
};
yield server.encryptAndAddRecord(transformer, "storage-sync-crypto", FAKE_KEYRING);
});
// Now we have this new user with a different kB.
- yield* withSignedInUser(loggedInUser, function* () {
- yield cryptoCollection._clear();
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ yield 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 = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let collectionKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
ok(collectionKeys.hasKeysFor([extensionId]),
`ensureCanSync should always return a keyring with a key for ${extensionId}`);
extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
deepEqual(server.getDeletedBuckets(), ["default"],
"Kinto server should have been wiped when keyring was thrown away");
let posts = server.getPosts();
@@ -780,64 +799,65 @@ add_task(function* checkSyncKeyRing_over
// The POST was to an empty server, so etag shouldn't be respected
equal(postedKeys.headers.Authorization, "Bearer some-access-token",
"keyring upload should be authorized");
equal(postedKeys.headers["If-None-Match"], "*",
"keyring upload should be to empty Kinto server");
equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
"keyring upload should be to keyring path");
- let body = yield new KeyRingEncryptionRemoteTransformer().decode(postedKeys.body.data);
+ let body = yield new KeyRingEncryptionRemoteTransformer(fxaService).decode(postedKeys.body.data);
ok(body.uuid, "new keyring should have a UUID");
equal(typeof body.uuid, "string", "keyring UUIDs should be strings");
notEqual(body.uuid, "abcd",
"new keyring should not have the same UUID as previous keyring");
ok(body.keys,
"new keyring should have a keys attribute");
ok(body.keys.default, "new keyring should have a default key");
// We should keep the extension key that was in our uploaded version.
deepEqual(extensionKey, body.keys.collections[extensionId],
"ensureCanSync should have returned keyring with the same key that was uploaded");
// This should be a no-op; the keys were uploaded as part of ensurekeysfor
- yield ExtensionStorageSync.checkSyncKeyRing();
+ yield extensionStorageSync.checkSyncKeyRing();
equal(server.getPosts().length, 1,
"checkSyncKeyRing should not need to post keys after they were reuploaded");
});
});
});
});
add_task(function* checkSyncKeyRing_flushes_on_uuid_change() {
// If we can decrypt the record, but the UUID has changed, that
// means another client has wiped the server and reuploaded a
// keyring, so reset sync state and reupload everything.
const extensionId = uuid();
const extension = {id: extensionId};
- const transformer = new KeyRingEncryptionRemoteTransformer();
yield* withSyncContext(function* (context) {
yield* withServer(function* (server) {
server.installCollection("storage-sync-crypto");
server.installDeleteBucket();
- yield* withSignedInUser(loggedInUser, function* () {
- yield cryptoCollection._clear();
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const transformer = new KeyRingEncryptionRemoteTransformer(fxaService);
+ yield extensionStorageSync.cryptoCollection._clear();
// Do an `ensureCanSync` to get access to keys and salt.
- let collectionKeys = yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ let collectionKeys = yield extensionStorageSync.ensureCanSync([extensionId]);
const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
server.installCollection(collectionId);
ok(collectionKeys.hasKeysFor([extensionId]),
`ensureCanSync should always return a keyring that has a key for ${extensionId}`);
const extensionKey = collectionKeys.keyForCollection(extensionId).keyPairB64;
// Set something to make sure that it gets re-uploaded when
// uuid changes.
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield extensionStorageSync.syncAll();
let posts = server.getPosts();
equal(posts.length, 2,
"should have posted a new keyring and an extension datum");
const postedKeys = posts[0];
equal(postedKeys.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
"should have posted keyring to /keys");
@@ -866,140 +886,136 @@ add_task(function* checkSyncKeyRing_flus
});
server.clearCollection("storage-sync-crypto");
server.etag = 765;
yield server.encryptAndAddRecordOnlyOnce(transformer, "storage-sync-crypto", newKeyRingData);
// Fake adding another extension just so that the keyring will
// really get synced.
const newExtension = uuid();
- const newKeyRing = yield ExtensionStorageSync.ensureCanSync([newExtension]);
+ const newKeyRing = yield extensionStorageSync.ensureCanSync([newExtension]);
// This should have detected the UUID change and flushed everything.
// The keyring should, however, be the same, since we just
// changed the UUID of the previously POSTed one.
deepEqual(newKeyRing.keyForCollection(extensionId).keyPairB64, extensionKey,
"ensureCanSync should have pulled down a new keyring with the same keys");
// Syncing should reupload the data for the extension.
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.syncAll();
posts = server.getPosts();
equal(posts.length, 4,
"should have posted keyring for new extension and reuploaded extension data");
const finalKeyRingPost = posts[2];
const reuploadedPost = posts[3];
equal(finalKeyRingPost.path, collectionRecordsPath("storage-sync-crypto") + "/keys",
"keyring for new extension should have been posted to /keys");
let finalKeyRing = yield transformer.decode(finalKeyRingPost.body.data);
equal(finalKeyRing.uuid, "abcd",
"newly uploaded keyring should preserve UUID from replacement keyring");
deepEqual(finalKeyRing.salts[extensionId], extensionSalt,
"newly uploaded keyring should preserve salts from existing salts");
// Confirm that the data got reuploaded
- const hashedId = "id-" + (yield cryptoCollection.hashWithExtensionSalt("key-my_2D_key", extensionId));
- equal(reuploadedPost.path, `${collectionRecordsPath(collectionId)}/${hashedId}`,
- "extension data should be posted to path corresponding to its key");
- let reuploadedData = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(reuploadedPost.body.data);
- equal(reuploadedData.key, "my-key",
- "extension data should have a key attribute corresponding to the extension data key");
+ let reuploadedData = yield assertExtensionRecord(fxaService, reuploadedPost, extension, "my-key");
equal(reuploadedData.data, 5,
"extension data should have a data attribute corresponding to the extension data value");
});
});
});
});
add_task(function* test_storage_sync_pulls_changes() {
const extensionId = defaultExtensionId;
const extension = defaultExtension;
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ const cryptoCollection = new CryptoCollection(fxaService);
+ let transformer = new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extensionId);
server.installCollection("storage-sync-crypto");
let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ yield extensionStorageSync.addOnChangedListener(extension, function() {
calls.push(arguments);
}, context);
- yield ExtensionStorageSync.ensureCanSync([extensionId]);
+ yield extensionStorageSync.ensureCanSync([extensionId]);
const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
server.installCollection(collectionId);
yield server.encryptAndAddRecord(transformer, collectionId, {
"id": "key-remote_2D_key",
"key": "remote-key",
"data": 6,
});
- yield ExtensionStorageSync.syncAll();
- const remoteValue = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ yield extensionStorageSync.syncAll();
+ const remoteValue = (yield extensionStorageSync.get(extension, "remote-key", context))["remote-key"];
equal(remoteValue, 6,
"ExtensionStorageSync.get() returns value retrieved from sync");
equal(calls.length, 1,
"syncing calls on-changed listener");
deepEqual(calls[0][0], {"remote-key": {newValue: 6}});
calls = [];
// Syncing again doesn't do anything
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.syncAll();
equal(calls.length, 0,
"syncing again shouldn't call on-changed listener");
// Updating the server causes us to pull down the new value
server.etag = 1000;
server.clearCollection(collectionId);
yield server.encryptAndAddRecord(transformer, collectionId, {
"id": "key-remote_2D_key",
"key": "remote-key",
"data": 7,
});
- yield ExtensionStorageSync.syncAll();
- const remoteValue2 = (yield ExtensionStorageSync.get(extension, "remote-key", context))["remote-key"];
+ yield extensionStorageSync.syncAll();
+ const remoteValue2 = (yield extensionStorageSync.get(extension, "remote-key", context))["remote-key"];
equal(remoteValue2, 7,
"ExtensionStorageSync.get() returns value updated from sync");
equal(calls.length, 1,
"syncing calls on-changed listener on update");
deepEqual(calls[0][0], {"remote-key": {oldValue: 6, newValue: 7}});
});
});
});
add_task(function* test_storage_sync_pushes_changes() {
// FIXME: This test relies on the fact that previous tests pushed
// keys and salts for the default extension ID
+ const extension = defaultExtension;
const extensionId = defaultExtensionId;
- const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
- const extension = defaultExtension;
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
server.installCollection(collectionId);
server.installCollection("storage-sync-crypto");
server.etag = 1000;
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield extensionStorageSync.set(extension, {"my-key": 5}, context);
// install this AFTER we set the key to 5...
let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
+ extensionStorageSync.addOnChangedListener(extension, function() {
calls.push(arguments);
}, context);
- yield ExtensionStorageSync.syncAll();
- const localValue = (yield ExtensionStorageSync.get(extension, "my-key", context))["my-key"];
- const hashedId = "id-" + (yield cryptoCollection.hashWithExtensionSalt("key-my_2D_key", extensionId));
+ yield extensionStorageSync.syncAll();
+ const localValue = (yield extensionStorageSync.get(extension, "my-key", context))["my-key"];
equal(localValue, 5,
"pushing an ExtensionStorageSync value shouldn't change local value");
+ const hashedId = "id-" + (yield cryptoCollection.hashWithExtensionSalt("key-my_2D_key", extensionId));
let posts = server.getPosts();
// FIXME: Keys were pushed in a previous test
equal(posts.length, 1,
"pushing a value should cause a post to the server");
const post = posts[0];
assertPostedNewRecord(post);
equal(post.path, `${collectionRecordsPath(collectionId)}/${hashedId}`,
@@ -1008,29 +1024,27 @@ add_task(function* test_storage_sync_pus
const encrypted = post.body.data;
ok(encrypted.ciphertext,
"pushing a value should post an encrypted record");
ok(!encrypted.data,
"pushing a value should not have any plaintext data");
equal(encrypted.id, hashedId,
"pushing a value should use a kinto-friendly record ID");
- const record = yield transformer.decode(encrypted);
- equal(record.key, "my-key",
- "when decrypted, a pushed value should have a key field corresponding to its storage.sync key");
+ const record = yield assertExtensionRecord(fxaService, post, extension, "my-key");
equal(record.data, 5,
"when decrypted, a pushed value should have a data field corresponding to its storage.sync value");
equal(record.id, "key-my_2D_key",
"when decrypted, a pushed value should have an id field corresponding to its record ID");
equal(calls.length, 0,
"pushing a value shouldn't call the on-changed listener");
- yield ExtensionStorageSync.set(extension, {"my-key": 6}, context);
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.set(extension, {"my-key": 6}, context);
+ yield extensionStorageSync.syncAll();
// Doesn't push keys because keys were pushed by a previous test.
posts = server.getPosts();
equal(posts.length, 2,
"updating a value should trigger another push");
const updatePost = posts[1];
assertPostedUpdatedRecord(updatePost, 1000);
equal(updatePost.path, `${collectionRecordsPath(collectionId)}/${hashedId}`,
@@ -1043,104 +1057,108 @@ add_task(function* test_storage_sync_pus
"pushing an updated value should not have any plaintext visible");
equal(updateEncrypted.id, hashedId,
"pushing an updated value should maintain the same ID");
});
});
});
add_task(function* test_storage_sync_pulls_deletes() {
- const collectionId = yield cryptoCollection.extensionIdToCollectionId(defaultExtensionId);
const extension = defaultExtension;
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ const cryptoCollection = new CryptoCollection(fxaService);
+ const collectionId = yield cryptoCollection.extensionIdToCollectionId(defaultExtensionId);
server.installCollection(collectionId);
server.installCollection("storage-sync-crypto");
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield extensionStorageSync.syncAll();
server.clearPosts();
let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
+ yield extensionStorageSync.addOnChangedListener(extension, function() {
calls.push(arguments);
}, context);
- yield server.encryptAndAddRecord(new CollectionKeyEncryptionRemoteTransformer(extension.id), collectionId, {
+ const transformer = new CollectionKeyEncryptionRemoteTransformer(new CryptoCollection(fxaService), extension.id);
+ yield server.encryptAndAddRecord(transformer, collectionId, {
"id": "key-my_2D_key",
"data": 6,
"_status": "deleted",
});
- yield ExtensionStorageSync.syncAll();
- const remoteValues = (yield ExtensionStorageSync.get(extension, "my-key", context));
+ yield extensionStorageSync.syncAll();
+ const remoteValues = (yield extensionStorageSync.get(extension, "my-key", context));
ok(!remoteValues["my-key"],
"ExtensionStorageSync.get() shows value was deleted by sync");
equal(server.getPosts().length, 0,
"pulling the delete shouldn't cause posts");
equal(calls.length, 1,
"syncing calls on-changed listener");
deepEqual(calls[0][0], {"my-key": {oldValue: 5}});
calls = [];
// Syncing again doesn't do anything
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.syncAll();
equal(calls.length, 0,
"syncing again shouldn't call on-changed listener");
});
});
});
add_task(function* test_storage_sync_pushes_deletes() {
const extensionId = uuid();
const extension = {id: extensionId};
- yield cryptoCollection._clear();
- yield cryptoCollection._setSalt(extensionId, cryptoCollection.getNewSalt());
- const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
+ yield* withSignedInUser(loggedInUser, function* (extensionStorageSync, fxaService) {
+ const cryptoCollection = new CryptoCollection(fxaService);
+ yield cryptoCollection._clear();
+ yield cryptoCollection._setSalt(extensionId, cryptoCollection.getNewSalt());
+ const collectionId = yield cryptoCollection.extensionIdToCollectionId(extensionId);
+
server.installCollection(collectionId);
server.installCollection("storage-sync-crypto");
server.etag = 1000;
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
+ yield extensionStorageSync.set(extension, {"my-key": 5}, context);
let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
+ extensionStorageSync.addOnChangedListener(extension, function() {
calls.push(arguments);
}, context);
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.syncAll();
let posts = server.getPosts();
equal(posts.length, 2,
"pushing a non-deleted value should post keys and post the value to the server");
- yield ExtensionStorageSync.remove(extension, ["my-key"], context);
+ yield extensionStorageSync.remove(extension, ["my-key"], context);
equal(calls.length, 1,
"deleting a value should call the on-changed listener");
- yield ExtensionStorageSync.syncAll();
+ yield extensionStorageSync.syncAll();
equal(calls.length, 1,
"pushing a deleted value shouldn't call the on-changed listener");
// Doesn't push keys because keys were pushed by a previous test.
const hashedId = "id-" + (yield cryptoCollection.hashWithExtensionSalt("key-my_2D_key", extensionId));
posts = server.getPosts();
equal(posts.length, 3,
"deleting a value should trigger another push");
const post = posts[2];
assertPostedUpdatedRecord(post, 1000);
equal(post.path, `${collectionRecordsPath(collectionId)}/${hashedId}`,
"pushing a deleted value should go to the same path");
ok(post.method, "PUT");
ok(post.body.data.ciphertext,
"deleting a value should have an encrypted body");
- const decoded = yield new CollectionKeyEncryptionRemoteTransformer(extensionId).decode(post.body.data);
+ const decoded = yield new CollectionKeyEncryptionRemoteTransformer(cryptoCollection, extensionId).decode(post.body.data);
equal(decoded._status, "deleted");
// Ideally, we'd check that decoded.deleted is not true, because
// the encrypted record shouldn't have it, but the decoder will
// add it when it sees _status == deleted
});
});
});