1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

save cross-signing keys from sync and verify new keys for user

This commit is contained in:
Hubert Chathi
2019-05-28 22:28:54 -04:00
parent 193ad9e09d
commit 53804cac5c
6 changed files with 507 additions and 160 deletions

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
import expect from 'expect'; import expect from 'expect';
import Promise from 'bluebird'; import Promise from 'bluebird';
const logger = require("../logger");
// load olm before the sdk if possible // load olm before the sdk if possible
import './olm-loader'; import './olm-loader';
@@ -241,3 +242,133 @@ module.exports.awaitDecryption = function(event) {
}); });
}); });
}; };
const HttpResponse = module.exports.HttpResponse = function(
httpLookups, acceptKeepalives,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.pendingLookup = null;
};
HttpResponse.prototype.request = function HttpResponse(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
return Promise.resolve();
}
const next = this.httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
if (!next) { // no more things to return
if (this.pendingLookup) {
if (this.pendingLookup.method === method
&& this.pendingLookup.path === path) {
return this.pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
this.pendingLookup = {
promise: Promise.defer().promise,
method: method,
path: path,
};
return this.pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return Promise.defer().promise;
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.USER_ID = "@alice:bar";
HttpResponse.filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
HttpResponse.SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
HttpResponse.SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: HttpResponse.SYNC_DATA,
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
];
};
module.exports.setHttpResponses = function setHttpResponses(
client, responses, acceptKeepalives,
) {
const httpResponseObj = new HttpResponse(responses, acceptKeepalives);
const httpReq = httpResponseObj.request.bind(httpResponseObj);
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
};

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -23,6 +24,8 @@ import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient'; import TestClient from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
async function makeTestClient(userInfo, options) { async function makeTestClient(userInfo, options) {
const client = (new TestClient( const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options, userInfo.userId, userInfo.deviceId, undefined, undefined, options,
@@ -86,12 +89,126 @@ describe("Cross Signing", function() {
const alice = await makeTestClient( const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"}, {userId: "@alice:example.com", deviceId: "Osborne2"},
); );
alice.on("cross-signing:newKey", function(e) {
// FIXME: ??? const masterKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const keyChangePromise = new Promise((resolve, reject) => {
alice.once("cross-signing:keysChanged", (e) => {
resolve(e);
});
}); });
alice.once("cross-signing:newKey", (e) => {
e.done(masterKey);
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
// feed sync result that includes ssk, usk, device key // feed sync result that includes ssk, usk, device key
// client should emit event asking about ssk const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
data: {
next_batch: "abcdefg",
device_lists: {
changed: [
"@alice:example.com",
"@bob:example.com",
],
},
},
},
{
method: "POST",
path: "/keys/query",
data: {
"failures": {},
"device_keys": {
"@alice:example.com": {
"Osborne2": aliceDevice,
},
},
"master_keys": {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
},
},
"self_signing_keys": {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["self-signing"],
keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
},
signatures: {
"@alice:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
},
},
},
},
},
},
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
];
setHttpResponses(alice, responses);
await alice.startClient();
// once ssk is confirmed, device key should be trusted // once ssk is confirmed, device key should be trusted
await keyChangePromise;
expect(alice.checkUserTrust("@alice:example.com")).toBe(6);
expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7);
}); });
it("should use trust chain to determine device verification", async function() { it("should use trust chain to determine device verification", async function() {

View File

@@ -21,14 +21,18 @@ limitations under the License.
import {pkSign, pkVerify} from './olmlib'; import {pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import logger from '../logger';
function getPublicKey(keyInfo) { function getPublicKey(keyInfo) {
return Object.entries(keyInfo.keys)[0]; return Object.entries(keyInfo.keys)[0];
} }
function getPrivateKey(self, type, check) { async function getPrivateKey(self, type, check) {
return new Promise((resolve, reject) => { let error;
const askForKey = (error) => { let pubkey;
let signing;
do {
[pubkey, signing] = await new Promise((resolve, reject) => {
self.emit("cross-signing:getKey", { self.emit("cross-signing:getKey", {
type: type, type: type,
error, error,
@@ -36,9 +40,11 @@ function getPrivateKey(self, type, check) {
// FIXME: the key needs to be interpreted? // FIXME: the key needs to be interpreted?
const signing = new global.Olm.PkSigning(); const signing = new global.Olm.PkSigning();
const pubkey = signing.init_with_seed(key); const pubkey = signing.init_with_seed(key);
const error = check(pubkey, signing); error = check(pubkey, signing);
if (error) { if (error) {
return askForKey(error); logger.error(error);
signing.free();
resolve([null, null]);
} }
resolve([pubkey, signing]); resolve([pubkey, signing]);
}, },
@@ -46,9 +52,9 @@ function getPrivateKey(self, type, check) {
reject(error || new Error("Cancelled")); reject(error || new Error("Cancelled"));
}, },
}); });
}; });
askForKey(); } while (!pubkey);
}); return [pubkey, signing];
} }
export class CrossSigningInfo extends EventEmitter { export class CrossSigningInfo extends EventEmitter {
@@ -64,12 +70,11 @@ export class CrossSigningInfo extends EventEmitter {
// you can't change the userId // you can't change the userId
Object.defineProperty(this, 'userId', { Object.defineProperty(this, 'userId', {
enumerabel: true, enumerable: true,
value: userId, value: userId,
}); });
this.keys = {}; this.keys = {};
this.fu = true; this.fu = true;
// FIXME: add chain of ssks?
} }
static fromStorage(obj, userId) { static fromStorage(obj, userId) {
@@ -85,20 +90,24 @@ export class CrossSigningInfo extends EventEmitter {
toStorage() { toStorage() {
return { return {
keys: this.keys, keys: this.keys,
verified: this.verified, fu: this.fu,
}; };
} }
/** Get the ID used to identify the user /** Get the ID used to identify the user
*
* @param {string} type The type of key to get the ID of. One of "master",
* "self_signing", or "user_signing". Defaults to "master".
* *
* @return {string} the ID * @return {string} the ID
*/ */
getId() { getId(type) {
return getPublicKey(this.keys.master)[1]; type = type || "master";
return this.keys[type] && getPublicKey(this.keys[type])[1];
} }
async resetKeys(level) { async resetKeys(level) {
if (level === undefined || level & 4) { if (level === undefined || level & 4 || !this.keys.master) {
level = CrossSigningLevel.MASTER; level = CrossSigningLevel.MASTER;
} else if (level === 0) { } else if (level === 0) {
return; return;
@@ -109,87 +118,120 @@ export class CrossSigningInfo extends EventEmitter {
let masterSigning; let masterSigning;
let masterPub; let masterPub;
if (level & 4) { try {
masterSigning = new global.Olm.PkSigning(); if (level & 4) {
privateKeys.master = masterSigning.generate_seed(); masterSigning = new global.Olm.PkSigning();
masterPub = masterSigning.init_with_seed(privateKeys.master); privateKeys.master = masterSigning.generate_seed();
keys.master = { masterPub = masterSigning.init_with_seed(privateKeys.master);
user_id: this.userId, keys.master = {
usage: ['master'], user_id: this.userId,
keys: { usage: ['master'],
['ed25519:' + masterPub]: masterPub, keys: {
}, ['ed25519:' + masterPub]: masterPub,
}; },
} else { };
[masterPub, masterSigning] = await getPrivateKey(this, "master", (pubkey) => { } else {
// make sure it agrees with the pubkey that we have [masterPub, masterSigning] = await getPrivateKey(
if (pubkey !== getPublicKey(this.keys.master)[1]) { this, "master", (pubkey) => {
return "Key does not match"; // make sure it agrees with the pubkey that we have
if (pubkey !== getPublicKey(this.keys.master)[1]) {
return "Key does not match";
}
return;
});
}
if (level & CrossSigningLevel.SELF_SIGNING) {
const sskSigning = new global.Olm.PkSigning();
try {
privateKeys.self_signing = sskSigning.generate_seed();
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
keys.self_signing = {
user_id: this.userId,
usage: ['self_signing'],
keys: {
['ed25519:' + sskPub]: sskPub,
},
};
pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
} finally {
sskSigning.free();
} }
return; }
});
}
if (level & CrossSigningLevel.SELF_SIGNING) { if (level & CrossSigningLevel.USER_SIGNING) {
const sskSigning = new global.Olm.PkSigning(); const uskSigning = new global.Olm.PkSigning();
privateKeys.self_signing = sskSigning.generate_seed(); try {
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); privateKeys.user_signing = uskSigning.generate_seed();
keys.self_signing = { const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
user_id: this.userId, keys.user_signing = {
usage: ['self_signing'], user_id: this.userId,
keys: { usage: ['user_signing'],
['ed25519:' + sskPub]: sskPub, keys: {
}, ['ed25519:' + uskPub]: uskPub,
}; },
pkSign(keys.self_signing, masterSigning, this.userId, masterPub); };
} pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
} finally {
uskSigning.free();
}
}
if (level & CrossSigningLevel.USER_SIGNING) { Object.assign(this.keys, keys);
const uskSigning = new global.Olm.PkSigning(); this.emit("cross-signing:savePrivateKeys", privateKeys);
privateKeys.user_signing = uskSigning.generate_seed(); } finally {
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); if (masterSigning) {
keys.user_signing = { masterSigning.free();
user_id: this.userId, }
usage: ['user_signing'],
keys: {
['ed25519:' + uskPub]: uskPub,
},
};
pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
} }
Object.assign(this.keys, keys);
this.emit("cross-signing:savePrivateKeys", privateKeys);
} }
setKeys(keys) { setKeys(keys) {
const signingKeys = {}; const signingKeys = {};
if (keys.master) { if (keys.master) {
if (keys.master.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in master key from " + this.userId;
logger.error(error);
throw new Error(error);
}
// First-Use is true if and only if we had no previous key for the user // First-Use is true if and only if we had no previous key for the user
this.fu = !(this.keys.self_signing); this.fu = !(this.keys.self_signing);
signingKeys.master = keys.master; signingKeys.master = keys.master;
if (!keys.user_signing || !keys.self_signing) { } else if (this.keys.master) {
throw new Error("Must have new self-signing and user-signing"
+ "keys when new master key is set");
}
} else {
signingKeys.master = this.keys.master; signingKeys.master = this.keys.master;
} else {
throw new Error("Tried to set cross-signing keys without a master key");
} }
const masterKey = getPublicKey(signingKeys.master)[1]; const masterKey = getPublicKey(signingKeys.master)[1];
// verify signatures // verify signatures
if (keys.user_signing) { if (keys.user_signing) {
if (keys.user_signing.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in user_signing key from " + this.userId;
logger.error(error);
throw new Error(error);
}
try { try {
pkVerify(keys.user_signing, masterKey, this.userId); pkVerify(keys.user_signing, masterKey, this.userId);
} catch (e) { } catch (e) {
logger.error("invalid signature on user-signing key");
// FIXME: what do we want to do here? // FIXME: what do we want to do here?
throw e; throw e;
} }
} }
if (keys.self_signing) { if (keys.self_signing) {
if (keys.self_signing.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in self_signing key from " + this.userId;
logger.error(error);
throw new Error(error);
}
try { try {
pkVerify(keys.self_signing, masterKey, this.userId); pkVerify(keys.self_signing, masterKey, this.userId);
} catch (e) { } catch (e) {
logger.error("invalid signature on self-signing key");
// FIXME: what do we want to do here? // FIXME: what do we want to do here?
throw e; throw e;
} }
@@ -198,6 +240,10 @@ export class CrossSigningInfo extends EventEmitter {
// if everything checks out, then save the keys // if everything checks out, then save the keys
if (keys.master) { if (keys.master) {
this.keys.master = keys.master; this.keys.master = keys.master;
// if the master key is set, then the old self-signing and
// user-signing keys are obsolete
delete this.keys.self_signing;
delete this.keys.user_signing;
} }
if (keys.self_signing) { if (keys.self_signing) {
this.keys.self_signing = keys.self_signing; this.keys.self_signing = keys.self_signing;
@@ -211,9 +257,13 @@ export class CrossSigningInfo extends EventEmitter {
const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => { const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => {
return; return;
}); });
const otherMaster = key.keys.master; try {
pkSign(otherMaster, usk, this.userId, pubkey); const otherMaster = key.keys.master;
return otherMaster; pkSign(otherMaster, usk, this.userId, pubkey);
return otherMaster;
} finally {
usk.free();
}
} }
async signDevice(userId, device) { async signDevice(userId, device) {
@@ -223,17 +273,34 @@ export class CrossSigningInfo extends EventEmitter {
const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => {
return; return;
}); });
const keyObj = { try {
algorithms: device.algorithms, const keyObj = {
keys: device.keys, algorithms: device.algorithms,
device_id: device.deviceId, keys: device.keys,
user_id: userId, device_id: device.deviceId,
}; user_id: userId,
pkSign(keyObj, ssk, this.userId, pubkey); };
return keyObj; pkSign(keyObj, ssk, this.userId, pubkey);
return keyObj;
} finally {
ssk.free();
}
} }
checkUserTrust(userCrossSigning) { checkUserTrust(userCrossSigning) {
if (this.userId === userCrossSigning.userId
&& this.getId() && this.getId() === userCrossSigning.getId()
&& this.getId("self_signing")
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")) {
return CrossSigningVerification.VERIFIED
| (this.fu ? CrossSigningVerification.TOFU
: CrossSigningVerification.UNVERIFIED);
}
if (!this.keys.user_signing) {
return 0;
}
let userTrusted; let userTrusted;
const userMaster = userCrossSigning.keys.master; const userMaster = userCrossSigning.keys.master;
const uskId = getPublicKey(this.keys.user_signing)[1]; const uskId = getPublicKey(this.keys.user_signing)[1];
@@ -253,6 +320,9 @@ export class CrossSigningInfo extends EventEmitter {
const userTrust = this.checkUserTrust(userCrossSigning); const userTrust = this.checkUserTrust(userCrossSigning);
const userSSK = userCrossSigning.keys.self_signing; const userSSK = userCrossSigning.keys.self_signing;
if (!userSSK) {
return 0;
}
const deviceObj = deviceToObject(device, userCrossSigning.userId); const deviceObj = deviceToObject(device, userCrossSigning.userId);
try { try {
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);

View File

@@ -1,6 +1,7 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -27,7 +28,7 @@ import {EventEmitter} from 'events';
import logger from '../logger'; import logger from '../logger';
import DeviceInfo from './deviceinfo'; import DeviceInfo from './deviceinfo';
import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning'; import {CrossSigningInfo} from './CrossSigning';
import olmlib from './olmlib'; import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -754,7 +755,9 @@ class DeviceListUpdateSerialiser {
downloadUsers, opts, downloadUsers, opts,
).then((res) => { ).then((res) => {
const dk = res.device_keys || {}; const dk = res.device_keys || {};
const master_keys = res.master_keys || {};
const ssks = res.self_signing_keys || {}; const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
// do each user in a separate promise, to avoid wedging the CPU // do each user in a separate promise, to avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158) // (https://github.com/vector-im/riot-web/issues/3158)
@@ -765,7 +768,11 @@ class DeviceListUpdateSerialiser {
for (const userId of downloadUsers) { for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => { prom = prom.delay(5).then(() => {
return this._processQueryResponseForUser( return this._processQueryResponseForUser(
userId, dk[userId], ssks[userId], userId, dk[userId], {
master: master_keys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
},
); );
}); });
} }
@@ -790,9 +797,11 @@ class DeviceListUpdateSerialiser {
return deferred.promise; return deferred.promise;
} }
async _processQueryResponseForUser(userId, dkResponse, sskResponse) { async _processQueryResponseForUser(
userId, dkResponse, crossSigningResponse, sskResponse,
) {
logger.log('got device keys for ' + userId + ':', dkResponse); logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got self-signing keys for ' + userId + ':', sskResponse); logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
{ {
// map from deviceid -> deviceinfo for this user // map from deviceid -> deviceinfo for this user
@@ -818,19 +827,23 @@ class DeviceListUpdateSerialiser {
this._deviceList._setRawStoredDevicesForUser(userId, storage); this._deviceList._setRawStoredDevicesForUser(userId, storage);
} }
// now do the same for the self-signing key // now do the same for the cross-signing keys
{ {
const ssk = this._deviceList.getRawStoredSskForUser(userId) || {}; if (crossSigningResponse && Object.keys(crossSigningResponse).length) {
const crossSigning
= this._deviceList.getStoredCrossSigningForUser(userId)
|| new CrossSigningInfo(userId);
const updated = await _updateStoredSelfSigningKeyForUser( crossSigning.setKeys(crossSigningResponse);
this._olmDevice, userId, ssk, sskResponse || {},
);
this._deviceList.setRawStoredSskForUser(userId, ssk); this._deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(),
);
// NB. Unlike most events in the js-sdk, this one is internal to the // NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted // js-sdk and is not re-emitted
if (updated) this._deviceList.emit('userSskUpdated', userId); this._deviceList.emit('userCrossSigningUpdated', userId);
}
} }
} }
} }
@@ -883,55 +896,6 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
return updated; return updated;
} }
async function _updateStoredSelfSigningKeyForUser(
_olmDevice, userId, userStore, userResult,
) {
// FIXME: this function may need modifying
let updated = false;
if (userResult.user_id !== userId) {
logger.warn("Mismatched user_id " + userResult.user_id +
" in self-signing key from " + userId);
return;
}
if (!userResult || !userResult.usage.includes('self_signing')) {
logger.warn(
"Self-signing key for " + userId +
" does not include 'self_signing' usage: ignoring",
);
return;
}
const keyCount = Object.keys(userResult.keys).length;
if (keyCount !== 1) {
logger.warn(
"Self-signing key block for " + userId + " has " +
keyCount + " keys: expected exactly 1. Ignoring.",
);
return;
}
let oldKeyId = null;
let oldKey = null;
if (userStore.keys && Object.keys(userStore.keys).length > 0) {
oldKeyId = Object.keys(userStore.keys)[0];
oldKey = userStore.keys[oldKeyId];
}
const newKeyId = Object.keys(userResult.keys)[0];
const newKey = userResult.keys[newKeyId];
if (oldKeyId !== newKeyId || oldKey !== newKey) {
updated = true;
logger.info(
"New self-signing key detected for " + userId +
": " + newKeyId + ", was previously " + oldKeyId,
);
userStore.user_id = userResult.user_id;
userStore.usage = userResult.usage;
userStore.keys = userResult.keys;
}
return updated;
}
/* /*
* Process a device in a /query response, and add it to the userStore * Process a device in a /query response, and add it to the userStore
* *
@@ -955,6 +919,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
} }
const unsigned = deviceResult.unsigned || {}; const unsigned = deviceResult.unsigned || {};
const signatures = deviceResult.signatures || {};
try { try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
@@ -987,5 +952,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
deviceStore.keys = deviceResult.keys || {}; deviceStore.keys = deviceResult.keys || {};
deviceStore.algorithms = deviceResult.algorithms || []; deviceStore.algorithms = deviceResult.algorithms || [];
deviceStore.unsigned = unsigned; deviceStore.unsigned = unsigned;
deviceStore.signatures = signatures;
return true; return true;
} }

View File

@@ -56,6 +56,7 @@ function DeviceInfo(deviceId) {
this.verified = DeviceVerification.UNVERIFIED; this.verified = DeviceVerification.UNVERIFIED;
this.known = false; this.known = false;
this.unsigned = {}; this.unsigned = {};
this.signatures = {}
} }
/** /**
@@ -88,6 +89,7 @@ DeviceInfo.prototype.toStorage = function() {
verified: this.verified, verified: this.verified,
known: this.known, known: this.known,
unsigned: this.unsigned, unsigned: this.unsigned,
signatures: this.signatures,
}; };
}; };

View File

@@ -2,6 +2,7 @@
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -31,11 +32,10 @@ const OlmDevice = require("./OlmDevice");
const olmlib = require("./olmlib"); const olmlib = require("./olmlib");
const algorithms = require("./algorithms"); const algorithms = require("./algorithms");
const DeviceInfo = require("./deviceinfo"); const DeviceInfo = require("./deviceinfo");
import SskInfo from './sskinfo';
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceList = require('./DeviceList').default; const DeviceList = require('./DeviceList').default;
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning'; import { CrossSigningInfo } from './CrossSigning';
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -104,7 +104,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
*/ */
export default function Crypto(baseApis, sessionStore, userId, deviceId, export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList, verificationMethods) { clientStore, cryptoStore, roomList, verificationMethods) {
this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this); this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this);
this._baseApis = baseApis; this._baseApis = baseApis;
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
@@ -146,7 +146,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
); );
// XXX: This isn't removed at any point, but then none of the event listeners // XXX: This isn't removed at any point, but then none of the event listeners
// this class sets seem to be removed at any point... :/ // this class sets seem to be removed at any point... :/
this._deviceList.on('userSskUpdated', this._onDeviceListUserSskUpdated); this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated);
// the last time we did a check for the number of one-time-keys on the // the last time we did a check for the number of one-time-keys on the
// server. // server.
@@ -269,6 +269,7 @@ Crypto.prototype.init = async function() {
*/ */
Crypto.prototype.resetCrossSigningKeys = async function(level) { Crypto.prototype.resetCrossSigningKeys = async function(level) {
await this._crossSigningInfo.resetKeys(level); await this._crossSigningInfo.resetKeys(level);
this._baseApis.emit("cross-signing:keysChanged", {});
}; };
/** /**
@@ -278,6 +279,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(level) {
*/ */
Crypto.prototype.setCrossSigningKeys = function(keys) { Crypto.prototype.setCrossSigningKeys = function(keys) {
this._crossSigningInfo.setKeys(keys); this._crossSigningInfo.setKeys(keys);
this._baseApis.emit("cross-signing:keysChanged", {});
}; };
/** /**
@@ -331,34 +333,93 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
/* /*
* Event handler for DeviceList's userNewDevices event * Event handler for DeviceList's userNewDevices event
*/ */
Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
if (userId === this._userId) { if (userId === this._userId) {
this.checkOwnSskTrust(); this.checkOwnCrossSigningTrust();
} }
}; };
/* /*
* Check the copy of our SSK that we have in the device list and see if it * Check the copy of our cross-signing key that we have in the device list and
* matches our private part. If it does, mark it as trusted. * see if we can get the private key. If so, mark it as trusted.
*/ */
Crypto.prototype.checkOwnSskTrust = async function() { Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const userId = this._userId; const userId = this._userId;
// If we see an update to our own SSK, check it against the SSK we have and, // If we see an update to our own master key, check it against the master
// if it matches, mark it as verified // key we have and, if it matches, mark it as verified
// First, get the pubkey of the one we can see // First, get the new cross-signing info
const seenSsk = this._deviceList.getStoredSskForUser(userId); const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (!seenSsk) { if (!newCrossSigning) {
logger.error( logger.error(
"Got SSK update event for user " + userId + "Got cross-signing update event for user " + userId +
" but no new SSK found!", " but no new cross-signing information found!",
); );
return; return;
} }
const seenPubkey = seenSsk.getFingerprint();
const seenPubkey = newCrossSigning.getId();
const changed = this._crossSigningInfo.getId() !== seenPubkey;
let privkey;
if (changed) {
// try to get the private key if the master key changed
logger.info("Got new master key", seenPubkey);
let error;
do {
privkey = await new Promise((resolve, reject) => {
this._baseApis.emit("cross-signing:newKey", {
publicKey: seenPubkey,
type: "master",
error,
done: (key) => {
// check key matches
const signing = new global.Olm.PkSigning();
try {
const pubkey = signing.init_with_seed(key);
if (pubkey !== seenPubkey) {
error = "Key does not match";
logger.info("Key does not match: got " + pubkey
+ " expected " + seenPubkey);
return;
}
} finally {
signing.free();
}
resolve(key);
},
cancel: (error) => {
reject(error || new Error("Cancelled by user"));
},
});
});
} while (!privkey);
this._baseApis.emit("cross-signing:savePrivateKeys", {master: privkey});
logger.info("Got private key");
}
// FIXME: fetch the private key?
if (this._crossSigningInfo.getId("self_signing")
!== newCrossSigning.getId("self_signing")) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
}
if (this._crossSigningInfo.getId("user_signing")
!== newCrossSigning.getId("user_signing")) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
}
this._crossSigningInfo.setKeys(newCrossSigning.keys);
// FIXME: save it ... somewhere?
if (changed) {
this._baseApis.emit("cross-signing:keysChanged", {});
}
// FIXME:
// Now dig out the account keys and get the pubkey of the one in there // Now dig out the account keys and get the pubkey of the one in there
/*
let accountKeys = null; let accountKeys = null;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
@@ -401,6 +462,7 @@ Crypto.prototype.checkOwnSskTrust = async function() {
localPubkey + ", published: " + seenPubkey, localPubkey + ", published: " + seenPubkey,
); );
} }
*/
}; };
/** /**
@@ -986,7 +1048,6 @@ Crypto.prototype.setDeviceVerification = async function(
const xsk = this._deviceList.getStoredCrossSigningForUser(userId); const xsk = this._deviceList.getStoredCrossSigningForUser(userId);
if (xsk.getId() === deviceId) { if (xsk.getId() === deviceId) {
if (verified) { if (verified) {
xsk.verified = CrossSigningVerification.VERIFIED;
const device = await this._crossSigningInfo.signUser(xsk); const device = await this._crossSigningInfo.signUser(xsk);
// FIXME: mark xsk as dirty in device list // FIXME: mark xsk as dirty in device list
this._baseApis.uploadKeySignatures({ this._baseApis.uploadKeySignatures({