summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js1073
1 files changed, 0 insertions, 1073 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
deleted file mode 100644
index 4258289e3..000000000
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync.js
+++ /dev/null
@@ -1,1073 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-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://gre/modules/ExtensionStorageSync.jsm");
-const {
- CollectionKeyEncryptionRemoteTransformer,
- cryptoCollection,
- idToKey,
- extensionIdToCollectionId,
- 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");
-
-/* globals BulkKeyBundle, CommonUtils, EncryptionRemoteTransformer */
-/* globals KeyRingEncryptionRemoteTransformer */
-/* globals Utils */
-
-function handleCannedResponse(cannedResponse, request, response) {
- response.setStatusLine(null, cannedResponse.status.status,
- cannedResponse.status.statusText);
- // send the headers
- for (let headerLine of cannedResponse.sampleHeaders) {
- let headerElements = headerLine.split(":");
- response.setHeader(headerElements[0], headerElements[1].trimLeft());
- }
- response.setHeader("Date", (new Date()).toUTCString());
-
- response.write(cannedResponse.responseBody);
-}
-
-function collectionRecordsPath(collectionId) {
- return `/buckets/default/collections/${collectionId}/records`;
-}
-
-class KintoServer {
- constructor() {
- // Set up an HTTP Server
- this.httpServer = new HttpServer();
- this.httpServer.start(-1);
-
- // Map<CollectionId, Set<Object>> corresponding to the data in the
- // Kinto server
- this.collections = new Map();
-
- // ETag to serve with responses
- this.etag = 1;
-
- this.port = this.httpServer.identity.primaryPort;
- // POST requests we receive from the client go here
- this.posts = [];
- // DELETEd buckets will go here.
- this.deletedBuckets = [];
- // Anything in here will force the next POST to generate a conflict
- this.conflicts = [];
-
- this.installConfigPath();
- this.installBatchPath();
- this.installCatchAll();
- }
-
- clearPosts() {
- this.posts = [];
- }
-
- getPosts() {
- return this.posts;
- }
-
- getDeletedBuckets() {
- return this.deletedBuckets;
- }
-
- installConfigPath() {
- const configPath = "/v1/";
- const responseBody = JSON.stringify({
- "settings": {"batch_max_requests": 25},
- "url": `http://localhost:${this.port}/v1/`,
- "documentation": "https://kinto.readthedocs.org/",
- "version": "1.5.1",
- "commit": "cbc6f58",
- "hello": "kinto",
- });
- const configResponse = {
- "sampleHeaders": [
- "Access-Control-Allow-Origin: *",
- "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- "Content-Type: application/json; charset=UTF-8",
- "Server: waitress",
- ],
- "status": {status: 200, statusText: "OK"},
- "responseBody": responseBody,
- };
-
- function handleGetConfig(request, response) {
- if (request.method != "GET") {
- dump(`ARGH, got ${request.method}\n`);
- }
- return handleCannedResponse(configResponse, request, response);
- }
-
- this.httpServer.registerPathHandler(configPath, handleGetConfig);
- }
-
- installBatchPath() {
- const batchPath = "/v1/batch";
-
- function handlePost(request, response) {
- let bodyStr = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
- let body = JSON.parse(bodyStr);
- let defaults = body.defaults;
- for (let req of body.requests) {
- let headers = Object.assign({}, defaults && defaults.headers || {}, req.headers);
- // FIXME: assert auth is "Bearer ...token..."
- this.posts.push(Object.assign({}, req, {headers}));
- }
-
- response.setStatusLine(null, 200, "OK");
- response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.setHeader("Date", (new Date()).toUTCString());
-
- let postResponse = {
- responses: body.requests.map(req => {
- let oneBody;
- if (req.method == "DELETE") {
- let id = req.path.match(/^\/buckets\/default\/collections\/.+\/records\/(.+)$/)[1];
- oneBody = {
- "data": {
- "deleted": true,
- "id": id,
- "last_modified": this.etag,
- },
- };
- } else {
- oneBody = {"data": Object.assign({}, req.body.data, {last_modified: this.etag}),
- "permissions": []};
- }
-
- return {
- path: req.path,
- status: 201, // FIXME -- only for new posts??
- headers: {"ETag": 3000}, // FIXME???
- body: oneBody,
- };
- }),
- };
-
- if (this.conflicts.length > 0) {
- const {collectionId, encrypted} = this.conflicts.shift();
- this.collections.get(collectionId).add(encrypted);
- dump(`responding with etag ${this.etag}\n`);
- postResponse = {
- responses: body.requests.map(req => {
- return {
- path: req.path,
- status: 412,
- headers: {"ETag": this.etag}, // is this correct??
- body: {
- details: {
- existing: encrypted,
- },
- },
- };
- }),
- };
- }
-
- response.write(JSON.stringify(postResponse));
-
- // "sampleHeaders": [
- // "Access-Control-Allow-Origin: *",
- // "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
- // "Server: waitress",
- // "Etag: \"4000\""
- // ],
- }
-
- this.httpServer.registerPathHandler(batchPath, handlePost.bind(this));
- }
-
- installCatchAll() {
- this.httpServer.registerPathHandler("/", (request, response) => {
- dump(`got request: ${request.method}:${request.path}?${request.queryString}\n`);
- dump(`${CommonUtils.readBytesFromInputStream(request.bodyInputStream)}\n`);
- });
- }
-
- installCollection(collectionId) {
- this.collections.set(collectionId, new Set());
-
- const remoteRecordsPath = "/v1" + collectionRecordsPath(encodeURIComponent(collectionId));
-
- function handleGetRecords(request, response) {
- if (request.method != "GET") {
- do_throw(`only GET is supported on ${remoteRecordsPath}`);
- }
-
- response.setStatusLine(null, 200, "OK");
- response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.setHeader("Date", (new Date()).toUTCString());
- response.setHeader("ETag", this.etag.toString());
-
- const records = this.collections.get(collectionId);
- // Can't JSON a Set directly, so convert to Array
- let data = Array.from(records);
- if (request.queryString.includes("_since=")) {
- data = data.filter(r => !(r._inPast || false));
- }
-
- // Remove records that we only needed to serve once.
- // FIXME: come up with a more coherent idea of time here.
- // See bug 1321570.
- for (const record of records) {
- if (record._onlyOnce) {
- records.delete(record);
- }
- }
-
- const body = JSON.stringify({
- "data": data,
- });
- response.write(body);
- }
-
- this.httpServer.registerPathHandler(remoteRecordsPath, handleGetRecords.bind(this));
- }
-
- installDeleteBucket() {
- this.httpServer.registerPrefixHandler("/v1/buckets/", (request, response) => {
- if (request.method != "DELETE") {
- dump(`got a non-delete action on bucket: ${request.method} ${request.path}\n`);
- return;
- }
-
- const noPrefix = request.path.slice("/v1/buckets/".length);
- const [bucket, afterBucket] = noPrefix.split("/", 1);
- if (afterBucket && afterBucket != "") {
- dump(`got a delete for a non-bucket: ${request.method} ${request.path}\n`);
- }
-
- this.deletedBuckets.push(bucket);
- // Fake like this actually deletes the records.
- for (const [, set] of this.collections) {
- set.clear();
- }
-
- response.write(JSON.stringify({
- data: {
- deleted: true,
- last_modified: 1475161309026,
- id: "b09f1618-d789-302d-696e-74ec53ee18a8", // FIXME
- },
- }));
- });
- }
-
- // Utility function to install a keyring at the start of a test.
- installKeyRing(keysData, etag, {conflict = false} = {}) {
- this.installCollection("storage-sync-crypto");
- const keysRecord = {
- "id": "keys",
- "keys": keysData,
- "last_modified": etag,
- };
- this.etag = etag;
- const methodName = conflict ? "encryptAndAddRecordWithConflict" : "encryptAndAddRecord";
- this[methodName](new KeyRingEncryptionRemoteTransformer(),
- "storage-sync-crypto", keysRecord);
- }
-
- // Add an already-encrypted record.
- addRecord(collectionId, record) {
- this.collections.get(collectionId).add(record);
- }
-
- // Add a record that is only served if no `_since` is present.
- //
- // Since in real life, Kinto only serves a record as part of a
- // changes feed if `_since` is before the record's modification
- // time, this can be helpful to test certain kinds of syncing logic.
- //
- // FIXME: tracking of "time" in this mock server really needs to be
- // implemented correctly rather than these hacks. See bug 1321570.
- addRecordInPast(collectionId, record) {
- record._inPast = true;
- this.addRecord(collectionId, record);
- }
-
- encryptAndAddRecord(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- this.addRecord(collectionId, encrypted);
- });
- }
-
- // Like encryptAndAddRecord, but add a flag that will only serve
- // this record once.
- //
- // Since in real life, Kinto only serves a record as part of a changes feed
- // once, this can be useful for testing complicated syncing logic.
- //
- // FIXME: This kind of logic really needs to be subsumed into some
- // more-realistic tracking of "time" (simulated by etags). See bug 1321570.
- encryptAndAddRecordOnlyOnce(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- encrypted._onlyOnce = true;
- this.addRecord(collectionId, encrypted);
- });
- }
-
- // Conflicts block the next push and then appear in the collection specified.
- encryptAndAddRecordWithConflict(transformer, collectionId, record) {
- return transformer.encode(record).then(encrypted => {
- this.conflicts.push({collectionId, encrypted});
- });
- }
-
- clearCollection(collectionId) {
- this.collections.get(collectionId).clear();
- }
-
- stop() {
- this.httpServer.stop(() => { });
- }
-}
-
-// Run a block of code with access to a KintoServer.
-function* withServer(f) {
- let server = new KintoServer();
- // Point the sync.storage client to use the test server we've just started.
- Services.prefs.setCharPref("webextensions.storage.sync.serverURL",
- `http://localhost:${server.port}/v1`);
- try {
- yield* f(server);
- } finally {
- server.stop();
- }
-}
-
-// Run a block of code with access to both a sync context and a
-// KintoServer. This is meant as a workaround for eslint's refusal to
-// let me have 5 nested callbacks.
-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.
-function* withSignedInUser(user, f) {
- const oldESSFxAccounts = ExtensionStorageSync._fxaService;
- const oldERTFxAccounts = EncryptionRemoteTransformer.prototype._fxaService;
- ExtensionStorageSync._fxaService = EncryptionRemoteTransformer.prototype._fxaService = {
- 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;
- }
-}
-
-// 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.
-function assertAuthenticatedRequest(post) {
- equal(post.headers.Authorization, "Bearer some-access-token");
-}
-
-// Assert that this post was made with the correct request headers to
-// create a new resource while protecting against someone else
-// creating it at the same time (in other words, "If-None-Match: *").
-// Also calls assertAuthenticatedRequest(post).
-function assertPostedNewRecord(post) {
- assertAuthenticatedRequest(post);
- equal(post.headers["If-None-Match"], "*");
-}
-
-// Assert that this post was made with the correct request headers to
-// update an existing resource while protecting against concurrent
-// modification (in other words, `If-Match: "${etag}"`).
-// 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) {
- equal(post.path, collectionRecordsPath("storage-sync-crypto") + "/keys");
-
- let body = yield new KeyRingEncryptionRemoteTransformer().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`);
- return body;
-});
-
-// assertEqual, but for keyring[extensionId] == key.
-function assertKeyRingKey(keyRing, extensionId, expectedKey, message) {
- if (!message) {
- 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);
-}
-
-// 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 ANOTHER_KB = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0";
-const loggedInUser = {
- uid: "0123456789abcdef0123456789abcdef",
- kB: BORING_KB,
- oauthTokens: {
- "sync:addon-storage": {
- token: "some-access-token",
- },
- },
-};
-const defaultCollectionId = extensionIdToCollectionId(loggedInUser, defaultExtensionId);
-
-function uuid() {
- const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
- return uuidgen.generateUUID().toString();
-}
-
-add_task(function* test_key_to_id() {
- equal(keyToId("foo"), "key-foo");
- equal(keyToId("my-new-key"), "key-my_2D_new_2D_key");
- equal(keyToId(""), "key-");
- equal(keyToId("™"), "key-_2122_");
- equal(keyToId("\b"), "key-_8_");
- equal(keyToId("abc\ndef"), "key-abc_A_def");
- equal(keyToId("Kinto's fancy_string"), "key-Kinto_27_s_20_fancy_5F_string");
-
- const KEYS = ["foo", "my-new-key", "", "Kinto's fancy_string", "™", "\b"];
- for (let key of KEYS) {
- equal(idToKey(keyToId(key)), key);
- }
-
- equal(idToKey("hi"), null);
- equal(idToKey("-key-hi"), null);
- equal(idToKey("key--abcd"), null);
- equal(idToKey("key-%"), null);
- equal(idToKey("key-_HI"), null);
- 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 newKBUser = Object.assign(loggedInUser, {kB: ANOTHER_KB});
- const extensionId = "{9419cce6-5435-11e6-84bf-54ee758d6342}";
- const extensionId2 = "{9419cce6-5435-11e6-84bf-54ee758d6343}";
-
- // "random" 32-char hex userid
- equal(extensionIdToCollectionId(loggedInUser, extensionId),
- "abf4e257dad0c89027f8f25bd196d4d69c100df375655a0c49f4cea7b791ea7d");
- equal(extensionIdToCollectionId(loggedInUser, extensionId),
- extensionIdToCollectionId(newKBUser, extensionId));
- equal(extensionIdToCollectionId(loggedInUser, extensionId2),
- "6584b0153336fb274912b31a3225c15a92b703cdc3adfe1917c1aa43122a52b8");
-});
-
-add_task(function* ensureKeysFor_posts_new_keys() {
- const extensionId = uuid();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- let newKeys = yield ExtensionStorageSync.ensureKeysFor([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);
- ok(body.keys.collections[extensionId], `keys object should have a key for ${extensionId}`);
-
- // Try adding another key to make sure that the first post was
- // OK, even on a new profile.
- yield cryptoCollection._clear();
- server.clearPosts();
- // Restore the first posted keyring
- server.addRecordInPast("storage-sync-crypto", post.body.data);
- const extensionId2 = uuid();
- newKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId2]);
- ok(newKeys.hasKeysFor([extensionId]), `didn't forget key for ${extensionId}`);
- ok(newKeys.hasKeysFor([extensionId2]), `new key generated for ${extensionId2}`);
-
- posts = server.getPosts();
- // FIXME: some kind of bug where we try to repush the
- // server_wins version multiple times in a single sync. We
- // actually push 5 times as of this writing.
- // See bug 1321571.
- // equal(posts.length, 1);
- const newPost = posts[posts.length - 1];
- const newBody = yield assertPostedEncryptedKeys(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}`);
-
- });
- });
-});
-
-add_task(function* ensureKeysFor_pulls_key() {
- // ensureKeysFor is implemented by adding a key to our local record
- // and doing a sync. This means that if the same key exists
- // remotely, we get a "conflict". Ensure that we handle this
- // correctly -- we keep the server key (since presumably it's
- // already been used to encrypt records) and we don't wipe out other
- // collections' keys.
- const extensionId = uuid();
- const extensionId2 = uuid();
- const DEFAULT_KEY = new BulkKeyBundle("[default]");
- DEFAULT_KEY.generateRandom();
- const RANDOM_KEY = new BulkKeyBundle(extensionId);
- RANDOM_KEY.generateRandom();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- const keysData = {
- "default": DEFAULT_KEY.keyPairB64,
- "collections": {
- [extensionId]: RANDOM_KEY.keyPairB64,
- },
- };
- server.installKeyRing(keysData, 999);
-
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- assertKeyRingKey(collectionKeys, extensionId, RANDOM_KEY);
-
- let posts = server.getPosts();
- equal(posts.length, 0,
- "ensureKeysFor 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;
- server.clearCollection("storage-sync-crypto");
- server.installKeyRing(keysData, 1000);
-
- let newCollectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId, extensionId2]);
- assertKeyRingKey(newCollectionKeys, extensionId2, newKey);
- assertKeyRingKey(newCollectionKeys, extensionId, RANDOM_KEY,
- `ensureKeysFor shouldn't lose the old key for ${extensionId}`);
-
- posts = server.getPosts();
- equal(posts.length, 0, "ensureKeysFor shouldn't push when updating keys");
- });
- });
-});
-
-add_task(function* ensureKeysFor_handles_conflicts() {
- // Syncing is done through a pull followed by a push of any merged
- // changes. Accordingly, the only way to have a "true" conflict --
- // 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();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- const keysData = {
- "default": DEFAULT_KEY.keyPairB64,
- "collections": {
- [extensionId]: RANDOM_KEY.keyPairB64,
- },
- };
- server.installKeyRing(keysData, 765, {conflict: true});
-
- yield cryptoCollection._clear();
-
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([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);
- // 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;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection("storage-sync-crypto");
- server.etag = 765;
-
- yield cryptoCollection._clear();
-
- // Do an `ensureKeysFor` to generate some keys.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor 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");
- });
-
- // 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();
-
- 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);
- deepEqual(body.keys.collections[extensionId], extensionKey,
- `the posted keyring should have the same key for ${extensionId} as the old one`);
- });
-
- // Verify that with the old kB, we can't decrypt the record.
- yield* withSignedInUser(loggedInUser, function* () {
- let error;
- try {
- yield new KeyRingEncryptionRemoteTransformer().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* () {
- const FAKE_KEYRING = {
- id: "keys",
- keys: {},
- 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();
-
- // Do an `ensureKeysFor` to generate some keys.
- // This will try to sync, notice that the record is
- // undecryptable, and clear the server.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor 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();
- equal(posts.length, 1,
- "new keyring should have been uploaded");
- const postedKeys = posts[0];
- // 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);
- 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],
- "ensureKeysFor 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();
- 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 collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
- const transformer = new KeyRingEncryptionRemoteTransformer();
- yield* withSyncContext(function* (context) {
- yield* withServer(function* (server) {
- server.installCollection("storage-sync-crypto");
- server.installCollection(collectionId);
- server.installDeleteBucket();
- yield* withSignedInUser(loggedInUser, function* () {
- yield cryptoCollection._clear();
-
- // Do an `ensureKeysFor` to get access to keys.
- let collectionKeys = yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- ok(collectionKeys.hasKeysFor([extensionId]),
- `ensureKeysFor 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();
-
- 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");
-
- let body = yield transformer.decode(postedKeys.body.data);
- ok(body.uuid,
- "keyring should have a UUID");
- ok(body.keys,
- "keyring should have a keys attribute");
- ok(body.keys.default,
- "keyring should have a default key");
- deepEqual(extensionKey, body.keys.collections[extensionId],
- "new keyring should have the same key that we uploaded");
-
- // Another client comes along and replaces the UUID.
- // In real life, this would mean changing the keys too, but
- // this test verifies that just changing the UUID is enough.
- const newKeyRingData = Object.assign({}, body, {
- uuid: "abcd",
- // Technically, last_modified should be served outside the
- // object, but the transformer will pass it through in
- // either direction, so this is OK.
- last_modified: 765,
- });
- 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.ensureKeysFor([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,
- "ensureKeysFor should have pulled down a new keyring with the same keys");
-
- // Syncing should reupload the data for the extension.
- 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");
-
- // Confirm that the data got reuploaded
- equal(reuploadedPost.path, collectionRecordsPath(collectionId) + "/key-my_2D_key",
- "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");
- 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 collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
-
- let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield ExtensionStorageSync.ensureKeysFor([extensionId]);
- 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"];
- 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();
-
- 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"];
- 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() {
- const extensionId = defaultExtensionId;
- const collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- let transformer = new CollectionKeyEncryptionRemoteTransformer(extensionId);
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
-
- // install this AFTER we set the key to 5...
- let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- 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");
-
- let posts = server.getPosts();
- 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) + "/key-my_2D_key",
- "pushing a value should have a path corresponding to its id");
-
- 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, "key-my_2D_key",
- "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");
- 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();
-
- // 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) + "/key-my_2D_key",
- "pushing an updated value should go to the same path");
-
- const updateEncrypted = updatePost.body.data;
- ok(updateEncrypted.ciphertext,
- "pushing an updated value should still be encrypted");
- ok(!updateEncrypted.data,
- "pushing an updated value should not have any plaintext visible");
- equal(updateEncrypted.id, "key-my_2D_key",
- "pushing an updated value should maintain the same ID");
- });
- });
-});
-
-add_task(function* test_storage_sync_pulls_deletes() {
- const collectionId = defaultCollectionId;
- const extension = defaultExtension;
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
- yield ExtensionStorageSync.syncAll();
- server.clearPosts();
-
- let calls = [];
- yield ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- yield server.addRecord(collectionId, {
- "id": "key-my_2D_key",
- "deleted": true,
- });
-
- 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();
-
- equal(calls.length, 0,
- "syncing again shouldn't call on-changed listener");
- });
- });
-});
-
-add_task(function* test_storage_sync_pushes_deletes() {
- const extensionId = uuid();
- const collectionId = extensionIdToCollectionId(loggedInUser, extensionId);
- const extension = {id: extensionId};
- yield cryptoCollection._clear();
- yield* withContextAndServer(function* (context, server) {
- yield* withSignedInUser(loggedInUser, function* () {
- server.installCollection(collectionId);
- server.installCollection("storage-sync-crypto");
- server.etag = 1000;
-
- yield ExtensionStorageSync.set(extension, {"my-key": 5}, context);
-
- let calls = [];
- ExtensionStorageSync.addOnChangedListener(extension, function() {
- calls.push(arguments);
- }, context);
-
- 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);
- equal(calls.length, 1,
- "deleting a value should call the on-changed listener");
-
- 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.
- 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) + "/key-my_2D_key",
- "pushing a deleted value should go to the same path");
- ok(post.method, "DELETE");
- ok(!post.body,
- "deleting a value shouldn't have a body");
- });
- });
-});