diff options
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js')
-rw-r--r-- | comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js | 703 |
1 files changed, 703 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js new file mode 100644 index 0000000000..be8c9607f4 --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js @@ -0,0 +1,703 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UserTrustLevel = exports.DeviceTrustLevel = exports.CrossSigningLevel = exports.CrossSigningInfo = void 0; +exports.createCryptoStoreCacheCallbacks = createCryptoStoreCacheCallbacks; +exports.requestKeysDuringVerification = requestKeysDuringVerification; +var _olmlib = require("./olmlib"); +var _logger = require("../logger"); +var _indexeddbCryptoStore = require("../crypto/store/indexeddb-crypto-store"); +var _aes = require("./aes"); +var _cryptoApi = require("../crypto-api"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* + Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ /** + * Cross signing methods + */ +const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; +function publicKeyFromKeyInfo(keyInfo) { + // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } + // We assume only a single key, and we want the bare form without type + // prefix, so we select the values. + return Object.values(keyInfo.keys)[0]; +} +class CrossSigningInfo { + /** + * Information about a user's cross-signing keys + * + * @param userId - the user that the information is about + * @param callbacks - Callbacks used to interact with the app + * Requires getCrossSigningKey and saveCrossSigningKeys + * @param cacheCallbacks - Callbacks used to interact with the cache + */ + constructor(userId, callbacks = {}, cacheCallbacks = {}) { + this.userId = userId; + this.callbacks = callbacks; + this.cacheCallbacks = cacheCallbacks; + _defineProperty(this, "keys", {}); + _defineProperty(this, "firstUse", true); + // This tracks whether we've ever verified this user with any identity. + // When you verify a user, any devices online at the time that receive + // the verifying signature via the homeserver will latch this to true + // and can use it in the future to detect cases where the user has + // become unverified later for any reason. + _defineProperty(this, "crossSigningVerifiedBefore", false); + } + static fromStorage(obj, userId) { + const res = new CrossSigningInfo(userId); + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + // @ts-ignore - ts doesn't like this and nor should we + res[prop] = obj[prop]; + } + } + return res; + } + toStorage() { + return { + keys: this.keys, + firstUse: this.firstUse, + crossSigningVerifiedBefore: this.crossSigningVerifiedBefore + }; + } + + /** + * Calls the app callback to ask for a private key + * + * @param type - The key type ("master", "self_signing", or "user_signing") + * @param expectedPubkey - The matching public key or undefined to use + * the stored public key for the given key type. + * @returns An array with [ public key, Olm.PkSigning ] + */ + async getCrossSigningKey(type, expectedPubkey) { + const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; + if (!this.callbacks.getCrossSigningKey) { + throw new Error("No getCrossSigningKey callback supplied"); + } + if (expectedPubkey === undefined) { + expectedPubkey = this.getId(type); + } + function validateKey(key) { + if (!key) return; + const signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(key); + if (gotPubkey === expectedPubkey) { + return [gotPubkey, signing]; + } + signing.free(); + } + let privkey = null; + if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { + privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); + } + const cacheresult = validateKey(privkey); + if (cacheresult) { + return cacheresult; + } + privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey); + const result = validateKey(privkey); + if (result) { + if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { + await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); + } + return result; + } + + /* No keysource even returned a key */ + if (!privkey) { + throw new Error("getCrossSigningKey callback for " + type + " returned falsey"); + } + + /* We got some keys from the keysource, but none of them were valid */ + throw new Error("Key type " + type + " from getCrossSigningKey callback did not match"); + } + + /** + * Check whether the private keys exist in secret storage. + * XXX: This could be static, be we often seem to have an instance when we + * want to know this anyway... + * + * @param secretStorage - The secret store using account data + * @returns map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key + */ + async isStoredInSecretStorage(secretStorage) { + // check what SSSS keys have encrypted the master key (if any) + const stored = (await secretStorage.isStored("m.cross_signing.master")) || {}; + // then check which of those SSSS keys have also encrypted the SSK and USK + function intersect(s) { + for (const k of Object.keys(stored)) { + if (!s[k]) { + delete stored[k]; + } + } + } + for (const type of ["self_signing", "user_signing"]) { + intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {}); + } + return Object.keys(stored).length ? stored : null; + } + + /** + * Store private keys in secret storage for use by other devices. This is + * typically called in conjunction with the creation of new cross-signing + * keys. + * + * @param keys - The keys to store + * @param secretStorage - The secret store using account data + */ + static async storeInSecretStorage(keys, secretStorage) { + for (const [type, privateKey] of keys) { + const encodedKey = (0, _olmlib.encodeBase64)(privateKey); + await secretStorage.store(`m.cross_signing.${type}`, encodedKey); + } + } + + /** + * Get private keys from secret storage created by some other device. This + * also passes the private keys to the app-specific callback. + * + * @param type - The type of key to get. One of "master", + * "self_signing", or "user_signing". + * @param secretStorage - The secret store using account data + * @returns The private key + */ + static async getFromSecretStorage(type, secretStorage) { + const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); + if (!encodedKey) { + return null; + } + return (0, _olmlib.decodeBase64)(encodedKey); + } + + /** + * Check whether the private keys exist in the local key cache. + * + * @param type - The type of key to get. One of "master", + * "self_signing", or "user_signing". Optional, will check all by default. + * @returns True if all keys are stored in the local cache. + */ + async isStoredInKeyCache(type) { + const cacheCallbacks = this.cacheCallbacks; + if (!cacheCallbacks) return false; + const types = type ? [type] : ["master", "self_signing", "user_signing"]; + for (const t of types) { + if (!(await cacheCallbacks.getCrossSigningKeyCache?.(t))) { + return false; + } + } + return true; + } + + /** + * Get cross-signing private keys from the local cache. + * + * @returns A map from key type (string) to private key (Uint8Array) + */ + async getCrossSigningKeysFromCache() { + const keys = new Map(); + const cacheCallbacks = this.cacheCallbacks; + if (!cacheCallbacks) return keys; + for (const type of ["master", "self_signing", "user_signing"]) { + const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type); + if (!privKey) { + continue; + } + keys.set(type, privKey); + } + return keys; + } + + /** + * Get the ID used to identify the user. This can also be used to test for + * the existence of a given key type. + * + * @param type - The type of key to get the ID of. One of "master", + * "self_signing", or "user_signing". Defaults to "master". + * + * @returns the ID + */ + getId(type = "master") { + if (!this.keys[type]) return null; + const keyInfo = this.keys[type]; + return publicKeyFromKeyInfo(keyInfo); + } + + /** + * Create new cross-signing keys for the given key types. The public keys + * will be held in this class, while the private keys are passed off to the + * `saveCrossSigningKeys` application callback. + * + * @param level - The key types to reset + */ + async resetKeys(level) { + if (!this.callbacks.saveCrossSigningKeys) { + throw new Error("No saveCrossSigningKeys callback supplied"); + } + + // If we're resetting the master key, we reset all keys + if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) { + level = CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | CrossSigningLevel.SELF_SIGNING; + } else if (level === 0) { + return; + } + const privateKeys = {}; + const keys = {}; + let masterSigning; + let masterPub; + try { + if (level & CrossSigningLevel.MASTER) { + masterSigning = new global.Olm.PkSigning(); + privateKeys.master = masterSigning.generate_seed(); + masterPub = masterSigning.init_with_seed(privateKeys.master); + keys.master = { + user_id: this.userId, + usage: ["master"], + keys: { + ["ed25519:" + masterPub]: masterPub + } + }; + } else { + [masterPub, masterSigning] = await this.getCrossSigningKey("master"); + } + if (level & CrossSigningLevel.SELF_SIGNING) { + const sskSigning = new global.Olm.PkSigning(); + try { + privateKeys.self_signing = sskSigning.generate_seed(); + const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); + keys.self_signing = { + user_id: this.userId, + usage: ["self_signing"], + keys: { + ["ed25519:" + sskPub]: sskPub + } + }; + (0, _olmlib.pkSign)(keys.self_signing, masterSigning, this.userId, masterPub); + } finally { + sskSigning.free(); + } + } + if (level & CrossSigningLevel.USER_SIGNING) { + const uskSigning = new global.Olm.PkSigning(); + try { + privateKeys.user_signing = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); + keys.user_signing = { + user_id: this.userId, + usage: ["user_signing"], + keys: { + ["ed25519:" + uskPub]: uskPub + } + }; + (0, _olmlib.pkSign)(keys.user_signing, masterSigning, this.userId, masterPub); + } finally { + uskSigning.free(); + } + } + Object.assign(this.keys, keys); + this.callbacks.saveCrossSigningKeys(privateKeys); + } finally { + if (masterSigning) { + masterSigning.free(); + } + } + } + + /** + * unsets the keys, used when another session has reset the keys, to disable cross-signing + */ + clearKeys() { + this.keys = {}; + } + setKeys(keys) { + const signingKeys = {}; + if (keys.master) { + if (keys.master.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId; + _logger.logger.error(error); + throw new Error(error); + } + if (!this.keys.master) { + // this is the first key we've seen, so first-use is true + this.firstUse = true; + } else if (publicKeyFromKeyInfo(keys.master) !== this.getId()) { + // this is a different key, so first-use is false + this.firstUse = false; + } // otherwise, same key, so no change + signingKeys.master = keys.master; + } else if (this.keys.master) { + signingKeys.master = this.keys.master; + } else { + throw new Error("Tried to set cross-signing keys without a master key"); + } + const masterKey = publicKeyFromKeyInfo(signingKeys.master); + + // verify signatures + if (keys.user_signing) { + if (keys.user_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in user_signing key from " + this.userId; + _logger.logger.error(error); + throw new Error(error); + } + try { + (0, _olmlib.pkVerify)(keys.user_signing, masterKey, this.userId); + } catch (e) { + _logger.logger.error("invalid signature on user-signing key"); + // FIXME: what do we want to do here? + throw e; + } + } + if (keys.self_signing) { + if (keys.self_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in self_signing key from " + this.userId; + _logger.logger.error(error); + throw new Error(error); + } + try { + (0, _olmlib.pkVerify)(keys.self_signing, masterKey, this.userId); + } catch (e) { + _logger.logger.error("invalid signature on self-signing key"); + // FIXME: what do we want to do here? + throw e; + } + } + + // if everything checks out, then save the keys + if (keys.master) { + this.keys.master = keys.master; + // if the master key is set, then the old self-signing and user-signing keys are obsolete + delete this.keys["self_signing"]; + delete this.keys["user_signing"]; + } + if (keys.self_signing) { + this.keys.self_signing = keys.self_signing; + } + if (keys.user_signing) { + this.keys.user_signing = keys.user_signing; + } + } + updateCrossSigningVerifiedBefore(isCrossSigningVerified) { + // It is critical that this value latches forward from false to true but + // never back to false to avoid a downgrade attack. + if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) { + this.crossSigningVerifiedBefore = true; + } + } + async signObject(data, type) { + if (!this.keys[type]) { + throw new Error("Attempted to sign with " + type + " key but no such key present"); + } + const [pubkey, signing] = await this.getCrossSigningKey(type); + try { + (0, _olmlib.pkSign)(data, signing, this.userId, pubkey); + return data; + } finally { + signing.free(); + } + } + async signUser(key) { + if (!this.keys.user_signing) { + _logger.logger.info("No user signing key: not signing user"); + return; + } + return this.signObject(key.keys.master, "user_signing"); + } + async signDevice(userId, device) { + if (userId !== this.userId) { + throw new Error(`Trying to sign ${userId}'s device; can only sign our own device`); + } + if (!this.keys.self_signing) { + _logger.logger.info("No self signing key: not signing device"); + return; + } + return this.signObject({ + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId + }, "self_signing"); + } + + /** + * Check whether a given user is trusted. + * + * @param userCrossSigning - Cross signing info for user + * + * @returns + */ + 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") && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { + return new UserTrustLevel(true, true, this.firstUse); + } + if (!this.keys.user_signing) { + // If there's no user signing key, they can't possibly be verified. + // They may be TOFU trusted though. + return new UserTrustLevel(false, false, userCrossSigning.firstUse); + } + let userTrusted; + const userMaster = userCrossSigning.keys.master; + const uskId = this.getId("user_signing"); + try { + (0, _olmlib.pkVerify)(userMaster, uskId, this.userId); + userTrusted = true; + } catch (e) { + userTrusted = false; + } + return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse); + } + + /** + * Check whether a given device is trusted. + * + * @param userCrossSigning - Cross signing info for user + * @param device - The device to check + * @param localTrust - Whether the device is trusted locally + * @param trustCrossSignedDevices - Whether we trust cross signed devices + * + * @returns + */ + checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { + const userTrust = this.checkUserTrust(userCrossSigning); + const userSSK = userCrossSigning.keys.self_signing; + if (!userSSK) { + // if the user has no self-signing key then we cannot make any + // trust assertions about this device from cross-signing + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); + } + const deviceObj = deviceToObject(device, userCrossSigning.userId); + try { + // if we can verify the user's SSK from their master key... + (0, _olmlib.pkVerify)(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + // ...and this device's key from their SSK... + (0, _olmlib.pkVerify)(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); + // ...then we trust this device as much as far as we trust the user + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices); + } catch (e) { + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); + } + } + + /** + * @returns Cache callbacks + */ + getCacheCallbacks() { + return this.cacheCallbacks; + } +} +exports.CrossSigningInfo = CrossSigningInfo; +function deviceToObject(device, userId) { + return { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + signatures: device.signatures + }; +} +let CrossSigningLevel = /*#__PURE__*/function (CrossSigningLevel) { + CrossSigningLevel[CrossSigningLevel["MASTER"] = 4] = "MASTER"; + CrossSigningLevel[CrossSigningLevel["USER_SIGNING"] = 2] = "USER_SIGNING"; + CrossSigningLevel[CrossSigningLevel["SELF_SIGNING"] = 1] = "SELF_SIGNING"; + return CrossSigningLevel; +}({}); +/** + * Represents the ways in which we trust a user + */ +exports.CrossSigningLevel = CrossSigningLevel; +class UserTrustLevel { + constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { + this.crossSigningVerified = crossSigningVerified; + this.crossSigningVerifiedBefore = crossSigningVerifiedBefore; + this.tofu = tofu; + } + + /** + * @returns true if this user is verified via any means + */ + isVerified() { + return this.isCrossSigningVerified(); + } + + /** + * @returns true if this user is verified via cross signing + */ + isCrossSigningVerified() { + return this.crossSigningVerified; + } + + /** + * @returns true if we ever verified this user before (at least for + * the history of verifications observed by this device). + */ + wasCrossSigningVerified() { + return this.crossSigningVerifiedBefore; + } + + /** + * @returns true if this user's key is trusted on first use + */ + isTofu() { + return this.tofu; + } +} + +/** + * Represents the ways in which we trust a device. + * + * @deprecated Use {@link DeviceVerificationStatus}. + */ +exports.UserTrustLevel = UserTrustLevel; +class DeviceTrustLevel extends _cryptoApi.DeviceVerificationStatus { + constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices, signedByOwner = false) { + super({ + crossSigningVerified, + tofu, + localVerified, + trustCrossSignedDevices, + signedByOwner + }); + } + static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { + return new DeviceTrustLevel(userTrustLevel.isCrossSigningVerified(), userTrustLevel.isTofu(), localVerified, trustCrossSignedDevices, true); + } + + /** + * @returns true if this device is verified via cross signing + */ + isCrossSigningVerified() { + return this.crossSigningVerified; + } + + /** + * @returns true if this device is verified locally + */ + isLocallyVerified() { + return this.localVerified; + } + + /** + * @returns true if this device is trusted from a user's key + * that is trusted on first use + */ + isTofu() { + return this.tofu; + } +} +exports.DeviceTrustLevel = DeviceTrustLevel; +function createCryptoStoreCacheCallbacks(store, olmDevice) { + return { + getCrossSigningKeyCache: async function (type, _expectedPublicKey) { + const key = await new Promise(resolve => { + store.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + store.getSecretStorePrivateKey(txn, resolve, type); + }); + }); + if (key && key.ciphertext) { + const pickleKey = Buffer.from(olmDevice.pickleKey); + const decrypted = await (0, _aes.decryptAES)(key, pickleKey, type); + return (0, _olmlib.decodeBase64)(decrypted); + } else { + return key; + } + }, + storeCrossSigningKeyCache: async function (type, key) { + if (!(key instanceof Uint8Array)) { + throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`); + } + const pickleKey = Buffer.from(olmDevice.pickleKey); + const encryptedKey = await (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(key), pickleKey, type); + return store.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + store.storeSecretStorePrivateKey(txn, type, encryptedKey); + }); + } + }; +} +/** + * Request cross-signing keys from another device during verification. + * + * @param baseApis - base Matrix API interface + * @param userId - The user ID being verified + * @param deviceId - The device ID being verified + */ +async function requestKeysDuringVerification(baseApis, userId, deviceId) { + // If this is a self-verification, ask the other party for keys + if (baseApis.getUserId() !== userId) { + return; + } + _logger.logger.log("Cross-signing: Self-verification done; requesting keys"); + // This happens asynchronously, and we're not concerned about waiting for + // it. We return here in order to test. + return new Promise((resolve, reject) => { + const client = baseApis; + const original = client.crypto.crossSigningInfo; + + // We already have all of the infrastructure we need to validate and + // cache cross-signing keys, so instead of replicating that, here we set + // up callbacks that request them from the other device and call + // CrossSigningInfo.getCrossSigningKey() to validate/cache + const crossSigning = new CrossSigningInfo(original.userId, { + getCrossSigningKey: async type => { + _logger.logger.debug("Cross-signing: requesting secret", type, deviceId); + const { + promise + } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]); + const result = await promise; + const decoded = (0, _olmlib.decodeBase64)(result); + return Uint8Array.from(decoded); + } + }, original.getCacheCallbacks()); + crossSigning.keys = original.keys; + + // XXX: get all keys out if we get one key out + // https://github.com/vector-im/element-web/issues/12604 + // then change here to reject on the timeout + // Requests can be ignored, so don't wait around forever + const timeout = new Promise(resolve => { + setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout")); + }); + + // also request and cache the key backup key + const backupKeyPromise = (async () => { + const cachedKey = await client.crypto.getSessionBackupPrivateKey(); + if (!cachedKey) { + _logger.logger.info("No cached backup key found. Requesting..."); + const secretReq = client.requestSecret("m.megolm_backup.v1", [deviceId]); + const base64Key = await secretReq.promise; + _logger.logger.info("Got key backup key, decoding..."); + const decodedKey = (0, _olmlib.decodeBase64)(base64Key); + _logger.logger.info("Decoded backup key, storing..."); + await client.crypto.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey)); + _logger.logger.info("Backup key stored. Starting backup restore..."); + const backupInfo = await client.getKeyBackupVersion(); + // no need to await for this - just let it go in the bg + client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { + _logger.logger.info("Backup restored."); + }); + } + })(); + + // We call getCrossSigningKey() for its side-effects + Promise.race([Promise.all([crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise]), timeout]).then(resolve, reject); + }).catch(e => { + _logger.logger.warn("Cross-signing: failure while requesting keys:", e); + }); +}
\ No newline at end of file |