Bug 1152764 - Loop should encrypt room context information for rooms that aren't encrypted. r=mikedeboer

This commit is contained in:
Mark Banner 2015-05-20 14:15:20 +01:00
Родитель 56e03f5eda
Коммит 3b53cd9857
6 изменённых файлов: 263 добавлений и 4 удалений

Просмотреть файл

@ -8,6 +8,7 @@ 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");
Cu.import("resource://gre/modules/Timer.jsm");
const {MozLoopService, LOOP_SESSION_TYPE} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
@ -35,6 +36,13 @@ this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
// The maximum number of clients that we support currently.
const CLIENT_MAX_SIZE = 2;
// Wait at least 5 seconds before doing opportunistic encryption.
const MIN_TIME_BEFORE_ENCRYPTION = 5 * 1000;
// Wait at maximum of 30 minutes before doing opportunistic encryption.
const MAX_TIME_BEFORE_ENCRYPTION = 30 * 60 * 1000;
// Wait time between individual re-encryption cycles (1 second).
const TIME_BETWEEN_ENCRYPTIONS = 1000;
const roomsPushNotification = function(version, channelID) {
return LoopRoomsInternal.onNotification(version, channelID);
};
@ -117,6 +125,23 @@ const checkForParticipantsUpdate = function(room, updatedRoom) {
}
};
/**
* These are wrappers which can be overriden by tests to allow us to manually
* handle the timeouts.
*/
let timerHandlers = {
/**
* Wrapper for setTimeout.
*
* @param {Function} callback The callback function.
* @param {Number} delay The delay in milliseconds.
* @return {Number} The timer identifier.
*/
startTimer(callback, delay) {
return setTimeout(callback, delay);
}
};
/**
* The Rooms class.
*
@ -137,6 +162,19 @@ let LoopRoomsInternal = {
return gRoomsCache;
},
/**
* @var {Object} encryptionQueue This stores the list of rooms awaiting
* encryption and associated timers.
*/
encryptionQueue: {
queue: [],
timer: null,
reset: function() {
this.queue = [];
this.timer = null;
}
},
/**
* @var {String} sessionType The type of user session. May be 'FXA' or 'GUEST'.
*/
@ -160,6 +198,64 @@ let LoopRoomsInternal = {
return count;
},
/**
* Processes the encryption queue. Takes the next item off the queue,
* restarts the timer if necessary.
*
* Although this is only called from a timer callback, it is an async function
* so that tests can call it and be deterministic.
*/
processEncryptionQueue: Task.async(function* () {
let roomToken = this.encryptionQueue.queue.shift();
// Performed in sync fashion so that we don't queue a timer until it has
// completed, and to make it easier to run tests.
let roomData = this.rooms.get(roomToken);
if (roomData) {
try {
// Passing the empty object for roomData is enough for the room to be
// re-encrypted.
yield LoopRooms.promise("update", roomToken, {});
} catch (error) {
MozLoopService.log.error("Upgrade encryption of room failed", error);
// No need to remove the room from the list as that's done in the shift above.
}
}
if (this.encryptionQueue.queue.length) {
this.encryptionQueue.timer =
timerHandlers.startTimer(this.processEncryptionQueue.bind(this), TIME_BETWEEN_ENCRYPTIONS);
} else {
this.encryptionQueue.timer = null;
}
}),
/**
* Queues a room for encryption sometime in the future. This is done so as
* not to overload the server or the browser when we initially request the
* list of rooms.
*
* @param {String} roomToken The token for the room that needs encrypting.
*/
queueForEncryption: function(roomToken) {
if (!this.encryptionQueue.queue.includes(roomToken)) {
this.encryptionQueue.queue.push(roomToken);
}
// Set up encryption to happen at a random time later. There's a minimum
// wait time - we don't need to do this straight away, so no need if the user
// is starting up. We then add a random factor on top of that. This is to
// try and avoid any potential with a set of clients being restarted at the
// same time and flooding the server.
if (!this.encryptionQueue.timer) {
let waitTime = (MAX_TIME_BEFORE_ENCRYPTION - MIN_TIME_BEFORE_ENCRYPTION) *
Math.random() + MIN_TIME_BEFORE_ENCRYPTION;
this.encryptionQueue.timer =
timerHandlers.startTimer(this.processEncryptionQueue.bind(this), waitTime);
}
},
/**
* Gets or creates a room key for a room.
*
@ -312,7 +408,9 @@ let LoopRoomsInternal = {
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.
MozLoopService.log.debug("Fell back to saved key, queuing for encryption",
roomData.roomToken);
this.queueForEncryption(roomData.roomToken);
} else if (!savedRoomKey || key != savedRoomKey) {
// Decryption succeeded, but we don't have the right key saved.
try {
@ -371,6 +469,10 @@ let LoopRoomsInternal = {
};
delete room.roomName;
// This room doesn't have context, so we'll save it for a later encryption
// cycle.
this.queueForEncryption(room.roomToken);
this.saveAndNotifyUpdate(room, isUpdate);
} else {
// XXX Don't decrypt if same?
@ -725,13 +827,16 @@ let LoopRoomsInternal = {
update: function(roomToken, roomData, callback) {
let room = this.rooms.get(roomToken);
let url = "/rooms/" + encodeURIComponent(roomToken);
if (!room.decryptedContext) {
room.decryptedContext = {
roomName: roomData.roomName || room.roomName
};
} else {
room.decryptedContext.roomName = roomData.roomName || room.roomName;
// room.roomName is the final fallback as this is pre-encryption support.
// Bug 1166283 is tracking the removal of the fallback.
room.decryptedContext.roomName = roomData.roomName ||
room.decryptedContext.roomName ||
room.roomName;
}
if (roomData.urls && roomData.urls.length) {
// For now we only support adding one URL to the room context.

Просмотреть файл

@ -18,7 +18,7 @@ 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", {});
const { LoopRoomsInternal, timerHandlers } = Cu.import("resource:///modules/loop/LoopRooms.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");

Просмотреть файл

@ -9,6 +9,8 @@ Cu.import("resource:///modules/loop/LoopRooms.jsm");
Cu.import("resource:///modules/Chat.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
timerHandlers.startTimer = callback => callback();
let openChatOrig = Chat.open;
const kGuestKey = "uGIs-kGbYt1hBBwjyW7MLQ";

Просмотреть файл

@ -4,6 +4,8 @@
"use strict";
timerHandlers.startTimer = callback => callback();
Cu.import("resource://services-common/utils.js");
const { LOOP_ROOMS_CACHE_FILENAME } = Cu.import("resource:///modules/loop/LoopRoomsCache.jsm", {});

Просмотреть файл

@ -0,0 +1,149 @@
/* 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";
Cu.import("resource://services-common/utils.js");
const loopCrypto = Cu.import("resource:///modules/loop/crypto.js", {}).LoopCrypto;
const { LOOP_ROOMS_CACHE_FILENAME } = Cu.import("resource:///modules/loop/LoopRoomsCache.jsm", {});
let gTimerArgs = [];
timerHandlers.startTimer = function(callback, delay) {
gTimerArgs.push({callback, delay});
return gTimerArgs.length;
};
let gRoomPatches = [];
const kContextEnabledPref = "loop.contextInConverations.enabled";
const kFxAKey = "uGIs-kGbYt1hBBwjyW7MLQ";
// Rooms details as responded by the server.
const kRoomsResponses = new Map([
["_nxD4V4FflQ", {
roomToken: "_nxD4V4FflQ",
roomName: "First Room Name",
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ"
}],
["QzBbvGmIZWU", {
roomToken: "QzBbvGmIZWU",
roomName: "Loopy Discussion",
roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU"
}]
]);
// 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");
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) => {
let roomDetail = extend({}, room);
if (req.method == "PATCH") {
let data = getJSONData(req.bodyInputStream);
Assert.ok("context" in data, "should have encrypted context");
gRoomPatches.push(data);
delete roomDetail.roomName;
roomDetail.context = data.context;
res.setStatusLine(null, 200, "OK");
res.write(JSON.stringify(roomDetail));
res.processAsync();
res.finish();
} else {
res.setStatusLine(null, 200, "OK");
res.write(JSON.stringify(room));
res.processAsync();
res.finish();
}
});
});
mockPushHandler.registrationPushURL = kEndPointUrl;
yield MozLoopService.promiseRegisteredWithServers();
});
// Test if getting rooms saves unknown keys correctly.
add_task(function* test_get_rooms_upgrades_to_encryption() {
let rooms = yield LoopRooms.promise("getAll");
// Check that we've saved the encryption keys correctly.
Assert.equal(LoopRoomsInternal.encryptionQueue.queue.length, 2, "Should have two rooms queued");
Assert.equal(gTimerArgs.length, 1, "Should have started a timer");
// Now pretend the timer has fired.
yield gTimerArgs[0].callback();
Assert.equal(gRoomPatches.length, 1, "Should have patched one room");
Assert.equal(gTimerArgs.length, 2, "Should have started a second timer");
yield gTimerArgs[1].callback();
Assert.equal(gRoomPatches.length, 2, "Should have patches a second room");
Assert.equal(gTimerArgs.length, 2, "Should not have queued another timer");
// Now check that we've got the right data stored in the rooms.
rooms = yield LoopRooms.promise("getAll");
Assert.equal(rooms.length, 2, "Should have two rooms");
// We have to decrypt the info, no other way.
for (let room of rooms) {
let roomData = yield loopCrypto.decryptBytes(room.roomKey, room.context.value);
Assert.deepEqual(JSON.parse(roomData),
{ roomName: kRoomsResponses.get(room.roomToken).roomName },
"Should have encrypted the data correctly");
}
});
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();
}

Просмотреть файл

@ -9,6 +9,7 @@ skip-if = toolkit == 'gonk'
[test_looprooms.js]
[test_looprooms_encryption_in_fxa.js]
[test_looprooms_first_notification.js]
[test_looprooms_upgrade_to_encryption.js]
[test_loopservice_directcall.js]
[test_loopservice_dnd.js]
[test_loopservice_encryptionkey.js]