From 53804cac5cff7f4d5848f46b3b71809284accd40 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 28 May 2019 22:28:54 -0400 Subject: [PATCH] save cross-signing keys from sync and verify new keys for user --- spec/test-utils.js | 131 +++++++++++++++ spec/unit/crypto/cross-signing.spec.js | 123 +++++++++++++- src/crypto/CrossSigning.js | 224 ++++++++++++++++--------- src/crypto/DeviceList.js | 90 ++++------ src/crypto/deviceinfo.js | 2 + src/crypto/index.js | 97 +++++++++-- 6 files changed, 507 insertions(+), 160 deletions(-) diff --git a/spec/test-utils.js b/spec/test-utils.js index d0b673568..780de5ac8 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -1,6 +1,7 @@ "use strict"; import expect from 'expect'; import Promise from 'bluebird'; +const logger = require("../logger"); // load olm before the sdk if possible 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); +}; diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js index e6788e42d..592920713 100644 --- a/spec/unit/crypto/cross-signing.spec.js +++ b/spec/unit/crypto/cross-signing.spec.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (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 {HttpResponse, setHttpResponses} from '../../test-utils'; + async function makeTestClient(userInfo, options) { const client = (new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, @@ -86,12 +89,126 @@ describe("Cross Signing", function() { const alice = await makeTestClient( {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 - // 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 + 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() { diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js index 7e7d1312e..197d371fd 100644 --- a/src/crypto/CrossSigning.js +++ b/src/crypto/CrossSigning.js @@ -21,14 +21,18 @@ limitations under the License. import {pkSign, pkVerify} from './olmlib'; import {EventEmitter} from 'events'; +import logger from '../logger'; function getPublicKey(keyInfo) { return Object.entries(keyInfo.keys)[0]; } -function getPrivateKey(self, type, check) { - return new Promise((resolve, reject) => { - const askForKey = (error) => { +async function getPrivateKey(self, type, check) { + let error; + let pubkey; + let signing; + do { + [pubkey, signing] = await new Promise((resolve, reject) => { self.emit("cross-signing:getKey", { type: type, error, @@ -36,9 +40,11 @@ function getPrivateKey(self, type, check) { // FIXME: the key needs to be interpreted? const signing = new global.Olm.PkSigning(); const pubkey = signing.init_with_seed(key); - const error = check(pubkey, signing); + error = check(pubkey, signing); if (error) { - return askForKey(error); + logger.error(error); + signing.free(); + resolve([null, null]); } resolve([pubkey, signing]); }, @@ -46,9 +52,9 @@ function getPrivateKey(self, type, check) { reject(error || new Error("Cancelled")); }, }); - }; - askForKey(); - }); + }); + } while (!pubkey); + return [pubkey, signing]; } export class CrossSigningInfo extends EventEmitter { @@ -64,12 +70,11 @@ export class CrossSigningInfo extends EventEmitter { // you can't change the userId Object.defineProperty(this, 'userId', { - enumerabel: true, + enumerable: true, value: userId, }); this.keys = {}; this.fu = true; - // FIXME: add chain of ssks? } static fromStorage(obj, userId) { @@ -85,20 +90,24 @@ export class CrossSigningInfo extends EventEmitter { toStorage() { return { keys: this.keys, - verified: this.verified, + fu: this.fu, }; } /** 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 */ - getId() { - return getPublicKey(this.keys.master)[1]; + getId(type) { + type = type || "master"; + return this.keys[type] && getPublicKey(this.keys[type])[1]; } async resetKeys(level) { - if (level === undefined || level & 4) { + if (level === undefined || level & 4 || !this.keys.master) { level = CrossSigningLevel.MASTER; } else if (level === 0) { return; @@ -109,87 +118,120 @@ export class CrossSigningInfo extends EventEmitter { let masterSigning; let masterPub; - if (level & 4) { - masterSigning = new global.Olm.PkSigning(); - privateKeys.master = masterSigning.generate_seed(); - masterPub = masterSigning.init_with_seed(privateKeys.master); - keys.master = { - user_id: this.userId, - usage: ['master'], - keys: { - ['ed25519:' + masterPub]: masterPub, - }, - }; - } else { - [masterPub, masterSigning] = await getPrivateKey(this, "master", (pubkey) => { - // make sure it agrees with the pubkey that we have - if (pubkey !== getPublicKey(this.keys.master)[1]) { - return "Key does not match"; + try { + if (level & 4) { + masterSigning = new global.Olm.PkSigning(); + privateKeys.master = masterSigning.generate_seed(); + masterPub = masterSigning.init_with_seed(privateKeys.master); + keys.master = { + user_id: this.userId, + usage: ['master'], + keys: { + ['ed25519:' + masterPub]: masterPub, + }, + }; + } else { + [masterPub, masterSigning] = await getPrivateKey( + this, "master", (pubkey) => { + // 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) { - const sskSigning = new global.Olm.PkSigning(); - 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); - } + if (level & CrossSigningLevel.USER_SIGNING) { + const uskSigning = new global.Olm.PkSigning(); + try { + privateKeys.user_signing = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); + keys.user_signing = { + user_id: this.userId, + usage: ['user_signing'], + keys: { + ['ed25519:' + uskPub]: uskPub, + }, + }; + pkSign(keys.user_signing, masterSigning, this.userId, masterPub); + } finally { + uskSigning.free(); + } + } - if (level & CrossSigningLevel.USER_SIGNING) { - const uskSigning = new global.Olm.PkSigning(); - privateKeys.user_signing = uskSigning.generate_seed(); - const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); - keys.user_signing = { - 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); + } finally { + if (masterSigning) { + masterSigning.free(); + } } - - Object.assign(this.keys, keys); - this.emit("cross-signing:savePrivateKeys", privateKeys); } setKeys(keys) { const signingKeys = {}; 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 this.fu = !(this.keys.self_signing); signingKeys.master = keys.master; - if (!keys.user_signing || !keys.self_signing) { - throw new Error("Must have new self-signing and user-signing" - + "keys when new master key is set"); - } - } else { + } else if (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]; // verify signatures 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 { pkVerify(keys.user_signing, masterKey, this.userId); } catch (e) { + logger.error("invalid signature on user-signing key"); // FIXME: what do we want to do here? throw e; } } 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 { pkVerify(keys.self_signing, masterKey, this.userId); } catch (e) { + logger.error("invalid signature on self-signing key"); // FIXME: what do we want to do here? throw e; } @@ -198,6 +240,10 @@ export class CrossSigningInfo extends EventEmitter { // if everything checks out, then save the keys if (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) { 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) => { return; }); - const otherMaster = key.keys.master; - pkSign(otherMaster, usk, this.userId, pubkey); - return otherMaster; + try { + const otherMaster = key.keys.master; + pkSign(otherMaster, usk, this.userId, pubkey); + return otherMaster; + } finally { + usk.free(); + } } async signDevice(userId, device) { @@ -223,17 +273,34 @@ export class CrossSigningInfo extends EventEmitter { const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => { return; }); - const keyObj = { - algorithms: device.algorithms, - keys: device.keys, - device_id: device.deviceId, - user_id: userId, - }; - pkSign(keyObj, ssk, this.userId, pubkey); - return keyObj; + try { + const keyObj = { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + }; + pkSign(keyObj, ssk, this.userId, pubkey); + return keyObj; + } finally { + ssk.free(); + } } 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; const userMaster = userCrossSigning.keys.master; const uskId = getPublicKey(this.keys.user_signing)[1]; @@ -253,6 +320,9 @@ export class CrossSigningInfo extends EventEmitter { const userTrust = this.checkUserTrust(userCrossSigning); const userSSK = userCrossSigning.keys.self_signing; + if (!userSSK) { + return 0; + } const deviceObj = deviceToObject(device, userCrossSigning.userId); try { pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 0c715cf07..ec9cb9bcd 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations 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"); 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 DeviceInfo from './deviceinfo'; -import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning'; +import {CrossSigningInfo} from './CrossSigning'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -754,7 +755,9 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; + const master_keys = res.master_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 // (https://github.com/vector-im/riot-web/issues/3158) @@ -765,7 +768,11 @@ class DeviceListUpdateSerialiser { for (const userId of downloadUsers) { prom = prom.delay(5).then(() => { 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; } - async _processQueryResponseForUser(userId, dkResponse, sskResponse) { + async _processQueryResponseForUser( + userId, dkResponse, crossSigningResponse, sskResponse, + ) { 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 @@ -818,19 +827,23 @@ class DeviceListUpdateSerialiser { 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( - this._olmDevice, userId, ssk, sskResponse || {}, - ); + crossSigning.setKeys(crossSigningResponse); - 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 - // js-sdk and is not re-emitted - if (updated) this._deviceList.emit('userSskUpdated', userId); + // NB. Unlike most events in the js-sdk, this one is internal to the + // js-sdk and is not re-emitted + this._deviceList.emit('userCrossSigningUpdated', userId); + } } } } @@ -883,55 +896,6 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, 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 * @@ -955,6 +919,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { } const unsigned = deviceResult.unsigned || {}; + const signatures = deviceResult.signatures || {}; try { await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); @@ -987,5 +952,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { deviceStore.keys = deviceResult.keys || {}; deviceStore.algorithms = deviceResult.algorithms || []; deviceStore.unsigned = unsigned; + deviceStore.signatures = signatures; return true; } diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js index aa5c4afac..c996e4635 100644 --- a/src/crypto/deviceinfo.js +++ b/src/crypto/deviceinfo.js @@ -56,6 +56,7 @@ function DeviceInfo(deviceId) { this.verified = DeviceVerification.UNVERIFIED; this.known = false; this.unsigned = {}; + this.signatures = {} } /** @@ -88,6 +89,7 @@ DeviceInfo.prototype.toStorage = function() { verified: this.verified, known: this.known, unsigned: this.unsigned, + signatures: this.signatures, }; }; diff --git a/src/crypto/index.js b/src/crypto/index.js index cbcfab78b..657048198 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -2,6 +2,7 @@ Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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"); 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 algorithms = require("./algorithms"); const DeviceInfo = require("./deviceinfo"); -import SskInfo from './sskinfo'; const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; -import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning'; +import { CrossSigningInfo } from './CrossSigning'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; 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, clientStore, cryptoStore, roomList, verificationMethods) { - this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this); + this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this); this._baseApis = baseApis; 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 // 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 // server. @@ -269,6 +269,7 @@ Crypto.prototype.init = async function() { */ Crypto.prototype.resetCrossSigningKeys = async function(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) { 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 */ -Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) { +Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(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 - * matches our private part. If it does, mark it as trusted. + * Check the copy of our cross-signing key that we have in the device list and + * 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; - // If we see an update to our own SSK, check it against the SSK we have and, - // if it matches, mark it as verified + // If we see an update to our own master key, check it against the master + // key we have and, if it matches, mark it as verified - // First, get the pubkey of the one we can see - const seenSsk = this._deviceList.getStoredSskForUser(userId); - if (!seenSsk) { + // First, get the new cross-signing info + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (!newCrossSigning) { logger.error( - "Got SSK update event for user " + userId + - " but no new SSK found!", + "Got cross-signing update event for user " + userId + + " but no new cross-signing information found!", ); 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 + /* let accountKeys = null; await this._cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -401,6 +462,7 @@ Crypto.prototype.checkOwnSskTrust = async function() { localPubkey + ", published: " + seenPubkey, ); } + */ }; /** @@ -986,7 +1048,6 @@ Crypto.prototype.setDeviceVerification = async function( const xsk = this._deviceList.getStoredCrossSigningForUser(userId); if (xsk.getId() === deviceId) { if (verified) { - xsk.verified = CrossSigningVerification.VERIFIED; const device = await this._crossSigningInfo.signUser(xsk); // FIXME: mark xsk as dirty in device list this._baseApis.uploadKeySignatures({