1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-08 19:08:34 +03:00

various cross-signing fixes and improvements

This commit is contained in:
Hubert Chathi
2019-06-12 11:47:12 -04:00
parent 98815ffdf6
commit 4c6fa89053
7 changed files with 239 additions and 91 deletions

View File

@@ -50,11 +50,16 @@ describe("Cross Signing", function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async function(e) {return;};
alice.uploadKeySignatures = async function(e) {return;};
// set Alice's cross-signing key
let privateKeys;
alice.on("cross-signing:savePrivateKeys", function(e) {
privateKeys = e;
});
alice.on("cross-signing:getKey", function(e) {
e.done(privateKeys[e.type]);
});
await alice.resetCrossSigningKeys();
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
@@ -69,10 +74,6 @@ describe("Cross Signing", function() {
},
});
// Alice verifies Bob's key
alice.on("cross-signing:getKey", function(e) {
expect(e.type).toBe("user_signing");
e.done(privateKeys.user_signing);
});
const promise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = (...args) => {
resolve(...args);
@@ -110,6 +111,10 @@ describe("Cross Signing", function() {
alice.once("cross-signing:newKey", (e) => {
e.done(masterKey);
});
alice.on("cross-signing:getKey", (e) => {
// will be called to sign our own device
e.done(selfSigningKey);
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
@@ -198,8 +203,13 @@ describe("Cross Signing", function() {
},
},
},
{
method: "POST",
path: "/keys/signatures/upload",
data: {},
},
];
setHttpResponses(alice, responses);
setHttpResponses(alice, responses, true, true);
await alice.startClient();
@@ -213,11 +223,16 @@ describe("Cross Signing", function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async function(e) {return;};
alice.uploadKeySignatures = async function(e) {return;};
// set Alice's cross-signing key
let privateKeys;
alice.on("cross-signing:savePrivateKeys", function(e) {
privateKeys = e;
});
alice.on("cross-signing:getKey", function(e) {
e.done(privateKeys[e.type]);
});
await alice.resetCrossSigningKeys();
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
@@ -275,10 +290,6 @@ describe("Cross Signing", function() {
expect(alice.checkUserTrust("@bob:example.com")).toBe(2);
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(2);
// Alice verifies Bob's SSK
alice.on("cross-signing:getKey", function(e) {
expect(e.type).toBe("user_signing");
e.done(privateKeys.user_signing);
});
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
@@ -292,17 +303,18 @@ describe("Cross Signing", function() {
);
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async function(e) {return;};
alice.uploadKeySignatures = async function(e) {return;};
// set Alice's cross-signing key
let privateKeys;
alice.on("cross-signing:savePrivateKeys", function(e) {
privateKeys = e;
});
await alice.resetCrossSigningKeys();
alice.once("cross-signing:getKey", (e) => {
alice.on("cross-signing:getKey", (e) => {
e.done(privateKeys[e.type]);
});
await alice.resetCrossSigningKeys();
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
@@ -450,11 +462,16 @@ describe("Cross Signing", function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async function(e) {return;};
alice.uploadKeySignatures = async function(e) {return;};
// set Alice's cross-signing key
let privateKeys;
alice.on("cross-signing:savePrivateKeys", function(e) {
privateKeys = e;
});
alice.on("cross-signing:getKey", (e) => {
e.done(privateKeys[e.type]);
});
await alice.resetCrossSigningKeys();
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
@@ -506,12 +523,9 @@ describe("Cross Signing", function() {
// Bob's device key should be untrusted
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0);
// Alice verifies Bob's SSK
alice.on("cross-signing:getKey", function(e) {
expect(e.type).toBe("user_signing");
e.done(privateKeys.user_signing);
});
alice.uploadKeySignatures = () => {};
console.log("verifying bob's device");
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
console.log("done");
// Bob's device key should be untrusted
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0);
});
@@ -520,10 +534,15 @@ describe("Cross Signing", function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async function(e) {return;};
alice.uploadKeySignatures = async function(e) {return;};
let privateKeys;
alice.on("cross-signing:savePrivateKeys", function(e) {
privateKeys = e;
});
alice.on("cross-signing:getKey", function(e) {
e.done(privateKeys[e.type]);
});
await alice.resetCrossSigningKeys();
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
@@ -577,10 +596,6 @@ describe("Cross Signing", function() {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
alice.on("cross-signing:getKey", function(e) {
expect(e.type).toBe("user_signing");
e.done(privateKeys.user_signing);
});
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
@@ -624,8 +639,7 @@ describe("Cross Signing", function() {
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(0);
// Alice verifies Bob's SSK
alice.on("cross-signing:getKey", function(e) {
expect(e.type).toBe("user_signing");
e.done(privateKeys.user_signing);
e.done(privateKeys[e.type]);
});
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true);

View File

@@ -1676,9 +1676,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
);
};
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(keys) {
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
const data = Object.assign({}, keys, {auth});
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/device_signing/upload", undefined, keys,
undefined, "POST", "/keys/device_signing/upload", undefined, data,
httpApi.PREFIX_UNSTABLE,
);
};

View File

@@ -559,6 +559,9 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
"crypto.warning",
"crypto.devicesUpdated",
"cross-signing:savePrivateKeys",
"cross-signing:getKey",
]);
logger.log("Crypto: initialising crypto object...");
@@ -795,14 +798,14 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
};
/**
* returns a function that just calls the corresponding function from this._crypto.
* add methods that call the corresponding method in this._crypto
*
* @param {string} name the function to call
*
* @return {Function} a wrapper function
* @param {class} MatrixClient the class to add the method to
* @param {string} names the names of the methods to call
*/
function wrapCryptoFunc(name) {
return function(...args) {
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");
}
@@ -810,12 +813,19 @@ function wrapCryptoFunc(name) {
return this._crypto[name](...args); // eslint-disable-line no-invalid-this
};
}
}
MatrixClient.prototype.checkUserTrust
= wrapCryptoFunc("checkUserTrust");
wrapCryptoFuncs(MatrixClient, [
"checkUserTrust",
"checkDeviceTrust",
]);
MatrixClient.prototype.checkDeviceTrust
= wrapCryptoFunc("checkDeviceTrust");
wrapCryptoFuncs(MatrixClient, [
"storeSecret",
"getSecret",
"isSecretStored",
"requestSecret",
]);
/**
* Get e2e information on the device that sent an event
@@ -848,14 +858,11 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
return device.isVerified();
};
MatrixClient.prototype.resetCrossSigningKeys
= wrapCryptoFunc("resetCrossSigningKeys");
MatrixClient.prototype.setCrossSigningKeys
= wrapCryptoFunc("setCrossSigningKeys");
MatrixClient.prototype.getCrossSigningId
= wrapCryptoFunc("getCrossSigningId");
wrapCryptoFuncs(MatrixClient, [
"resetCrossSigningKeys",
"getCrossSigningId",
"getStoredCrossSigningForUser",
]);
/**
* Cancel a room key request for this event if one is ongoing and resend the

View File

@@ -195,8 +195,13 @@ export class CrossSigningInfo extends EventEmitter {
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);
if (!this.keys.master) {
// this is the first key we've seen, so first-use is true
this.fu = true;
} else if (getPublicKey(keys.master)[1] !== this.getId()) {
// this is a different key, so first-use is false
this.fu = false;
} // otherwise, same key, so no change
signingKeys.master = keys.master;
} else if (this.keys.master) {
signingKeys.master = this.keys.master;
@@ -254,7 +259,11 @@ export class CrossSigningInfo extends EventEmitter {
}
async signUser(key) {
if (!this.keys.user_signing) {
return;
}
const [pubkey, usk] = await getPrivateKey(this, "user_signing", (key) => {
// FIXME:
return;
});
try {
@@ -268,9 +277,15 @@ export class CrossSigningInfo extends EventEmitter {
async signDevice(userId, device) {
if (userId !== this.userId) {
throw new Error("Urgh!");
throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`,
);
}
if (!this.keys.self_signing) {
return;
}
const [pubkey, ssk] = await getPrivateKey(this, "self_signing", (key) => {
// FIXME:
return;
});
try {
@@ -288,6 +303,8 @@ export class CrossSigningInfo extends EventEmitter {
}
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")
@@ -298,7 +315,8 @@ export class CrossSigningInfo extends EventEmitter {
}
if (!this.keys.user_signing) {
return 0;
return (userCrossSigning.fu ? CrossSigningVerification.TOFU
: CrossSigningVerification.UNVERIFIED);
}
let userTrusted;

View File

@@ -655,6 +655,7 @@ export default class DeviceList extends EventEmitter {
}
});
this.saveIfDirty();
this.emit("crypto.devicesUpdated", users);
};
return prom;

View File

@@ -17,6 +17,9 @@ limitations under the License.
import {EventEmitter} from 'events';
import logger from '../logger';
import olmlib from './olmlib';
import { randomString } from '../randomstring';
import { keyForNewBackup } from './backup_password';
import { encodeRecoveryKey, decodeRecoveryKey } from './recoverykey';
/** Implements MSC-1946
*/
@@ -28,6 +31,56 @@ export default class SecretStorage extends EventEmitter {
this._incomingRequests = {};
}
async addKey(type, opts) {
const keyData = {
algorithm: opts.algorithm,
};
switch (opts.algorithm) {
case "m.secret_storage.v1.curve25519-aes-sha2":
{
const decryption = new global.Olm.PkDecryption();
try {
if (opts.passphrase) {
const key = await keyForNewBackup(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}`);
}
let keyName;
do {
keyName = randomString(32);
} while (!this._baseApis.getAccountData(`m.secret_storage.key.${keyName}`));
// FIXME: sign keyData?
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyName}`, keyData,
);
return keyName;
}
/** store an encrypted secret on the server
*
* @param {string} name The name of the secret
@@ -285,7 +338,11 @@ export default class SecretStorage extends EventEmitter {
});
}
} else if (content.action === "request") {
// if from us and device is trusted (or else check trust)
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 + ")");
@@ -308,6 +365,15 @@ export default class SecretStorage extends EventEmitter {
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(),

View File

@@ -25,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");
@@ -107,6 +108,7 @@ 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;
@@ -148,6 +150,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('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.
@@ -200,18 +203,35 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._verificationTransactions = new Map();
this._crossSigningInfo = new CrossSigningInfo(userId);
this._crossSigningInfo.on("cross-signing:savePrivateKeys", (...args) => {
this._baseApis.emit("cross-signing:savePrivateKeys", ...args);
});
this._crossSigningInfo.on("cross-signing:getKey", (...args) => {
this._baseApis.emit("cross-signing:getKey", ...args);
});
this._reEmitter.reEmit(this._crossSigningInfo, [
"cross-signing:savePrivateKeys",
"cross-signing:getKey",
]);
this._secretStorage = new SecretStorage(baseApis);
// TODO: expose SecretStorage methods
}
utils.inherits(Crypto, EventEmitter);
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);
};
/**
* Initialise the crypto module so that it is ready for use
*
@@ -271,19 +291,22 @@ Crypto.prototype.init = async function() {
* keys will be created for the given level and below. Defaults to
* regenerating all keys.
*/
Crypto.prototype.resetCrossSigningKeys = async function(level) {
Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
await this._crossSigningInfo.resetKeys(level);
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("cross-signing:keysChanged", {});
};
/**
* Set the user's cross-signing keys to use.
*
* @param {object} keys A mapping of key type to key data.
*/
Crypto.prototype.setCrossSigningKeys = function(keys) {
this._crossSigningInfo.setKeys(keys);
this._baseApis.emit("cross-signing:keysChanged", {});
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,
},
});
};
/**
@@ -298,6 +321,10 @@ Crypto.prototype.getCrossSigningId = function(type) {
return this._crossSigningInfo.getId(type);
};
Crypto.prototype.getStoredCrossSigningForUser = function(userId) {
return this._deviceList.getStoredCrossSigningForUser(userId);
};
/**
* Check whether a given user is trusted.
*
@@ -419,19 +446,29 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
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"));
}
const oldSelfSigningId = this._crossSigningInfo.getId("self_signing");
const oldUserSigningId = this._crossSigningInfo.getId("user_signing")
this._crossSigningInfo.setKeys(newCrossSigning.keys);
// FIXME: save it ... somewhere?
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,
);
await this._baseApis.uploadKeySignatures({
[this._userId]: {
[this._deviceId]: signedDevice,
},
});
}
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
}
if (changed) {
this._baseApis.emit("cross-signing:keysChanged", {});
}
@@ -1062,12 +1099,13 @@ Crypto.prototype.setDeviceVerification = async function(
if (xsk && xsk.getId() === deviceId) {
if (verified) {
const device = await this._crossSigningInfo.signUser(xsk);
// FIXME: mark xsk as dirty in device list
if (device) {
this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
}
return device;
} else {
// FIXME: ???
@@ -1109,14 +1147,17 @@ Crypto.prototype.setDeviceVerification = async function(
// do cross-signing
if (verified && userId === this._userId) {
const device = await this._crossSigningInfo.signDevice(userId, dev);
// FIXME: mark device as dirty in device list
const device = await this._crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
if (device) {
this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
}
}
return DeviceInfo.fromStorage(dev, deviceId);
};