diff --git a/.babelrc b/.babelrc index 572b4baff1c..67eaa671877 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["es2015"], + "presets": ["es2015", "es2016"], "plugins": [ "transform-class-properties", diff --git a/package.json b/package.json index f736c0f608c..c0539baf180 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", + "babel-preset-es2016": "^6.24.1", "browserify": "^16.2.3", "browserify-shim": "^3.8.13", "eslint": "^5.12.0", diff --git a/spec/test-utils.js b/spec/test-utils.js index d22ebc5dff4..9a81906c343 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -242,3 +242,144 @@ module.exports.awaitDecryption = function(event) { }); }); }; + + +const HttpResponse = module.exports.HttpResponse = function( + httpLookups, acceptKeepalives, ignoreUnhandledSync, +) { + this.httpLookups = httpLookups; + this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives; + this.ignoreUnhandledSync = ignoreUnhandledSync; + 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 (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) { + logger.log("MatrixClient[UT] Ignoring."); + return Promise.defer().promise; + } + 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); + } else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) { + logger.log("MatrixClient[UT] Ignoring."); + this.httpLookups.unshift(next); + return Promise.defer().promise; + } + 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, ignoreUnhandledSyncs, +) { + const httpResponseObj = new HttpResponse( + responses, acceptKeepalives, ignoreUnhandledSyncs, + ); + + 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/backup.spec.js b/spec/unit/crypto/backup.spec.js index 8a6fa4b8dd0..236a7ed28a2 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -29,6 +29,7 @@ import testUtils from '../../test-utils'; import OlmDevice from '../../../lib/crypto/OlmDevice'; import Crypto from '../../../lib/crypto'; import logger from '../../../src/logger'; +import olmlib from '../../../lib/crypto/olmlib'; const Olm = global.Olm; @@ -83,6 +84,16 @@ const BACKUP_INFO = { }, }; +const keys = {}; + +function getCrossSigningKey(type) { + return keys[type]; +} + +function saveCrossSigningKeys(k) { + Object.assign(keys, k); +} + function makeTestClient(sessionStore, cryptoStore) { const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", @@ -108,6 +119,7 @@ function makeTestClient(sessionStore, cryptoStore) { deviceId: "device", sessionStore: sessionStore, cryptoStore: cryptoStore, + cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, }); } @@ -296,6 +308,71 @@ describe("MegolmBackup", function() { }); }); + it('signs backups with the cross-signing master key', async function() { + const groupSession = new Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new Olm.InboundGroupSession(); + ibGroupSession.create(groupSession.session_key()); + + const client = makeTestClient(sessionStore, cryptoStore); + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: client, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + + await client.initCrypto(); + let privateKeys; + client.uploadDeviceSigningKeys = async function(e) {return;}; + client.uploadKeySignatures = async function(e) {return;}; + client.on("crossSigning.saveCrossSigningKeys", function(e) { + privateKeys = e; + }); + client.on("crossSigning.getKey", function(e) { + e.done(privateKeys[e.type]); + }); + await client.resetCrossSigningKeys(); + let numCalls = 0; + await new Promise(async (resolve, reject) => { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { + ++numCalls; + expect(numCalls).toBeLessThanOrEqualTo(1); + if (numCalls >= 2) { + // exit out of retry loop if there's something wrong + reject(new Error("authedRequest called too many timmes")); + return Promise.resolve({}); + } + expect(method).toBe("POST"); + expect(path).toBe("/room_keys/version"); + try { + // make sure auth_data is signed by the master key + olmlib.pkVerify( + data.auth_data, client.getCrossSigningId(), "@alice:bar", + ); + } catch (e) { + reject(e); + return Promise.resolve({}); + } + resolve(); + return Promise.resolve({}); + }; + await client.createKeyBackupVersion({ + algorithm: "m.megolm_backup.v1", + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, + }); + }); + expect(numCalls).toBe(1); + }); + it('retries when a backup fails', function() { const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); diff --git a/spec/unit/crypto/cross-signing.spec.js b/spec/unit/crypto/cross-signing.spec.js new file mode 100644 index 00000000000..c26356d98e1 --- /dev/null +++ b/spec/unit/crypto/cross-signing.spec.js @@ -0,0 +1,800 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import '../../olm-loader'; + +import expect from 'expect'; +import anotherjson from 'another-json'; + +import olmlib from '../../../lib/crypto/olmlib'; + +import TestClient from '../../TestClient'; + +import {HttpResponse, setHttpResponses} from '../../test-utils'; + +async function makeTestClient(userInfo, options, keys) { + if (!keys) keys = {}; + + function getCrossSigningKey(type) { + return keys[type]; + } + + function saveCrossSigningKeys(k) { + Object.assign(keys, k); + } + + if (!options) options = {}; + options.cryptoCallbacks = Object.assign( + {}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {}, + ); + const client = (new TestClient( + userInfo.userId, userInfo.deviceId, undefined, undefined, options, + )).client; + + await client.initCrypto(); + + return client; +} + +describe("Cross Signing", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + beforeEach(async function() { + await global.Olm.init(); + }); + + it("should sign the master key with the device key", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = expect.createSpy() + .andCall(async (auth, keys) => { + await olmlib.verifySignature( + alice._crypto._olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + ); + }); + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + await alice.resetCrossSigningKeys(); + expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled(); + }); + + it("should upload a signature when a user is verified", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + await alice.resetCrossSigningKeys(); + // Alice downloads Bob's device key + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + master: { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + "ed25519:bobs+master+pubkey": "bobs+master+pubkey", + }, + }, + }, + }); + // Alice verifies Bob's key + const promise = new Promise((resolve, reject) => { + alice.uploadKeySignatures = (...args) => { + resolve(...args); + }; + }); + await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true); + // Alice should send a signature of Bob's key to the server + await promise; + }); + + it("should get cross-signing keys from sync", async function() { + 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 alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + // will be called to sign our own device + getCrossSigningKey: type => { + if (type === 'master') { + return masterKey; + } else { + return selfSigningKey; + } + }, + }, + }, + ); + + const keyChangePromise = new Promise((resolve, reject) => { + alice.once("crossSigning.keysChanged", async (e) => { + resolve(e); + await alice.checkOwnCrossSigningTrust(); + }); + }); + + const uploadSigsPromise = new Promise((resolve, reject) => { + alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => { + await olmlib.verifySignature( + alice._crypto._olmDevice, + content["@alice:example.com"][ + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" + ], + "@alice:example.com", + "Osborne2", alice._crypto._olmDevice.deviceEd25519Key, + ); + olmlib.pkVerify( + content["@alice:example.com"]["Osborne2"], + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + "@alice:example.com", + ); + resolve(); + }); + }); + + 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 master key, ssk, device key + 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, true, true); + + await alice.startClient(); + + // once ssk is confirmed, device key should be trusted + await keyChangePromise; + await uploadSigsPromise; + + const aliceTrust = alice.checkUserTrust("@alice:example.com"); + expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceTrust.isTofu()).toBeTruthy(); + expect(aliceTrust.isVerified()).toBeTruthy(); + + const aliceDeviceTrust = alice.checkDeviceTrust("@alice:example.com", "Osborne2"); + expect(aliceDeviceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isTofu()).toBeTruthy(); + expect(aliceDeviceTrust.isVerified()).toBeTruthy(); + }); + + it("should use trust chain to determine device verification", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + await alice.resetCrossSigningKeys(); + // Alice downloads Bob's ssk and device key + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + master: { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, + }, + }, + self_signing: bobSSK, + }, + firstUse: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + const sig = bobSigning.sign(anotherjson.stringify(bobDevice)); + bobDevice.signatures = { + "@bob:example.com": { + ["ed25519:" + bobPubkey]: sig, + }, + }; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + // Bob's device key should be TOFU + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isVerified()).toBeFalsy(); + expect(bobTrust.isTofu()).toBeTruthy(); + + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); + + // Alice verifies Bob's SSK + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + + // Bob's device key should be trusted + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust2.isTofu()).toBeTruthy(); + + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy(); + expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeTruthy(); + }); + + it("should trust signatures received from other devices", async function() { + const aliceKeys = {}; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + null, + aliceKeys, + ); + alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com"); + alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {}; + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + + // set Alice's cross-signing key + await alice.resetCrossSigningKeys(); + + 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._crypto._deviceList.once("userCrossSigningUpdated", (userId) => { + if (userId === "@bob:example.com") { + resolve(); + } + }); + }); + + 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); + + const bobOlmAccount = new global.Olm.Account(); + bobOlmAccount.create(); + const bobKeys = JSON.parse(bobOlmAccount.identity_keys()); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobKeys.ed25519, + "curve25519:Dynabook": bobKeys.curve25519, + }, + }; + const deviceStr = anotherjson.stringify(bobDevice); + bobDevice.signatures = { + "@bob:example.com": { + "ed25519:Dynabook": bobOlmAccount.sign(deviceStr), + }, + }; + olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com"); + + const bobMaster = { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + }, + }; + olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com"); + + // Alice downloads Bob's keys + // - device key + // - ssk + // - master key signed by her usk (pretend that it was signed by another + // of Alice's devices) + 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: [ + "@bob:example.com", + ], + }, + }, + }, + { + method: "POST", + path: "/keys/query", + data: { + "failures": {}, + "device_keys": { + "@alice:example.com": { + "Osborne2": aliceDevice, + }, + "@bob:example.com": { + "Dynabook": bobDevice, + }, + }, + "master_keys": { + "@bob:example.com": bobMaster, + }, + "self_signing_keys": { + "@bob:example.com": { + user_id: "@bob:example.com", + usage: ["self-signing"], + keys: { + "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": + "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ", + }, + signatures: { + "@bob:example.com": { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB" + + "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ", + }, + }, + }, + }, + }, + }, + { + method: "POST", + path: "/keys/upload/Osborne2", + data: { + one_time_key_counts: { + curve25519: 100, + signed_curve25519: 100, + }, + }, + }, + ]; + setHttpResponses(alice, responses); + + await alice.startClient(); + + await keyChangePromise; + + // Bob's device key should be trusted + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust.isTofu()).toBeTruthy(); + + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); + }); + + it("should dis-trust an unsigned device", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + // set Alice's cross-signing key + await alice.resetCrossSigningKeys(); + // Alice downloads Bob's ssk and device key + // (NOTE: device key is not signed by ssk) + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + master: { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, + }, + }, + self_signing: bobSSK, + }, + firstUse: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + // Bob's device key should be untrusted + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeFalsy(); + expect(bobDeviceTrust.isTofu()).toBeFalsy(); + + // Alice verifies Bob's SSK + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + + // Bob's device key should be untrusted + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeFalsy(); + }); + + it("should dis-trust a user when their ssk changes", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + await alice.resetCrossSigningKeys(); + // Alice downloads Bob's keys + const bobMasterSigning = new global.Olm.PkSigning(); + const bobMasterPrivkey = bobMasterSigning.generate_seed(); + const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey); + const bobSigning = new global.Olm.PkSigning(); + const bobPrivkey = bobSigning.generate_seed(); + const bobPubkey = bobSigning.init_with_seed(bobPrivkey); + const bobSSK = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey]: bobPubkey, + }, + }; + const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK)); + bobSSK.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey]: sskSig, + }, + }; + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + master: { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + ["ed25519:" + bobMasterPubkey]: bobMasterPubkey, + }, + }, + self_signing: bobSSK, + }, + firstUse: 1, + unsigned: {}, + }); + const bobDevice = { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": "somePubkey", + "ed25519:Dynabook": "someOtherPubkey", + }, + }; + const bobDeviceString = anotherjson.stringify(bobDevice); + const sig = bobSigning.sign(bobDeviceString); + bobDevice.signatures = {}; + bobDevice.signatures["@bob:example.com"] = {}; + bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + // Alice verifies Bob's SSK + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true); + + // Bob's device key should be trusted + const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust.isVerified()).toBeTruthy(); + expect(bobDeviceTrust.isTofu()).toBeTruthy(); + + // Alice downloads new SSK for Bob + const bobMasterSigning2 = new global.Olm.PkSigning(); + const bobMasterPrivkey2 = bobMasterSigning2.generate_seed(); + const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2); + const bobSigning2 = new global.Olm.PkSigning(); + const bobPrivkey2 = bobSigning2.generate_seed(); + const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2); + const bobSSK2 = { + user_id: "@bob:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + bobPubkey2]: bobPubkey2, + }, + }; + const sskSig2 = bobMasterSigning2.sign(anotherjson.stringify(bobSSK2)); + bobSSK2.signatures = { + "@bob:example.com": { + ["ed25519:" + bobMasterPubkey2]: sskSig2, + }, + }; + alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", { + keys: { + master: { + user_id: "@bob:example.com", + usage: ["master"], + keys: { + ["ed25519:" + bobMasterPubkey2]: bobMasterPubkey2, + }, + }, + self_signing: bobSSK2, + }, + firstUse: 0, + unsigned: {}, + }); + // Bob's and his device should be untrusted + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isVerified()).toBeFalsy(); + expect(bobTrust.isTofu()).toBeFalsy(); + + const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust2.isVerified()).toBeFalsy(); + expect(bobDeviceTrust2.isTofu()).toBeFalsy(); + + // Alice verifies Bob's SSK + alice.uploadKeySignatures = () => {}; + await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true); + + // Bob should be trusted but not his device + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isVerified()).toBeTruthy(); + + const bobDeviceTrust3 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust3.isVerified()).toBeFalsy(); + + // Alice gets new signature for device + const sig2 = bobSigning2.sign(bobDeviceString); + bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: bobDevice, + }); + + // Bob's device should be trusted again (but not TOFU) + const bobTrust3 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust3.isVerified()).toBeTruthy(); + + const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook"); + expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy(); + }); + + it("should offer to upgrade device verifications to cross-signing", async function() { + let upgradeResolveFunc; + + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + shouldUpgradeDeviceVerifications: (verifs) => { + expect(verifs.users["@bob:example.com"]).toExist(); + upgradeResolveFunc(); + return ["@bob:example.com"]; + }, + }, + }, + ); + const bob = await makeTestClient( + {userId: "@bob:example.com", deviceId: "Dynabook"}, + ); + + bob.uploadDeviceSigningKeys = async () => {}; + bob.uploadKeySignatures = async () => {}; + // set Bob's cross-signing key + await bob.resetCrossSigningKeys(); + alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", { + Dynabook: { + algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], + keys: { + "curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key, + "ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key, + }, + verified: 1, + known: true, + }, + }); + alice._crypto._deviceList.storeCrossSigningForUser( + "@bob:example.com", + bob._crypto._crossSigningInfo.toStorage(), + ); + + alice.uploadDeviceSigningKeys = async () => {}; + alice.uploadKeySignatures = async () => {}; + // when alice sets up cross-signing, she should notice that bob's + // cross-signing key is signed by his Dynabook, which alice has + // verified, and ask if the device verification should be upgraded to a + // cross-signing verification + let upgradePromise = new Promise((resolve) => { + upgradeResolveFunc = resolve; + }); + await alice.resetCrossSigningKeys(); + await upgradePromise; + + const bobTrust = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust.isTofu()).toBeTruthy(); + + // "forget" that Bob is trusted + delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"] + .keys.master.signatures["@alice:example.com"]; + + const bobTrust2 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); + expect(bobTrust2.isTofu()).toBeTruthy(); + + upgradePromise = new Promise((resolve) => { + upgradeResolveFunc = resolve; + }); + alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com"); + await upgradePromise; + + const bobTrust3 = alice.checkUserTrust("@bob:example.com"); + expect(bobTrust3.isCrossSigningVerified()).toBeTruthy(); + expect(bobTrust3.isTofu()).toBeTruthy(); + }); +}); diff --git a/spec/unit/crypto/secrets.spec.js b/spec/unit/crypto/secrets.spec.js new file mode 100644 index 00000000000..0fa1aa56e09 --- /dev/null +++ b/spec/unit/crypto/secrets.spec.js @@ -0,0 +1,247 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import '../../olm-loader'; + +import expect from 'expect'; +import { MatrixEvent } from '../../../lib/models/event'; + +import olmlib from '../../../lib/crypto/olmlib'; + +import TestClient from '../../TestClient'; +import { makeTestClients } from './verification/util'; + +async function makeTestClient(userInfo, options) { + const client = (new TestClient( + userInfo.userId, userInfo.deviceId, undefined, undefined, options, + )).client; + + await client.initCrypto(); + + return client; +} + +describe("Secrets", function() { + if (!global.Olm) { + console.warn('Not running megolm backup unit tests: libolm not present'); + return; + } + + beforeEach(async function() { + await global.Olm.init(); + }); + + it("should store and retrieve a secret", async function() { + const decryption = new global.Olm.PkDecryption(); + const pubkey = decryption.generate_key(); + const privkey = decryption.get_private_key(); + + const signing = new global.Olm.PkSigning(); + const signingKey = signing.generate_seed(); + const signingPubKey = signing.init_with_seed(signingKey); + + const signingkeyInfo = { + user_id: "@alice:example.com", + usage: ['master'], + keys: { + ['ed25519:' + signingPubKey]: signingPubKey, + }, + }; + + const getKey = expect.createSpy().andCall(e => { + expect(Object.keys(e.keys)).toEqual(["abc"]); + return ['abc', privkey]; + }); + + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => signingKey, + getSecretStorageKey: getKey, + }, + }, + ); + alice._crypto._crossSigningInfo.setKeys({ + master: signingkeyInfo, + }); + + const secretStorage = alice._crypto._secretStorage; + + alice.setAccountData = async function(eventType, contents, callback) { + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: eventType, + content: contents, + }), + ]); + if (callback) { + callback(); + } + }; + + const keyAccountData = { + algorithm: "m.secret_storage.v1.curve25519-aes-sha2", + pubkey: pubkey, + }; + await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master'); + + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: "m.secret_storage.key.abc", + content: keyAccountData, + }), + ]); + + expect(secretStorage.isStored("foo")).toBe(false); + + await secretStorage.store("foo", "bar", ["abc"]); + + expect(secretStorage.isStored("foo")).toBe(true); + expect(await secretStorage.get("foo")).toBe("bar"); + + expect(getKey).toHaveBeenCalled(); + }); + + it("should throw if given a key that doesn't exist", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar", ["this secret does not exist"]); + // should be able to use expect(...).toThrow() but mocha still fails + // the test even when it throws for reasons I have no inclination to debug + expect(true).toBeFalsy(); + } catch (e) { + } + }); + + it("should refuse to encrypt with zero keys", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar", []); + expect(true).toBeFalsy(); + } catch (e) { + } + }); + + it("should encrypt with default key if keys is null", async function() { + let keys = {}; + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + { + cryptoCallbacks: { + getCrossSigningKey: t => keys[t], + saveCrossSigningKeys: k => keys = k, + }, + }, + ); + alice.setAccountData = async function(eventType, contents, callback) { + alice.store.storeAccountDataEvents([ + new MatrixEvent({ + type: eventType, + content: contents, + }), + ]); + }; + alice.resetCrossSigningKeys(); + + const newKeyId = await alice.addSecretKey( + 'm.secret_storage.v1.curve25519-aes-sha2', + ); + // we don't await on this because it waits for the event to come down the sync + // which won't happen in the test setup + alice.setDefaultKeyId(newKeyId); + await alice.storeSecret("foo", "bar"); + + const accountData = alice.getAccountData('foo'); + expect(accountData.getContent().encrypted).toBeTruthy(); + }); + + it("should refuse to encrypt if no keys given and no default key", async function() { + const alice = await makeTestClient( + {userId: "@alice:example.com", deviceId: "Osborne2"}, + ); + + try { + await alice.storeSecret("foo", "bar"); + expect(true).toBeFalsy(); + } catch (e) { + } + }); + + it("should request secrets from other clients", async function() { + const [osborne2, vax] = await makeTestClients( + [ + {userId: "@alice:example.com", deviceId: "Osborne2"}, + {userId: "@alice:example.com", deviceId: "VAX"}, + ], + { + cryptoCallbacks: { + onSecretRequested: e => { + expect(e.name).toBe("foo"); + return "bar"; + }, + }, + }, + ); + + const vaxDevice = vax.client._crypto._olmDevice; + const osborne2Device = osborne2.client._crypto._olmDevice; + const secretStorage = osborne2.client._crypto._secretStorage; + + osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + "VAX": { + user_id: "@alice:example.com", + device_id: "VAX", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:VAX": vaxDevice.deviceEd25519Key, + "curve25519:VAX": vaxDevice.deviceCurve25519Key, + }, + }, + }); + vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", { + "Osborne2": { + user_id: "@alice:example.com", + device_id: "Osborne2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Osborne2": osborne2Device.deviceEd25519Key, + "curve25519:Osborne2": osborne2Device.deviceCurve25519Key, + }, + }, + }); + + await osborne2Device.generateOneTimeKeys(1); + const otks = (await osborne2Device.getOneTimeKeys()).curve25519; + await osborne2Device.markKeysAsPublished(); + + await vax.client._crypto._olmDevice.createOutboundSession( + osborne2Device.deviceCurve25519Key, + Object.values(otks)[0], + ); + + const request = await secretStorage.request("foo", ["VAX"]); + const secret = await request.promise; + + expect(secret).toBe("bar"); + }); +}); diff --git a/spec/unit/crypto/verification/request.spec.js b/spec/unit/crypto/verification/request.spec.js index ef0ea8586b3..355597d04e6 100644 --- a/spec/unit/crypto/verification/request.spec.js +++ b/spec/unit/crypto/verification/request.spec.js @@ -51,7 +51,7 @@ describe("verification request", function() { verificationMethods: [verificationMethods.SAS], }, ); - alice._crypto._deviceList.getRawStoredDevicesForUser = function() { + alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { keys: { @@ -60,20 +60,20 @@ describe("verification request", function() { }, }; }; - alice.downloadKeys = () => { + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.downloadKeys = () => { + bob.client.downloadKeys = () => { return Promise.resolve(); }; - bob.on("crypto.verification.request", (request) => { + bob.client.on("crypto.verification.request", (request) => { const bobVerifier = request.beginKeyVerification(verificationMethods.SAS); bobVerifier.verify(); // XXX: Private function access (but it's a test, so we're okay) bobVerifier._endTimer(); }); - const aliceVerifier = await alice.requestVerification("@bob:example.com"); + const aliceVerifier = await alice.client.requestVerification("@bob:example.com"); expect(aliceVerifier).toBeAn(SAS); // XXX: Private function access (but it's a test, so we're okay) diff --git a/spec/unit/crypto/verification/sas.spec.js b/spec/unit/crypto/verification/sas.spec.js index 8d08b867e23..cee4d14491e 100644 --- a/spec/unit/crypto/verification/sas.spec.js +++ b/spec/unit/crypto/verification/sas.spec.js @@ -22,6 +22,7 @@ try { } import expect from 'expect'; +import olmlib from '../../../../lib/crypto/olmlib'; import sdk from '../../../..'; @@ -36,6 +37,9 @@ const MatrixEvent = sdk.MatrixEvent; import {makeTestClients} from './util'; +let ALICE_DEVICES; +let BOB_DEVICES; + describe("SAS verification", function() { if (!global.Olm) { logger.warn('Not running device verification unit tests: libolm not present'); @@ -81,38 +85,43 @@ describe("SAS verification", function() { }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.getDeviceEd25519Key = () => { - return "alice+base64+ed25519+key"; + const aliceDevice = alice.client._crypto._olmDevice; + const bobDevice = bob.client._crypto._olmDevice; + + ALICE_DEVICES = { + Osborne2: { + user_id: "@alice:example.com", + device_id: "Osborne2", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Osborne2": aliceDevice.deviceEd25519Key, + "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, + }, + }, }; - alice.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Dynabook": "bob+base64+ed25519+key", - }, + + BOB_DEVICES = { + Dynabook: { + user_id: "@bob:example.com", + device_id: "Dynabook", + algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], + keys: { + "ed25519:Dynabook": bobDevice.deviceEd25519Key, + "curve25519:Dynabook": bobDevice.deviceCurve25519Key, }, - "Dynabook", - ); + }, }; + + alice.client._crypto._deviceList.storeDevicesForUser( + "@bob:example.com", BOB_DEVICES, + ); alice.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.getStoredDevice = () => { - return DeviceInfo.fromStorage( - { - keys: { - "ed25519:Osborne2": "alice+base64+ed25519+key", - }, - }, - "Osborne2", - ); - }; - bob.getDeviceEd25519Key = () => { - return "bob+base64+ed25519+key"; - }; + bob.client._crypto._deviceList.storeDevicesForUser( + "@alice:example.com", ALICE_DEVICES, + ); bob.downloadKeys = () => { return Promise.resolve(); }; @@ -121,7 +130,7 @@ describe("SAS verification", function() { bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.on("crypto.verification.start", (verifier) => { + bob.client.on("crypto.verification.start", (verifier) => { verifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -142,8 +151,8 @@ describe("SAS verification", function() { }); }); - aliceVerifier = alice.beginKeyVerification( - verificationMethods.SAS, bob.getUserId(), bob.deviceId, + aliceVerifier = alice.client.beginKeyVerification( + verificationMethods.SAS, bob.client.getUserId(), bob.deviceId, ); aliceVerifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { @@ -165,66 +174,165 @@ describe("SAS verification", function() { it("should verify a key", async function() { let macMethod; - const origSendToDevice = alice.sendToDevice; - bob.sendToDevice = function(type, map) { + const origSendToDevice = alice.client.sendToDevice; + bob.client.sendToDevice = function(type, map) { if (type === "m.key.verification.accept") { - macMethod = map[alice.getUserId()][alice.deviceId] + macMethod = map[alice.client.getUserId()][alice.client.deviceId] .message_authentication_code; } return origSendToDevice.call(this, type, map); }; + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, + }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), + alice.httpBackend.flush(), + bob.httpBackend.flush(), ]); // make sure that it uses the preferred method expect(macMethod).toBe("hkdf-hmac-sha256"); // make sure Alice and Bob verified each other - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + const bobDevice + = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); + expect(bobDevice.isVerified()).toBeTruthy(); + const aliceDevice + = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); + expect(aliceDevice.isVerified()).toBeTruthy(); }); it("should be able to verify using the old MAC", async function() { // pretend that Alice can only understand the old (incorrect) MAC, // and make sure that she can still verify with Bob let macMethod; - const origSendToDevice = alice.sendToDevice; - alice.sendToDevice = function(type, map) { + const origSendToDevice = alice.client.sendToDevice; + alice.client.sendToDevice = function(type, map) { if (type === "m.key.verification.start") { // Note: this modifies not only the message that Bob // receives, but also the copy of the message that Alice // has, since it is the same object. If this does not // happen, the verification will fail due to a hash // commitment mismatch. - map[bob.getUserId()][bob.deviceId] + map[bob.client.getUserId()][bob.client.deviceId] .message_authentication_codes = ['hmac-sha256']; } return origSendToDevice.call(this, type, map); }; - bob.sendToDevice = function(type, map) { + bob.client.sendToDevice = function(type, map) { if (type === "m.key.verification.accept") { - macMethod = map[alice.getUserId()][alice.deviceId] + macMethod = map[alice.client.getUserId()][alice.client.deviceId] .message_authentication_code; } return origSendToDevice.call(this, type, map); }; + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, + }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), + alice.httpBackend.flush(), + bob.httpBackend.flush(), ]); expect(macMethod).toBe("hmac-sha256"); - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + const bobDevice + = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); + expect(bobDevice.isVerified()).toBeTruthy(); + const aliceDevice + = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); + expect(aliceDevice.isVerified()).toBeTruthy(); + }); + + it("should verify a cross-signing key", async function() { + alice.httpBackend.when('POST', '/keys/device_signing/upload').respond( + 200, {}, + ); + alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + alice.httpBackend.flush(undefined, 2); + await alice.client.resetCrossSigningKeys(); + bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {}); + bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {}); + bob.httpBackend.flush(undefined, 2); + + await bob.client.resetCrossSigningKeys(); + + bob.client._crypto._deviceList.storeCrossSigningForUser( + "@alice:example.com", { + keys: alice.client._crypto._crossSigningInfo.keys, + }, + ); + + alice.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@bob:example.com": BOB_DEVICES, + }, + }); + bob.httpBackend.when('POST', '/keys/query').respond(200, { + failures: {}, + device_keys: { + "@alice:example.com": ALICE_DEVICES, + }, + }); + + const verifyProm = Promise.all([ + aliceVerifier.verify(), + bobPromise.then((verifier) => { + bob.httpBackend.when( + 'POST', '/keys/signatures/upload', + ).respond(200, {}); + bob.httpBackend.flush(undefined, 2); + return verifier.verify(); + }), + ]); + + await alice.httpBackend.flush(undefined, 1); + console.log("alice reqs flushed"); + + await verifyProm; + + const bobDeviceTrust = alice.client.checkDeviceTrust( + "@bob:example.com", "Dynabook", + ); + expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy(); + + const aliceTrust = bob.client.checkUserTrust("@alice:example.com"); + expect(aliceTrust.isCrossSigningVerified()).toBeTruthy(); + expect(aliceTrust.isTofu()).toBeTruthy(); + + const aliceDeviceTrust = bob.client.checkDeviceTrust( + "@alice:example.com", "Osborne2", + ); + expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy(); + expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy(); }); }); @@ -238,17 +346,17 @@ describe("SAS verification", function() { verificationMethods: [verificationMethods.SAS], }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.downloadKeys = () => { + alice.client.setDeviceVerified = expect.createSpy(); + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.downloadKeys = () => { + bob.client.setDeviceVerified = expect.createSpy(); + bob.client.downloadKeys = () => { return Promise.resolve(); }; const bobPromise = new Promise((resolve, reject) => { - bob.on("crypto.verification.start", (verifier) => { + bob.client.on("crypto.verification.start", (verifier) => { verifier.on("show_sas", (e) => { e.mismatch(); }); @@ -256,8 +364,8 @@ describe("SAS verification", function() { }); }); - const aliceVerifier = alice.beginKeyVerification( - verificationMethods.SAS, bob.getUserId(), bob.deviceId, + const aliceVerifier = alice.client.beginKeyVerification( + verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId, ); const aliceSpy = expect.createSpy(); @@ -268,9 +376,9 @@ describe("SAS verification", function() { ]); expect(aliceSpy).toHaveBeenCalled(); expect(bobSpy).toHaveBeenCalled(); - expect(alice.setDeviceVerified) + expect(alice.client.setDeviceVerified) .toNotHaveBeenCalled(); - expect(bob.setDeviceVerified) + expect(bob.client.setDeviceVerified) .toNotHaveBeenCalled(); }); @@ -293,11 +401,11 @@ describe("SAS verification", function() { }, ); - alice.setDeviceVerified = expect.createSpy(); - alice.getDeviceEd25519Key = () => { + alice.client.setDeviceVerified = expect.createSpy(); + alice.client.getDeviceEd25519Key = () => { return "alice+base64+ed25519+key"; }; - alice.getStoredDevice = () => { + alice.client.getStoredDevice = () => { return DeviceInfo.fromStorage( { keys: { @@ -307,12 +415,12 @@ describe("SAS verification", function() { "Dynabook", ); }; - alice.downloadKeys = () => { + alice.client.downloadKeys = () => { return Promise.resolve(); }; - bob.setDeviceVerified = expect.createSpy(); - bob.getStoredDevice = () => { + bob.client.setDeviceVerified = expect.createSpy(); + bob.client.getStoredDevice = () => { return DeviceInfo.fromStorage( { keys: { @@ -322,10 +430,10 @@ describe("SAS verification", function() { "Osborne2", ); }; - bob.getDeviceEd25519Key = () => { + bob.client.getDeviceEd25519Key = () => { return "bob+base64+ed25519+key"; }; - bob.downloadKeys = () => { + bob.client.downloadKeys = () => { return Promise.resolve(); }; @@ -333,13 +441,13 @@ describe("SAS verification", function() { bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.on("event", async (event) => { + bob.client.on("event", async (event) => { const content = event.getContent(); if (event.getType() === "m.room.message" && content.msgtype === "m.key.verification.request") { expect(content.methods).toInclude(SAS.NAME); - expect(content.to).toBe(bob.getUserId()); - const verifier = bob.acceptVerificationDM(event, SAS.NAME); + expect(content.to).toBe(bob.client.getUserId()); + const verifier = bob.client.acceptVerificationDM(event, SAS.NAME); verifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -362,8 +470,8 @@ describe("SAS verification", function() { }); }); - aliceVerifier = await alice.requestVerificationDM( - bob.getUserId(), "!room_id", [verificationMethods.SAS], + aliceVerifier = await alice.client.requestVerificationDM( + bob.client.getUserId(), "!room_id", [verificationMethods.SAS], ); aliceVerifier.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { @@ -390,10 +498,10 @@ describe("SAS verification", function() { ]); // make sure Alice and Bob verified each other - expect(alice.setDeviceVerified) - .toHaveBeenCalledWith(bob.getUserId(), bob.deviceId); - expect(bob.setDeviceVerified) - .toHaveBeenCalledWith(alice.getUserId(), alice.deviceId); + expect(alice.client.setDeviceVerified) + .toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId); + expect(bob.client.setDeviceVerified) + .toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId); }); }); }); diff --git a/spec/unit/crypto/verification/util.js b/spec/unit/crypto/verification/util.js index 5b28a11c42a..2c6ccca7677 100644 --- a/spec/unit/crypto/verification/util.js +++ b/spec/unit/crypto/verification/util.js @@ -33,11 +33,16 @@ export async function makeTestClients(userInfos, options) { type: type, content: msg, }); - setTimeout( - () => clientMap[userId][deviceId] - .emit("toDeviceEvent", event), - 0, - ); + const client = clientMap[userId][deviceId]; + if (event.isEncrypted()) { + event.attemptDecryption(client._crypto) + .then(() => client.emit("toDeviceEvent", event)); + } else { + setTimeout( + () => client.emit("toDeviceEvent", event), + 0, + ); + } } } } @@ -53,9 +58,9 @@ export async function makeTestClients(userInfos, options) { room_id: room, event_id: eventId, }); - for (const client of clients) { + for (const tc of clients) { setTimeout( - () => client.emit("event", event), + () => tc.client.emit("event", event), 0, ); } @@ -64,20 +69,27 @@ export async function makeTestClients(userInfos, options) { }; for (const userInfo of userInfos) { - const client = (new TestClient( + let keys = {}; + if (!options) options = {}; + if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; + if (!options.cryptoCallbacks.saveCrossSigningKeys) { + options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; + options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; + } + const testClient = new TestClient( userInfo.userId, userInfo.deviceId, undefined, undefined, options, - )).client; + ); if (!(userInfo.userId in clientMap)) { clientMap[userInfo.userId] = {}; } - clientMap[userInfo.userId][userInfo.deviceId] = client; - client.sendToDevice = sendToDevice; - client.sendEvent = sendEvent; - clients.push(client); + clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; + testClient.client.sendToDevice = sendToDevice; + testClient.client.sendEvent = sendEvent; + clients.push(testClient); } - await Promise.all(clients.map((client) => client.initCrypto())); + await Promise.all(clients.map((testClient) => testClient.client.initCrypto())); return clients; } diff --git a/src/base-apis.js b/src/base-apis.js index 0726833bf4f..24dd0ada857 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1718,6 +1718,15 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) { return this._http.authedRequest(callback, "POST", path, undefined, content); }; +MatrixBaseApis.prototype.uploadKeySignatures = function(content) { + return this._http.authedRequest( + undefined, "POST", '/keys/signatures/upload', undefined, + content, { + prefix: httpApi.PREFIX_UNSTABLE, + }, + ); +}; + /** * Download device keys * @@ -1802,6 +1811,14 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) { return this._http.authedRequest(undefined, "GET", path, qps, undefined); }; +MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) { + const data = Object.assign({}, keys, {auth}); + return this._http.authedRequest( + undefined, "POST", "/keys/device_signing/upload", undefined, data, { + prefix: httpApi.PREFIX_UNSTABLE, + }, + ); +}; // Identity Server Operations // ========================== diff --git a/src/client.js b/src/client.js index b14078fe74c..f78c842f59b 100644 --- a/src/client.js +++ b/src/client.js @@ -51,7 +51,7 @@ import logger from './logger'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; -import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; +import { keyFromPassphrase, keyFromAuthData } from './crypto/key_passphrase'; import { randomString } from './randomstring'; // Disable warnings for now: we use deprecated bluebird functions @@ -175,6 +175,68 @@ function keyFromRecoverySession(session, decryptionKey) { * @param {boolean} [opts.fallbackICEServerAllowed] * Optional. Whether to allow a fallback ICE server should be used for negotiating a * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. + * + * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {function} [opts.cryptoCallbacks.getCrossSigningKey] + * Optional (required for cross-signing). Function to call when a cross-signing private key is needed. + * Args: + * {string} type The type of key needed. Will be one of "master", + * "self_signing", or "user_signing" + * {Uint8Array} publicKey The public key matching the expected private key. + * This can be passed to checkPrivateKey() along with the private key + * in order to check that a given private key matches what is being + * requested. + * Should return a promise that resolves with the private key as a + * UInt8Array or rejects with an error. + * + * @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys] + * Optional (required for cross-signing). Called when new private keys + * for cross-signing need to be saved. + * Args: + * {object} keys the private keys to save. Map of key name to private key + * as a UInt8Array. The getPrivateKey callback above will be called + * with the corresponding key name when the keys are required again. + * + * @param {function} [opts.cryptoCallbacks.shouldUpgradeDeviceVerifications] + * Optional. Called when there are device-to-device verifications that can be + * upgraded into cross-signing verifications. + * Args: + * {object} users The users whose device verifications can be + * upgraded to cross-signing verifications. This will be a map of user IDs + * to objects with the properties `devices` (array of the user's devices + * that verified their cross-signing key), and `crossSigningInfo` (the + * user's cross-signing information) + * Should return a promise which resolves with an array of the user IDs who + * should be cross-signed. + * + * @param {function} [opts.cryptoCallbacks.getSecretStorageKey] + * Optional. Function called when an encryption key for secret storage + * is required. One or more keys will be described in the keys object. + * The callback function should return with an array of: + * [, ] or null if it cannot provide + * any of the keys. + * Args: + * {object} keys Information about the keys: + * { + * : { + * pubkey: {UInt8Array} + * } + * } + * + * @param {function} [opts.cryptoCallbacks.onSecretRequested] + * Optional. Function called when a request for a secret is received from another + * device. + * Args: + * {string} name The name of the secret being requested. + * {string} user_id (string) The user ID of the client requesting + * {string} device_id The device ID of the client requesting the secret. + * {string} request_id The ID of the request. Used to match a + * corresponding `crypto.secrets.request_cancelled`. The request ID will be + * unique per sender, device pair. + * {DeviceTrustLevel} device_trust: The trust status of the device requesting + * the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}. */ function MatrixClient(opts) { opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); @@ -235,6 +297,7 @@ function MatrixClient(opts) { this._cryptoStore = opts.cryptoStore; this._sessionStore = opts.sessionStore; this._verificationMethods = opts.verificationMethods; + this._cryptoCallbacks = opts.cryptoCallbacks; this._forceTURN = opts.forceTURN || false; this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; @@ -611,6 +674,10 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", + "crypto.devicesUpdated", + "deviceVerificationChanged", + "userVerificationChanged", + "crossSigning.keysChanged", ]); logger.log("Crypto: initialising crypto object..."); @@ -779,10 +846,9 @@ async function _setDeviceVerification( if (!client._crypto) { throw new Error("End-to-End encryption disabled"); } - const dev = await client._crypto.setDeviceVerification( + await client._crypto.setDeviceVerification( userId, deviceId, verified, blocked, known, ); - client.emit("deviceVerificationChanged", userId, deviceId, dev); } /** @@ -880,6 +946,189 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() { return this._crypto.getGlobalBlacklistUnverifiedDevices(); }; +/** + * Add methods that call the corresponding method in this._crypto + * + * @param {class} MatrixClient the class to add the method to + * @param {string} names the names of the methods to call + */ +function wrapCryptoFuncs(MatrixClient, names) { + for (const name of names) { + MatrixClient.prototype[name] = function(...args) { + if (!this._crypto) { // eslint-disable-line no-invalid-this + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto[name](...args); // eslint-disable-line no-invalid-this + }; + } +} + +/** + * Generate new cross-signing keys. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#resetCrossSigningKeys + * @param {object} authDict Auth data to supply for User-Interactive auth. + * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New + * keys will be created for the given level and below. Defaults to + * regenerating all keys. + */ + +/** + * Get the user's cross-signing key ID. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getCrossSigningId + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + +/** + * Get the cross signing information for a given user. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getStoredCrossSigningForUser + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing information for the user. + */ + +/** + * Check whether a given user is trusted. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkUserTrust + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ + +/** + * Check whether a given device is trusted. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkDeviceTrust + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ + +wrapCryptoFuncs(MatrixClient, [ + "resetCrossSigningKeys", + "getCrossSigningId", + "getStoredCrossSigningForUser", + "checkUserTrust", + "checkDeviceTrust", + "checkOwnCrossSigningTrust", + "checkPrivateKey", +]); + +/** + * Check if the sender of an event is verified + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {MatrixEvent} event event to be checked + * + * @returns {DeviceTrustLevel} + */ +MatrixClient.prototype.checkEventSenderTrust = async function(event) { + const device = await this.getEventSenderDeviceInfo(event); + if (!device) { + return 0; + } + return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId); +}; + +/** + * Add a key for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#addSecretKey + * @param {string} algorithm the algorithm used by the key + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. This object may be modified to pass + * information back about the key. + * @param {string} [keyName] the name of the key. If not given, a random + * name will be generated. + * + * @return {string} the name of the key + */ + +/** + * Store an encrypted secret on the server + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#storeSecret + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined + * to use the default (will throw if no default key is set). + */ + +/** + * Get a secret from storage. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getSecret + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + +/** + * Check if a secret is stored on the server. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#isSecretStored + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted + * key + * + * @return {boolean} whether or not the secret is stored + */ + +/** + * Request a secret from another device. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#requestSecret + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + +/** + * Get the current default key ID for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getDefaultKeyId + * + * @return {string} The default key ID or null if no default key ID is set + */ + +/** + * Set the current default key ID for encrypting secrets. + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#setDefaultKeyId + * @param {string} keyId The new default key ID + */ + +wrapCryptoFuncs(MatrixClient, [ + "addSecretKey", + "storeSecret", + "getSecret", + "isSecretStored", + "requestSecret", + "getDefaultKeyId", + "setDefaultKeyId", +]); + /** * Get e2e information on the device that sent an event * @@ -1131,7 +1380,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { let publicKey; const authData = {}; if (password) { - const keyInfo = await keyForNewBackup(password); + const keyInfo = await keyFromPassphrase(password); publicKey = decryption.init_with_private_key(keyInfo.key); authData.private_key_salt = keyInfo.salt; authData.private_key_iterations = keyInfo.iterations; @@ -1158,7 +1407,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { * @param {object} info Info object from prepareKeyBackupVersion * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = function(info) { +MatrixClient.prototype.createKeyBackupVersion = async function(info) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -1167,19 +1416,27 @@ MatrixClient.prototype.createKeyBackupVersion = function(info) { algorithm: info.algorithm, auth_data: info.auth_data, }; - return this._crypto._signObject(data.auth_data).then(() => { - return this._http.authedRequest( - undefined, "POST", "/room_keys/version", undefined, data, - {prefix: httpApi.PREFIX_UNSTABLE}, - ); - }).then((res) => { - this.enableKeyBackup({ - algorithm: info.algorithm, - auth_data: info.auth_data, - version: res.version, - }); - return res; + + // Now sign the backup auth data. Do it as this device first because crypto._signObject + // is dumb and bluntly replaces the whole signatures block... + // this can probably go away very soon in favour of just signing with the SSK. + await this._crypto._signObject(data.auth_data); + + if (this._crypto._crossSigningInfo.getId()) { + // now also sign the auth data with the master key + await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); + } + + const res = await this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + {prefix: httpApi.PREFIX_UNSTABLE}, + ); + this.enableKeyBackup({ + algorithm: info.algorithm, + auth_data: info.auth_data, + version: res.version, }); + return res; }; MatrixClient.prototype.deleteKeyBackupVersion = function(version) { @@ -1285,7 +1542,7 @@ MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; MatrixClient.prototype.restoreKeyBackupWithPassword = async function( password, targetRoomId, targetSessionId, backupInfo, ) { - const privKey = await keyForExistingBackup(backupInfo, password); + const privKey = await keyFromAuthData(backupInfo.auth_data, password); return this._restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, ); @@ -4860,6 +5117,32 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * @param {module:crypto/deviceinfo} deviceInfo updated device information */ +/** + * Fires when the trust status of a user changes + * If userId is the userId of the logged in user, this indicated a change + * in the trust status of the cross-signing data on the account. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"userTrustStatusChanged" + * @param {string} userId the userId of the user in question + * @param {UserTrustLevel} trustLevel The new trust level of the user + */ + +/** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"crossSigning.keysChanged" + */ + /** * Fires whenever new user-scoped account_data is added. * @event module:client~MatrixClient#"accountData" @@ -4917,6 +5200,21 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * perform the key verification */ +/** + * Fires when a secret request has been cancelled. If the client is prompting + * the user to ask whether they want to share a secret, the prompt can be + * dismissed. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" + * @param {object} data + * @param {string} data.user_id The user ID of the client that had requested the secret. + * @param {string} data.device_id The device ID of the client that had requested the + * secret. + * @param {string} data.request_id The ID of the original request. + */ + // EventEmitter JSDocs /** diff --git a/src/crypto/CrossSigning.js b/src/crypto/CrossSigning.js new file mode 100644 index 00000000000..90d95014aef --- /dev/null +++ b/src/crypto/CrossSigning.js @@ -0,0 +1,483 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Cross signing methods + * @module crypto/CrossSigning + */ + +import {pkSign, pkVerify} from './olmlib'; +import {EventEmitter} from 'events'; +import logger from '../logger'; + +function publicKeyFromKeyInfo(keyInfo) { + return Object.entries(keyInfo.keys)[0]; +} + +export class CrossSigningInfo extends EventEmitter { + /** + * Information about a user's cross-signing keys + * + * @class + * + * @param {string} userId the user that the information is about + * @param {object} callbacks Callbacks used to interact with the app + * Requires getCrossSigningKey and saveCrossSigningKeys + */ + constructor(userId, callbacks) { + super(); + + // you can't change the userId + Object.defineProperty(this, 'userId', { + enumerable: true, + value: userId, + }); + this._callbacks = callbacks || {}; + this.keys = {}; + this.firstUse = true; + } + + /** + * Calls the app callback to ask for a private key + * @param {string} type The key type ("master", "self_signing", or "user_signing") + * @param {Uint8Array} expectedPubkey The matching public key or undefined to use + * the stored public key for the given key type. + */ + async getCrossSigningKey(type, expectedPubkey) { + if (!this._callbacks.getCrossSigningKey) { + throw new Error("No getCrossSigningKey callback supplied"); + } + + if (expectedPubkey === undefined) { + expectedPubkey = this.getId(type); + } + + const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); + if (!privkey) { + throw new Error( + "getCrossSigningKey callback for " + type + " returned falsey", + ); + } + const signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privkey); + if (gotPubkey !== expectedPubkey) { + signing.free(); + throw new Error( + "Key type " + type + " from getCrossSigningKey callback did not match", + ); + } else { + return [gotPubkey, signing]; + } + } + + static fromStorage(obj, userId) { + const res = new CrossSigningInfo(userId); + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + return res; + } + + toStorage() { + return { + keys: this.keys, + firstUse: this.firstUse, + }; + } + + /** + * 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(type) { + type = type || "master"; + if (!this.keys[type]) return null; + const keyInfo = this.keys[type]; + return publicKeyFromKeyInfo(keyInfo)[1]; + } + + + async resetKeys(level) { + if (!this._callbacks.saveCrossSigningKeys) { + throw new Error("No saveCrossSigningKeys callback supplied"); + } + + // If we're resetting the master key, we reset all keys + if ( + level === undefined || + level & CrossSigningLevel.MASTER || + !this.keys.master + ) { + level = ( + CrossSigningLevel.MASTER | + CrossSigningLevel.USER_SIGNING | + CrossSigningLevel.SELF_SIGNING + ); + } else if (level === 0) { + return; + } + + const privateKeys = {}; + const keys = {}; + let masterSigning; + let masterPub; + + try { + if (level & CrossSigningLevel.MASTER) { + 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 this.getCrossSigningyKey("master"); + } + + 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(); + } + } + + 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(); + } + } + + Object.assign(this.keys, keys); + this._callbacks.saveCrossSigningKeys(privateKeys); + } finally { + if (masterSigning) { + masterSigning.free(); + } + } + } + + 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); + } + if (!this.keys.master) { + // this is the first key we've seen, so first-use is true + this.firstUse = true; + } else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) { + // this is a different key, so first-use is false + this.firstUse = false; + } // otherwise, same key, so no change + signingKeys.master = keys.master; + } 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 = publicKeyFromKeyInfo(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; + } + } + + // 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; + } + if (keys.user_signing) { + this.keys.user_signing = keys.user_signing; + } + } + + async signObject(data, type) { + if (!this.keys[type]) { + throw new Error( + "Attempted to sign with " + type + " key but no such key present", + ); + } + const [pubkey, signing] = await this.getCrossSigningKey(type); + try { + pkSign(data, signing, this.userId, pubkey); + return data; + } finally { + signing.free(); + } + } + + async signUser(key) { + if (!this.keys.user_signing) { + return; + } + return this.signObject(key.keys.master, "user_signing"); + } + + async signDevice(userId, device) { + if (userId !== this.userId) { + throw new Error( + `Trying to sign ${userId}'s device; can only sign our own device`, + ); + } + if (!this.keys.self_signing) { + return; + } + return this.signObject( + { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + }, "self_signing", + ); + } + + /** + * Check whether a given user is trusted. + * + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * + * @returns {UserTrustLevel} + */ + checkUserTrust(userCrossSigning) { + // if we're checking our own key, then it's trusted if the master key + // and self-signing key match + if (this.userId === userCrossSigning.userId + && this.getId() && this.getId() === userCrossSigning.getId() + && this.getId("self_signing") + && this.getId("self_signing") === userCrossSigning.getId("self_signing") + ) { + return new UserTrustLevel(true, this.firstUse); + } + + if (!this.keys.user_signing) { + // If there's no user signing key, they can't possibly be verified. + // They may be TOFU trusted though. + return new UserTrustLevel(false, userCrossSigning.firstUse); + } + + let userTrusted; + const userMaster = userCrossSigning.keys.master; + const uskId = this.getId('user_signing'); + try { + pkVerify(userMaster, uskId, this.userId); + userTrusted = true; + } catch (e) { + userTrusted = false; + } + return new UserTrustLevel(userTrusted, userCrossSigning.firstUse); + } + + /** + * Check whether a given device is trusted. + * + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param {module:crypto/deviceinfo} device The device to check + * @param {bool} localTrust Whether the device is trusted locally + * + * @returns {DeviceTrustLevel} + */ + checkDeviceTrust(userCrossSigning, device, localTrust) { + const userTrust = this.checkUserTrust(userCrossSigning); + + const userSSK = userCrossSigning.keys.self_signing; + if (!userSSK) { + // if the user has no self-signing key then we cannot make any + // trust assertions about this device from cross-signing + return new DeviceTrustLevel(false, false, localTrust); + } + + const deviceObj = deviceToObject(device, userCrossSigning.userId); + try { + // if we can verify the user's SSK from their master key... + pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + // ...and this device's key from their SSK... + pkVerify( + deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId, + ); + // ...then we trust this device as much as far as we trust the user + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust); + } catch (e) { + return new DeviceTrustLevel(false, false, localTrust); + } + } +} + +function deviceToObject(device, userId) { + return { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + signatures: device.signatures, + }; +} + +export const CrossSigningLevel = { + MASTER: 4, + USER_SIGNING: 2, + SELF_SIGNING: 1, +}; + +/** + * Represents the ways in which we trust a user + */ +export class UserTrustLevel { + constructor(crossSigningVerified, tofu) { + this._crossSigningVerified = crossSigningVerified; + this._tofu = tofu; + } + + /** + * @returns {bool} true if this user is verified via any means + */ + isVerified() { + return this.isCrossSigningVerified(); + } + + /** + * @returns {bool} true if this user is verified via cross signing + */ + isCrossSigningVerified() { + return this._crossSigningVerified; + } + + /** + * @returns {bool} true if this user's key is trusted on first use + */ + isTofu() { + return this._tofu; + } +} + +/** + * Represents the ways in which we trust a device + */ +export class DeviceTrustLevel { + constructor(crossSigningVerified, tofu, localVerified) { + this._crossSigningVerified = crossSigningVerified; + this._tofu = tofu; + this._localVerified = localVerified; + } + + static fromUserTrustLevel(userTrustLevel, localVerified) { + return new DeviceTrustLevel( + userTrustLevel._crossSigningVerified, + userTrustLevel._tofu, + localVerified, + ); + } + + /** + * @returns {bool} true if this device is verified via any means + */ + isVerified() { + return this.isCrossSigningVerified() || this.isLocallyVerified(); + } + + /** + * @returns {bool} true if this device is verified via cross signing + */ + isCrossSigningVerified() { + return this._crossSigningVerified; + } + + /** + * @returns {bool} true if this device is verified locally + */ + isLocallyVerified() { + return this._localVerified; + } + + /** + * @returns {bool} true if this device is trusted from a user's key + * that is trusted on first use + */ + isTofu() { + return this._tofu; + } +} diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 5e50189e40e..94b3140b0ed 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. @@ -23,9 +24,11 @@ limitations under the License. */ import Promise from 'bluebird'; +import {EventEmitter} from 'events'; import logger from '../logger'; import DeviceInfo from './deviceinfo'; +import {CrossSigningInfo} from './CrossSigning'; import olmlib from './olmlib'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -60,8 +63,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3; /** * @alias module:crypto/DeviceList */ -export default class DeviceList { +export default class DeviceList extends EventEmitter { constructor(baseApis, cryptoStore, olmDevice) { + super(); + this._cryptoStore = cryptoStore; // userId -> { @@ -71,6 +76,11 @@ export default class DeviceList { // } this._devices = {}; + // userId -> { + // [key info] + // } + this._crossSigningInfo = {}; + // map of identity keys to the user who owns it this._userByIdentityKey = {}; @@ -111,6 +121,7 @@ export default class DeviceList { 'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => { this._devices = deviceData ? deviceData.devices : {}, + this._ssks = deviceData ? deviceData.self_signing_keys || {} : {}; this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; this._syncToken = deviceData ? deviceData.syncToken : null; @@ -201,6 +212,7 @@ export default class DeviceList { 'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => { this._cryptoStore.storeEndToEndDeviceData({ devices: this._devices, + self_signing_keys: this._ssks, trackingStatus: this._deviceTrackingStatus, syncToken: this._syncToken, }, txn); @@ -334,6 +346,17 @@ export default class DeviceList { return this._devices[userId]; } + getStoredCrossSigningForUser(userId) { + if (!this._crossSigningInfo[userId]) return null; + + return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); + } + + storeCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; + this._dirty = true; + } + /** * Get the stored keys for a single device * @@ -561,6 +584,10 @@ export default class DeviceList { } } + setRawStoredCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; + } + /** * Fire off download update requests for the given users, and update the * device list tracking status for them, and the @@ -624,6 +651,7 @@ export default class DeviceList { } }); this.saveIfDirty(); + this.emit("crypto.devicesUpdated", users); }; return prom; @@ -724,6 +752,9 @@ class DeviceListUpdateSerialiser { downloadUsers, opts, ).then((res) => { const dk = res.device_keys || {}; + const masterKeys = 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) @@ -733,7 +764,13 @@ class DeviceListUpdateSerialiser { let prom = Promise.resolve(); for (const userId of downloadUsers) { prom = prom.delay(5).then(() => { - return this._processQueryResponseForUser(userId, dk[userId]); + return this._processQueryResponseForUser( + userId, dk[userId], { + master: masterKeys[userId], + self_signing: ssks[userId], + user_signing: usks[userId], + }, + ); }); } @@ -757,30 +794,58 @@ class DeviceListUpdateSerialiser { return deferred.promise; } - async _processQueryResponseForUser(userId, response) { - logger.log('got keys for ' + userId + ':', response); + async _processQueryResponseForUser( + userId, dkResponse, crossSigningResponse, sskResponse, + ) { + logger.log('got device keys for ' + userId + ':', dkResponse); + logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); + + { + // map from deviceid -> deviceinfo for this user + const userStore = {}; + const devs = this._deviceList.getRawStoredDevicesForUser(userId); + if (devs) { + Object.keys(devs).forEach((deviceId) => { + const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); + userStore[deviceId] = d; + }); + } + + await _updateStoredDeviceKeysForUser( + this._olmDevice, userId, userStore, dkResponse || {}, + ); - // map from deviceid -> deviceinfo for this user - const userStore = {}; - const devs = this._deviceList.getRawStoredDevicesForUser(userId); - if (devs) { - Object.keys(devs).forEach((deviceId) => { - const d = DeviceInfo.fromStorage(devs[deviceId], deviceId); - userStore[deviceId] = d; + // put the updates into the object that will be returned as our results + const storage = {}; + Object.keys(userStore).forEach((deviceId) => { + storage[deviceId] = userStore[deviceId].toStorage(); }); + + this._deviceList._setRawStoredDevicesForUser(userId, storage); } - await _updateStoredDeviceKeysForUser( - this._olmDevice, userId, userStore, response || {}, - ); + // now do the same for the cross-signing keys + { + // FIXME: should we be ignoring empty cross-signing responses, or + // should we be dropping the keys? + if (crossSigningResponse + && (crossSigningResponse.master || crossSigningResponse.self_signing + || crossSigningResponse.user_signing)) { + const crossSigning + = this._deviceList.getStoredCrossSigningForUser(userId) + || new CrossSigningInfo(userId); - // put the updates into thr object that will be returned as our results - const storage = {}; - Object.keys(userStore).forEach((deviceId) => { - storage[deviceId] = userStore[deviceId].toStorage(); - }); + crossSigning.setKeys(crossSigningResponse); + + this._deviceList.setRawStoredCrossSigningForUser( + userId, crossSigning.toStorage(), + ); - this._deviceList._setRawStoredDevicesForUser(userId, storage); + // 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); + } + } } } @@ -854,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); @@ -886,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/Secrets.js b/src/crypto/Secrets.js new file mode 100644 index 00000000000..508b541ab4d --- /dev/null +++ b/src/crypto/Secrets.js @@ -0,0 +1,527 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {EventEmitter} from 'events'; +import logger from '../logger'; +import olmlib from './olmlib'; +import { randomString } from '../randomstring'; +import { keyFromPassphrase } from './key_passphrase'; +import { encodeRecoveryKey } from './recoverykey'; +import { pkVerify } from './olmlib'; + +/** + * Implements Secure Secret Storage and Sharing (MSC1946) + * @module crypto/Secrets + */ +export default class SecretStorage extends EventEmitter { + constructor(baseApis, cryptoCallbacks, crossSigningInfo) { + super(); + this._baseApis = baseApis; + this._cryptoCallbacks = cryptoCallbacks; + this._crossSigningInfo = crossSigningInfo; + this._requests = {}; + this._incomingRequests = {}; + } + + getDefaultKeyId() { + const defaultKeyEvent = this._baseApis.getAccountData( + 'm.secret_storage.default_key', + ); + if (!defaultKeyEvent) return null; + return defaultKeyEvent.getContent().key; + } + + setDefaultKeyId(keyId) { + return new Promise((resolve) => { + const listener = (ev) => { + if ( + ev.getType() === 'm.secret_storage.default_key' && + ev.getContent().key === keyId + ) { + this._baseApis.removeListener('accountData', listener); + resolve(); + } + }; + this._baseApis.on('accountData', listener); + + this._baseApis.setAccountData( + 'm.secret_storage.default_key', + { key: keyId }, + ); + }); + } + + /** + * Add a key for encrypting secrets. + * + * @param {string} algorithm the algorithm used by the key. + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. This object may be modified to pass + * information back about the key. + * @param {string} [keyID] the ID of the key. If not given, a random + * ID will be generated. + * + * @return {string} the ID of the key + */ + async addKey(algorithm, opts, keyID) { + const keyData = {algorithm}; + + if (!opts) opts = {}; + + if (opts.name) { + keyData.name = opts.name; + } + + switch (algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const decryption = new global.Olm.PkDecryption(); + try { + if (opts.passphrase) { + const key = await keyFromPassphrase(opts.passphrase); + keyData.passphrase = { + algorithm: "m.pbkdf2", + iterations: key.iterations, + salt: key.salt, + }; + opts.encodedkey = encodeRecoveryKey(key.key); + keyData.pubkey = decryption.init_with_private_key(key.key); + } else if (opts.privkey) { + keyData.pubkey = decryption.init_with_private_key(opts.privkey); + opts.encodedkey = encodeRecoveryKey(opts.privkey); + } else { + keyData.pubkey = decryption.generate_key(); + opts.encodedkey = encodeRecoveryKey(decryption.get_private_key()); + } + } finally { + decryption.free(); + } + break; + } + default: + throw new Error(`Unknown key algorithm ${opts.algorithm}`); + } + + if (!keyID) { + do { + keyID = randomString(32); + } while (this._baseApis.getAccountData(`m.secret_storage.key.${keyID}`)); + } + + await this._crossSigningInfo.signObject(keyData, 'master'); + + await this._baseApis.setAccountData( + `m.secret_storage.key.${keyID}`, keyData, + ); + + return keyID; + } + + // TODO: need a function to get all the secret keys + + /** + * Store an encrypted secret on the server + * + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret + * or null/undefined to use the default key. + */ + async store(name, secret, keys) { + const encrypted = {}; + + if (!keys) { + const defaultKeyId = this.getDefaultKeyId(); + if (!defaultKeyId) { + throw new Error("No keys specified and no default key present"); + } + keys = [defaultKeyId]; + } + + if (keys.length === 0) { + throw new Error("Zero keys given to encrypt with!"); + } + + for (const keyName of keys) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ); + if (!keyInfo) { + throw new Error("Unknown key: " +keyName); + } + const keyInfoContent = keyInfo.getContent(); + + // check signature of key info + pkVerify( + keyInfoContent, + this._crossSigningInfo.getId('master'), + this._crossSigningInfo.userId, + ); + + // encrypt secret, based on the algorithm + switch (keyInfoContent.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const encryption = new global.Olm.PkEncryption(); + try { + encryption.set_recipient_key(keyInfoContent.pubkey); + encrypted[keyName] = encryption.encrypt(secret); + } finally { + encryption.free(); + } + break; + } + default: + logger.warn("unknown algorithm for secret storage key " + keyName + + ": " + keyInfoContent.algorithm); + // do nothing if we don't understand the encryption algorithm + } + } + + // save encrypted secret + await this._baseApis.setAccountData(name, {encrypted}); + } + + /** + * Get a secret from storage. + * + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + async get(name) { + const secretInfo = this._baseApis.getAccountData(name); + if (!secretInfo) { + return; + } + + const secretContent = secretInfo.getContent(); + + if (!secretContent.encrypted) { + throw new Error("Content is not encrypted!"); + } + + // get possible keys to decrypt + const keys = {}; + for (const keyName of Object.keys(secretContent.encrypted)) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ).getContent(); + const encInfo = secretContent.encrypted[keyName]; + switch (keyInfo.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac + && encInfo.ephemeral) { + keys[keyName] = keyInfo; + } + break; + default: + // do nothing if we don't understand the encryption algorithm + } + } + + let keyName; + let decryption; + try { + // fetch private key from app + [keyName, decryption] = await this._getSecretStorageKey(keys); + + // decrypt secret + const encInfo = secretContent.encrypted[keyName]; + switch (keys[keyName].algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + return decryption.decrypt( + encInfo.ephemeral, encInfo.mac, encInfo.ciphertext, + ); + } + } finally { + if (decryption) decryption.free(); + } + } + + /** + * Check if a secret is stored on the server. + * + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted key + * + * @return {boolean} whether or not the secret is stored + */ + isStored(name, checkKey) { + // check if secret exists + const secretInfo = this._baseApis.getAccountData(name); + if (!secretInfo) { + return false; + } + + if (checkKey === undefined) checkKey = true; + + const secretContent = secretInfo.getContent(); + + if (!secretContent.encrypted) { + return false; + } + + // check if secret is encrypted by a known/trusted secret and + // encryption looks sane + for (const keyName of Object.keys(secretContent.encrypted)) { + // get key information from key storage + const keyInfo = this._baseApis.getAccountData( + "m.secret_storage.key." + keyName, + ).getContent(); + const encInfo = secretContent.encrypted[keyName]; + if (checkKey) { + pkVerify( + keyInfo, + this._crossSigningInfo.getId('master'), + this._crossSigningInfo.userId, + ); + } + switch (keyInfo.algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac + && encInfo.ephemeral) { + return true; + } + break; + default: + // do nothing if we don't understand the encryption algorithm + } + } + return false; + } + + /** + * Request a secret from another device + * + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + request(name, devices) { + const requestId = this._baseApis.makeTxnId(); + + const requestControl = this._requests[requestId] = { + devices, + }; + const promise = new Promise((resolve, reject) => { + requestControl.resolve = resolve; + requestControl.reject = reject; + }); + const cancel = (reason) => { + // send cancellation event + const cancelData = { + action: "request_cancellation", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId, + }; + const toDevice = {}; + for (const device of devices) { + toDevice[device] = cancelData; + } + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice, + }); + + // and reject the promise so that anyone waiting on it will be + // notified + requestControl.reject(new Error(reason || "Cancelled")); + }; + + // send request to devices + const requestData = { + name, + action: "request", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId, + }; + const toDevice = {}; + for (const device of devices) { + toDevice[device] = requestData; + } + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice, + }); + + return { + request_id: requestId, + promise, + cancel, + }; + } + + async _onRequestReceived(event) { + const sender = event.getSender(); + const content = event.getContent(); + if (sender !== this._baseApis.getUserId() + || !(content.name && content.action + && content.requesting_device_id && content.request_id)) { + // ignore requests from anyone else, for now + return; + } + const deviceId = content.requesting_device_id; + // check if it's a cancel + if (content.action === "request_cancellation") { + if (this._incomingRequests[deviceId] + && this._incomingRequests[deviceId][content.request_id]) { + logger.info("received request cancellation for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")"); + this.baseApis.emit("crypto.secrets.requestCancelled", { + user_id: sender, + device_id: deviceId, + request_id: content.request_id, + }); + } + } else if (content.action === "request") { + if (deviceId === this._baseApis.deviceId) { + // no point in trying to send ourself the secret + return; + } + + // check if we have the secret + logger.info("received request for secret (" + sender + + ", " + deviceId + ", " + content.request_id + ")"); + if (!this._cryptoCallbacks.onSecretRequested) { + return; + } + const secret = await this._cryptoCallbacks.onSecretRequested({ + user_id: sender, + device_id: deviceId, + request_id: content.request_id, + name: content.name, + device_trust: this._baseApis.checkDeviceTrust(sender, deviceId), + }); + if (secret) { + const payload = { + type: "m.secret.send", + content: { + request_id: content.request_id, + secret: secret, + }, + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.ensureOlmSessionsForDevices( + this._baseApis._crypto._olmDevice, + this._baseApis, + { + [sender]: [ + await this._baseApis.getStoredDevice(sender, deviceId), + ], + }, + ); + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._baseApis.getUserId(), + this._baseApis.deviceId, + this._baseApis._crypto._olmDevice, + sender, + this._baseApis._crypto.getStoredDevice(sender, deviceId), + payload, + ); + const contentMap = { + [sender]: { + [deviceId]: encryptedContent, + }, + }; + + this._baseApis.sendToDevice("m.room.encrypted", contentMap); + } + } + } + + _onSecretReceived(event) { + if (event.getSender() !== this._baseApis.getUserId()) { + // we shouldn't be receiving secrets from anyone else, so ignore + // because someone could be trying to send us bogus data + return; + } + const content = event.getContent(); + logger.log("got secret share for request ", content.request_id); + const requestControl = this._requests[content.request_id]; + if (requestControl) { + // make sure that the device that sent it is one of the devices that + // we requested from + const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey( + olmlib.OLM_ALGORITHM, + event.getSenderKey(), + ); + if (!deviceInfo) { + logger.log( + "secret share from unknown device with key", event.getSenderKey(), + ); + return; + } + if (!requestControl.devices.includes(deviceInfo.deviceId)) { + logger.log("unsolicited secret share from device", deviceInfo.deviceId); + return; + } + + requestControl.resolve(content.secret); + } + } + + async _getSecretStorageKey(keys) { + if (!this._cryptoCallbacks.getSecretStorageKey) { + throw new Error("No getSecretStorageKey callback supplied"); + } + + const returned = await Promise.resolve( + this._cryptoCallbacks.getSecretStorageKey({keys}), + ); + + if (!returned) { + throw new Error("getSecretStorageKey callback returned falsey"); + } + if (returned.length < 2) { + throw new Error("getSecretStorageKey callback returned invalid data"); + } + + const [keyName, privateKey] = returned; + if (!keys[keyName]) { + throw new Error("App returned unknown key from getSecretStorageKey!"); + } + + switch (keys[keyName].algorithm) { + case "m.secret_storage.v1.curve25519-aes-sha2": + { + const decryption = new global.Olm.PkDecryption(); + let pubkey; + try { + pubkey = decryption.init_with_private_key(privateKey); + } catch (e) { + decryption.free(); + throw new Error("getSecretStorageKey callback returned invalid key"); + } + if (pubkey !== keys[keyName].pubkey) { + decryption.free(); + throw new Error( + "getSecretStorageKey callback returned incorrect key", + ); + } + return [keyName, decryption]; + } + default: + throw new Error("Unknown key type: " + keys[keyName].algorithm); + } + } +} diff --git a/src/crypto/deviceinfo.js b/src/crypto/deviceinfo.js index aa5c4afac38..2dbb2393d84 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 11b6ff07ae9..4b670d5a36f 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. @@ -24,6 +25,7 @@ limitations under the License. const anotherjson = require('another-json'); import Promise from 'bluebird'; import {EventEmitter} from 'events'; +import ReEmitter from '../ReEmitter'; import logger from '../logger'; const utils = require("../utils"); @@ -34,6 +36,8 @@ const DeviceInfo = require("./deviceinfo"); const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceList = require('./DeviceList').default; import { randomString } from '../randomstring'; +import { CrossSigningInfo, UserTrustLevel, DeviceTrustLevel } from './CrossSigning'; +import SecretStorage from './Secrets'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; @@ -100,6 +104,10 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200; */ export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { + this._onDeviceListUserCrossSigningUpdated = + this._onDeviceListUserCrossSigningUpdated.bind(this); + + this._reEmitter = new ReEmitter(this); this._baseApis = baseApis; this._sessionStore = sessionStore; this._userId = userId; @@ -138,6 +146,12 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._deviceList = new DeviceList( baseApis, cryptoStore, this._olmDevice, ); + // 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( + 'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated, + ); + this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated"]); // the last time we did a check for the number of one-time-keys on the // server. @@ -188,6 +202,14 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, this._lastNewSessionForced = {}; this._verificationTransactions = new Map(); + + this._crossSigningInfo = new CrossSigningInfo( + userId, this._baseApis._cryptoCallbacks, + ); + + this._secretStorage = new SecretStorage( + baseApis, this._baseApis._cryptoCallbacks, this._crossSigningInfo, + ); } utils.inherits(Crypto, EventEmitter); @@ -236,10 +258,413 @@ Crypto.prototype.init = async function() { this._deviceList.saveIfDirty(); } + await this._cryptoStore.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.getCrossSigningKeys(txn, (keys) => { + if (keys) { + this._crossSigningInfo.setKeys(keys); + } + }); + }, + ); + // make sure we are keeping track of our own devices + // (this is important for key backups & things) + this._deviceList.startTrackingDeviceList(this._userId); + logger.log("Crypto: checking for key backup..."); this._checkAndStartKeyBackup(); }; +Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) { + return this._secretStorage.addKey(algorithm, opts, keyID); +}; + +Crypto.prototype.storeSecret = function(name, secret, keys) { + return this._secretStorage.store(name, secret, keys); +}; + +Crypto.prototype.getSecret = function(name) { + return this._secretStorage.get(name); +}; + +Crypto.prototype.isSecretStored = function(name, checkKey) { + return this._secretStorage.isStored(name, checkKey); +}; + +Crypto.prototype.requestSecret = function(name, devices) { + if (!devices) { + devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); + } + return this._secretStorage.request(name, devices); +}; + +Crypto.prototype.getDefaultKeyId = function() { + return this._secretStorage.getDefaultKeyId(); +}; + +Crypto.prototype.setDefaultKeyId = function(k) { + return this._secretStorage.setDefaultKeyId(k); +}; + +/** + * Checks that a given private key matches a given public key + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {Uint8Array} expectedPublicKey The public key supplied by the getCrossSigningKey callback + * @returns {boolean} true if the key matches, otherwise false + */ +Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) { + let signing = null; + try { + signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privateKey); + // make sure it agrees with the given pubkey + return gotPubkey === expectedPublicKey; + } finally { + if (signing) signing.free(); + } +}; + +/** + * Generate new cross-signing keys. + * + * @param {object} authDict Auth data to supply for User-Interactive auth. + * @param {CrossSigningLevel} [level] the level of cross-signing to reset. New + * keys will be created for the given level and below. Defaults to + * regenerating all keys. + */ +Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { + await this._crossSigningInfo.resetKeys(level); + await this._signObject(this._crossSigningInfo.keys.master); + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); + }, + ); + + // send keys to server + const keys = {}; + for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) { + keys[name + "_key"] = key; + } + await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys); + this._baseApis.emit("crossSigning.keysChanged", {}); + + // sign the current device with the new key, and upload to the server + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); + await this._baseApis.uploadKeySignatures({ + [this._userId]: { + [this._deviceId]: signedDevice, + }, + }); + + // check all users for signatures + // FIXME: do this in batches + const users = {}; + for (const [userId, crossSigningInfo] + of Object.entries(this._deviceList._crossSigningInfo)) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade( + userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId), + ); + if (upgradeInfo) { + users[userId] = upgradeInfo; + } + } + + const shouldUpgradeCb = ( + this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (Object.keys(users).length > 0 && shouldUpgradeCb) { + try { + const usersToUpgrade = await shouldUpgradeCb({users: users}); + if (usersToUpgrade) { + for (const userId of usersToUpgrade) { + if (userId in users) { + await this._baseApis.setDeviceVerified( + userId, users[userId].crossSigningInfo.getId(), + ); + } + } + } + } catch (e) { + logger.log( + "shouldUpgradeDeviceVerifications threw an error: not upgrading", e, + ); + } + } +}; + +/** + * Check if a user's cross-signing key is a candidate for upgrading from device + * verification. + * + * @param {string} userId the user whose cross-signing information is to be checked + * @param {object} crossSigningInfo the cross-signing information to check + */ +Crypto.prototype._checkForDeviceVerificationUpgrade = async function( + userId, crossSigningInfo, +) { + // only upgrade if this is the first cross-signing key that we've seen for + // them, and if their cross-signing key isn't already verified + const trustLevel = this._crossSigningInfo.checkUserTrust(crossSigningInfo); + if (crossSigningInfo.firstUse && !trustLevel.verified) { + const devices = this._deviceList.getRawStoredDevicesForUser(userId); + const deviceIds = await this._checkForValidDeviceSignature( + userId, crossSigningInfo.keys.master, devices, + ); + if (deviceIds.length) { + return { + devices: deviceIds.map( + deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId), + ), + crossSigningInfo, + }; + } + } +}; + +/** + * Check if the cross-signing key is signed by a verified device. + * + * @param {string} userId the user ID whose key is being checked + * @param {object} key the key that is being checked + * @param {object} devices the user's devices. Should be a map from device ID + * to device info + */ +Crypto.prototype._checkForValidDeviceSignature = async function(userId, key, devices) { + const deviceIds = []; + if (devices && key.signatures && key.signatures[userId]) { + for (const signame of Object.keys(key.signatures[userId])) { + const [, deviceId] = signame.split(':', 2); + if (deviceId in devices + && devices[deviceId].verified === DeviceVerification.VERIFIED) { + try { + await olmlib.verifySignature( + this._olmDevice, + key, + userId, + deviceId, + devices[deviceId].keys[signame], + ); + deviceIds.push(deviceId); + } catch (e) {} + } + } + } + return deviceIds; +}; + +/** + * Get the user's cross-signing key ID. + * + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ +Crypto.prototype.getCrossSigningId = function(type) { + return this._crossSigningInfo.getId(type); +}; + +/** + * Get the cross signing information for a given user. + * + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing informmation for the user. + */ +Crypto.prototype.getStoredCrossSigningForUser = function(userId) { + return this._deviceList.getStoredCrossSigningForUser(userId); +}; + +/** + * Check whether a given user is trusted. + * + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ +Crypto.prototype.checkUserTrust = function(userId) { + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (!userCrossSigning) { + return new UserTrustLevel(false, false); + } + return this._crossSigningInfo.checkUserTrust(userCrossSigning); +}; + +/** + * Check whether a given device is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ +Crypto.prototype.checkDeviceTrust = function(userId, deviceId) { + const device = this._deviceList.getStoredDevice(userId, deviceId); + const trustedLocally = device && device.isVerified(); + + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (device && userCrossSigning) { + return this._crossSigningInfo.checkDeviceTrust( + userCrossSigning, device, trustedLocally, + ); + } else { + return new DeviceTrustLevel(false, false, trustedLocally); + } +}; + +/* + * Event handler for DeviceList's userNewDevices event + */ +Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) { + if (userId === this._userId) { + // An update to our own cross-signing key. + // Get the new key first: + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; + const currentPubkey = this._crossSigningInfo.getId(); + const changed = currentPubkey !== seenPubkey; + + if (currentPubkey && seenPubkey && !changed) { + // If it's not changed, just make sure everything is up to date + await this.checkOwnCrossSigningTrust(); + } else { + this.emit("crossSigning.keysChanged", {}); + // We'll now be in a state where cross-signing on the account is not trusted + // because our locally stored cross-signing keys will not match the ones + // on the server for our account. The app must call checkOwnCrossSigningTrust() + // to fix this. + // XXX: Do we need to do something to emit events saying every device has become + // untrusted? + } + } else { + await this._checkDeviceVerifications(userId); + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + } +}; + +/* + * 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.checkOwnCrossSigningTrust = async function() { + const userId = this._userId; + + // 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 new cross-signing info + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + if (!newCrossSigning) { + logger.error( + "Got cross-signing update event for user " + userId + + " but no new cross-signing information found!", + ); + return; + } + + const seenPubkey = newCrossSigning.getId(); + const changed = this._crossSigningInfo.getId() !== seenPubkey; + if (changed) { + // try to get the private key if the master key changed + logger.info("Got new master key", seenPubkey); + + let signing = null; + try { + const ret = await this._crossSigningInfo.getCrossSigningKey( + 'master', seenPubkey, + ); + signing = ret[1]; + } finally { + signing.free(); + } + + logger.info("Got matching private key from callback for new public master key"); + } + + const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); + const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); + + // Update the version of our keys in our cross-signing object and the local store + this._crossSigningInfo.setKeys(newCrossSigning.keys); + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], + (txn) => { + this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); + }, + ); + + const keySignatures = {}; + + if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) { + logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); + + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + const signedDevice = await this._crossSigningInfo.signDevice( + this._userId, device, + ); + keySignatures[this._deviceId] = signedDevice; + } + if (oldUserSigningId !== newCrossSigning.getId("user_signing")) { + logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); + } + + if (changed) { + await this._signObject(this._crossSigningInfo.keys.master); + keySignatures[this._crossSigningInfo.getId()] + = this._crossSigningInfo.keys.master; + } + + if (Object.keys(keySignatures).length) { + await this._baseApis.uploadKeySignatures({[this._userId]: keySignatures}); + } + + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + + // Now we may be able to trust our key backup + await this.checkKeyBackup(); + // FIXME: if we previously trusted the backup, should we automatically sign + // the backup with the new key (if not already signed)? +}; + +/** + * Check if the master key is signed by a verified device, and if so, prompt + * the application to mark it as verified. + * + * @param {string} userId the user ID whose key should be checked + */ +Crypto.prototype._checkDeviceVerifications = async function(userId) { + if (this._crossSigningInfo.keys.user_signing) { + const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId); + if (crossSigningInfo) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade( + userId, crossSigningInfo, + ); + const shouldUpgradeCb = ( + this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications + ); + if (upgradeInfo && shouldUpgradeCb) { + const usersToUpgrade = await shouldUpgradeCb({ + users: { + [userId]: upgradeInfo, + }, + }); + if (usersToUpgrade.includes(userId)) { + await this._baseApis.setDeviceVerified( + userId, crossSigningInfo.getId(), + ); + } + } + } + } +}; + /** * Check the server for an active key backup and * if one is present and has a valid signature from @@ -362,7 +787,35 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { logger.log("Ignoring unknown signature type: " + keyIdParts[0]); continue; } + // Could be an SSK but just say this is the device ID for backwards compat const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID? + + // first check to see if it's from our cross-signing key + const crossSigningId = this._crossSigningInfo.getId(); + if (crossSigningId === keyId) { + sigInfo.cross_signing_key = crossSigningId; + try { + await olmlib.verifySignature( + this._olmDevice, + backupInfo.auth_data, + this._userId, + sigInfo.deviceId, + crossSigningId, + ); + sigInfo.valid = true; + } catch (e) { + logger.warning( + "Bad signature from cross signing key " + crossSigningId, e, + ); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + continue; + } + + // Now look for a sig from a device + // At some point this can probably go away and we'll just support + // it being signed by the SSK const device = this._deviceList.getStoredDevice( this._userId, sigInfo.deviceId, ); @@ -394,10 +847,14 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ret.sigs.push(sigInfo); } - ret.usable = ( - ret.sigs.some((s) => s.valid && s.device.isVerified()) || - ret.trusted_locally - ); + ret.usable = ret.sigs.some((s) => { + return ( + s.valid && ( + (s.device && s.device.isVerified()) || + (s.cross_signing_key) + ) + ); + }); return ret; }; @@ -493,7 +950,7 @@ Crypto.prototype.uploadDeviceKeys = function() { }; return crypto._signObject(deviceKeys).then(() => { - crypto._baseApis.uploadKeysRequest({ + return crypto._baseApis.uploadKeysRequest({ device_keys: deviceKeys, }, { // for now, we set the device id explicitly, as we may not be using the @@ -721,6 +1178,36 @@ Crypto.prototype.saveDeviceList = function(delay) { Crypto.prototype.setDeviceVerification = async function( userId, deviceId, verified, blocked, known, ) { + // get rid of any `undefined`s here so we can just check + // for null rather than null or undefined + if (verified === undefined) verified = null; + if (blocked === undefined) blocked = null; + if (known === undefined) known = null; + + // Check if the 'device' is actually a cross signing key + // The js-sdk's verification treats cross-signing keys as devices + // and so uses this method to mark them verified. + const xsk = this._deviceList.getStoredCrossSigningForUser(userId); + if (xsk && xsk.getId() === deviceId) { + if (blocked !== null || known !== null) { + throw new Error("Cannot set blocked or known for a cross-signing key"); + } + if (!verified) { + throw new Error("Cannot set a cross-signing key as unverified"); + } + const device = await this._crossSigningInfo.signUser(xsk); + if (device) { + await this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + // This will emit events when it comes back down the sync + // (we could do local echo to speed things up) + } + return device; + } + const devices = this._deviceList.getRawStoredDevicesForUser(userId); if (!devices || !devices[deviceId]) { throw new Error("Unknown device " + userId + ":" + deviceId); @@ -742,7 +1229,7 @@ Crypto.prototype.setDeviceVerification = async function( } let knownStatus = dev.known; - if (known !== null && known !== undefined) { + if (known !== null) { knownStatus = known; } @@ -752,7 +1239,25 @@ Crypto.prototype.setDeviceVerification = async function( this._deviceList.storeDevicesForUser(userId, devices); this._deviceList.saveIfDirty(); } - return DeviceInfo.fromStorage(dev, deviceId); + + // do cross-signing + if (verified && userId === this._userId) { + const device = await this._crossSigningInfo.signDevice( + userId, DeviceInfo.fromStorage(dev, deviceId), + ); + if (device) { + await this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device, + }, + }); + // XXX: we'll need to wait for the device list to be updated + } + } + + const deviceObj = DeviceInfo.fromStorage(dev, deviceId); + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + return deviceObj; }; @@ -1617,6 +2122,8 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // at which point we'll start tracking all the users of that room. logger.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); + // we always track our own device list (for key backups etc) + this._deviceList.startTrackingDeviceList(this._userId); this._roomDeviceTrackingState = {}; } }; @@ -1730,6 +2237,10 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { this._onRoomKeyRequestEvent(event); + } else if (event.getType() === "m.secret.request") { + this._secretStorage._onRequestReceived(event); + } else if (event.getType() === "m.secret.send") { + this._secretStorage._onSecretReceived(event); } else if (event.getType() === "m.key.verification.request") { this._onKeyVerificationRequest(event); } else if (event.getType() === "m.key.verification.start") { @@ -2388,11 +2899,17 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) { * @param {Object} obj Object to which we will add a 'signatures' property */ Crypto.prototype._signObject = async function(obj) { - const sigs = {}; - sigs[this._userId] = {}; + const sigs = obj.signatures || {}; + const unsigned = obj.unsigned; + + delete obj.signatures; + delete obj.unsigned; + + sigs[this._userId] = sigs[this._userId] || {}; sigs[this._userId]["ed25519:" + this._deviceId] = await this._olmDevice.sign(anotherjson.stringify(obj)); obj.signatures = sigs; + if (unsigned !== undefined) obj.unsigned = unsigned; }; diff --git a/src/crypto/backup_password.js b/src/crypto/key_passphrase.js similarity index 88% rename from src/crypto/backup_password.js rename to src/crypto/key_passphrase.js index 1a6d1f28426..057bae84cbc 100644 --- a/src/crypto/backup_password.js +++ b/src/crypto/key_passphrase.js @@ -1,5 +1,6 @@ /* Copyright 2018 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. @@ -18,13 +19,11 @@ import { randomString } from '../randomstring'; const DEFAULT_ITERATIONS = 500000; -export async function keyForExistingBackup(backupData, password) { +export async function keyFromAuthData(authData, password) { if (!global.Olm) { throw new Error("Olm is not available"); } - const authData = backupData.auth_data; - if (!authData.private_key_salt || !authData.private_key_iterations) { throw new Error( "Salt and/or iterations not found: " + @@ -33,12 +32,12 @@ export async function keyForExistingBackup(backupData, password) { } return await deriveKey( - password, backupData.auth_data.private_key_salt, - backupData.auth_data.private_key_iterations, + password, authData.private_key_salt, + authData.private_key_iterations, ); } -export async function keyForNewBackup(password) { +export async function keyFromPassphrase(password) { if (!global.Olm) { throw new Error("Olm is not available"); } diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 5491d3c5a33..93a492e26b6 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -328,11 +328,74 @@ const _verifySignature = module.exports.verifySignature = async function( // prepare the canonical json: remove unsigned and signatures, and stringify with // anotherjson - delete obj.unsigned; - delete obj.signatures; - const json = anotherjson.stringify(obj); + const mangledObj = Object.assign({}, obj); + delete mangledObj.unsigned; + delete mangledObj.signatures; + const json = anotherjson.stringify(mangledObj); olmDevice.verifySignature( signingKey, json, signature, ); }; + +/** + * Sign a JSON object using public key cryptography + * @param {Object} obj Object to sign. The object will be modified to include + * the new signature + * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * seed + * @param {string} userId The user ID who owns the signing key + * @param {string} pubkey The public key (ignored if key is a seed) + * @returns {string} the signature for the object + */ +module.exports.pkSign = function(obj, key, userId, pubkey) { + let createdKey = false; + if (key instanceof Uint8Array) { + const keyObj = new global.Olm.PkSigning(); + pubkey = keyObj.init_with_seed(key); + key = keyObj; + createdKey = true; + } + const sigs = obj.signatures || {}; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + try { + const mysigs = sigs[userId] || {}; + sigs[userId] = mysigs; + + return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj)); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + if (createdKey) { + key.free(); + } + } +}; + +/** + * Verify a signed JSON object + * @param {Object} obj Object to verify + * @param {string} pubkey The public key to use to verify + * @param {string} userId The user ID who signed the object + */ +module.exports.pkVerify = function(obj, pubkey, userId) { + const keyId = "ed25519:" + pubkey; + if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) { + throw new Error("No signature"); + } + const signature = obj.signatures[userId][keyId]; + const util = new global.Olm.Utility(); + const sigs = obj.signatures; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + try { + util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + util.free(); + } +}; diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 20828d2e8e6..98c5d8945e4 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -332,6 +332,23 @@ export class Backend { objectStore.put(newData, "-"); } + getCrossSigningKeys(txn, func) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get("crossSigningKeys"); + getReq.onsuccess = function() { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeCrossSigningKeys(txn, keys) { + const objectStore = txn.objectStore("account"); + objectStore.put(keys, "crossSigningKeys"); + } + // Olm Sessions countEndToEndSessions(txn, func) { diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 480ff9f1a30..b2c2fe4d058 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -290,7 +290,7 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getAccount(txn, func); } - /* + /** * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * @@ -301,6 +301,28 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().storeAccount(txn, newData); } + /** + * Get the public part of the cross-signing keys (eg. self-signing key, + * user signing key). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account keys object: + * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + */ + getCrossSigningKeys(txn, func) { + this._backendPromise.value().getCrossSigningKeys(txn, func); + } + + /** + * Write the cross-signing keys back to the store + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} keys keys object as getCrossSigningKeys() + */ + storeCrossSigningKeys(txn, keys) { + this._backendPromise.value().storeCrossSigningKeys(txn, keys); + } + // Olm sessions /** diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 7af2bf676cc..88e664ba09e 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store'; const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; @@ -284,6 +285,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { ); } + getCrossSigningKeys(txn, func) { + const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); + func(keys); + } + + storeCrossSigningKeys(txn, keys) { + setJsonItem( + this.store, KEY_CROSS_SIGNING_KEYS, keys, + ); + } + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 49df5f238e1..2897be81e7e 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -33,6 +33,7 @@ export default class MemoryCryptoStore { constructor() { this._outgoingRoomKeyRequests = []; this._account = null; + this._crossSigningKeys = null; // Map of {devicekey -> {sessionId -> session pickle}} this._sessions = {}; @@ -234,6 +235,14 @@ export default class MemoryCryptoStore { this._account = newData; } + getCrossSigningKeys(txn, func) { + func(this._crossSigningKeys); + } + + storeCrossSigningKeys(txn, keys) { + this._crossSigningKeys = keys; + } + // Olm Sessions countEndToEndSessions(txn, func) { diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.js index dbb244f295d..cf65663c743 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.js @@ -22,6 +22,7 @@ limitations under the License. import {MatrixEvent} from '../../models/event'; import {EventEmitter} from 'events'; import logger from '../../logger'; +import DeviceInfo from '../deviceinfo'; import {newTimeoutError} from "./Error"; const timeoutException = new Error("Verification timed out"); @@ -271,11 +272,24 @@ export default class VerificationBase extends EventEmitter { for (const [keyId, keyInfo] of Object.entries(keys)) { const deviceId = keyId.split(':', 2)[1]; const device = await this._baseApis.getStoredDevice(userId, deviceId); - if (!device) { - logger.warn(`verification: Could not find device ${deviceId} to verify`); - } else { + if (device) { await verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); + } else { + const crossSigningInfo = this._baseApis._crypto._deviceList + .getStoredCrossSigningForUser(userId); + if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { + await verifier(keyId, DeviceInfo.fromStorage({ + keys: { + [keyId]: deviceId, + }, + }, deviceId), keyInfo); + verifiedDevices.push(deviceId); + } else { + logger.warn( + `verification: Could not find device ${deviceId} to verify`, + ); + } } } diff --git a/src/crypto/verification/SAS.js b/src/crypto/verification/SAS.js index a2af399cfbe..d1cb0efe169 100644 --- a/src/crypto/verification/SAS.js +++ b/src/crypto/verification/SAS.js @@ -355,19 +355,32 @@ export default class SAS extends Base { } _sendMAC(olmSAS, method) { - const keyId = `ed25519:${this._baseApis.deviceId}`; const mac = {}; + const keyList = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this._baseApis.getUserId() + this._baseApis.deviceId + this.userId + this.deviceId + this.transactionId; - mac[keyId] = olmSAS[macMethods[method]]( + const deviceKeyId = `ed25519:${this._baseApis.deviceId}`; + mac[deviceKeyId] = olmSAS[macMethods[method]]( this._baseApis.getDeviceEd25519Key(), - baseInfo + keyId, + baseInfo + deviceKeyId, ); + keyList.push(deviceKeyId); + + const crossSigningId = this._baseApis.getCrossSigningId(); + if (crossSigningId) { + const crossSigningKeyId = `ed25519:${crossSigningId}`; + mac[crossSigningKeyId] = olmSAS[macMethods[method]]( + crossSigningId, + baseInfo + crossSigningKeyId, + ); + keyList.push(crossSigningKeyId); + } + const keys = olmSAS[macMethods[method]]( - keyId, + keyList.sort().join(","), baseInfo + "KEY_IDS", ); this._send("m.key.verification.mac", { mac, keys }); diff --git a/yarn.lock b/yarn.lock index 8fc5beb1a5b..b3335052676 100644 --- a/yarn.lock +++ b/yarn.lock @@ -431,6 +431,15 @@ babel-generator@^6.26.0: source-map "^0.5.7" trim-right "^1.0.1" +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ= + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + babel-helper-call-delegate@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" @@ -451,6 +460,15 @@ babel-helper-define-map@^6.24.1: babel-types "^6.26.0" lodash "^4.17.4" +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo= + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + babel-helper-function-name@^6.24.1, babel-helper-function-name@^6.8.0: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" @@ -539,6 +557,11 @@ babel-plugin-syntax-class-properties@^6.8.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" integrity sha1-1+sjt5oxf4VDlixQW4J8fWysJ94= +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= + babel-plugin-transform-async-to-bluebird@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz#46ea3e7c5af629782ac9f1ed1b7cd38f8425afd4" @@ -749,6 +772,15 @@ babel-plugin-transform-es2015-unicode-regex@^6.24.1: babel-runtime "^6.22.0" regexpu-core "^2.0.0" +babel-plugin-transform-exponentiation-operator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4= + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + babel-plugin-transform-regenerator@^6.24.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" @@ -810,6 +842,13 @@ babel-preset-es2015@^6.18.0: babel-plugin-transform-es2015-unicode-regex "^6.24.1" babel-plugin-transform-regenerator "^6.24.1" +babel-preset-es2016@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz#f900bf93e2ebc0d276df9b8ab59724ebfd959f8b" + integrity sha1-+QC/k+LrwNJ235uKtZck6/2Vn4s= + dependencies: + babel-plugin-transform-exponentiation-operator "^6.24.1" + babel-register@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"