Skip to content

Commit

Permalink
Merge pull request #200 from matrix-org/rav/oh_hai_new_device
Browse files Browse the repository at this point in the history
OH HAI
  • Loading branch information
richvdh authored Sep 8, 2016
2 parents 43f3a1e + 1da633e commit 2af349e
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 11 deletions.
8 changes: 8 additions & 0 deletions lib/crypto/algorithms/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership
) {};

/**
* Called when a new device announces itself in the room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};


/**
* base type for decryption implementations
Expand Down
52 changes: 41 additions & 11 deletions lib/crypto/algorithms/megolm.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ function MegolmEncryption(params) {
this._outboundSessionId = null;
this._discardNewSession = false;

// users who have joined since we last sent a message.
// devices which have joined since we last sent a message.
// userId -> {deviceId -> true}, or
// userId -> true
this._usersPendingKeyShare = {};
this._devicesPendingKeyShare = {};
this._sharePromise = null;
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
Expand Down Expand Up @@ -81,9 +82,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
return this._sharePromise;
}

// prep already done, but check for new users
var shareMap = this._usersPendingKeyShare;
this._usersPendingKeyShare = {};
// prep already done, but check for new devices
var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {};

// check each user is (still) a member of the room
for (var userId in shareMap) {
Expand All @@ -98,7 +99,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
}
}

this._sharePromise = this._shareKeyWithUsers(
this._sharePromise = this._shareKeyWithDevices(
sessionId, shareMap
).finally(function() {
self._sharePromise = null;
Expand Down Expand Up @@ -128,7 +129,7 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {

// we're going to share the key with all current members of the room,
// so we can reset this.
this._usersPendingKeyShare = {};
this._devicesPendingKeyShare = {};

var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
Expand All @@ -147,7 +148,7 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
return this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._shareKeyWithUsers(session_id, shareMap);
return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() {
if (self._discardNewSession) {
// we've had cause to reset the session_id since starting this process.
Expand All @@ -168,12 +169,12 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
* @private
*
* @param {string} session_id
* @param {Object<string, boolean>} shareMap
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithUsers = function(session_id, shareMap) {
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
var self = this;

var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
Expand All @@ -199,13 +200,21 @@ MegolmEncryption.prototype._shareKeyWithUsers = function(session_id, shareMap) {
continue;
}

var devicesToShareWith = shareMap[userId];
var deviceInfos = devicemap[userId];

for (var deviceId in deviceInfos) {
if (!deviceInfos.hasOwnProperty(deviceId)) {
continue;
}

if (devicesToShareWith === true) {
// all devices
} else if (!devicesToShareWith[deviceId]) {
// not a new device
continue;
}

console.log(
"sharing keys with device " + userId + ":" + deviceId
);
Expand Down Expand Up @@ -281,7 +290,7 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers

if (newMembership === 'join') {
// new member in the room.
this._usersPendingKeyShare[member.userId] = true;
this._devicesPendingKeyShare[member.userId] = true;
return;
}

Expand All @@ -306,6 +315,27 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
}
};

/**
* @inheritdoc
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
var d = this._devicesPendingKeyShare[userId];

if (d === true) {
// we already want to share keys with all devices for this user
return;
}

if (!d) {
this._devicesPendingKeyShare[userId] = d = {};
}

d[deviceId] = true;
};


/**
* Megolm decryption implementation
Expand Down
118 changes: 118 additions & 0 deletions lib/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
}

function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("sync", function(syncState, oldState, data) {
if (syncState == "PREPARED") {
// XXX ugh. we're assuming the eventEmitter is a MatrixClient.
// how can we avoid doing so?
var rooms = eventEmitter.getRooms();
crypto._onInitialSyncCompleted(rooms);
}
});

eventEmitter.on(
"RoomMember.membership",
crypto._onRoomMembership.bind(crypto)
Expand All @@ -96,6 +105,8 @@ function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("toDeviceEvent", function(event) {
if (event.getType() == "m.room_key") {
crypto._onRoomKeyEvent(event);
} else if (event.getType() == "m.new_device") {
crypto._onNewDeviceEvent(event);
}
});

Expand Down Expand Up @@ -815,6 +826,73 @@ Crypto.prototype._onCryptoEvent = function(event) {
}
};

/**
* handle the completion of the initial sync.
*
* Announces the new device.
*
* @private
* @param {module:models/room[]} rooms list of rooms the client knows about
*/
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
if (this._sessionStore.getDeviceAnnounced()) {
return;
}

// we need to tell all the devices in all the rooms we are members of that
// we have arrived.
// build a list of rooms for each user.
var roomsByUser = {};
for (var i = 0; i < rooms.length; i++) {
var room = rooms[i];

// check for rooms with encryption enabled
var alg = this._roomAlgorithms[room.roomId];
if (!alg) {
continue;
}

// ignore any rooms which we have left
var me = room.getMember(this._userId);
if (!me || (
me.membership !== "join" && me.membership !== "invite"
)) {
continue;
}

var members = room.getJoinedMembers();
for (var j = 0; j < members.length; j++) {
var m = members[j];
if (!roomsByUser[m.userId]) {
roomsByUser[m.userId] = [];
}
roomsByUser[m.userId].push(room.roomId);
}
}

// build a per-device message for each user
var content = {};
for (var userId in roomsByUser) {
if (!roomsByUser.hasOwnProperty(userId)) {
continue;
}
content[userId] = {
"*": {
device_id: this._deviceId,
rooms: roomsByUser[userId],
},
};
}

var self = this;
this._baseApis.sendToDevice(
"m.new_device", // OH HAI!
content
).done(function() {
self._sessionStore.setDeviceAnnounced();
});
};

/**
* Handle a key event
*
Expand Down Expand Up @@ -863,6 +941,46 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
alg.onRoomMembership(event, member, oldMembership);
};


/**
* Called when a new device announces itself
*
* @private
* @param {module:models/event.MatrixEvent} event announcement event
*/
Crypto.prototype._onNewDeviceEvent = function(event) {
var content = event.getContent();
var userId = event.getSender();
var deviceId = content.device_id;
var rooms = content.rooms;

if (!rooms || !deviceId) {
console.warn("new_device event missing keys");
return;
}

var self = this;
this.downloadKeys(
[userId], true
).then(function() {
for (var i = 0; i < rooms.length; i++) {
var roomId = rooms[i];
var alg = self._roomAlgorithms[roomId];
if (!alg) {
// not encrypting in this room
continue;
}
alg.onNewDevice(userId, deviceId);
}
}).catch(function(e) {
console.error(
"Error updating device keys for new device " + userId + ":" +
deviceId,
e
);
}).done();
};

/**
* @see module:crypto/algorithms/base.DecryptionError
*/
Expand Down
17 changes: 17 additions & 0 deletions lib/store/session/webstorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},

/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},

/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},

/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
Expand Down Expand Up @@ -134,6 +150,7 @@ WebStorageSessionStore.prototype = {
};

var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";

function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
Expand Down

0 comments on commit 2af349e

Please sign in to comment.