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();
};
/**
* 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.

View File

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

View File

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

View File

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

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).
*
* @param {*} txn An active transaction. See doTxn().