Bug 1142522 - Part 2 Hook up encryption for room contexts in guest mode. r=mikedeboer
authorMark Banner <standard8@mozilla.com>
Mon, 13 Apr 2015 11:54:26 +0100
changeset 257713 a58bfd07a6e879f01835ab2a35446a0dff2febc3
parent 257712 e9281275b655866aa06cbdaf1f1dbbdb79a9f6a5
child 257714 5a26c1f65709b4be852621aa50bfbe0254c8fbfd
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1142522
milestone40.0a1
Bug 1142522 - Part 2 Hook up encryption for room contexts in guest mode. r=mikedeboer
browser/components/loop/LoopRooms.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/js/roomStore.js
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/desktop-local/roomStore_test.js
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/xpcshell/test_looprooms.js
browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
browser/components/loop/test/xpcshell/xpcshell.ini
browser/components/loop/ui/fake-mozLoop.js
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -14,16 +14,21 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return new EventEmitter();
 });
 XPCOMUtils.defineLazyGetter(this, "gLoopBundle", function() {
   return Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
 });
+XPCOMUtils.defineLazyModuleGetter(this, "loopUtils",
+  "resource:///modules/loop/utils.js", "utils")
+XPCOMUtils.defineLazyModuleGetter(this, "loopCrypto",
+  "resource:///modules/loop/crypto.js", "LoopCrypto");
+
 
 this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
 
 // The maximum number of clients that we support currently.
 const CLIENT_MAX_SIZE = 2;
 
 const roomsPushNotification = function(version, channelID) {
   return LoopRoomsInternal.onNotification(version, channelID);
@@ -137,16 +142,205 @@ let LoopRoomsInternal = {
         continue;
       }
       count += room.participants.length;
     }
     return count;
   },
 
   /**
+   * Gets or creates a room key for a room.
+   *
+   * It assumes that the room data is decrypted.
+   *
+   * @param {Object} roomData The roomData to get the key for.
+   * @return {Promise} A promise that is resolved whith the room key.
+   */
+  promiseGetOrCreateRoomKey: Task.async(function* (roomData) {
+    if (roomData.roomKey) {
+      return roomData.roomKey;
+    }
+
+    return yield loopCrypto.generateKey();
+  }),
+
+  /**
+   * Encrypts a room key for sending to the server using the profile encryption
+   * key.
+   *
+   * @param {String} key The JSON web key to encrypt.
+   * @return {Promise} A promise that is resolved with the encrypted room key.
+   */
+  promiseEncryptedRoomKey: Task.async(function* (key) {
+    let profileKey = yield MozLoopService.promiseProfileEncryptionKey();
+
+    let encryptedRoomKey = yield loopCrypto.encryptBytes(profileKey, key);
+    return encryptedRoomKey;
+  }),
+
+  /**
+   * Decryptes a room key from the server using the profile encryption key.
+   *
+   * @param  {String} encryptedKey The room key to decrypt.
+   * @return {Promise} A promise that is resolved with the decrypted room key.
+   */
+  promiseDecryptRoomKey: Task.async(function* (encryptedKey) {
+    let profileKey = yield MozLoopService.promiseProfileEncryptionKey();
+
+    let decryptedRoomKey = yield loopCrypto.decryptBytes(profileKey, encryptedKey);
+    return decryptedRoomKey;
+  }),
+
+  /**
+   * Encrypts room data in a format appropriate to sending to the loop
+   * server.
+   *
+   * @param  {Object} roomData The room data to encrypt.
+   * @return {Promise} A promise that is resolved with an object containing
+   *                   two objects:
+   *                   - encrypted: The encrypted data to send. This excludes
+   *                                any decrypted data.
+   *                   - all: The roomData with both encrypted and decrypted
+   *                          information.
+   */
+  promiseEncryptRoomData: Task.async(function* (roomData) {
+    // For now, disable encryption/context if context is disabled, or if
+    // FxA is turned on.
+    if (!MozLoopService.getLoopPref("contextInConverations.enabled") ||
+        this.sessionType == LOOP_SESSION_TYPE.FXA) {
+      var serverRoomData = extend({}, roomData);
+      delete serverRoomData.decryptedContext;
+
+      // We can only save roomName as non-encypted data for now.
+      serverRoomData.roomName = roomData.decryptedContext.roomName;
+
+      return {
+        all: roomData,
+        encrypted: serverRoomData
+      };
+    }
+
+    var newRoomData = extend({}, roomData);
+
+    if (!newRoomData.context) {
+      newRoomData.context = {};
+    }
+
+    // First get the room key.
+    let key = yield this.promiseGetOrCreateRoomKey(newRoomData);
+
+    newRoomData.context.wrappedKey = yield this.promiseEncryptedRoomKey(key);
+
+    // Now encrypt the actual data.
+    newRoomData.context.value = yield loopCrypto.encryptBytes(key,
+      JSON.stringify(newRoomData.decryptedContext));
+
+    // The algorithm is currently hard-coded as AES-GCM, in case of future
+    // changes.
+    newRoomData.context.alg = "AES-GCM";
+    newRoomData.roomKey = key;
+
+    var serverRoomData = extend({}, newRoomData);
+
+    // We must not send these items to the server.
+    delete serverRoomData.decryptedContext;
+    delete serverRoomData.roomKey;
+
+    return {
+      encrypted: serverRoomData,
+      all: newRoomData
+    };
+  }),
+
+  /**
+   * Decrypts room data recevied from the server.
+   *
+   * @param  {Object} roomData The roomData with encrypted context.
+   * @return {Promise} A promise that is resolved with the decrypted room data.
+   */
+  promiseDecryptRoomData: Task.async(function* (roomData) {
+    if (!roomData.context) {
+      return roomData;
+    }
+
+    if (!roomData.context.wrappedKey) {
+      throw new Error("Missing wrappedKey");
+    }
+
+    // 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);
+
+    roomData.roomKey = key;
+    roomData.decryptedContext = JSON.parse(decryptedData);
+
+    // Strip any existing key from the url.
+    roomData.roomUrl = roomData.roomUrl.split("#")[0];
+    // Now add the key to the url.
+    roomData.roomUrl = roomData.roomUrl + "#" + roomData.roomKey;
+
+    return roomData;
+  }),
+
+  /**
+   * Saves room information and notifies updates to any listeners.
+   *
+   * @param {Object}  roomData The new room data to save.
+   * @param {Boolean} isUpdate true if this is an update, false if its an add.
+   */
+  saveAndNotifyUpdate: function(roomData, isUpdate) {
+    this.rooms.set(roomData.roomToken, roomData);
+
+    let eventName = isUpdate ? "update" : "add";
+    eventEmitter.emit(eventName, roomData);
+    eventEmitter.emit(eventName + ":" + roomData.roomToken, roomData);
+  },
+
+  /**
+   * Either adds or updates the room to the room store and notifies to any
+   * listeners.
+   *
+   * This will decrypt information if necessary, and adjust for the legacy
+   * "roomName" field.
+   *
+   * @param {Object}  room     The new room to add.
+   * @param {Boolean} isUpdate true if this is an update to an existing room.
+   */
+  addOrUpdateRoom: Task.async(function* (room, isUpdate) {
+    if (!room.context) {
+      // We don't do anything with roomUrl here as it doesn't need a key
+      // string adding at this stage.
+
+      // No encrypted data, use the old roomName field.
+      // XXX Bug 1152764 will add functions for automatically encrypting the room
+      // name.
+      room.decryptedContext = {
+        roomName: room.roomName
+      };
+      delete room.roomName;
+
+      this.saveAndNotifyUpdate(room, isUpdate);
+    } else {
+      // XXX Don't decrypt if same?
+      try {
+        let roomData = yield this.promiseDecryptRoomData(room);
+
+        this.saveAndNotifyUpdate(roomData, isUpdate);
+      } catch (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);
+      };
+    }
+  }),
+
+  /**
    * Fetch a list of rooms that the currently registered user is a member of.
    *
    * @param {String}   [version] If set, we will fetch a list of changed rooms since
    *                             `version`. Optional.
    * @param {Function} callback  Function that will be invoked once the operation
    *                             finished. The first argument passed will be an
    *                             `Error` object or `null`. The second argument will
    *                             be the list of rooms, if it was fetched successfully.
@@ -184,21 +378,17 @@ let LoopRoomsInternal = {
 
           eventEmitter.emit("delete", room);
           eventEmitter.emit("delete:" + room.roomToken, room);
         } else {
           if (orig) {
             checkForParticipantsUpdate(orig, room);
           }
 
-          this.rooms.set(room.roomToken, room);
-
-          let eventName = orig ? "update" : "add";
-          eventEmitter.emit(eventName, room);
-          eventEmitter.emit(eventName + ":" + room.roomToken, room);
+          yield this.addOrUpdateRoom(room, !!orig);
         }
       }
 
       // If there's no rooms in the list, remove the guest created room flag, so that
       // we don't keep registering for guest when we don't need to.
       if (this.sessionType == LOOP_SESSION_TYPE.GUEST && !this.rooms.size) {
         this.setGuestCreatedRoom(false);
       }
@@ -227,72 +417,77 @@ let LoopRoomsInternal = {
     let needsUpdate = !("participants" in room);
     if (!gDirty && !needsUpdate) {
       // Dirty flag is not set AND the necessary data is available, so we can
       // simply return the room.
       callback(null, room);
       return;
     }
 
-    MozLoopService.hawkRequest(this.sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
-      .then(response => {
-        let data = JSON.parse(response.body);
+    Task.spawn(function* () {
+      let response = yield MozLoopService.hawkRequest(this.sessionType,
+        "/rooms/" + encodeURIComponent(roomToken), "GET");
 
-        room.roomToken = roomToken;
+      let data = JSON.parse(response.body);
 
-        if (data.deleted) {
-          this.rooms.delete(room.roomToken);
+      room.roomToken = roomToken;
+
+      if (data.deleted) {
+        this.rooms.delete(room.roomToken);
 
-          extend(room, data);
-          eventEmitter.emit("delete", room);
-          eventEmitter.emit("delete:" + room.roomToken, room);
-        } else {
-          checkForParticipantsUpdate(room, data);
-          extend(room, data);
-          this.rooms.set(roomToken, room);
+        extend(room, data);
+        eventEmitter.emit("delete", room);
+        eventEmitter.emit("delete:" + room.roomToken, room);
+      } else {
+        checkForParticipantsUpdate(room, data);
+        extend(room, data);
 
-          let eventName = !needsUpdate ? "update" : "add";
-          eventEmitter.emit(eventName, room);
-          eventEmitter.emit(eventName + ":" + roomToken, room);
-        }
-        callback(null, room);
-      }, err => callback(err)).catch(err => callback(err));
+        yield this.addOrUpdateRoom(room, !needsUpdate);
+      }
+      callback(null, room);
+    }.bind(this)).catch(callback);
   },
 
   /**
    * Create a room.
    *
    * @param {Object}   room     Properties to be sent to the LoopServer
    * @param {Function} callback Function that will be invoked once the operation
    *                            finished. The first argument passed will be an
    *                            `Error` object or `null`. The second argument will
    *                            be the room, if it was created successfully.
    */
   create: function(room, callback) {
-    if (!("roomName" in room) || !("roomOwner" in room) ||
+    if (!("decryptedContext" in room) || !("roomOwner" in room) ||
         !("maxSize" in room)) {
       callback(new Error("Missing required property to create a room"));
       return;
     }
 
-    MozLoopService.hawkRequest(this.sessionType, "/rooms", "POST", room)
-      .then(response => {
-        let data = JSON.parse(response.body);
-        extend(room, data);
-        // Do not keep this value - it is a request to the server.
-        delete room.expiresIn;
-        this.rooms.set(room.roomToken, room);
+    Task.spawn(function* () {
+      let {all, encrypted} = yield this.promiseEncryptRoomData(room);
+
+      // Save both sets of data...
+      room = all;
+      // ...but only send the encrypted data.
+      let response = yield MozLoopService.hawkRequest(this.sessionType, "/rooms",
+        "POST", encrypted);
 
-        if (this.sessionType == LOOP_SESSION_TYPE.GUEST) {
-          this.setGuestCreatedRoom(true);
-        }
+      extend(room, JSON.parse(response.body));
+      // Do not keep this value - it is a request to the server.
+      delete room.expiresIn;
+      this.rooms.set(room.roomToken, room);
 
-        eventEmitter.emit("add", room);
-        callback(null, room);
-      }, error => callback(error)).catch(error => callback(error));
+      if (this.sessionType == LOOP_SESSION_TYPE.GUEST) {
+        this.setGuestCreatedRoom(true);
+      }
+
+      eventEmitter.emit("add", room);
+      callback(null, room);
+    }.bind(this)).catch(callback);
   },
 
   /**
    * Sets whether or not the user has created a room in guest mode.
    *
    * @param {Boolean} created If the user has created the room.
    */
   setGuestCreatedRoom: function(created) {
@@ -436,26 +631,50 @@ let LoopRoomsInternal = {
    * @param {Function} callback   Function that will be invoked once the operation
    *                              finished. The first argument passed will be an
    *                              `Error` object or `null`.
    */
   rename: function(roomToken, newRoomName, callback) {
     let room = this.rooms.get(roomToken);
     let url = "/rooms/" + encodeURIComponent(roomToken);
 
-    let origRoom = this.rooms.get(roomToken);
-    let patchData = {
-      roomName: newRoomName
-    };
-    MozLoopService.hawkRequest(this.sessionType, url, "PATCH", patchData)
-      .then(response => {
-        let data = JSON.parse(response.body);
-        extend(room, data);
-        callback(null, room);
-      }, error => callback(error)).catch(error => callback(error));
+    let roomData = this.rooms.get(roomToken);
+    if (!roomData.decryptedContext) {
+      roomData.decryptedContext = {
+        roomName: newRoomName
+      };
+    } else {
+      roomData.decryptedContext.roomName = newRoomName;
+    }
+
+    Task.spawn(function* () {
+      let {all, encrypted} = yield this.promiseEncryptRoomData(roomData);
+
+      // For patch, we only send the context data.
+      let sendData = {
+        context: encrypted.context
+      };
+
+      // If we're not encrypting currently, then only send the roomName.
+      if (!Services.prefs.getBoolPref("loop.contextInConverations.enabled") ||
+          this.sessionType == LOOP_SESSION_TYPE.FXA) {
+        sendData = {
+          roomName: newRoomName
+        };
+      }
+
+      let response = yield MozLoopService.hawkRequest(this.sessionType,
+          url, "PATCH", sendData);
+
+      let newRoomData = all;
+
+      extend(newRoomData, JSON.parse(response.body));
+      this.rooms.set(roomToken, newRoomData);
+      callback(null, newRoomData);
+    }.bind(this)).catch(callback);
   },
 
   /**
    * Callback used to indicate changes to rooms data on the LoopServer.
    *
    * @param {String} version   Version number assigned to this change set.
    * @param {String} channelID Notification channel identifier.
    */
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -58,16 +58,20 @@ Cu.importGlobalProperties(["URL"]);
 this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
   "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
   "resource:///modules/loop/MozLoopAPI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
   "resource://gre/modules/media/RTCStatsReport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "loopUtils",
+  "resource:///modules/loop/utils.js", "utils")
+XPCOMUtils.defineLazyModuleGetter(this, "loopCrypto",
+  "resource:///modules/loop/crypto.js", "LoopCrypto");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
                                   "resource://services-common/utils.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
                                   "resource://services-crypto/utils.js");
@@ -1315,16 +1319,46 @@ this.MozLoopService = {
    *
    * @return {Object}
    */
   get userProfile() {
     return getJSONPref("loop.fxa_oauth.tokendata") &&
            getJSONPref("loop.fxa_oauth.profile");
   },
 
+  /**
+   * Gets the encryption key for this profile.
+   */
+  promiseProfileEncryptionKey: function() {
+    return new Promise((resolve, reject) => {
+      if (this.userProfile) {
+        // We're an FxA user.
+        // XXX Bug 1153788 will implement this for FxA.
+        reject(new Error("unimplemented"));
+        return;
+      }
+
+      // XXX Temporarily save in preferences until we've got some
+      // extra storage (bug 1152761).
+      if (!Services.prefs.prefHasUserValue("loop.key")) {
+        // Get a new value.
+        loopCrypto.generateKey().then(key => {
+          Services.prefs.setCharPref("loop.key", key);
+          resolve(key);
+        }).catch(function(error) {
+          MozLoopService.log.error(error);
+          reject(error);
+        });
+        return;
+      }
+
+      resolve(MozLoopService.getLoopPref("key"));
+    });
+  },
+
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   get log() {
     return log;
   },
 
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -553,40 +553,39 @@ loop.panel = (function(_, mozL10n) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       return this.props.room.participants.length > 0;
     },
 
     render: function() {
-      var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
       var copyButtonClasses = React.addons.classSet({
         "copy-link": true,
         "checked": this.state.urlCopied
       });
 
       return (
         React.createElement("div", {className: roomClasses, onMouseLeave: this.handleMouseLeave, 
              onClick: this.handleClickEntry}, 
           React.createElement("h2", null, 
             React.createElement("span", {className: "room-notification"}), 
-            React.createElement(EditInPlace, {text: room.roomName, onChange: this.renameRoom}), 
+            React.createElement(EditInPlace, {text: this.props.room.decryptedContext.roomName, 
+                         onChange: this.renameRoom}), 
             React.createElement("button", {className: copyButtonClasses, 
               title: mozL10n.get("rooms_list_copy_url_tooltip"), 
               onClick: this.handleCopyButtonClick}), 
             React.createElement("button", {className: "delete-link", 
               title: mozL10n.get("rooms_list_delete_tooltip"), 
               onClick: this.handleDeleteButtonClick})
-          ), 
-          React.createElement("p", null, React.createElement("a", {className: "room-url-link", href: "#"}, room.roomUrl))
+          )
         )
       );
     }
   });
 
   /**
    * Room list.
    */
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -553,40 +553,39 @@ loop.panel = (function(_, mozL10n) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       return this.props.room.participants.length > 0;
     },
 
     render: function() {
-      var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
       var copyButtonClasses = React.addons.classSet({
         "copy-link": true,
         "checked": this.state.urlCopied
       });
 
       return (
         <div className={roomClasses} onMouseLeave={this.handleMouseLeave}
              onClick={this.handleClickEntry}>
           <h2>
             <span className="room-notification" />
-            <EditInPlace text={room.roomName} onChange={this.renameRoom} />
+            <EditInPlace text={this.props.room.decryptedContext.roomName}
+                         onChange={this.renameRoom} />
             <button className={copyButtonClasses}
               title={mozL10n.get("rooms_list_copy_url_tooltip")}
               onClick={this.handleCopyButtonClick} />
             <button className="delete-link"
               title={mozL10n.get("rooms_list_delete_tooltip")}
               onClick={this.handleDeleteButtonClick} />
           </h2>
-          <p><a className="room-url-link" href="#">{room.roomUrl}</a></p>
         </div>
       );
     }
   });
 
   /**
    * Room list.
    */
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -27,16 +27,17 @@ loop.store = loop.store || {};
   /**
    * Room validation schema. See validate.js.
    * @type {Object}
    */
   var roomSchema = {
     roomToken:    String,
     roomUrl:      String,
     // roomName:     String - Optional.
+    // roomKey:      String - Optional.
     maxSize:      Number,
     participants: Array,
     ctime:        Number
   };
 
   /**
    * Room type. Basically acts as a typed object constructor.
    *
@@ -222,17 +223,17 @@ loop.store = loop.store || {};
      *                               {{conversationLabel}} placeholder.
      * @return {Number}
      */
     findNextAvailableRoomNumber: function(nameTemplate) {
       var searchTemplate = nameTemplate.replace("{{conversationLabel}}", "");
       var searchRegExp = new RegExp("^" + searchTemplate + "(\\d+)$");
 
       var roomNumbers = this._storeState.rooms.map(function(room) {
-        var match = searchRegExp.exec(room.roomName);
+        var match = searchRegExp.exec(room.decryptedContext.roomName);
         return match && match[1] ? parseInt(match[1], 10) : 0;
       });
 
       if (!roomNumbers.length) {
         return 1;
       }
 
       return Math.max.apply(null, roomNumbers) + 1;
@@ -256,17 +257,19 @@ loop.store = loop.store || {};
      */
     createRoom: function(actionData) {
       this.setStoreState({
         pendingCreation: true,
         error: null,
       });
 
       var roomCreationData = {
-        roomName:  this._generateNewRoomName(actionData.nameTemplate),
+        decryptedContext: {
+          roomName:  this._generateNewRoomName(actionData.nameTemplate)
+        },
         roomOwner: actionData.roomOwner,
         maxSize:   this.maxRoomCreationSize
       };
 
       this._notifications.remove("create-room-error");
 
       this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) {
         if (err) {
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -176,17 +176,17 @@ loop.store.ActiveRoomStore = (function()
               error: error,
               failedJoinRequest: false
             }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
-            roomName: roomData.roomName,
+            roomName: roomData.decryptedContext.roomName,
             roomOwner: roomData.roomOwner,
             roomUrl: roomData.roomUrl,
             socialShareButtonAvailable: this._mozLoop.isSocialShareButtonAvailable(),
             socialShareProviders: this._mozLoop.getSocialShareProviders()
           }));
 
           // For the conversation window, we need to automatically
           // join the room.
