You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
save cross-signing keys from sync and verify new keys for user
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
const logger = require("../logger");
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
@@ -241,3 +242,133 @@ module.exports.awaitDecryption = function(event) {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const HttpResponse = module.exports.HttpResponse = function(
|
||||
httpLookups, acceptKeepalives,
|
||||
) {
|
||||
this.httpLookups = httpLookups;
|
||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
||||
this.pendingLookup = null;
|
||||
};
|
||||
|
||||
HttpResponse.prototype.request = function HttpResponse(
|
||||
cb, method, path, qp, data, prefix,
|
||||
) {
|
||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const next = this.httpLookups.shift();
|
||||
const logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (this.pendingLookup) {
|
||||
if (this.pendingLookup.method === method
|
||||
&& this.pendingLookup.path === path) {
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path,
|
||||
);
|
||||
}
|
||||
this.pendingLookup = {
|
||||
promise: Promise.defer().promise,
|
||||
method: method,
|
||||
path: path,
|
||||
};
|
||||
return this.pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
});
|
||||
}
|
||||
|
||||
if (next.thenCall) {
|
||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return Promise.defer().promise;
|
||||
};
|
||||
|
||||
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
HttpResponse.PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {},
|
||||
};
|
||||
|
||||
HttpResponse.USER_ID = "@alice:bar";
|
||||
|
||||
HttpResponse.filterResponse = function(userId) {
|
||||
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
return {
|
||||
method: "POST",
|
||||
path: filterPath,
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
HttpResponse.SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: HttpResponse.SYNC_DATA,
|
||||
};
|
||||
|
||||
HttpResponse.defaultResponses = function(userId) {
|
||||
return [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
HttpResponse.filterResponse(userId),
|
||||
HttpResponse.SYNC_RESPONSE,
|
||||
];
|
||||
};
|
||||
|
||||
module.exports.setHttpResponses = function setHttpResponses(
|
||||
client, responses, acceptKeepalives,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(responses, acceptKeepalives);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client._http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
client._http.authedRequest.andCall(httpReq);
|
||||
client._http.authedRequestWithPrefix.andCall(httpReq);
|
||||
client._http.requestWithPrefix.andCall(httpReq);
|
||||
client._http.request.andCall(httpReq);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +24,8 @@ import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
import TestClient from '../../TestClient';
|
||||
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
@@ -86,12 +89,126 @@ describe("Cross Signing", function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.on("cross-signing:newKey", function(e) {
|
||||
// FIXME: ???
|
||||
|
||||
const masterKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
|
||||
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
|
||||
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("cross-signing:keysChanged", (e) => {
|
||||
resolve(e);
|
||||
});
|
||||
});
|
||||
|
||||
alice.once("cross-signing:newKey", (e) => {
|
||||
e.done(masterKey);
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||
|
||||
// feed sync result that includes ssk, usk, device key
|
||||
// client should emit event asking about ssk
|
||||
const responses = [
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
HttpResponse.filterResponse("@alice:example.com"),
|
||||
{
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: {
|
||||
next_batch: "abcdefg",
|
||||
device_lists: {
|
||||
changed: [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/query",
|
||||
data: {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@alice:example.com": {
|
||||
"Osborne2": aliceDevice,
|
||||
},
|
||||
},
|
||||
"master_keys": {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
|
||||
},
|
||||
},
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@alice:example.com": {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self-signing"],
|
||||
keys: {
|
||||
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
},
|
||||
signatures: {
|
||||
"@alice:example.com": {
|
||||
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
|
||||
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
|
||||
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
signed_curve25519: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses);
|
||||
|
||||
await alice.startClient();
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
expect(alice.checkUserTrust("@alice:example.com")).toBe(6);
|
||||
expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7);
|
||||
});
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
|
||||
@@ -21,14 +21,18 @@ limitations under the License.
|
||||
|
||||
import {pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../logger';
|
||||
|
||||
function getPublicKey(keyInfo) {
|
||||
return Object.entries(keyInfo.keys)[0];
|
||||
}
|
||||
|
||||
function getPrivateKey(self, type, check) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const askForKey = (error) => {
|
||||
async function getPrivateKey(self, type, check) {
|
||||
let error;
|
||||
let pubkey;
|
||||
let signing;
|
||||
do {
|
||||
[pubkey, signing] = await new Promise((resolve, reject) => {
|
||||
self.emit("cross-signing:getKey", {
|
||||
type: type,
|
||||
error,
|
||||
@@ -36,9 +40,11 @@ function getPrivateKey(self, type, check) {
|
||||
// FIXME: the key needs to be interpreted?
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const pubkey = signing.init_with_seed(key);
|
||||
const error = check(pubkey, signing);
|
||||
error = check(pubkey, signing);
|
||||
if (error) {
|
||||
return askForKey(error);
|
||||
logger.error(error);
|
||||
signing.free();
|
||||
resolve([null, null]);
|
||||
}
|
||||
resolve([pubkey, signing]);
|
||||
},
|
||||
@@ -46,9 +52,9 @@ function getPrivateKey(self, type, check) {
|
||||
reject(error || new Error("Cancelled"));
|
||||
},
|
||||
});
|
||||
};
|
||||
askForKey();
|
||||
});
|
||||
});
|
||||
} while (!pubkey);
|
||||
return [pubkey, signing];
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
@@ -64,12 +70,11 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
|
||||
// you can't change the userId
|
||||
Object.defineProperty(this, 'userId', {
|
||||
enumerabel: true,
|
||||
enumerable: true,
|
||||
value: userId,
|
||||
});
|
||||
this.keys = {};
|
||||
this.fu = true;
|
||||
// FIXME: add chain of ssks?
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
@@ -85,20 +90,24 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
toStorage() {
|
||||
return {
|
||||
keys: this.keys,
|
||||
verified: this.verified,
|
||||
fu: this.fu,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the ID used to identify the user
|
||||
*
|
||||
* @param {string} type The type of key to get the ID of. One of "master",
|
||||
* "self_signing", or "user_signing". Defaults to "master".
|
||||
*
|
||||
* @return {string} the ID
|
||||
*/
|
||||
getId() {
|
||||
return getPublicKey(this.keys.master)[1];
|
||||
getId(type) {
|
||||
type = type || "master";
|
||||
return this.keys[type] && getPublicKey(this.keys[type])[1];
|
||||
}
|
||||
|
||||
async resetKeys(level) {
|
||||
if (level === undefined || level & 4) {
|
||||
if (level === undefined || level & 4 || !this.keys.master) {
|
||||
level = CrossSigningLevel.MASTER;
|
||||
} else if (level === 0) {
|
||||
return;
|
||||
@@ -109,87 +118,120 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
if (level & 4) {
|
||||
masterSigning = new global.Olm.PkSigning();
|
||||
privateKeys.master = masterSigning.generate_seed();
|
||||
masterPub = masterSigning.init_with_seed(privateKeys.master);
|
||||
keys.master = {
|
||||
user_id: this.userId,
|
||||
usage: ['master'],
|
||||
keys: {
|
||||
['ed25519:' + masterPub]: masterPub,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
[masterPub, masterSigning] = await getPrivateKey(this, "master", (pubkey) => {
|
||||
// make sure it agrees with the pubkey that we have
|
||||
if (pubkey !== getPublicKey(this.keys.master)[1]) {
|
||||
return "Key does not match";
|
||||
try {
|
||||
if (level & 4) {
|
||||
masterSigning = new global.Olm.PkSigning();
|
||||
privateKeys.master = masterSigning.generate_seed();
|
||||
masterPub = masterSigning.init_with_seed(privateKeys.master);
|
||||
keys.master = {
|
||||
user_id: this.userId,
|
||||
usage: ['master'],
|
||||
keys: {
|
||||
['ed25519:' + masterPub]: masterPub,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
[masterPub, masterSigning] = await getPrivateKey(
|
||||
this, "master", (pubkey) => {
|
||||
// make sure it agrees with the pubkey that we have
|
||||
if (pubkey !== getPublicKey(this.keys.master)[1]) {
|
||||
return "Key does not match";
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.SELF_SIGNING) {
|
||||
const sskSigning = new global.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.self_signing = sskSigning.generate_seed();
|
||||
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
|
||||
keys.self_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['self_signing'],
|
||||
keys: {
|
||||
['ed25519:' + sskPub]: sskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
|
||||
} finally {
|
||||
sskSigning.free();
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.SELF_SIGNING) {
|
||||
const sskSigning = new global.Olm.PkSigning();
|
||||
privateKeys.self_signing = sskSigning.generate_seed();
|
||||
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
|
||||
keys.self_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['self_signing'],
|
||||
keys: {
|
||||
['ed25519:' + sskPub]: sskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
|
||||
}
|
||||
if (level & CrossSigningLevel.USER_SIGNING) {
|
||||
const uskSigning = new global.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.user_signing = uskSigning.generate_seed();
|
||||
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
|
||||
keys.user_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['user_signing'],
|
||||
keys: {
|
||||
['ed25519:' + uskPub]: uskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
|
||||
} finally {
|
||||
uskSigning.free();
|
||||
}
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.USER_SIGNING) {
|
||||
const uskSigning = new global.Olm.PkSigning();
|
||||
privateKeys.user_signing = uskSigning.generate_seed();
|
||||
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
|
||||
keys.user_signing = {
|
||||
user_id: this.userId,
|
||||
usage: ['user_signing'],
|
||||
keys: {
|
||||
['ed25519:' + uskPub]: uskPub,
|
||||
},
|
||||
};
|
||||
pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
|
||||
Object.assign(this.keys, keys);
|
||||
this.emit("cross-signing:savePrivateKeys", privateKeys);
|
||||
} finally {
|
||||
if (masterSigning) {
|
||||
masterSigning.free();
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this.keys, keys);
|
||||
this.emit("cross-signing:savePrivateKeys", privateKeys);
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
const signingKeys = {};
|
||||
if (keys.master) {
|
||||
if (keys.master.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in master key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
// First-Use is true if and only if we had no previous key for the user
|
||||
this.fu = !(this.keys.self_signing);
|
||||
signingKeys.master = keys.master;
|
||||
if (!keys.user_signing || !keys.self_signing) {
|
||||
throw new Error("Must have new self-signing and user-signing"
|
||||
+ "keys when new master key is set");
|
||||
}
|
||||
} else {
|
||||
} else if (this.keys.master) {
|
||||
signingKeys.master = this.keys.master;
|
||||
} else {
|
||||
throw new Error("Tried to set cross-signing keys without a master key");
|
||||
}
|
||||
const masterKey = getPublicKey(signingKeys.master)[1];
|
||||
|
||||
// verify signatures
|
||||
if (keys.user_signing) {
|
||||
if (keys.user_signing.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in user_signing key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
try {
|
||||
pkVerify(keys.user_signing, masterKey, this.userId);
|
||||
} catch (e) {
|
||||
logger.error("invalid signature on user-signing key");
|
||||
// FIXME: what do we want to do here?
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (keys.self_signing) {
|
||||
if (keys.self_signing.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
" in self_signing key from " + this.userId;
|
||||
logger.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
try {
|
||||
pkVerify(keys.self_signing, masterKey, this.userId);
|
||||
} catch (e) {
|
||||
logger.error("invalid signature on self-signing key");
|
||||
// FIXME: what do we want to do here?
|
||||
throw e;
|
||||
}
|
||||
@@ -198,6 +240,10 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
// if everything checks out, then save the keys
|
||||
if (keys.master) {
|
||||
this.keys.master = keys.master;
|
||||
// if the master key is set, then the old self-signing and
|
||||
// user-signing keys are obsolete
|
||||
delete this.keys.self_signing;
|
||||
delete this.keys.user_signing;
|
||||
}
|
||||
if (keys.self_signing) {
|
||||
this.keys.self_signing = keys.self_signing;
|
||||
@@ -211,9 +257,13 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => {
|
||||
return;
|
||||
});
|
||||
const otherMaster = key.keys.master;
|
||||
pkSign(otherMaster, usk, this.userId, pubkey);
|
||||
return otherMaster;
|
||||
try {
|
||||
const otherMaster = key.keys.master;
|
||||
pkSign(otherMaster, usk, this.userId, pubkey);
|
||||
return otherMaster;
|
||||
} finally {
|
||||
usk.free();
|
||||
}
|
||||
}
|
||||
|
||||
async signDevice(userId, device) {
|
||||
@@ -223,17 +273,34 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => {
|
||||
return;
|
||||
});
|
||||
const keyObj = {
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
device_id: device.deviceId,
|
||||
user_id: userId,
|
||||
};
|
||||
pkSign(keyObj, ssk, this.userId, pubkey);
|
||||
return keyObj;
|
||||
try {
|
||||
const keyObj = {
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
device_id: device.deviceId,
|
||||
user_id: userId,
|
||||
};
|
||||
pkSign(keyObj, ssk, this.userId, pubkey);
|
||||
return keyObj;
|
||||
} finally {
|
||||
ssk.free();
|
||||
}
|
||||
}
|
||||
|
||||
checkUserTrust(userCrossSigning) {
|
||||
if (this.userId === userCrossSigning.userId
|
||||
&& this.getId() && this.getId() === userCrossSigning.getId()
|
||||
&& this.getId("self_signing")
|
||||
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")) {
|
||||
return CrossSigningVerification.VERIFIED
|
||||
| (this.fu ? CrossSigningVerification.TOFU
|
||||
: CrossSigningVerification.UNVERIFIED);
|
||||
}
|
||||
|
||||
if (!this.keys.user_signing) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let userTrusted;
|
||||
const userMaster = userCrossSigning.keys.master;
|
||||
const uskId = getPublicKey(this.keys.user_signing)[1];
|
||||
@@ -253,6 +320,9 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
const userTrust = this.checkUserTrust(userCrossSigning);
|
||||
|
||||
const userSSK = userCrossSigning.keys.self_signing;
|
||||
if (!userSSK) {
|
||||
return 0;
|
||||
}
|
||||
const deviceObj = deviceToObject(device, userCrossSigning.userId);
|
||||
try {
|
||||
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +28,7 @@ import {EventEmitter} from 'events';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
@@ -754,7 +755,9 @@ class DeviceListUpdateSerialiser {
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const master_keys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
|
||||
// do each user in a separate promise, to avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
@@ -765,7 +768,11 @@ class DeviceListUpdateSerialiser {
|
||||
for (const userId of downloadUsers) {
|
||||
prom = prom.delay(5).then(() => {
|
||||
return this._processQueryResponseForUser(
|
||||
userId, dk[userId], ssks[userId],
|
||||
userId, dk[userId], {
|
||||
master: master_keys[userId],
|
||||
self_signing: ssks[userId],
|
||||
user_signing: usks[userId],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -790,9 +797,11 @@ class DeviceListUpdateSerialiser {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
async _processQueryResponseForUser(userId, dkResponse, sskResponse) {
|
||||
async _processQueryResponseForUser(
|
||||
userId, dkResponse, crossSigningResponse, sskResponse,
|
||||
) {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
logger.log('got self-signing keys for ' + userId + ':', sskResponse);
|
||||
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
|
||||
|
||||
{
|
||||
// map from deviceid -> deviceinfo for this user
|
||||
@@ -818,19 +827,23 @@ class DeviceListUpdateSerialiser {
|
||||
this._deviceList._setRawStoredDevicesForUser(userId, storage);
|
||||
}
|
||||
|
||||
// now do the same for the self-signing key
|
||||
// now do the same for the cross-signing keys
|
||||
{
|
||||
const ssk = this._deviceList.getRawStoredSskForUser(userId) || {};
|
||||
if (crossSigningResponse && Object.keys(crossSigningResponse).length) {
|
||||
const crossSigning
|
||||
= this._deviceList.getStoredCrossSigningForUser(userId)
|
||||
|| new CrossSigningInfo(userId);
|
||||
|
||||
const updated = await _updateStoredSelfSigningKeyForUser(
|
||||
this._olmDevice, userId, ssk, sskResponse || {},
|
||||
);
|
||||
crossSigning.setKeys(crossSigningResponse);
|
||||
|
||||
this._deviceList.setRawStoredSskForUser(userId, ssk);
|
||||
this._deviceList.setRawStoredCrossSigningForUser(
|
||||
userId, crossSigning.toStorage(),
|
||||
);
|
||||
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
if (updated) this._deviceList.emit('userSskUpdated', userId);
|
||||
// NB. Unlike most events in the js-sdk, this one is internal to the
|
||||
// js-sdk and is not re-emitted
|
||||
this._deviceList.emit('userCrossSigningUpdated', userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,55 +896,6 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function _updateStoredSelfSigningKeyForUser(
|
||||
_olmDevice, userId, userStore, userResult,
|
||||
) {
|
||||
// FIXME: this function may need modifying
|
||||
let updated = false;
|
||||
|
||||
if (userResult.user_id !== userId) {
|
||||
logger.warn("Mismatched user_id " + userResult.user_id +
|
||||
" in self-signing key from " + userId);
|
||||
return;
|
||||
}
|
||||
if (!userResult || !userResult.usage.includes('self_signing')) {
|
||||
logger.warn(
|
||||
"Self-signing key for " + userId +
|
||||
" does not include 'self_signing' usage: ignoring",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const keyCount = Object.keys(userResult.keys).length;
|
||||
if (keyCount !== 1) {
|
||||
logger.warn(
|
||||
"Self-signing key block for " + userId + " has " +
|
||||
keyCount + " keys: expected exactly 1. Ignoring.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let oldKeyId = null;
|
||||
let oldKey = null;
|
||||
if (userStore.keys && Object.keys(userStore.keys).length > 0) {
|
||||
oldKeyId = Object.keys(userStore.keys)[0];
|
||||
oldKey = userStore.keys[oldKeyId];
|
||||
}
|
||||
const newKeyId = Object.keys(userResult.keys)[0];
|
||||
const newKey = userResult.keys[newKeyId];
|
||||
if (oldKeyId !== newKeyId || oldKey !== newKey) {
|
||||
updated = true;
|
||||
logger.info(
|
||||
"New self-signing key detected for " + userId +
|
||||
": " + newKeyId + ", was previously " + oldKeyId,
|
||||
);
|
||||
|
||||
userStore.user_id = userResult.user_id;
|
||||
userStore.usage = userResult.usage;
|
||||
userStore.keys = userResult.keys;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process a device in a /query response, and add it to the userStore
|
||||
*
|
||||
@@ -955,6 +919,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
}
|
||||
|
||||
const unsigned = deviceResult.unsigned || {};
|
||||
const signatures = deviceResult.signatures || {};
|
||||
|
||||
try {
|
||||
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||
@@ -987,5 +952,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
deviceStore.keys = deviceResult.keys || {};
|
||||
deviceStore.algorithms = deviceResult.algorithms || [];
|
||||
deviceStore.unsigned = unsigned;
|
||||
deviceStore.signatures = signatures;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -31,11 +32,10 @@ const OlmDevice = require("./OlmDevice");
|
||||
const olmlib = require("./olmlib");
|
||||
const algorithms = require("./algorithms");
|
||||
const DeviceInfo = require("./deviceinfo");
|
||||
import SskInfo from './sskinfo';
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
import { randomString } from '../randomstring';
|
||||
import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning';
|
||||
import { CrossSigningInfo } from './CrossSigning';
|
||||
|
||||
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
@@ -104,7 +104,7 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
*/
|
||||
export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
clientStore, cryptoStore, roomList, verificationMethods) {
|
||||
this._onDeviceListUserSskUpdated = this._onDeviceListUserSskUpdated.bind(this);
|
||||
this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this);
|
||||
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
@@ -146,7 +146,7 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
);
|
||||
// XXX: This isn't removed at any point, but then none of the event listeners
|
||||
// this class sets seem to be removed at any point... :/
|
||||
this._deviceList.on('userSskUpdated', this._onDeviceListUserSskUpdated);
|
||||
this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated);
|
||||
|
||||
// the last time we did a check for the number of one-time-keys on the
|
||||
// server.
|
||||
@@ -269,6 +269,7 @@ Crypto.prototype.init = async function() {
|
||||
*/
|
||||
Crypto.prototype.resetCrossSigningKeys = async function(level) {
|
||||
await this._crossSigningInfo.resetKeys(level);
|
||||
this._baseApis.emit("cross-signing:keysChanged", {});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -278,6 +279,7 @@ Crypto.prototype.resetCrossSigningKeys = async function(level) {
|
||||
*/
|
||||
Crypto.prototype.setCrossSigningKeys = function(keys) {
|
||||
this._crossSigningInfo.setKeys(keys);
|
||||
this._baseApis.emit("cross-signing:keysChanged", {});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -331,34 +333,93 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
|
||||
/*
|
||||
* Event handler for DeviceList's userNewDevices event
|
||||
*/
|
||||
Crypto.prototype._onDeviceListUserSskUpdated = async function(userId) {
|
||||
Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
||||
if (userId === this._userId) {
|
||||
this.checkOwnSskTrust();
|
||||
this.checkOwnCrossSigningTrust();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Check the copy of our SSK that we have in the device list and see if it
|
||||
* matches our private part. If it does, mark it as trusted.
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
*/
|
||||
Crypto.prototype.checkOwnSskTrust = async function() {
|
||||
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const userId = this._userId;
|
||||
|
||||
// If we see an update to our own SSK, check it against the SSK we have and,
|
||||
// if it matches, mark it as verified
|
||||
// If we see an update to our own master key, check it against the master
|
||||
// key we have and, if it matches, mark it as verified
|
||||
|
||||
// First, get the pubkey of the one we can see
|
||||
const seenSsk = this._deviceList.getStoredSskForUser(userId);
|
||||
if (!seenSsk) {
|
||||
// First, get the new cross-signing info
|
||||
const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (!newCrossSigning) {
|
||||
logger.error(
|
||||
"Got SSK update event for user " + userId +
|
||||
" but no new SSK found!",
|
||||
"Got cross-signing update event for user " + userId +
|
||||
" but no new cross-signing information found!",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const seenPubkey = seenSsk.getFingerprint();
|
||||
|
||||
const seenPubkey = newCrossSigning.getId();
|
||||
const changed = this._crossSigningInfo.getId() !== seenPubkey;
|
||||
let privkey;
|
||||
if (changed) {
|
||||
// try to get the private key if the master key changed
|
||||
logger.info("Got new master key", seenPubkey);
|
||||
|
||||
let error;
|
||||
do {
|
||||
privkey = await new Promise((resolve, reject) => {
|
||||
this._baseApis.emit("cross-signing:newKey", {
|
||||
publicKey: seenPubkey,
|
||||
type: "master",
|
||||
error,
|
||||
done: (key) => {
|
||||
// check key matches
|
||||
const signing = new global.Olm.PkSigning();
|
||||
try {
|
||||
const pubkey = signing.init_with_seed(key);
|
||||
if (pubkey !== seenPubkey) {
|
||||
error = "Key does not match";
|
||||
logger.info("Key does not match: got " + pubkey
|
||||
+ " expected " + seenPubkey);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
signing.free();
|
||||
}
|
||||
resolve(key);
|
||||
},
|
||||
cancel: (error) => {
|
||||
reject(error || new Error("Cancelled by user"));
|
||||
},
|
||||
});
|
||||
});
|
||||
} while (!privkey);
|
||||
this._baseApis.emit("cross-signing:savePrivateKeys", {master: privkey});
|
||||
|
||||
logger.info("Got private key");
|
||||
}
|
||||
|
||||
// FIXME: fetch the private key?
|
||||
if (this._crossSigningInfo.getId("self_signing")
|
||||
!== newCrossSigning.getId("self_signing")) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
}
|
||||
if (this._crossSigningInfo.getId("user_signing")
|
||||
!== newCrossSigning.getId("user_signing")) {
|
||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
||||
}
|
||||
|
||||
this._crossSigningInfo.setKeys(newCrossSigning.keys);
|
||||
// FIXME: save it ... somewhere?
|
||||
|
||||
if (changed) {
|
||||
this._baseApis.emit("cross-signing:keysChanged", {});
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
// Now dig out the account keys and get the pubkey of the one in there
|
||||
/*
|
||||
let accountKeys = null;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
@@ -401,6 +462,7 @@ Crypto.prototype.checkOwnSskTrust = async function() {
|
||||
localPubkey + ", published: " + seenPubkey,
|
||||
);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -986,7 +1048,6 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
const xsk = this._deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (xsk.getId() === deviceId) {
|
||||
if (verified) {
|
||||
xsk.verified = CrossSigningVerification.VERIFIED;
|
||||
const device = await this._crossSigningInfo.signUser(xsk);
|
||||
// FIXME: mark xsk as dirty in device list
|
||||
this._baseApis.uploadKeySignatures({
|
||||
|
||||
Reference in New Issue
Block a user