diff --git a/src/client.js b/src/client.js index 4e1b33f4e..ec1dcf6c7 100644 --- a/src/client.js +++ b/src/client.js @@ -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. diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 6a5ec635f..0c715cf07 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -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; diff --git a/src/crypto/index.js b/src/crypto/index.js index 955a52b89..cbcfab78b 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -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); }; diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index f403308d7..db4d21b18 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -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(); + } +}; diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 1b2aa076a..89195f345 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -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().