diff --git a/browser/components/loop/modules/LoopRooms.jsm b/browser/components/loop/modules/LoopRooms.jsm index a258eeebe94f..d3374c2d615c 100644 --- a/browser/components/loop/modules/LoopRooms.jsm +++ b/browser/components/loop/modules/LoopRooms.jsm @@ -7,13 +7,11 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - const {MozLoopService, LOOP_SESSION_TYPE} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", - "resource://services-common/utils.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() { const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); return new EventEmitter(); @@ -21,11 +19,8 @@ XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() { XPCOMUtils.defineLazyGetter(this, "gLoopBundle", function() { return Services.strings.createBundle('chrome://browser/locale/loop/loop.properties'); }); - -XPCOMUtils.defineLazyModuleGetter(this, "LoopRoomsCache", - "resource:///modules/loop/LoopRoomsCache.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "loopUtils", - "resource:///modules/loop/utils.js", "utils"); + "resource:///modules/loop/utils.js", "utils") XPCOMUtils.defineLazyModuleGetter(this, "loopCrypto", "resource:///modules/loop/crypto.js", "LoopCrypto"); @@ -46,8 +41,6 @@ const roomsPushNotification = function(version, channelID) { let gDirty = true; // Global variable that keeps track of the currently used account. let gCurrentUser = null; -// Global variable that keeps track of the room cache. -let gRoomsCache = null; /** * Extend a `target` object with the properties defined in `source`. @@ -130,13 +123,6 @@ let LoopRoomsInternal = { */ rooms: new Map(), - get roomsCache() { - if (!gRoomsCache) { - gRoomsCache = new LoopRoomsCache(); - } - return gRoomsCache; - }, - /** * @var {String} sessionType The type of user session. May be 'FXA' or 'GUEST'. */ @@ -294,40 +280,12 @@ let LoopRoomsInternal = { throw new Error("Missing wrappedKey"); } - let savedRoomKey = yield this.roomsCache.getKey(this.sessionType, roomData.roomToken); - let fallback = false; - let key; - - try { - key = yield this.promiseDecryptRoomKey(roomData.context.wrappedKey); - } catch (error) { - // If we don't have a key saved, then we can't do anything. - if (!savedRoomKey) { - throw error; - } - - // We failed to decrypt the room key, so has our FxA key changed? - // If so, we fall-back to the saved room key. - key = savedRoomKey; - fallback = true; - } + // Bug 1152761 will cause us to additionally store keys locally. We'll + // need to add some code for recovery in case decryption fails. + let key = yield this.promiseDecryptRoomKey(roomData.context.wrappedKey); let decryptedData = yield loopCrypto.decryptBytes(key, roomData.context.value); - if (fallback) { - // Fallback decryption succeeded, so we need to re-encrypt the room key and - // save the data back again. - // XXX Bug 1152764 will implement this or make it a separate bug. - } else if (!savedRoomKey || key != savedRoomKey) { - // Decryption succeeded, but we don't have the right key saved. - try { - yield this.roomsCache.setKey(this.sessionType, roomData.roomToken, key); - } - catch (error) { - MozLoopService.log.error("Failed to save room key:", error); - } - } - roomData.roomKey = key; roomData.decryptedContext = JSON.parse(decryptedData); @@ -384,7 +342,7 @@ let LoopRoomsInternal = { this.saveAndNotifyUpdate(roomData, isUpdate); } catch (error) { - MozLoopService.log.error("Failed to decrypt room data: ", error); + MozLoopService.log.error("Failed to decrypt room data: " + error); // Do what we can to save the room data. room.decryptedContext = {}; this.saveAndNotifyUpdate(room, isUpdate); @@ -537,9 +495,6 @@ let LoopRoomsInternal = { this.setGuestCreatedRoom(true); } - // Now we've got the room token, we can save the key to disk. - yield this.roomsCache.setKey(this.sessionType, room.roomToken, room.roomKey); - eventEmitter.emit("add", room); callback(null, room); }.bind(this)).catch(callback); @@ -749,10 +704,6 @@ let LoopRoomsInternal = { sendData = { roomName: newRoomName }; - } else { - // This might be an upgrade to encrypted rename, so store the key - // just in case. - yield this.roomsCache.setKey(this.sessionType, all.roomToken, all.roomKey); } let response = yield MozLoopService.hawkRequest(this.sessionType, diff --git a/browser/components/loop/modules/LoopRoomsCache.jsm b/browser/components/loop/modules/LoopRoomsCache.jsm deleted file mode 100644 index e83ff11eb8a3..000000000000 --- a/browser/components/loop/modules/LoopRoomsCache.jsm +++ /dev/null @@ -1,159 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -const {MozLoopService, LOOP_SESSION_TYPE} = - Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); -XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", - "resource://services-common/utils.js"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); - -this.EXPORTED_SYMBOLS = ["LoopRoomsCache"]; - -const LOOP_ROOMS_CACHE_FILENAME = "loopRoomsCache.json"; - -/** - * RoomsCache is a cache for saving simple rooms data to the disk in case we - * need it for back-up purposes, e.g. recording room keys for FxA if the user - * changes their password. - * - * The format of the data is: - * - * { - * : { - * : { - * "key": - * } - * } - * } - * - * It is intended to try and keep the data forward and backwards compatible in - * a reasonable manner, hence why the structure is more complex than it needs - * to be to store tokens and keys. - * - * @param {Object} options The options for the RoomsCache, containing: - * - {String} baseDir The base directory in which to save the file. - * - {String} filename The filename for the cache file. - */ -function LoopRoomsCache(options) { - options = options || {}; - - this.baseDir = options.baseDir || OS.Constants.Path.profileDir; - this.path = OS.Path.join( - this.baseDir, - options.filename || LOOP_ROOMS_CACHE_FILENAME - ); - this._cache = null; -} - -LoopRoomsCache.prototype = { - /** - * Updates the local copy of the cache and saves it to disk. - * - * @param {Object} contents An object to be saved in json format. - * @return {Promise} A promise that is resolved once the save is complete. - */ - _setCache: function(contents) { - this._cache = contents; - - return OS.File.makeDir(this.baseDir, {ignoreExisting: true}).then(() => { - return CommonUtils.writeJSON(contents, this.path); - }); - }, - - /** - * Returns the local copy of the cache if there is one, otherwise it reads - * it from the disk. - * - * @return {Promise} A promise that is resolved once the read is complete. - */ - _getCache: Task.async(function* () { - if (this._cache) { - return this._cache; - } - - try { - return (this._cache = yield CommonUtils.readJSON(this.path)); - } catch(error) { - // This is really complex due to OSFile's error handling, see bug 1160109. - if ((OS.Constants.libc && error.unixErrno != OS.Constants.libc.ENOENT) || - (OS.Constants.Win && error.winLastError != OS.Constants.Win.ERROR_FILE_NOT_FOUND)) { - MozLoopService.log.debug("Error reading the cache:", error); - } - return (this._cache = {}); - } - }), - - /** - * Function for testability purposes. Clears the cache. - * - * @return {Promise} A promise that is resolved once the clear is complete. - */ - clear: function() { - this._cache = null; - return OS.File.remove(this.path) - }, - - /** - * Gets a room key from the cache. - * - * @param {LOOP_SESSION_TYPE} sessionType The session type for the room. - * @param {String} roomToken The token for the room. - * @return {Promise} A promise that is resolved when the data has been read - * with the value of the key, or null if it isn't present. - */ - getKey: Task.async(function* (sessionType, roomToken) { - if (sessionType != LOOP_SESSION_TYPE.FXA) { - return null; - } - - let sessionData = (yield this._getCache())[sessionType]; - - if (!sessionData || !sessionData[roomToken]) { - return null; - } - return sessionData[roomToken].key; - }), - - /** - * Stores a room key into the cache. Note, if the key has not changed, - * the store will not be re-written. - * - * @param {LOOP_SESSION_TYPE} sessionType The session type for the room. - * @param {String} roomToken The token for the room. - * @param {String} roomKey The encryption key for the room. - * @return {Promise} A promise that is resolved when the data has been stored. - */ - setKey: Task.async(function* (sessionType, roomToken, roomKey) { - if (sessionType != LOOP_SESSION_TYPE.FXA) { - return; - } - - let cache = yield this._getCache(); - - // Create these objects if they don't exist. - // We aim to do this creation and setting of the room key in a - // forwards-compatible way so that if new fields are added to rooms later - // then we don't mess them up (if there's no keys). - if (!cache[sessionType]) { - cache[sessionType] = {}; - } - - if (!cache[sessionType][roomToken]) { - cache[sessionType][roomToken] = {}; - } - - // Only save it if there's no key, or it is different. - if (!cache[sessionType][roomToken].key || - cache[sessionType][roomToken].key != roomKey) { - cache[sessionType][roomToken].key = roomKey; - return yield this._setCache(cache); - } - }) -}; diff --git a/browser/components/loop/moz.build b/browser/components/loop/moz.build index 11cd3bcca98e..a2cbc2469d8e 100644 --- a/browser/components/loop/moz.build +++ b/browser/components/loop/moz.build @@ -20,7 +20,6 @@ EXTRA_JS_MODULES.loop += [ 'modules/LoopCalls.jsm', 'modules/LoopContacts.jsm', 'modules/LoopRooms.jsm', - 'modules/LoopRoomsCache.jsm', 'modules/LoopStorage.jsm', 'modules/MozLoopAPI.jsm', 'modules/MozLoopPushHandler.jsm', diff --git a/browser/components/loop/test/xpcshell/head.js b/browser/components/loop/test/xpcshell/head.js index 412254bcf7c4..59e90bb29b32 100644 --- a/browser/components/loop/test/xpcshell/head.js +++ b/browser/components/loop/test/xpcshell/head.js @@ -3,9 +3,6 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; -// Initialize this before the imports, as some of them need it. -do_get_profile(); - Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Http.jsm"); @@ -14,9 +11,7 @@ Cu.import("resource:///modules/loop/MozLoopService.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource:///modules/loop/LoopCalls.jsm"); Cu.import("resource:///modules/loop/LoopRooms.jsm"); -Cu.import("resource://gre/modules/osfile.jsm"); const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); -const { LoopRoomsInternal } = Cu.import("resource:///modules/loop/LoopRooms.jsm", {}); XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler", "resource:///modules/loop/MozLoopPushHandler.jsm"); @@ -214,10 +209,3 @@ MockWebSocketChannel.prototype = { this.listener.onServerClose(this.context, err || -1); }, }; - -const extend = function(target, source) { - for (let key of Object.getOwnPropertyNames(source)) { - target[key] = source[key]; - } - return target; -}; diff --git a/browser/components/loop/test/xpcshell/test_looprooms.js b/browser/components/loop/test/xpcshell/test_looprooms.js index 018f17270231..5ab2d6c69a9f 100644 --- a/browser/components/loop/test/xpcshell/test_looprooms.js +++ b/browser/components/loop/test/xpcshell/test_looprooms.js @@ -182,6 +182,13 @@ const kCreateRoomData = { const kChannelGuest = MozLoopService.channelIDs.roomsGuest; const kChannelFxA = MozLoopService.channelIDs.roomsFxA; +const extend = function(target, source) { + for (let key of Object.getOwnPropertyNames(source)) { + target[key] = source[key]; + } + return target; +}; + const normalizeRoom = function(room) { let newRoom = extend({}, room); let name = newRoom.decryptedContext.roomName; diff --git a/browser/components/loop/test/xpcshell/test_looprooms_encryption_in_fxa.js b/browser/components/loop/test/xpcshell/test_looprooms_encryption_in_fxa.js deleted file mode 100644 index bdb882988ff7..000000000000 --- a/browser/components/loop/test/xpcshell/test_looprooms_encryption_in_fxa.js +++ /dev/null @@ -1,267 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -Cu.import("resource://services-common/utils.js"); -const { LOOP_ROOMS_CACHE_FILENAME } = Cu.import("resource:///modules/loop/LoopRoomsCache.jsm", {}); - -const kContextEnabledPref = "loop.contextInConverations.enabled"; - -const kFxAKey = "uGIs-kGbYt1hBBwjyW7MLQ"; - -// Rooms details as responded by the server. -const kRoomsResponses = new Map([ - ["_nxD4V4FflQ", { - // Encrypted with roomKey "FliIGLUolW-xkKZVWstqKw". - // roomKey is wrapped with kFxAKey. - context: { - wrappedKey: "F3V27oPB+FgjFbVPML2PupONYqoIZ53XRU4BqG46Lr3eyIGumgCEqgjSe/MXAXiQ//8=", - value: "df7B4SNxhOI44eJjQavCevADyCCxz6/DEZbkOkRUMVUxzS42FbzN6C2PqmCKDYUGyCJTwJ0jln8TLw==", - alg: "AES-GCM" - }, - roomToken: "_nxD4V4FflQ", - roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ" - }], - ["QzBbvGmIZWU", { - context: { - wrappedKey: "AFu7WwFNjhWR5J6L8ks7S6H/1ktYVEw3yt1eIIWVaMabZaB3vh5612/FNzua4oS2oCM=", - value: "sqj+xRNEty8K3Q1gSMd5bIUYKu34JfiO2+LIMlJrOetFIbJdBoQ+U8JZNaTFl6Qp3RULZ41x0zeSBSk=", - alg: "AES-GCM" - }, - roomToken: "QzBbvGmIZWU", - roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU" - }] -]); - -const kExpectedRooms = new Map([ - ["_nxD4V4FflQ", { - context: { - wrappedKey: "F3V27oPB+FgjFbVPML2PupONYqoIZ53XRU4BqG46Lr3eyIGumgCEqgjSe/MXAXiQ//8=", - value: "df7B4SNxhOI44eJjQavCevADyCCxz6/DEZbkOkRUMVUxzS42FbzN6C2PqmCKDYUGyCJTwJ0jln8TLw==", - alg: "AES-GCM" - }, - decryptedContext: { - roomName: "First Room Name" - }, - roomKey: "FliIGLUolW-xkKZVWstqKw", - roomToken: "_nxD4V4FflQ", - roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ#FliIGLUolW-xkKZVWstqKw" - }], - ["QzBbvGmIZWU", { - context: { - wrappedKey: "AFu7WwFNjhWR5J6L8ks7S6H/1ktYVEw3yt1eIIWVaMabZaB3vh5612/FNzua4oS2oCM=", - value: "sqj+xRNEty8K3Q1gSMd5bIUYKu34JfiO2+LIMlJrOetFIbJdBoQ+U8JZNaTFl6Qp3RULZ41x0zeSBSk=", - alg: "AES-GCM" - }, - decryptedContext: { - roomName: "Loopy Discussion", - }, - roomKey: "h2H8Sa9QxLCTTiXNmJVtRA", - roomToken: "QzBbvGmIZWU", - roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU" - }] -]); - -const kCreateRoomProps = { - decryptedContext: { - roomName: "Say Hello", - }, - roomOwner: "Gavin", - maxSize: 2 -}; - -const kCreateRoomData = { - roomToken: "Vo2BFQqIaAM", - roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ", - expiresAt: 1405534180 -}; - -function getCachePath() { - return OS.Path.join(OS.Constants.Path.profileDir, LOOP_ROOMS_CACHE_FILENAME); -} - -function readRoomsCache() { - return CommonUtils.readJSON(getCachePath()); -} - -function saveRoomsCache(contents) { - delete LoopRoomsInternal.roomsCache._cache; - return CommonUtils.writeJSON(contents, getCachePath()); -} - -function clearRoomsCache() { - return LoopRoomsInternal.roomsCache.clear(); -} - -// This is a cut-down version of the one in test_looprooms.js. -add_task(function* setup_server() { - loopServer.registerPathHandler("/registration", (req, res) => { - res.setStatusLine(null, 200, "OK"); - res.processAsync(); - res.finish(); - }); - - loopServer.registerPathHandler("/rooms", (req, res) => { - res.setStatusLine(null, 200, "OK"); - - if (req.method == "POST") { - Assert.ok(req.bodyInputStream, "POST request should have a payload"); - let body = CommonUtils.readBytesFromInputStream(req.bodyInputStream); - let data = JSON.parse(body); - - Assert.ok(!("decryptedContext" in data), "should not have any decrypted data"); - Assert.ok("context" in data, "should have context"); - - res.write(JSON.stringify(kCreateRoomData)); - } else { - res.write(JSON.stringify([...kRoomsResponses.values()])); - } - - res.processAsync(); - res.finish(); - }); - - function returnRoomDetails(res, roomName) { - roomDetail.roomName = roomName; - res.setStatusLine(null, 200, "OK"); - res.write(JSON.stringify(roomDetail)); - res.processAsync(); - res.finish(); - } - - function getJSONData(body) { - return JSON.parse(CommonUtils.readBytesFromInputStream(body)); - } - - // Add a request handler for each room in the list. - [...kRoomsResponses.values()].forEach(function(room) { - loopServer.registerPathHandler("/rooms/" + encodeURIComponent(room.roomToken), (req, res) => { - if (req.method == "POST") { - let data = getJSONData(req.bodyInputStream); - res.setStatusLine(null, 200, "OK"); - res.write(JSON.stringify(data)); - res.processAsync(); - res.finish(); - } else if (req.method == "PATCH") { - let data = getJSONData(req.bodyInputStream); - Assert.ok("context" in data, "should have encrypted context"); - // We return a fake encrypted name here as the context is - // encrypted. - returnRoomDetails(res, "fakeEncrypted"); - } else { - res.setStatusLine(null, 200, "OK"); - res.write(JSON.stringify(room)); - res.processAsync(); - res.finish(); - } - }); - }); - - loopServer.registerPathHandler("/rooms/error401", (req, res) => { - res.setStatusLine(null, 401, "Not Found"); - res.processAsync(); - res.finish(); - }); - - loopServer.registerPathHandler("/rooms/errorMalformed", (req, res) => { - res.setStatusLine(null, 200, "OK"); - res.write("{\"some\": \"Syntax Error!\"}}}}}}"); - res.processAsync(); - res.finish(); - }); - - mockPushHandler.registrationPushURL = kEndPointUrl; - - yield MozLoopService.promiseRegisteredWithServers(); -}); - - -// Test if getting rooms saves unknown keys correctly. -add_task(function* test_get_rooms_saves_unknown_keys() { - let rooms = yield LoopRooms.promise("getAll"); - - // Check that we've saved the encryption keys correctly. - let roomsCache = yield readRoomsCache(); - for (let room of [...kExpectedRooms.values()]) { - if (room.context.wrappedKey) { - Assert.equal(roomsCache[LOOP_SESSION_TYPE.FXA][room.roomToken].key, room.roomKey); - } - } - - yield clearRoomsCache(); -}); - -// Test that when we get a room it updates the saved key if it is different. -add_task(function* test_get_rooms_saves_different_keys() { - let roomsCache = {}; - roomsCache[LOOP_SESSION_TYPE.FXA] = { - QzBbvGmIZWU: {key: "fakeKey"} - }; - yield saveRoomsCache(roomsCache); - - const kRoomToken = "QzBbvGmIZWU"; - - let room = yield LoopRooms.promise("get", kRoomToken); - - // Check that we've saved the encryption keys correctly. - roomsCache = yield readRoomsCache(); - - Assert.notEqual(roomsCache[LOOP_SESSION_TYPE.FXA][kRoomToken].key, "fakeKey"); - Assert.equal(roomsCache[LOOP_SESSION_TYPE.FXA][kRoomToken].key, room.roomKey); - - yield clearRoomsCache(); -}); - -// Test that if roomKey decryption fails, the saved key is used for decryption. -add_task(function* test_get_rooms_uses_saved_key() { - const kRoomToken = "_nxD4V4FflQ"; - const kExpected = kExpectedRooms.get(kRoomToken) - - let roomsCache = {}; - roomsCache[LOOP_SESSION_TYPE.FXA] = { - "_nxD4V4FflQ": {key: kExpected.roomKey} - }; - yield saveRoomsCache(roomsCache); - - // Change the encryption key for FxA, so that decoding the room key will break. - Services.prefs.setCharPref("loop.key.fxa", "invalidKey"); - - let room = yield LoopRooms.promise("get", kRoomToken); - - Assert.deepEqual(room, kExpected); - - Services.prefs.setCharPref("loop.key.fxa", kFxAKey); - yield clearRoomsCache(); -}); - -// Test that when a room is created the new key is saved. -add_task(function* test_create_room_saves_key() { - let room = yield LoopRooms.promise("create", kCreateRoomProps); - - let roomsCache = yield readRoomsCache(); - - Assert.equal(roomsCache[LOOP_SESSION_TYPE.FXA][room.roomToken].key, room.roomKey); - - yield clearRoomsCache(); -}); - -function run_test() { - setupFakeLoopServer(); - - Services.prefs.setCharPref("loop.key.fxa", kFxAKey); - Services.prefs.setBoolPref(kContextEnabledPref, true); - - // Pretend we're signed into FxA. - MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" }; - MozLoopServiceInternal.fxAOAuthProfile = { email: "fake@invalid.com" }; - - do_register_cleanup(function () { - Services.prefs.clearUserPref(kContextEnabledPref); - Services.prefs.clearUserPref("loop.key.fxa"); - - MozLoopServiceInternal.fxAOAuthTokenData = null; - MozLoopServiceInternal.fxAOAuthProfile = null; - }); - - run_next_test(); -} diff --git a/browser/components/loop/test/xpcshell/xpcshell.ini b/browser/components/loop/test/xpcshell/xpcshell.ini index 201c6709a6df..40ba2ce8d768 100644 --- a/browser/components/loop/test/xpcshell/xpcshell.ini +++ b/browser/components/loop/test/xpcshell/xpcshell.ini @@ -7,7 +7,6 @@ skip-if = toolkit == 'gonk' [test_loopapi_hawk_request.js] [test_looppush_initialize.js] [test_looprooms.js] -[test_looprooms_encryption_in_fxa.js] [test_loopservice_directcall.js] [test_loopservice_dnd.js] [test_loopservice_encryptionkey.js]