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
add methods for signing and checking users and devices with cross-signing
This commit is contained in:
@@ -762,6 +762,29 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
|
||||
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
|
||||
*
|
||||
@@ -793,6 +816,12 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
|
||||
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
|
||||
* request.
|
||||
|
||||
@@ -27,7 +27,7 @@ import {EventEmitter} from 'events';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import SskInfo from './sskinfo';
|
||||
import {CrossSigningInfo, CrossSigningVerification} from './CrossSigning';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
|
||||
@@ -78,7 +78,7 @@ export default class DeviceList extends EventEmitter {
|
||||
// userId -> {
|
||||
// [key info]
|
||||
// }
|
||||
this._ssks = {};
|
||||
this._crossSigningInfo = {};
|
||||
|
||||
// map of identity keys to the user who owns it
|
||||
this._userByIdentityKey = {};
|
||||
@@ -345,18 +345,18 @@ export default class DeviceList extends EventEmitter {
|
||||
return this._devices[userId];
|
||||
}
|
||||
|
||||
getRawStoredSskForUser(userId) {
|
||||
return this._ssks[userId];
|
||||
getRawStoredCrossSigningForUser(userId) {
|
||||
return this._crossSigningInfo[userId];
|
||||
}
|
||||
|
||||
getStoredSskForUser(userId) {
|
||||
if (!this._ssks[userId]) return null;
|
||||
getStoredCrossSigningForUser(userId) {
|
||||
if (!this._crossSigningInfo[userId]) return null;
|
||||
|
||||
return SskInfo.fromStorage(this._ssks[userId]);
|
||||
return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId);
|
||||
}
|
||||
|
||||
storeSskForUser(userId, ssk) {
|
||||
this._ssks[userId] = ssk;
|
||||
storeCrossSigningForUser(userId, info) {
|
||||
this._crossSigningInfo[userId] = info;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
@@ -587,8 +587,8 @@ export default class DeviceList extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
setRawStoredSskForUser(userId, ssk) {
|
||||
this._ssks[userId] = ssk;
|
||||
setRawStoredCrossSigningForUser(userId, info) {
|
||||
this._crossSigningInfo[userId] = info;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -838,6 +838,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
userResult) {
|
||||
// FIXME: this isn't correct any more
|
||||
let updated = false;
|
||||
|
||||
// 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(
|
||||
_olmDevice, userId, userStore, userResult,
|
||||
) {
|
||||
// FIXME: this function may need modifying
|
||||
let updated = false;
|
||||
|
||||
if (userResult.user_id !== userId) {
|
||||
@@ -925,8 +927,6 @@ async function _updateStoredSelfSigningKeyForUser(
|
||||
userStore.user_id = userResult.user_id;
|
||||
userStore.usage = userResult.usage;
|
||||
userStore.keys = userResult.keys;
|
||||
// reset verification status since its now a new key
|
||||
userStore.verified = SskInfo.SskVerification.UNVERIFIED;
|
||||
}
|
||||
|
||||
return updated;
|
||||
|
||||
@@ -35,6 +35,7 @@ import SskInfo from './sskinfo';
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
import { randomString } from '../randomstring';
|
||||
import { CrossSigningInfo, CrossSigningLevel, CrossSigningVerification } from './CrossSigning';
|
||||
|
||||
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
@@ -196,6 +197,14 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._lastNewSessionForced = {};
|
||||
|
||||
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);
|
||||
|
||||
@@ -251,6 +260,74 @@ Crypto.prototype.init = async function() {
|
||||
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
|
||||
*/
|
||||
@@ -906,6 +983,24 @@ Crypto.prototype.setSskVerification = async function(userId, verified) {
|
||||
Crypto.prototype.setDeviceVerification = async function(
|
||||
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);
|
||||
if (!devices || !devices[deviceId]) {
|
||||
throw new Error("Unknown device " + userId + ":" + deviceId);
|
||||
@@ -937,6 +1032,18 @@ Crypto.prototype.setDeviceVerification = async function(
|
||||
this._deviceList.storeDevicesForUser(userId, devices);
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -337,3 +337,65 @@ const _verifySignature = module.exports.verifySignature = async function(
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
|
||||
Reference in New Issue
Block a user