1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-18 05:42:00 +03:00

Start integrating end-to-end into the matrix-client. Add a storage class to store end-to-end sessions. Implement the one-time key upload API, and start sketching out the encryption and decryption functions

This commit is contained in:
Mark Haines
2015-07-16 18:21:25 +01:00
parent 8cb973e605
commit 2ee5977ad2
4 changed files with 429 additions and 14 deletions

View File

@@ -17,6 +17,9 @@ var Room = require("./models/room");
var User = require("./models/user");
var utils = require("./utils");
// TODO: package this somewhere separate.
var Olm = require("./olm");
// TODO:
// Internal: rate limiting
@@ -44,11 +47,48 @@ var utils = require("./utils");
*/
function MatrixClient(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
utils.checkObjectHasNoAdditionalKeys(opts,
["baseUrl", "request", "accessToken", "userId", "store", "scheduler"]
);
utils.checkObjectHasNoAdditionalKeys(opts, [
"baseUrl", "request", "accessToken", "userId", "store", "scheduler",
"sessionStore", "deviceId"
]);
this.store = opts.store || new StubStore();
this.sessionStore = opts.sessionStore || null;
this.accountKey = "DEFAULT_KEY";
this.deviceId = opts.deviceId;
if (this.sessionStore !== null) {
var e2eAccount = this.sessionStore.getEndToEndAccount();
var account = new Olm.Account();
try {
if (e2eAccount == null) {
account.create();
} else {
account.unpickle(this.accountKey, e2eAccount);
}
var e2eKeys = JSON.parse(account.identity_keys());
var json = '{"algorithms":["m.olm.v1.curve25519-aes-sha2"]';
json += ',"device_id":"' + this.deviceId + '"';
json += ',"keys":';
json += '{"ed25519:' + this.deviceId + '":';
json += JSON.stringify(e2eKeys.ed25519);
json += ',"curve25519:' + this.deviceId + '":';
json += JSON.stringify(e2eKeys.curve25519);
json += '}';
json += ',"user_id":' + JSON.stringify(opts.userId);
json += '}';
var signature = account.sign(json);
this.deviceKeys = JSON.parse(json);
var signatures = {};
signatures[opts.userId] = {};
signatures[opts.userId]["ed25519:" + this.deviceId] = signature;
this.deviceKeys.signatures = signatures;
this.deviceCurve25519Key = e2eKeys["curve25519"];
var pickled = account.pickle(this.accountKey);
this.sessionStore.storeEndToEndAccount(pickled);
} finally {
account.free();
}
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
var self = this;
@@ -76,6 +116,61 @@ function MatrixClient(opts) {
}
utils.inherits(MatrixClient, EventEmitter);
MatrixClient.prototype.uploadKeys = function(deferred) {
var first_time = deferred === undefined;
deferred = deferred || q.defer();
var path = "/keys/upload/" + this.deviceId;
var pickled = this.sessionStore.getEndToEndAccount();
var account = new Olm.Account();
try {
account.unpickle(this.accountKey, pickled);
var oneTimeKeys = JSON.parse(account.one_time_keys());
var maxOneTimeKeys = account.max_number_of_one_time_keys();
} finally {
account.free();
}
var oneTimeJson = {};
for (var keyId in oneTimeKeys.curve25519) {
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
}
var content = {
device_keys: this.deviceKeys,
one_time_keys: oneTimeJson
};
var self = this;
this._http.authedRequestWithPrefix(
undefined, "POST", path, undefined, content, httpApi.PREFIX_V2_ALPHA
).then(function(res) {
var keyLimit = Math.floor(maxOneTimeKeys / 2);
var keyCount = res.one_time_key_counts.curve25519 || 0;
var generateKeys = (keyCount < keyLimit);
var pickled = self.sessionStore.getEndToEndAccount();
var account = new Olm.Account();
try {
account.unpickle(self.accountKey, pickled);
account.mark_keys_as_published();
if (generateKeys) {
account.generate_one_time_keys(keyLimit - keyCount);
}
pickled = account.pickle(self.accountKey);
self.sessionStore.storeEndToEndAccount(pickled);
} finally {
account.free();
}
if (generateKeys && first_time) {
self.uploadKeys(deferred);
} else {
deferred.resolve();
}
});
return deferred.promise;
};
/**
* Get the room for the given room ID.
* @param {string} roomId The room ID
@@ -267,6 +362,15 @@ MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, sta
*/
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
callback) {
if (eventType === "m.room.message" && this.sessionStore) {
var e2eRoomInfo = this.sessionStore.getEndToEndRoom(roomId);
if (e2eRoomInfo) {
return _sendEncryptedMessage(
client, roomId, e2eRoomInfo, content, txnId, callback
);
}
}
if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
if (!txnId) {
txnId = "m" + new Date().getTime();
@@ -295,6 +399,164 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
return _sendEvent(this, room, localEvent, callback);
};
function _sendEncryptedMessage(client, roomId, e2eRoomInfo, eventType, content,
txnId, callback) {
if (!client.sessionStore) {
throw new Error(
"Client must have an end-to-end session store to encrypt messages"
);
}
if (e2eRoomInfo.algorithm === "m.olm.v1.curve25519-aes-sha2") {
var participantKeys = [];
for (var i = 0; i < e2eRoomInfo.participants.length; ++i) {
var userId = e2eRoomInfo.participants[i];
var userCiphertext = {};
devices = client.sessionStore.getEndToEndDevicesForUser(userId);
for (var deviceId in devices) {
var keys = devices[deviceId];
for (keyId in keys.keys) {
if (keyId.startsWith("curve25519")) {
participantKeys.push(keys.keys[keyId]);
}
}
}
}
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
roomId: roomId,
type: eventType,
fingerprint: participantHash,
sender_device: client.deviceId,
content: content
};
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (var i = 0; i < participantKeys.length(); ++i) {
var deviceKey = participantKeys[i];
var sessions = client.sessionStore.getEndToEndSessions(
deviceKey
);
var sessionIds = [];
for (sessionId in sessions) {
sessionIds.push(sessionId);
}
// Use the session with the lowest ID.
sessionIds.sort()
if (sessionIds.length == 0) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
var sessionId = sessionIds[0];
var session = new Olm.Session();
try {
session.unpickle(client.accountKey, sessions[sessionId]);
ciphertext[deviceKey] = session.encrypt(payloadString);
var pickled = session.pickle(client.accountKey);
client.sessionStore.storeEndToEndSession(
userId, deviceId, sessionId, pickled
);
} finally {
session.free();
}
}
var encryptedContent = {
algorithm: e2eRoomInfo.algorithm,
sender_key: client.deviceCurve25519Key,
ciphertext: ciphertext
};
return client.sendEvent(
roomId, "m.room.encrypted", encryptedContent, txnId, callback
);
} else {
throw new Error("Unknown end-to-end algorithm");
}
}
function _decryptMessage(client, event) {
var content = event.getContent();
if (content.algorithm === "m.olm.v1.curve25519-aes-sha2") {
var sender = event.getSender();
var deviceKey = content.sender_key;
if (!client.deviceCurve25519Key in content.ciphertext) {
return _badEncryptedMessage(event, "Not included in recipients");
}
var message = content.ciphertext[client.deviceCurve25519Key];
var sessions = client.sessionStore.getEndToEndSessions(deviceKey);
var payloadString = null;
var foundSession = false;
for (sessionId in sessions) {
var session = new Olm.Session();
try {
session.unpickle(client.accountKey, sessions[sessionId]);
if (message.type == 0 && session.matches(message.body)) {
foundSession = true;
}
payloadString = session.decrypt(message.type, message.body);
var pickled = session.pickle(client.accountKey);
client.sessionStore.storeEndToEndSession(
deviceKey, sessionId, pickled
);
} catch(e) {
// Failed to decrypt with an existing session.
} finally {
session.free();
}
}
if (message.type == 0 && !foundSession && payloadString !== null) {
var account = Olm.Account();
var session = Olm.Session();
try {
var account_data = client.sessionStore.getEndToEndAccount();
account.unpickle(client.accountKey, account_data);
session.create_inbound_from(account, deviceKey, message.body);
payloadString = session.decrypt(message.type, message.body);
account.remove_one_time_keys(session);
var pickledSession = session.pickle(client.accountKey);
var pickledAccount = account.pickle(client.accountKey);
var sessionId = session.session_id();
client.sessionStore.storeEndToEndSession(
deviceKey, sessionId, pickledSession
);
client.sessionStore.storeEndToEndAccount(pickledAccount);
} catch(e) {
// Failed to decrypt with a new session.
} finally {
session.free();
account.free();
}
}
if (payloadString !== null) {
var payload = JSON.parse(payloadString);
return new MatrixEvent({
// TODO: Add rest of the event keys.
// TODO: Add a key to indicate that the event was encrypted.
type: payload.type,
content: payload.content,
user_id: event.getSender()
});
} else {
return _badEncryptedMessge(event, "Bad Encrypted Message");
}
}
}
function _badEncryptedMessge(event, reason) {
return new MatrixEvent({
type: "m.room.message",
// TODO: Add rest of the event keys.
content: {
msgtype: "m.bad.encrypted",
body: reason,
content: event.getContent()
}
});
}
function _sendEvent(client, room, event, callback) {
var defer = q.defer();
var promise;
@@ -900,7 +1162,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
var defer = q.defer();
var self = this;
this._http.authedRequest(callback, "GET", path, params).done(function(res) {
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper);
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
room.addEventsToTimeline(matrixEvents, true);
room.oldState.paginationToken = res.end;
if (res.chunk.length < limit) {
@@ -1055,7 +1317,8 @@ function doInitialSync(client, historyLen) {
var i, j;
// intercept the results and put them into our store
if (!(client.store instanceof StubStore)) {
utils.forEach(utils.map(data.presence, _PojoToMatrixEventMapper),
utils.forEach(
utils.map(data.presence, _PojoToMatrixEventMapper(client)),
function(e) {
var user = createNewUser(client, e.getContent().user_id);
user.setPresenceEvent(e);
@@ -1081,7 +1344,7 @@ function doInitialSync(client, historyLen) {
}
_processRoomEvents(
room, data.rooms[i].state, data.rooms[i].messages
client, room, data.rooms[i].state, data.rooms[i].messages
);
// cache the name/summary/etc prior to storage since we don't
@@ -1266,7 +1529,7 @@ function _syncRoom(client, room) {
client._syncingRooms[room.roomId] = defer.promise;
client.roomInitialSync(room.roomId, 8).done(function(res) {
room.timeline = []; // blow away any previous messages.
_processRoomEvents(room, res.state, res.messages);
_processRoomEvents(client, room, res.state, res.messages);
room.recalculate(client.credentials.userId);
client.store.storeRoom(room);
client.emit("Room", room);
@@ -1279,15 +1542,15 @@ function _syncRoom(client, room) {
return defer.promise;
}
function _processRoomEvents(room, stateEventList, messageChunk) {
function _processRoomEvents(client, room, stateEventList, messageChunk) {
// "old" and "current" state are the same initially; they
// start diverging if the user paginates.
// We must deep copy otherwise membership changes in old state
// will leak through to current state!
var oldStateEvents = utils.map(
utils.deepCopy(stateEventList), _PojoToMatrixEventMapper
utils.deepCopy(stateEventList), _PojoToMatrixEventMapper(client)
);
var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper);
var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper(client));
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(stateEvents);
@@ -1299,7 +1562,7 @@ function _processRoomEvents(room, stateEventList, messageChunk) {
room.addEventsToTimeline(
utils.map(
messageChunk ? messageChunk.chunk : [],
_PojoToMatrixEventMapper
_PojoToMatrixEventMapper(client)
).reverse(), true
);
if (messageChunk) {
@@ -1377,8 +1640,16 @@ function _resolve(callback, defer, res) {
defer.resolve(res);
}
function _PojoToMatrixEventMapper(plainOldJsObject) {
return new MatrixEvent(plainOldJsObject);
function _PojoToMatrixEventMapper(client) {
function mapper (plainOldJsObject) {
var event = new MatrixEvent(plainOldJsObject);
if (event.getType() === "m.room.encrypted") {
return _decryptMessage(client, event);
} else {
return event;
}
}
return mapper;
}
/** */