@@ -343,17 +343,17 @@ loop.store.ActiveRoomStore = (function()
     /**
      * Handles room updates notified by the mozLoop rooms API.
      *
      * @param {String} eventName The name of the event
      * @param {Object} roomData  The new roomData.
      */
     _handleRoomUpdate: function(eventName, roomData) {
       this.dispatchAction(new sharedActions.UpdateRoomInfo({
-        roomName: roomData.roomName,
+        roomName: roomData.decryptedContext.roomName,
         roomOwner: roomData.roomOwner,
         roomUrl: roomData.roomUrl
       }));
     },
 
     /**
      * Handles the deletion of a room, notified by the mozLoop rooms API.
      *
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -435,17 +435,19 @@ describe("loop.panel", function() {
   describe("loop.panel.RoomEntry", function() {
     var dispatcher, roomData;
 
     beforeEach(function() {
       dispatcher = new loop.Dispatcher();
       roomData = {
         roomToken: "QzBbvGmIZWU",
         roomUrl: "http://sample/QzBbvGmIZWU",
-        roomName: "Second Room Name",
+        decryptedContext: {
+          roomName: "Second Room Name"
+        },
         maxSize: 2,
         participants: [
           { displayName: "Alexis", account: "alexis@example.com",
             roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
           { displayName: "Adam",
             roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
         ],
         ctime: 1405517418
@@ -468,17 +470,18 @@ describe("loop.panel", function() {
         });
         domNode = roomEntry.getDOMNode();
 
         TestUtils.Simulate.click(domNode.querySelector(".edit-in-place"));
       });
 
       it("should render an edit form on room name click", function() {
         expect(domNode.querySelector("form")).not.eql(null);
-        expect(domNode.querySelector("input").value).eql(roomData.roomName);
+        expect(domNode.querySelector("input").value)
+          .eql(roomData.decryptedContext.roomName);
       });
 
       it("should dispatch a RenameRoom action when submitting the form",
         function() {
           var dispatch = sandbox.stub(dispatcher, "dispatch");
 
           TestUtils.Simulate.change(domNode.querySelector("input"), {
             target: {value: "New name"}
@@ -576,53 +579,54 @@ describe("loop.panel", function() {
         navigator.mozLoop.confirm.callsArgWith(1, null, false);
         TestUtils.Simulate.click(deleteButton);
 
         sinon.assert.calledOnce(navigator.mozLoop.confirm);
         sinon.assert.notCalled(dispatcher.dispatch);
       });
     });
 
-    describe("Room URL click", function() {
-
-      var roomEntry, urlLink;
+    describe("Room Entry click", function() {
+      var roomEntry, roomEntryNode;
 
       beforeEach(function() {
         sandbox.stub(dispatcher, "dispatch");
 
         roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
           room: new loop.store.Room(roomData)
         });
-        urlLink = roomEntry.getDOMNode().querySelector("p > a");
+        roomEntryNode = roomEntry.getDOMNode();
       });
 
       it("should dispatch an OpenRoom action", function() {
-        TestUtils.Simulate.click(urlLink);
+        TestUtils.Simulate.click(roomEntryNode);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
       });
 
       it("should call window.close", function() {
-        TestUtils.Simulate.click(urlLink);
+        TestUtils.Simulate.click(roomEntryNode);
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
     });
 
     describe("Room name updated", function() {
       it("should update room name", function() {
         var roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
           room: new loop.store.Room(roomData)
         });
         var updatedRoom = new loop.store.Room(_.extend({}, roomData, {
-          roomName: "New room name",
+          decryptedContext: {
+            roomName: "New room name"
+          },
           ctime: new Date().getTime()
         }));
 
         roomEntry.setProps({room: updatedRoom});
 
         expect(
           roomEntry.getDOMNode().querySelector(".edit-in-place").textContent)
         .eql("New room name");
--- a/browser/components/loop/test/desktop-local/roomStore_test.js
+++ b/browser/components/loop/test/desktop-local/roomStore_test.js
@@ -185,49 +185,48 @@ describe("loop.store.RoomStore", functio
           store.setStoreState({rooms: []});
 
           expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(1);
         });
 
       it("should find next available room number from a non empty room list",
         function() {
           store.setStoreState({
-            rooms: [{roomName: "RoomWord 1"}]
+            rooms: [{decryptedContext: {roomName: "RoomWord 1"}}]
           });
 
           expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(2);
         });
 
       it("should not be sensitive to initial list order", function() {
         store.setStoreState({
-          rooms: [{roomName: "RoomWord 99"}, {roomName: "RoomWord 98"}]
+          rooms: [{
+            decryptedContext: {
+              roomName: "RoomWord 99"
+            },
+          }, {
+            decryptedContext: {
+              roomName: "RoomWord 98"
+            }
+          }]
         });
 
         expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(100);
       });
     });
 
     describe("#createRoom", function() {
       var fakeNameTemplate = "Conversation {{conversationLabel}}";
       var fakeLocalRoomId = "777";
       var fakeOwner = "fake@invalid";
       var fakeRoomCreationData = {
         nameTemplate: fakeNameTemplate,
         roomOwner: fakeOwner
       };
 
-      var fakeCreatedRoom = {
-        roomName: "Conversation 1",
-        roomToken: "fake",
-        roomUrl: "http://invalid",
-        maxSize: 42,
-        participants: [],
-        ctime: 1234567890
-      };
-
       beforeEach(function() {
         sandbox.stub(dispatcher, "dispatch");
         store.setStoreState({pendingCreation: false, rooms: []});
       });
 
       it("should clear any existing room errors", function() {
         sandbox.stub(fakeMozLoop.rooms, "create");
 
@@ -239,17 +238,19 @@ describe("loop.store.RoomStore", functio
       });
 
       it("should request creation of a new room", function() {
         sandbox.stub(fakeMozLoop.rooms, "create");
 
         store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
 
         sinon.assert.calledWith(fakeMozLoop.rooms.create, {
-          roomName: "Conversation 1",
+          decryptedContext: {
+            roomName: "Conversation 1"
+          },
           roomOwner: fakeOwner,
           maxSize: store.maxRoomCreationSize
         });
       });
 
       it("should switch the pendingCreation state flag to true", function() {
         sandbox.stub(fakeMozLoop.rooms, "create");
 
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -234,17 +234,19 @@ describe("loop.store.ActiveRoomStore", f
   });
 
   describe("#setupWindowData", function() {
     var fakeToken, fakeRoomData;
 
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
-        roomName: "Monkeys",
+        decryptedContext: {
+          roomName: "Monkeys"
+        },
         roomOwner: "Alfred",
         roomUrl: "http://invalid"
       };
 
       store = new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: fakeMozLoop,
         sdkDriver: {}
       });
@@ -274,21 +276,24 @@ describe("loop.store.ActiveRoomStore", f
         store.setupWindowData(new sharedActions.SetupWindowData({
           windowId: "42",
           type: "room",
           roomToken: fakeToken
         }));
 
         sinon.assert.calledTwice(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.SetupRoomInfo(_.extend({
+          new sharedActions.SetupRoomInfo({
             roomToken: fakeToken,
+            roomName: fakeRoomData.decryptedContext.roomName,
+            roomOwner: fakeRoomData.roomOwner,
+            roomUrl: fakeRoomData.roomUrl,
             socialShareButtonAvailable: false,
             socialShareProviders: []
-          }, fakeRoomData)));
+          }));
       });
 
     it("should dispatch a JoinRoom action if the get is successful",
       function() {
         store.setupWindowData(new sharedActions.SetupWindowData({
           windowId: "42",
           type: "room",
           roomToken: fakeToken
@@ -1267,32 +1272,40 @@ describe("loop.store.ActiveRoomStore", f
           socialShareProviders: []
         }));
       });
 
       it("should dispatch an UpdateRoomInfo action", function() {
         sinon.assert.calledTwice(fakeMozLoop.rooms.on);
 
         var fakeRoomData = {
-          roomName: "fakeName",
+          decryptedContext: {
+            roomName: "fakeName"
+          },
           roomOwner: "you",
           roomUrl: "original"
         };
 
         fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.UpdateRoomInfo(fakeRoomData));
+          new sharedActions.UpdateRoomInfo({
+            roomName: fakeRoomData.decryptedContext.roomName,
+            roomOwner: fakeRoomData.roomOwner,
+            roomUrl: fakeRoomData.roomUrl
+          }));
       });
     });
 
     describe("delete:{roomToken}", function() {
       var fakeRoomData = {
-        roomName: "Its a room",
+        decryptedContext: {
+          roomName: "Its a room"
+        },
         roomOwner: "Me",
         roomToken: "fakeToken",
         roomUrl: "http://invalid"
       };
 
       beforeEach(function() {
         store.setupRoomInfo(new sharedActions.SetupRoomInfo(
           _.extend(fakeRoomData, {
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -4,23 +4,42 @@
 
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource:///modules/loop/LoopRooms.jsm");
 Cu.import("resource:///modules/Chat.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 let openChatOrig = Chat.open;
 
-const kRooms = new Map([
+const kContextEnabledPref = "loop.contextInConverations.enabled";
+
+const kGuestKey = "uGIs-kGbYt1hBBwjyW7MLQ";
+
+// Rooms details as responded by the server.
+const kRoomsResponses = new Map([
   ["_nxD4V4FflQ", {
     roomToken: "_nxD4V4FflQ",
-    roomName: "First Room Name",
+    // Encrypted with roomKey "FliIGLUolW-xkKZVWstqKw".
+    // roomKey is wrapped with kGuestKey.
+    context: {
+      wrappedKey: "F3V27oPB+FgjFbVPML2PupONYqoIZ53XRU4BqG46Lr3eyIGumgCEqgjSe/MXAXiQ//8=",
+      value: "df7B4SNxhOI44eJjQavCevADyCCxz6/DEZbkOkRUMVUxzS42FbzN6C2PqmCKDYUGyCJTwJ0jln8TLw==",
+      alg: "AES-GCM"
+    },
     roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
     maxSize: 2,
-    ctime: 1405517546
+    ctime: 1405517546,
+    participants: [{
+      displayName: "Alexis",
+      account: "alexis@example.com",
+      roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
+    }, {
+      displayName: "Adam",
+      roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
+    }]
   }],
   ["QzBbvGmIZWU", {
     roomToken: "QzBbvGmIZWU",
     roomName: "Second Room Name",
     roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU",
     maxSize: 2,
     ctime: 140551741
   }],
@@ -29,18 +48,71 @@ const kRooms = new Map([
     roomName: "Third Room Name",
     roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
     maxSize: 3,
     clientMaxSize: 2,
     ctime: 1405518241
   }]
 ]);
 
+const kExpectedRooms = new Map([
+  ["_nxD4V4FflQ", {
+    roomToken: "_nxD4V4FflQ",
+    context: {
+      wrappedKey: "F3V27oPB+FgjFbVPML2PupONYqoIZ53XRU4BqG46Lr3eyIGumgCEqgjSe/MXAXiQ//8=",
+      value: "df7B4SNxhOI44eJjQavCevADyCCxz6/DEZbkOkRUMVUxzS42FbzN6C2PqmCKDYUGyCJTwJ0jln8TLw==",
+      alg: "AES-GCM"
+    },
+    decryptedContext: {
+      roomName: "First Room Name"
+    },
+    roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ#FliIGLUolW-xkKZVWstqKw",
+    roomKey: "FliIGLUolW-xkKZVWstqKw",
+    maxSize: 2,
+    ctime: 1405517546,
+    participants: [{
+      displayName: "Alexis",
+      account: "alexis@example.com",
+      roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
+    }, {
+      displayName: "Adam",
+      roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
+    }]
+  }],
+  ["QzBbvGmIZWU", {
+    roomToken: "QzBbvGmIZWU",
+    decryptedContext: {
+      roomName: "Second Room Name"
+    },
+    roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU",
+    maxSize: 2,
+    ctime: 140551741
+  }],
+  ["3jKS_Els9IU", {
+    roomToken: "3jKS_Els9IU",
+    decryptedContext: {
+      roomName: "Third Room Name"
+    },
+    roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
+    maxSize: 3,
+    clientMaxSize: 2,
+    ctime: 1405518241
+  }]
+]);
+
 let roomDetail = {
-  roomName: "First Room Name",
+  decryptedContext: {
+    roomName: "First Room Name"
+  },
+  context: {
+    wrappedKey: "wrappedKey",
+    value: "encryptedValue",
+    alg: "AES-GCM"
+  },
+  roomKey: "fakeKey",
   roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
   roomOwner: "Alexis",
   maxSize: 2,
   clientMaxSize: 2,
   creationTime: 1405517546,
   expiresAt: 1405534180,
   participants: [{
     displayName: "Alexis",
@@ -83,55 +155,81 @@ const kRoomUpdates = {
     }]
   },
   "5": {
     deleted: true
   }
 };
 
 const kCreateRoomProps = {
+  decryptedContext: {
+    roomName: "UX Discussion",
+  },
+  roomOwner: "Alexis",
+  maxSize: 2
+};
+
+const kCreateRoomUnencryptedProps = {
   roomName: "UX Discussion",
   roomOwner: "Alexis",
   maxSize: 2
 };
 
 const kCreateRoomData = {
   roomToken: "_nxD4V4FflQ",
   roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
   expiresAt: 1405534180
 };
 
 const kChannelGuest = MozLoopService.channelIDs.roomsGuest;
 const kChannelFxA = MozLoopService.channelIDs.roomsFxA;
 
-const normalizeRoom = function(room) {
-  if (!("participants" in room)) {
-    let name = room.roomName;
-    for (let key of Object.getOwnPropertyNames(roomDetail)) {
-      room[key] = roomDetail[key];
-    }
-    room.roomName = name;
+const extend = function(target, source) {
+  for (let key of Object.getOwnPropertyNames(source)) {
+    target[key] = source[key];
   }
-  return room;
+  return target;
 };
 
+const normalizeRoom = function(room) {
+  let newRoom = extend({}, room);
+  let name = newRoom.decryptedContext.roomName;
+
+  for (let key of Object.getOwnPropertyNames(roomDetail)) {
+    // Handle sub-objects if necessary (e.g. context, decryptedContext).
+    if (typeof roomDetail[key] == "object") {
+      newRoom[key] = extend({}, roomDetail[key]);
+    } else {
+      newRoom[key] = roomDetail[key];
+    }
+  }
+
+  newRoom.decryptedContext.roomName = name;
+  return newRoom;
+};
+
+// This compares rooms by normalizing the room fields so that the contents
+// are the same between the two rooms - except for the room name
+// (see normalizeRoom). This means we can detect if fields are missing, but
+// we don't need to worry about the values being different, for example, in the
+// case of expiry times.
 const compareRooms = function(room1, room2) {
   Assert.deepEqual(normalizeRoom(room1), normalizeRoom(room2));
 };
 
 // LoopRooms emits various events. Test if they work as expected here.
 let gExpectedAdds = [];
 let gExpectedUpdates = [];
 let gExpectedDeletes = [];
 let gExpectedJoins = {};
 let gExpectedLeaves = {};
 let gExpectedRefresh = false;
 
 const onRoomAdded = function(e, room) {
-  let expectedIds = gExpectedAdds.map(room => room.roomToken);
+  let expectedIds = gExpectedAdds.map(expectedRoom => expectedRoom.roomToken);
   let idx = expectedIds.indexOf(room.roomToken);
   Assert.ok(idx > -1, "Added room should be expected");
   let expected = gExpectedAdds[idx];
   compareRooms(room, expected);
   gExpectedAdds.splice(idx, 1);
 };
 
 const onRoomUpdated = function(e, room) {
@@ -193,28 +291,41 @@ add_task(function* setup_server() {
 
   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.deepEqual(data, kCreateRoomProps);
+
+      if (Services.prefs.getBoolPref(kContextEnabledPref)) {
+        Assert.equal(data.roomOwner, kCreateRoomProps.roomOwner);
+        Assert.equal(data.maxSize, kCreateRoomProps.maxSize);
+        Assert.ok(!("decryptedContext" in data), "should not have any decrypted data");
+        Assert.ok("context" in data, "should have context");
+      } else {
+        Assert.deepEqual(data, kCreateRoomUnencryptedProps);
+      }
 
       res.write(JSON.stringify(kCreateRoomData));
     } else {
       if (req.queryString) {
         let qs = parseQueryString(req.queryString);
-        let room = kRooms.get("_nxD4V4FflQ");
+        let room = kRoomsResponses.get("_nxD4V4FflQ");
         room.participants = kRoomUpdates[qs.version].participants;
         room.deleted = kRoomUpdates[qs.version].deleted;
         res.write(JSON.stringify([room]));
       } else {
-        res.write(JSON.stringify([...kRooms.values()]));
+        // XXX Only return last 2 elements until FxA keys are implemented.
+        if (MozLoopServiceInternal.fxAOAuthTokenData) {
+          res.write(JSON.stringify([...kRoomsResponses.values()].slice(1, 3)));
+        } else {
+          res.write(JSON.stringify([...kRoomsResponses.values()]));
+        }
       }
     }
 
     res.processAsync();
     res.finish();
   });
 
   function returnRoomDetails(res, roomName) {
@@ -225,29 +336,41 @@ add_task(function* setup_server() {
     res.finish();
   }
 
   function getJSONData(body) {
     return JSON.parse(CommonUtils.readBytesFromInputStream(body));
   }
 
   // Add a request handler for each room in the list.
-  [...kRooms.values()].forEach(function(room) {
+  [...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);
-        returnRoomDetails(res, data.roomName);
+        if (Services.prefs.getBoolPref(kContextEnabledPref)) {
+          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 {
+          Assert.ok(!("context" in data), "should not have encrypted context");
+          returnRoomDetails(res, data.roomName);
+        }
       } else {
-        returnRoomDetails(res, room.roomName);
+        roomDetail.context = room.context;
+        res.setStatusLine(null, 200, "OK");
+        res.write(JSON.stringify(roomDetail));
+        res.processAsync();
+        res.finish();
       }
     });
   });
 
   loopServer.registerPathHandler("/rooms/error401", (req, res) => {
     res.setStatusLine(null, 401, "Not Found");
     res.processAsync();
     res.finish();
@@ -262,42 +385,59 @@ add_task(function* setup_server() {
 
   mockPushHandler.registrationPushURL = kEndPointUrl;
 
   yield MozLoopService.promiseRegisteredWithServers();
 });
 
 // Test if fetching a list of all available rooms works correctly.
 add_task(function* test_getAllRooms() {
-  gExpectedAdds.push(...kRooms.values());
+  gExpectedAdds.push(...kExpectedRooms.values());
   let rooms = yield LoopRooms.promise("getAll");
   Assert.equal(rooms.length, 3);
   for (let room of rooms) {
-    compareRooms(kRooms.get(room.roomToken), room);
+    compareRooms(kExpectedRooms.get(room.roomToken), room);
   }
 });
 
 // Test if fetching a room works correctly.
 add_task(function* test_getRoom() {
   let roomToken = "_nxD4V4FflQ";
   let room = yield LoopRooms.promise("get", roomToken);
-  Assert.deepEqual(room, kRooms.get(roomToken));
+  Assert.deepEqual(room, kExpectedRooms.get(roomToken));
 });
 
 // Test if fetching a room with incorrect token or return values yields an error.
 add_task(function* test_errorStates() {
   yield Assert.rejects(LoopRooms.promise("get", "error401"), /Not Found/, "Fetching a non-existent room should fail");
   yield Assert.rejects(LoopRooms.promise("get", "errorMalformed"), /SyntaxError/, "Wrong message format should reject");
 });
 
 // Test if creating a new room works as expected.
 add_task(function* test_createRoom() {
-  gExpectedAdds.push(kCreateRoomProps);
+  Services.prefs.setBoolPref(kContextEnabledPref, true);
+
+  var expectedRoom = extend({}, kCreateRoomProps);
+  expectedRoom.roomToken = kCreateRoomData.roomToken;
+
+  gExpectedAdds.push(expectedRoom);
   let room = yield LoopRooms.promise("create", kCreateRoomProps);
-  compareRooms(room, kCreateRoomProps);
+  compareRooms(room, expectedRoom);
+});
+
+// XXX Test unencrypted rooms. This will go away once we switch encryption on.
+add_task(function* test_createRoom_unencrypted() {
+  Services.prefs.setBoolPref(kContextEnabledPref, false);
+
+  var expectedRoom = extend({}, kCreateRoomProps);
+  expectedRoom.roomToken = kCreateRoomData.roomToken;
+
+  gExpectedAdds.push(expectedRoom);
+  let room = yield LoopRooms.promise("create", kCreateRoomProps);
+  compareRooms(room, expectedRoom);
 });
 
 // Test if opening a new room window works correctly.
 add_task(function* test_openRoom() {
   let openedUrl;
   Chat.open = function(contentWindow, origin, title, url) {
     openedUrl = url;
   };
@@ -311,29 +451,31 @@ add_task(function* test_openRoom() {
   let windowData = MozLoopService.getConversationWindowData(windowId);
 
   Assert.equal(windowData.type, "room", "window data should contain room as the type");
   Assert.equal(windowData.roomToken, "fakeToken", "window data should have the roomToken");
 });
 
 // Test if the rooms cache is refreshed after FxA signin or signout.
 add_task(function* test_refresh() {
-  gExpectedAdds.push(...kRooms.values());
+  // XXX Temporarily whilst FxA encryption isn't handled (bug 1153788).
+  Array.prototype.push.apply(gExpectedAdds, [...kExpectedRooms.values()].slice(1,3));
   gExpectedRefresh = true;
+
   // Make the switch.
   MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" };
   MozLoopServiceInternal.fxAOAuthProfile = {
     email: "fake@invalid.com",
     uid: "fake"
   };
 
   yield waitForCondition(() => !gExpectedRefresh);
   yield waitForCondition(() => gExpectedAdds.length === 0);
 
-  gExpectedAdds.push(...kRooms.values());
+  gExpectedAdds.push(...kExpectedRooms.values());
   gExpectedRefresh = true;
   // Simulate a logout.
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.fxAOAuthProfile = null;
 
   yield waitForCondition(() => !gExpectedRefresh);
   yield waitForCondition(() => gExpectedAdds.length === 0);
 
@@ -432,19 +574,27 @@ add_task(function* test_leaveRoom() {
   let roomToken = "_nxD4V4FflQ";
   let leaveData = yield LoopRooms.promise("leave", roomToken, "fakeLeaveSessionToken");
   Assert.equal(leaveData.action, "leave");
   Assert.equal(leaveData.sessionToken, "fakeLeaveSessionToken");
 });
 
 // Test if renaming a room works as expected.
 add_task(function* test_renameRoom() {
+  Services.prefs.setBoolPref(kContextEnabledPref, true);
   let roomToken = "_nxD4V4FflQ";
   let renameData = yield LoopRooms.promise("rename", roomToken, "fakeName");
-  Assert.equal(renameData.roomName, "fakeName");
+  Assert.equal(renameData.roomName, "fakeEncrypted", "should have set the new name");
+});
+
+add_task(function* test_renameRoom_unencrpyted() {
+  Services.prefs.setBoolPref(kContextEnabledPref, false);
+  let roomToken = "_nxD4V4FflQ";
+  let renameData = yield LoopRooms.promise("rename", roomToken, "fakeName");
+  Assert.equal(renameData.roomName, "fakeName", "should have set the new name");
 });
 
 add_task(function* test_roomDeleteNotifications() {
   gExpectedDeletes.push("_nxD4V4FflQ");
   roomsPushNotification("5", kChannelGuest);
   yield waitForCondition(() => gExpectedDeletes.length === 0);
 });
 
@@ -468,26 +618,30 @@ add_task(function* () {
   Assert.strictEqual(Object.getOwnPropertyNames(gExpectedLeaves).length, 0,
                      "No room leaves should be expected anymore");
   Assert.ok(!gExpectedRefresh, "No refreshes should be expected anymore");
  });
 
 function run_test() {
   setupFakeLoopServer();
 
+  Services.prefs.setCharPref("loop.key", kGuestKey);
+
   LoopRooms.on("add", onRoomAdded);
   LoopRooms.on("update", onRoomUpdated);
   LoopRooms.on("delete", onRoomDeleted);
   LoopRooms.on("joined", onRoomJoined);
   LoopRooms.on("left", onRoomLeft);
   LoopRooms.on("refresh", onRefresh);
 
   do_register_cleanup(function () {
     // Revert original Chat.open implementation
     Chat.open = openChatOrig;
+    Services.prefs.clearUserPref(kContextEnabledPref);
+    Services.prefs.clearUserPref("loop.key");
 
     MozLoopServiceInternal.fxAOAuthTokenData = null;
     MozLoopServiceInternal.fxAOAuthProfile = null;
 
     LoopRooms.off("add", onRoomAdded);
     LoopRooms.off("update", onRoomUpdated);
     LoopRooms.off("delete", onRoomDeleted);
     LoopRooms.off("joined", onRoomJoined);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global Services, Assert */
+
+const kGuestKeyPref = "loop.key";
+
+do_register_cleanup(function() {
+  Services.prefs.clearUserPref(kGuestKeyPref);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
+  MozLoopServiceInternal.fxAOAuthProfile = null;
+});
+
+add_task(function* test_guestCreateKey() {
+  // Ensure everything is cleared and we're not logged in.
+  Services.prefs.clearUserPref(kGuestKeyPref);
+  MozLoopServiceInternal.fxAOAuthTokenData = null;
+  MozLoopServiceInternal.fxAOAuthProfile = null;
+
+  let key = yield MozLoopService.promiseProfileEncryptionKey();
+
+  Assert.ok(typeof key == "string", "should generate a key");
+  Assert.equal(Services.prefs.getCharPref(kGuestKeyPref), key,
+    "should save the key");
+});
+
+add_task(function* test_guestGetKey() {
+  // Pretend there's an existing key.
+  const kFakeKey = "13572468";
+  Services.prefs.setCharPref(kGuestKeyPref, kFakeKey);
+
+  let key = yield MozLoopService.promiseProfileEncryptionKey();
+
+  Assert.equal(key, kFakeKey, "should return existing key");
+});
+
+add_task(function* test_fxaGetKey() {
+  // Set the userProfile to look like we're logged into FxA.
+  MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" };
+  MozLoopServiceInternal.fxAOAuthProfile = { email: "fake@invalid.com" };
+
+  // Currently unimplemented, add a test when we implement the code.
+  yield Assert.rejects(MozLoopService.promiseProfileEncryptionKey(),
+    /unimplemented/, "should reject as unimplemented");
+});
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -4,16 +4,17 @@ tail =
 firefox-appdir = browser
 skip-if = toolkit == 'gonk'
 
 [test_loopapi_hawk_request.js]
 [test_looppush_initialize.js]
 [test_looprooms.js]
 [test_loopservice_directcall.js]
 [test_loopservice_dnd.js]
+[test_loopservice_encryptionkey.js]
 [test_loopservice_hawk_errors.js]
 [test_loopservice_hawk_request.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
 [test_loopservice_registration_retry.js]
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -1,39 +1,45 @@
 /* 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/. */
 
 // Sample from https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms
 var fakeRooms = [
   {
     "roomToken": "_nxD4V4FflQ",
-    "roomName": "First Room Name",
+    "decryptedContext": {
+      "roomName": "First Room Name"
+    },
     "roomUrl": "http://localhost:3000/rooms/_nxD4V4FflQ",
     "roomOwner": "Alexis",
     "maxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517546,
     "expiresAt": 1405534180,
     "participants": []
   },
   {
     "roomToken": "QzBbvGmIZWU",
-    "roomName": "Second Room Name",
+    "decryptedContext": {
+      "roomName": "Second Room Name"
+    },
     "roomUrl": "http://localhost:3000/rooms/QzBbvGmIZWU",
     "roomOwner": "Alexis",
     "maxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517546,
     "expiresAt": 1405534180,
     "participants": []
   },
   {
     "roomToken": "3jKS_Els9IU",
-    "roomName": "UX Discussion",
+    "decryptedContext": {
+      "roomName": "UX Discussion",
+    },
     "roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU",
     "roomOwner": "Alexis",
     "maxSize": 2,
     "clientMaxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517818,
     "expiresAt": 1405534180,
     "participants": [