зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1152764 - Loop should encrypt room context information for rooms that aren't encrypted. r=mikedeboer
This commit is contained in:
Родитель
56e03f5eda
Коммит
3b53cd9857
|
@ -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]
|
||||
|
|
Загрузка…
Ссылка в новой задаче