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

add methods for signing and checking users and devices with cross-signing

This commit is contained in:
Hubert Chathi
2019-05-03 18:05:36 -04:00
parent 32814d1833
commit ec2f07e1aa
5 changed files with 212 additions and 14 deletions

View File

@@ -762,6 +762,29 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
return this._crypto.getGlobalBlacklistUnverifiedDevices(); return this._crypto.getGlobalBlacklistUnverifiedDevices();
}; };
/**
* returns a function that just calls the corresponding function from this._crypto.
*
* @param {string} name the function to call
*
* @return {Function} a wrapper function
*/
function wrapCryptoFunc(name) {
return 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
};
}
MatrixClient.prototype.checkUserTrust
= wrapCryptoFunc("checkUserTrust");
MatrixClient.prototype.checkDeviceTrust
= wrapCryptoFunc("checkDeviceTrust");
/** /**
* Get e2e information on the device that sent an event * Get e2e information on the device that sent an event
* *
@@ -793,6 +816,12 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
return device.isVerified(); return device.isVerified();
}; };
MatrixClient.prototype.resetCrossSigningKeys
= wrapCryptoFunc("resetCrossSigningKeys");
MatrixClient.prototype.setCrossSigningKeys
= wrapCryptoFunc("setCrossSigningKeys");
/** /**
* Cancel a room key request for this event if one is ongoing and resend the * Cancel a room key request for this event if one is ongoing and resend the
* request. * request.

View File

@@ -27,7 +27,7 @@ import {EventEmitter} from 'events';
import logger from '../logger'; import logger from '../logger';
import DeviceInfo from './deviceinfo'; import DeviceInfo from './deviceinfo';
import SskInfo from './sskinfo'; import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning';
import olmlib from './olmlib'; import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -78,7 +78,7 @@ export default class DeviceList extends EventEmitter {
// userId -> { // userId -> {
// [key info] // [key info]
// } // }
this._ssks = {}; this._crossSigningInfo = {};
// map of identity keys to the user who owns it // map of identity keys to the user who owns it
this._userByIdentityKey = {}; this._userByIdentityKey = {};
@@ -345,18 +345,18 @@ export default class DeviceList extends EventEmitter {
return this._devices[userId]; return this._devices[userId];
} }
getRawStoredSskForUser(userId) { getRawStoredCrossSigningForUser(userId) {
return this._ssks[userId]; return this._crossSigningInfo[userId];
} }
getStoredSskForUser(userId) { getStoredCrossSigningForUser(userId) {
if (!this._ssks[userId]) return null; if (!this._crossSigningInfo[userId]) return null;
return SskInfo.fromStorage(this._ssks[userId]); return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId);
} }
storeSskForUser(userId, ssk) { storeCrossSigningForUser(userId, info) {
this._ssks[userId] = ssk; this._crossSigningInfo[userId] = info;
this._dirty = true; this._dirty = true;
} }
@@ -587,8 +587,8 @@ export default class DeviceList extends EventEmitter {
} }
} }
setRawStoredSskForUser(userId, ssk) { setRawStoredCrossSigningForUser(userId, info) {
this._ssks[userId] = ssk; this._crossSigningInfo[userId] = info;
} }
/** /**
@@ -838,6 +838,7 @@ class DeviceListUpdateSerialiser {
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) { userResult) {
// FIXME: this isn't correct any more
let updated = false; let updated = false;
// remove any devices in the store which aren't in the response // remove any devices in the store which aren't in the response
@@ -885,6 +886,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
async function _updateStoredSelfSigningKeyForUser( async function _updateStoredSelfSigningKeyForUser(
_olmDevice, userId, userStore, userResult, _olmDevice, userId, userStore, userResult,
) { ) {
// FIXME: this function may need modifying
let updated = false; let updated = false;
if (userResult.user_id !== userId) { if (userResult.user_id !== userId) {
@@ -925,8 +927,6 @@ async function _updateStoredSelfSigningKeyForUser(
userStore.user_id = userResult.user_id; userStore.user_id = userResult.user_id;
userStore.usage = userResult.usage; userStore.usage = userResult.usage;
userStore.keys = userResult.keys; userStore.keys = userResult.keys;
// reset verification status since its now a new key
userStore.verified = SskInfo.SskVerification.UNVERIFIED;
} }
return updated; return updated;

View File

@@ -35,6 +35,7 @@ import SskInfo from './sskinfo';
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceList = require('./DeviceList').default; const DeviceList = require('./DeviceList').default;
import { randomString } from '../randomstring'; import { randomString } from '../randomstring';
import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning';
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -196,6 +197,14 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._lastNewSessionForced = {}; this._lastNewSessionForced = {};
this._verificationTransactions = new Map(); 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);
});
} }
utils.inherits(Crypto, EventEmitter); utils.inherits(Crypto, EventEmitter);
@@ -251,6 +260,74 @@ Crypto.prototype.init = async function() {
this._checkAndStartKeyBackup(); this._checkAndStartKeyBackup();
}; };
/**
* Generate new cross-signing keys.
*
* @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(level) {
await this._crossSigningInfo.resetKeys(level);
};
/**
* 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);
};
/**
* Check whether a given user is trusted.
*
* @param {string} userId The ID of the user to check.
*
* @returns {integer} a bit mask indicating how the user is trusted (if at all)
* - returnValue & 1: unused
* - returnValue & 2: trust-on-first-use cross-signing key
* - returnValue & 4: user's cross-signing key is verified
*
* TODO: is this a good way of representing it? Or we could return an object
* with different keys, or a set? The advantage of doing it this way is that
* you can define which methods you want to use, "&" with the appopriate mask,
* then test for truthiness. Or if you want to just trust everything, then use
* the value alone. However, I wonder if bit masks are too obscure...
*/
Crypto.prototype.checkUserTrust = function(userId) {
const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (!userCrossSigning) {
return 0;
}
return this._crossSigningInfo.checkUserTrust(userCrossSigning) << 1;
};
/**
* 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 {integer} a bit mask indicating how the user is trusted (if at all)
* - returnValue & 1: device marked as verified
* - returnValue & 2: trust-on-first-use cross-signing key
* - returnValue & 4: user's cross-signing key is verified and device is signed
*
* TODO: see checkUserTrust
*/
Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
const device = this._deviceList.getStoredDevice(userId, deviceId);
let rv = 0;
if (device.isVerified()) {
rv |= 1;
}
rv |= this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device) << 1;
return rv;
};
/* /*
* Event handler for DeviceList's userNewDevices event * Event handler for DeviceList's userNewDevices event
*/ */
@@ -906,6 +983,24 @@ Crypto.prototype.setSskVerification = async function(userId, verified) {
Crypto.prototype.setDeviceVerification = async function( Crypto.prototype.setDeviceVerification = async function(
userId, deviceId, verified, blocked, known, userId, deviceId, verified, blocked, known,
) { ) {
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({
[userId]: {
[deviceId]: device,
},
});
return device;
} else {
// FIXME: ???
}
return;
}
const devices = this._deviceList.getRawStoredDevicesForUser(userId); const devices = this._deviceList.getRawStoredDevicesForUser(userId);
if (!devices || !devices[deviceId]) { if (!devices || !devices[deviceId]) {
throw new Error("Unknown device " + userId + ":" + deviceId); throw new Error("Unknown device " + userId + ":" + deviceId);
@@ -937,6 +1032,18 @@ Crypto.prototype.setDeviceVerification = async function(
this._deviceList.storeDevicesForUser(userId, devices); this._deviceList.storeDevicesForUser(userId, devices);
this._deviceList.saveIfDirty(); this._deviceList.saveIfDirty();
} }
// do cross-signing
if (verified && userId === this._userId) {
const device = await this._crossSigningInfo.signDevice(userId, dev);
// FIXME: mark device as dirty in device list
this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
}
return DeviceInfo.fromStorage(dev, deviceId); return DeviceInfo.fromStorage(dev, deviceId);
}; };

View File

@@ -337,3 +337,65 @@ const _verifySignature = module.exports.verifySignature = async function(
signingKey, json, signature, 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

@@ -302,7 +302,7 @@ export default class IndexedDBCryptoStore {
} }
/** /**
* Get the account keys fort cross-signing (eg. self-signing key, * Get the account keys for cross-signing (eg. self-signing key,
* user signing key). * user signing key).
* *
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().