Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make DeviceInfo more useful, and refactor crypto methods to use it #171

Merged
merged 1 commit into from
Aug 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 161 additions & 54 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ limitations under the License.
*/
"use strict";


/**
* Internal module
*
* @module crypto
*/

Expand All @@ -36,24 +35,92 @@ var DeviceVerification = {
};

/**
* Stored information about a user's device
* Information about a user's device
*
* @constructor
*
* @typedef {Object} DeviceInfo
* @property {string} deviceId the ID of this device
*
* @property {string[]} altorithms list of algorithms supported by this device
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object} keys a map from <key type>:<id> -> key
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {DeviceVerification} verified whether the device has been
* verified by the user
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});

this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.unsigned = {};
}

/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
var res = new DeviceInfo(deviceId);
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};

/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
unsigned: this.unsigned,
};
};

/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};

/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayname = function() {
return this.unsigned.device_display_name || null;
};

/**
* Cryptography bits
*
* @constructor
* @alias module:crypto
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
Expand Down Expand Up @@ -191,18 +258,26 @@ function _uploadOneTimeKeys(crypto) {
*/
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
var self = this;

// map from userid -> deviceid -> DeviceInfo
var stored = {};

// list of userids we need to download keys for
var downloadUsers = [];

for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
stored[userId] = {};

stored[userId] = devices || {};
if (devices && !forceDownload) {
continue;
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
stored[userId][dev.deviceId] = dev;
}

if (devices.length === 0 || forceDownload) {
downloadUsers.push(userId);
}
downloadUsers.push(userId);
}

if (downloadUsers.length === 0) {
Expand All @@ -218,14 +293,26 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
continue;
}

// map from deviceid -> deviceinfo for this user
var userStore = stored[userId];
var updated = _updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, res.device_keys[userId]
);

if (updated) {
if (!updated) {
continue;
}

// update the session store
var storage = {};
for (var deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}

storage[deviceId] = userStore[deviceId].toStorage();
self._sessionStore.storeEndToEndDevicesForUser(
userId, userStore
userId, storage
);
}
}
Expand Down Expand Up @@ -295,7 +382,7 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}

// prepare the canonical json: remove 'unsigned' and sigxsnatures, and
// prepare the canonical json: remove 'unsigned' and signatures, and
// stringify with anotherjson
delete deviceResult.unsigned;
delete deviceResult.signatures;
Expand All @@ -310,12 +397,14 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}

// DeviceInfo
var deviceStore;

if (deviceId in userStore) {
// already have this device.
deviceStore = userStore[deviceId];

if (deviceStore.keys["ed25519:" + deviceId] != signKey) {
if (deviceStore.getFingerprint() != signKey) {
// this should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
Expand All @@ -325,9 +414,7 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
return false;
}
} else {
userStore[deviceId] = deviceStore = {
verified: DeviceVerification.UNVERIFIED
};
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
}

deviceStore.keys = deviceResult.keys;
Expand All @@ -337,42 +424,64 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
}


/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto~DeviceInfo[]} list of devices
*/
Crypto.prototype.getStoredDevicesForUser = function(userId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return [];
}
var res = [];
for (var deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
};


/**
* List the stored device keys for a user id
*
* @deprecated prefer {@link module:crypto#getStoredDevicesForUser}
*
* @param {string} userId the user to list keys for.
*
* @return {object[]} list of devices with "id", "verified", "blocked",
* "key", and "display_name" parameters.
*/
Crypto.prototype.listDeviceKeys = function(userId) {
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId);

var result = [];
if (devices) {
var deviceId;
var deviceIds = [];
for (deviceId in devices) {
if (devices.hasOwnProperty(deviceId)) {
deviceIds.push(deviceId);
}
}
deviceIds.sort();
for (var i = 0; i < deviceIds.length; ++i) {
deviceId = deviceIds[i];
var device = devices[deviceId];
var ed25519Key = device.keys["ed25519:" + deviceId];
var unsigned = device.unsigned || {};
if (ed25519Key) {
result.push({
id: deviceId,
key: ed25519Key,
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
display_name: unsigned.device_display_name,
});
}

for (var i = 0; i < devices.length; ++i) {
var device = devices[i];
var ed25519Key = device.getFingerprint();
if (ed25519Key) {
result.push({
id: device.deviceId,
key: ed25519Key,
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
display_name: device.getDisplayname(),
});
}
}

// sort by deviceid
result.sort(function(a, b) {
if (a.deviceId < b.deviceId) { return -1; }
if (a.deviceId > b.deviceId) { return 1; }
return 0;
});

return result;
};

Expand Down Expand Up @@ -411,7 +520,7 @@ Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key
}
var deviceKey = device.keys[keyId];
if (deviceKey == sender_key) {
return device;
return DeviceInfo.fromStorage(device, deviceId);
}
}
}
Expand Down Expand Up @@ -670,18 +779,16 @@ Crypto.prototype._encryptMessage = function(room, e2eRoomInfo, eventType, conten
var participantKeys = [];
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
for (var deviceId in devices) {
if (devices.hasOwnProperty(deviceId)) {
var dev = devices[deviceId];
if (dev.verified === DeviceVerification.BLOCKED) {
continue;
}
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
if (dev.blocked) {
continue;
}

for (var keyId in dev.keys) {
if (keyId.indexOf("curve25519:") === 0) {
participantKeys.push(dev.keys[keyId]);
}
for (var keyId in dev.keys) {
if (keyId.indexOf("curve25519:") === 0) {
participantKeys.push(dev.keys[keyId]);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions spec/integ/matrix-client-crypto.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ function aliDownloadsKeys() {
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
var p2 = aliQueryKeys();
Expand Down
43 changes: 23 additions & 20 deletions spec/integ/matrix-client-methods.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,27 +236,22 @@ describe("MatrixClient", function() {
});

client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
expect(res).toEqual({
boris: {
dev1: {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
},
},
chaz: {
dev2: {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
},
},
dave: {
// dave's key fails validation.
},
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});

assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});

// dave's key fails validation.
expect(res.dave).toEqual({});
}).catch(utils.failTest).done(done);

httpBackend.flush();
Expand All @@ -278,3 +273,11 @@ describe("MatrixClient", function() {
});
});
});

function assertObjectContains(obj, expected) {
for (var k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}