1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

Factor out OlmDevice from client.js

MatrixClient contains quite a lot of boilerplate for manipulating the Olm
things, which quite nicely factors out to a separate object.
This commit is contained in:
Richard van der Hoff
2016-06-07 17:06:43 +01:00
parent 0c8c7cf77a
commit 8c6e2591d9
3 changed files with 417 additions and 177 deletions

View File

@@ -41,10 +41,8 @@ var SCROLLBACK_DELAY_MS = 3000;
var CRYPTO_ENABLED = false;
try {
var Olm = require("olm");
if (Olm.Account && Olm.Session) {
CRYPTO_ENABLED = true;
}
var OlmDevice = require("./OlmDevice");
CRYPTO_ENABLED = true;
} catch (e) {
// Olm not installed.
}
@@ -69,10 +67,18 @@ var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
* {@link requestFunction} for more information.
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {string} opts.userId The user ID for this user.
* @param {Object} opts.store Optional. The data store to use. If not specified,
*
* @param {Object=} opts.store The data store to use. If not specified,
* this client will not store any HTTP responses.
*
* @param {string=} opts.deviceId A unique identifier for this device, for use
* in end-to-end crypto. If not specified, end-to-end crypto will be disabled.
*
* @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
* session data. If not specified, end-to-end crypto will be disabled.
*
* @param {Object} opts.scheduler Optional. The scheduler to use. If not
* specified, this client will not retry requests on failure. This client
* will supply its own processing function to
@@ -97,47 +103,37 @@ function MatrixClient(opts) {
this.store = opts.store || new StubStore();
this.sessionStore = opts.sessionStore || null;
this.accountKey = "DEFAULT_KEY";
this.deviceId = opts.deviceId;
this._olmDevice = null;
if (CRYPTO_ENABLED && 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":["' + OLM_ALGORITHM + '"]';
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);
var myDevices = this.sessionStore.getEndToEndDevicesForUser(
opts.userId
) || {};
myDevices[opts.deviceId] = this.deviceKeys;
this.sessionStore.storeEndToEndDevicesForUser(
opts.userId, myDevices
);
} finally {
account.free();
}
this._olmDevice = new OlmDevice(opts.sessionStore);
var json = '{"algorithms":["' + OLM_ALGORITHM + '"]';
json += ',"device_id":"' + this.deviceId + '"';
json += ',"keys":';
json += '{"ed25519:' + this.deviceId + '":';
json += JSON.stringify(this._olmDevice.deviceEd25519Key);
json += ',"curve25519:' + this.deviceId + '":';
json += JSON.stringify(this._olmDevice.deviceCurve25519Key);
json += '}';
json += ',"user_id":' + JSON.stringify(opts.userId);
json += '}';
var signature = this._olmDevice.sign(json);
this.deviceKeys = JSON.parse(json);
var signatures = {};
signatures[opts.userId] = {};
signatures[opts.userId]["ed25519:" + this.deviceId] = signature;
this.deviceKeys.signatures = signatures;
var myDevices = this.sessionStore.getEndToEndDevicesForUser(
opts.userId
) || {};
myDevices[opts.deviceId] = this.deviceKeys;
this.sessionStore.storeEndToEndDevicesForUser(
opts.userId, myDevices
);
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
@@ -311,54 +307,29 @@ MatrixClient.prototype.uploadKeys = function(maxKeys, deferred) {
var self = this;
return _doKeyUpload(this).then(function(res) {
var keyCount = res.one_time_key_counts.curve25519 || 0;
var pickled = self.sessionStore.getEndToEndAccount();
var numberToGenerate;
var account = new Olm.Account();
try {
account.unpickle(self.accountKey, pickled);
var maxOneTimeKeys = account.max_number_of_one_time_keys();
var keyLimit = Math.floor(maxOneTimeKeys / 2);
numberToGenerate = Math.max(keyLimit - keyCount, 0);
if (maxKeys !== undefined) {
numberToGenerate = Math.min(numberToGenerate, maxKeys);
}
if (numberToGenerate > 0) {
account.generate_one_time_keys(numberToGenerate);
}
pickled = account.pickle(self.accountKey);
self.sessionStore.storeEndToEndAccount(pickled);
} finally {
account.free();
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
var keyLimit = Math.floor(maxOneTimeKeys / 2);
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
if (maxKeys !== undefined) {
numberToGenerate = Math.min(numberToGenerate, maxKeys);
}
if (numberToGenerate > 0) {
return _doKeyUpload(self);
} else {
if (numberToGenerate <= 0) {
return;
}
self._olmDevice.generateOneTimeKeys(numberToGenerate);
return _doKeyUpload(self);
});
};
// build the upload request, and return a promise which resolves to the response
function _doKeyUpload(client) {
if (!CRYPTO_ENABLED || client.sessionStore === null) {
if (!client._olmDevice) {
return q.reject(new Error("End-to-end encryption disabled"));
}
var pickled = client.sessionStore.getEndToEndAccount();
if (!pickled) {
return q.reject(new Error("End-to-end account not found"));
}
var account = new Olm.Account();
var oneTimeKeys;
try {
account.unpickle(client.accountKey, pickled);
oneTimeKeys = JSON.parse(account.one_time_keys());
} finally {
account.free();
}
var oneTimeKeys = client._olmDevice.getOneTimeKeys();
var oneTimeJson = {};
for (var keyId in oneTimeKeys.curve25519) {
@@ -374,15 +345,7 @@ function _doKeyUpload(client) {
return client._http.authedRequestWithPrefix(
undefined, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE
).then(function(res) {
var account = new Olm.Account();
try {
account.unpickle(client.accountKey, pickled);
account.mark_keys_as_published();
pickled = account.pickle(client.accountKey);
client.sessionStore.storeEndToEndAccount(pickled);
} finally {
account.free();
}
client._olmDevice.markKeysAsPublished();
return res;
});
}
@@ -484,9 +447,10 @@ MatrixClient.prototype.listDeviceKeys = function(userId) {
* @return {Object} A promise that will resolve when encryption is setup.
*/
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
if (!this.sessionStore || !CRYPTO_ENABLED) {
if (!this._olmDevice) {
return q.reject(new Error("End-to-End encryption disabled"));
}
if (config.algorithm === OLM_ALGORITHM) {
if (!config.members) {
throw new Error(
@@ -505,7 +469,7 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
if (devices.hasOwnProperty(deviceId)) {
var keys = devices[deviceId];
var key = keys.keys["curve25519:" + deviceId];
if (key == this.deviceCurve25519Key) {
if (key == this._olmDevice.deviceCurve25519Key) {
continue;
}
if (!this.sessionStore.getEndToEndSessions(key)) {
@@ -543,21 +507,9 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
}
}
if (oneTimeKey) {
var session = new Olm.Session();
var account = new Olm.Account();
try {
var pickled = self.sessionStore.getEndToEndAccount();
account.unpickle(self.accountKey, pickled);
session.create_outbound(account, device[2], oneTimeKey);
var sessionId = session.session_id();
pickled = session.pickle(self.accountKey);
self.sessionStore.storeEndToEndSession(
device[2], sessionId, pickled
);
} finally {
session.free();
account.free();
}
self._olmDevice.createOutboundSession(
device[2], oneTimeKey
);
} else {
missing[device[0]] = missing[device[0]] || [];
missing[device[0]].push([device[1]]);
@@ -1032,18 +984,10 @@ function _encryptMessage(client, roomId, e2eRoomInfo, eventType, content,
var payloadString = JSON.stringify(payloadJson);
for (i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
if (deviceKey == client.deviceCurve25519Key) {
if (deviceKey == client._olmDevice.deviceCurve25519Key) {
continue;
}
var sessions = client.sessionStore.getEndToEndSessions(
deviceKey
);
var sessionIds = [];
for (var sessionId in sessions) {
if (sessions.hasOwnProperty(sessionId)) {
sessionIds.push(sessionId);
}
}
var sessionIds = client._olmDevice.getSessionIdsForDevice(deviceKey);
// Use the session with the lowest ID.
sessionIds.sort();
if (sessionIds.length === 0) {
@@ -1051,22 +995,14 @@ function _encryptMessage(client, roomId, e2eRoomInfo, eventType, content,
// we can't encrypt a message for it.
continue;
}
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(
deviceKey, sessionId, pickled
);
} finally {
session.free();
}
var sessionId = sessionIds[0];
ciphertext[deviceKey] = client._olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: e2eRoomInfo.algorithm,
sender_key: client.deviceCurve25519Key,
sender_key: client._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
@@ -1090,59 +1026,38 @@ function _decryptMessage(client, event) {
if (!ciphertext) {
return _badEncryptedMessage(event, "**Missing ciphertext**");
}
if (!(client.deviceCurve25519Key in content.ciphertext)) {
if (!(client._olmDevice.deviceCurve25519Key in content.ciphertext)) {
return _badEncryptedMessage(event, "**Not included in recipients**");
}
var message = content.ciphertext[client.deviceCurve25519Key];
var sessions = client.sessionStore.getEndToEndSessions(deviceKey);
var message = content.ciphertext[client._olmDevice.deviceCurve25519Key];
var sessionIds = client._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
var session;
for (var sessionId in sessions) {
if (sessions.hasOwnProperty(sessionId)) {
session = new Olm.Session();
try {
session.unpickle(client.accountKey, sessions[sessionId]);
if (message.type === 0 && session.matches_inbound(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.
console.log(
"Failed to decrypt with an existing session: " + e.message
);
} finally {
session.free();
}
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = client._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
);
payloadString = res.payload;
if (payloadString) {
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
}
if (message.type === 0 && !foundSession && payloadString === null) {
var account = new Olm.Account();
session = new 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);
sessionId = session.session_id();
client.sessionStore.storeEndToEndSession(
deviceKey, sessionId, pickledSession
payloadString = client._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
client.sessionStore.storeEndToEndAccount(pickledAccount);
} catch (e) {
// Failed to decrypt with a new session.
} finally {
session.free();
account.free();
}
}
@@ -3093,8 +3008,8 @@ MatrixClient.prototype.startClient = function(opts) {
this._clientOpts = opts;
if (CRYPTO_ENABLED && this.sessionStore !== null) {
this.uploadKeys(5);
if (this._olmDevice) {
this.uploadKeys(5).done();
}
// periodically poll for turn servers if we support voip