You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Sign one-time keys, and verify their signatures
We have decided that signing one-time keys is the lesser of two evils; accordingly, use a new key algorithm type (`signed_curve25519`), sign the one-time keys that we upload to the server, and verify the signatures on those we download. This will mean that develop won't be able to talk to master, but hey, we're in beta.
This commit is contained in:
@@ -976,24 +976,28 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
|
|||||||
*
|
*
|
||||||
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
||||||
*
|
*
|
||||||
* @param {module:client.callback=} callback
|
* @param {string} [key_algorithm = signed_curve25519] desired key type
|
||||||
*
|
*
|
||||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||||
* an error response ({@link module:http-api.MatrixError}).
|
* an error response ({@link module:http-api.MatrixError}).
|
||||||
*/
|
*/
|
||||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
|
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||||
var queries = {};
|
var queries = {};
|
||||||
|
|
||||||
|
if (key_algorithm === undefined) {
|
||||||
|
key_algorithm = "signed_curve25519";
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < devices.length; ++i) {
|
for (var i = 0; i < devices.length; ++i) {
|
||||||
var userId = devices[i][0];
|
var userId = devices[i][0];
|
||||||
var deviceId = devices[i][1];
|
var deviceId = devices[i][1];
|
||||||
var query = queries[userId] || {};
|
var query = queries[userId] || {};
|
||||||
queries[userId] = query;
|
queries[userId] = query;
|
||||||
query[deviceId] = "curve25519";
|
query[deviceId] = key_algorithm;
|
||||||
}
|
}
|
||||||
var content = {one_time_keys: queries};
|
var content = {one_time_keys: queries};
|
||||||
return this._http.authedRequestWithPrefix(
|
return this._http.authedRequestWithPrefix(
|
||||||
callback, "POST", "/keys/claim", undefined, content,
|
undefined, "POST", "/keys/claim", undefined, content,
|
||||||
httpApi.PREFIX_UNSTABLE
|
httpApi.PREFIX_UNSTABLE
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ Crypto.prototype.uploadKeys = function(maxKeys) {
|
|||||||
// these factors.
|
// these factors.
|
||||||
|
|
||||||
// We first find how many keys the server has for us.
|
// We first find how many keys the server has for us.
|
||||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
var keyCount = res.one_time_key_counts.signed_curve25519 || 0;
|
||||||
// We then check how many keys we can store in the Account object.
|
// We then check how many keys we can store in the Account object.
|
||||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||||
// Try to keep at most half that number on the server. This leaves the
|
// Try to keep at most half that number on the server. This leaves the
|
||||||
@@ -217,11 +217,7 @@ function _uploadDeviceKeys(crypto) {
|
|||||||
keys: crypto._deviceKeys,
|
keys: crypto._deviceKeys,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
};
|
};
|
||||||
|
crypto._signObject(deviceKeys);
|
||||||
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
|
||||||
deviceKeys.signatures = {};
|
|
||||||
deviceKeys.signatures[userId] = {};
|
|
||||||
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
|
||||||
|
|
||||||
return crypto._baseApis.uploadKeysRequest({
|
return crypto._baseApis.uploadKeysRequest({
|
||||||
device_keys: deviceKeys,
|
device_keys: deviceKeys,
|
||||||
@@ -239,9 +235,14 @@ function _uploadOneTimeKeys(crypto) {
|
|||||||
|
|
||||||
for (var keyId in oneTimeKeys.curve25519) {
|
for (var keyId in oneTimeKeys.curve25519) {
|
||||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
var k = {
|
||||||
|
key: oneTimeKeys.curve25519[keyId],
|
||||||
|
};
|
||||||
|
crypto._signObject(k);
|
||||||
|
oneTimeJson["signed_curve25519:" + keyId] = k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return crypto._baseApis.uploadKeysRequest({
|
return crypto._baseApis.uploadKeysRequest({
|
||||||
one_time_keys: oneTimeJson
|
one_time_keys: oneTimeJson
|
||||||
}, {
|
}, {
|
||||||
@@ -396,23 +397,9 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var unsigned = deviceResult.unsigned || {};
|
var unsigned = deviceResult.unsigned || {};
|
||||||
var signatures = deviceResult.signatures || {};
|
|
||||||
var userSigs = signatures[userId] || {};
|
|
||||||
var signature = userSigs[signKeyId];
|
|
||||||
if (!signature) {
|
|
||||||
console.log("Device " + userId + ":" + deviceId +
|
|
||||||
" is not signed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare the canonical json: remove 'unsigned' and signatures, and
|
|
||||||
// stringify with anotherjson
|
|
||||||
delete deviceResult.unsigned;
|
|
||||||
delete deviceResult.signatures;
|
|
||||||
var json = anotherjson.stringify(deviceResult);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_olmDevice.verifySignature(signKey, json, signature);
|
_verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to verify signature on device " +
|
console.log("Unable to verify signature on device " +
|
||||||
userId + ":" + deviceId + ":", e);
|
userId + ":" + deviceId + ":", e);
|
||||||
@@ -785,8 +772,9 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
|||||||
// That should eventually resolve itself, but it's poor form.
|
// That should eventually resolve itself, but it's poor form.
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var oneTimeKeyAlgorithm = "signed_curve25519";
|
||||||
return this._baseApis.claimOneTimeKeys(
|
return this._baseApis.claimOneTimeKeys(
|
||||||
devicesWithoutSession
|
devicesWithoutSession, oneTimeKeyAlgorithm
|
||||||
).then(function(res) {
|
).then(function(res) {
|
||||||
for (var i = 0; i < devicesWithoutSession.length; ++i) {
|
for (var i = 0; i < devicesWithoutSession.length; ++i) {
|
||||||
var device = devicesWithoutSession[i];
|
var device = devicesWithoutSession[i];
|
||||||
@@ -798,22 +786,48 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
|||||||
var deviceRes = userRes[deviceId];
|
var deviceRes = userRes[deviceId];
|
||||||
var oneTimeKey = null;
|
var oneTimeKey = null;
|
||||||
for (var keyId in deviceRes) {
|
for (var keyId in deviceRes) {
|
||||||
if (keyId.indexOf("curve25519:") === 0) {
|
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||||
oneTimeKey = deviceRes[keyId];
|
oneTimeKey = deviceRes[keyId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oneTimeKey) {
|
|
||||||
var sid = self._olmDevice.createOutboundSession(
|
if (!oneTimeKey) {
|
||||||
deviceInfo.getIdentityKey(), oneTimeKey
|
console.warn(
|
||||||
|
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||||
|
") for device " + userId + ":" + deviceId
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_verifySignature(
|
||||||
|
self._olmDevice, oneTimeKey, userId, deviceId,
|
||||||
|
deviceInfo.getFingerprint()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
"Unable to verify signature on one-time key for device " +
|
||||||
|
userId + ":" + deviceId + ":", e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sid;
|
||||||
|
try {
|
||||||
|
sid = self._olmDevice.createOutboundSession(
|
||||||
|
deviceInfo.getIdentityKey(), oneTimeKey.key
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// possibly a bad key
|
||||||
|
console.error("Error starting session with device " +
|
||||||
|
userId + ":" + deviceId + ": " + e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Started new sessionid " + sid +
|
console.log("Started new sessionid " + sid +
|
||||||
" for device " + userId + ":" + deviceId);
|
" for device " + userId + ":" + deviceId);
|
||||||
|
|
||||||
result[userId][deviceId].sessionId = sid;
|
result[userId][deviceId].sessionId = sid;
|
||||||
} else {
|
|
||||||
console.warn("No one-time keys for device " +
|
|
||||||
userId + ":" + deviceId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@@ -1181,6 +1195,54 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
|||||||
}).done();
|
}).done();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sign the given object with our ed25519 key
|
||||||
|
*
|
||||||
|
* @param {Object} obj Object to which we will add a 'signatures' property
|
||||||
|
*/
|
||||||
|
Crypto.prototype._signObject = function(obj) {
|
||||||
|
var sigs = {};
|
||||||
|
sigs[this._userId] = {};
|
||||||
|
sigs[this._userId]["ed25519:" + this._deviceId] =
|
||||||
|
this._olmDevice.sign(anotherjson.stringify(obj));
|
||||||
|
obj.signatures = sigs;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the signature on an object
|
||||||
|
*
|
||||||
|
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
|
||||||
|
*
|
||||||
|
* @param {Object} obj object to check signature on. Note that this will be
|
||||||
|
* stripped of its 'signatures' and 'unsigned' properties.
|
||||||
|
*
|
||||||
|
* @param {string} signingUserId ID of the user whose signature should be checked
|
||||||
|
*
|
||||||
|
* @param {string} signingDeviceId ID of the device whose signature should be checked
|
||||||
|
*
|
||||||
|
* @param {string} signingKey base64-ed ed25519 public key
|
||||||
|
*/
|
||||||
|
function _verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) {
|
||||||
|
var signKeyId = "ed25519:" + signingDeviceId;
|
||||||
|
var signatures = obj.signatures || {};
|
||||||
|
var userSigs = signatures[signingUserId] || {};
|
||||||
|
var signature = userSigs[signKeyId];
|
||||||
|
if (!signature) {
|
||||||
|
throw Error("No signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||||
|
// anotherjson
|
||||||
|
delete obj.unsigned;
|
||||||
|
delete obj.signatures;
|
||||||
|
var json = anotherjson.stringify(obj);
|
||||||
|
|
||||||
|
olmDevice.verifySignature(
|
||||||
|
signingKey, json, signature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see module:crypto/algorithms/base.DecryptionError
|
* @see module:crypto/algorithms/base.DecryptionError
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function expectKeyUpload(deviceId, httpBackend) {
|
|||||||
expect(content.one_time_keys).not.toBeDefined();
|
expect(content.one_time_keys).not.toBeDefined();
|
||||||
expect(content.device_keys).toBeDefined();
|
expect(content.device_keys).toBeDefined();
|
||||||
keys.device_keys = content.device_keys;
|
keys.device_keys = content.device_keys;
|
||||||
return {one_time_key_counts: {curve25519: 0}};
|
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||||
@@ -76,7 +76,7 @@ function expectKeyUpload(deviceId, httpBackend) {
|
|||||||
}
|
}
|
||||||
expect(count).toEqual(5);
|
expect(count).toEqual(5);
|
||||||
keys.one_time_keys = content.one_time_keys;
|
keys.one_time_keys = content.one_time_keys;
|
||||||
return {one_time_key_counts: {curve25519: count}};
|
return {one_time_key_counts: {signed_curve25519: count}};
|
||||||
});
|
});
|
||||||
|
|
||||||
return httpBackend.flush(uploadPath, 2).then(function() {
|
return httpBackend.flush(uploadPath, 2).then(function() {
|
||||||
@@ -176,10 +176,11 @@ function expectAliClaimKeys() {
|
|||||||
expect(bobOneTimeKeys).toBeDefined();
|
expect(bobOneTimeKeys).toBeDefined();
|
||||||
|
|
||||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
var claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||||
|
expect(claimType).toEqual("signed_curve25519");
|
||||||
for (var keyId in bobOneTimeKeys) {
|
for (var keyId in bobOneTimeKeys) {
|
||||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||||
if (keyId.indexOf("curve25519:") === 0) {
|
if (keyId.indexOf(claimType + ":") === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user