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

Merge pull request #832 from matrix-org/dbkr/cross_signing

Cross Signing Support
This commit is contained in:
David Baker
2019-11-15 16:09:06 +00:00
committed by GitHub
25 changed files with 3640 additions and 155 deletions

View File

@@ -1,5 +1,5 @@
{
"presets": ["es2015"],
"presets": ["es2015", "es2016"],
"plugins": [
"transform-class-properties",

View File

@@ -69,6 +69,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",

View File

@@ -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);
};

View File

@@ -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();

View File

@@ -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();
});
});

View File

@@ -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");
});
});

View File

@@ -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)

View File

@@ -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";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+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,
},
"Dynabook",
);
},
};
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,
},
},
};
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);
});
});
});

View File

@@ -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;
}

View File

@@ -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
// ==========================

View File

@@ -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:
* [<key name>, <UInt8Array private key>] or null if it cannot provide
* any of the keys.
* Args:
* {object} keys Information about the keys:
* {
* <key name>: {
* 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>} 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,
);
@@ -4870,6 +5127,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"
@@ -4927,6 +5210,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
/**

483
src/crypto/CrossSigning.js Normal file
View File

@@ -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;
}
}

View File

@@ -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;
{
// 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 || {},
);
// 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._setRawStoredDevicesForUser(userId, storage);
this._deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(),
);
// NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted
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;
}

527
src/crypto/Secrets.js Normal file
View File

@@ -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);
}
}
}

View File

@@ -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,
};
};

View File

@@ -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;
};

View File

@@ -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");
}

View File

@@ -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();
}
};

View File

@@ -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) {

View File

@@ -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
/**

View File

@@ -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));
}

View File

@@ -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) {

View File

@@ -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");
@@ -277,11 +278,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`,
);
}
}
}

View File

@@ -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 });

View File

@@ -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"