You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-25 05:23:13 +03:00
356 lines
12 KiB
JavaScript
356 lines
12 KiB
JavaScript
import { logger } from "../logger";
|
|
import { MatrixEvent } from "../models/event";
|
|
import { EventEmitter } from "events";
|
|
import { createCryptoStoreCacheCallbacks } from "./CrossSigning";
|
|
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
|
import {
|
|
PREFIX_UNSTABLE,
|
|
} from "../http-api";
|
|
|
|
/**
|
|
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
|
|
* Once done, `buildOperation()` can be called which allows to apply to operation.
|
|
*
|
|
* This is used as a helper by Crypto to keep track of all the network requests
|
|
* and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
|
|
* Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
|
|
* more than once.
|
|
*/
|
|
export class EncryptionSetupBuilder {
|
|
/**
|
|
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
|
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
|
*/
|
|
constructor(accountData, delegateCryptoCallbacks) {
|
|
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
|
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
|
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
|
|
|
this._crossSigningKeys = null;
|
|
this._keySignatures = null;
|
|
this._keyBackupInfo = null;
|
|
}
|
|
|
|
/**
|
|
* Adds new cross-signing public keys
|
|
*
|
|
* @param {function} authUpload Function called to await an interactive auth
|
|
* flow when uploading device signing keys.
|
|
* Args:
|
|
* {function} A function that makes the request requiring auth. Receives
|
|
* the auth data as an object. Can be called multiple times, first with
|
|
* an empty authDict, to obtain the flows.
|
|
* @param {Object} keys the new keys
|
|
*/
|
|
addCrossSigningKeys(authUpload, keys) {
|
|
this._crossSigningKeys = { authUpload, keys };
|
|
}
|
|
|
|
/**
|
|
* Adds the key backup info to be updated on the server
|
|
*
|
|
* Used either to create a new key backup, or add signatures
|
|
* from the new MSK.
|
|
*
|
|
* @param {Object} keyBackupInfo as received from/sent to the server
|
|
*/
|
|
addSessionBackup(keyBackupInfo) {
|
|
this._keyBackupInfo = keyBackupInfo;
|
|
}
|
|
|
|
/**
|
|
* Adds the session backup private key to be updated in the local cache
|
|
*
|
|
* Used after fixing the format of the key
|
|
*
|
|
* @param {Uint8Array} privateKey
|
|
*/
|
|
addSessionBackupPrivateKeyToCache(privateKey) {
|
|
this._sessionBackupPrivateKey = privateKey;
|
|
}
|
|
|
|
/**
|
|
* Add signatures from a given user and device/x-sign key
|
|
* Used to sign the new cross-signing key with the device key
|
|
*
|
|
* @param {String} userId
|
|
* @param {String} deviceId
|
|
* @param {String} signature
|
|
*/
|
|
addKeySignature(userId, deviceId, signature) {
|
|
if (!this._keySignatures) {
|
|
this._keySignatures = {};
|
|
}
|
|
const userSignatures = this._keySignatures[userId] || {};
|
|
this._keySignatures[userId] = userSignatures;
|
|
userSignatures[deviceId] = signature;
|
|
}
|
|
|
|
/**
|
|
* @param {String} type
|
|
* @param {Object} content
|
|
* @return {Promise}
|
|
*/
|
|
setAccountData(type, content) {
|
|
return this.accountDataClientAdapter.setAccountData(type, content);
|
|
}
|
|
|
|
/**
|
|
* builds the operation containing all the parts that have been added to the builder
|
|
* @return {EncryptionSetupOperation}
|
|
*/
|
|
buildOperation() {
|
|
const accountData = this.accountDataClientAdapter._values;
|
|
return new EncryptionSetupOperation(
|
|
accountData,
|
|
this._crossSigningKeys,
|
|
this._keyBackupInfo,
|
|
this._keySignatures,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stores the created keys locally.
|
|
*
|
|
* This does not yet store the operation in a way that it can be restored,
|
|
* but that is the idea in the future.
|
|
*
|
|
* @param {Crypto} crypto
|
|
* @return {Promise}
|
|
*/
|
|
async persist(crypto) {
|
|
// store private keys in cache
|
|
if (this._crossSigningKeys) {
|
|
const cacheCallbacks = createCryptoStoreCacheCallbacks(
|
|
crypto._cryptoStore, crypto._olmDevice);
|
|
for (const type of ["master", "self_signing", "user_signing"]) {
|
|
logger.log(`Cache ${type} cross-signing private key locally`);
|
|
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
|
|
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
|
|
}
|
|
// store own cross-sign pubkeys as trusted
|
|
await crypto._cryptoStore.doTxn(
|
|
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
|
(txn) => {
|
|
crypto._cryptoStore.storeCrossSigningKeys(
|
|
txn, this._crossSigningKeys.keys);
|
|
},
|
|
);
|
|
}
|
|
// store session backup key in cache
|
|
if (this._sessionBackupPrivateKey) {
|
|
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Can be created from EncryptionSetupBuilder, or
|
|
* (in a follow-up PR, not implemented yet) restored from storage, to retry.
|
|
*
|
|
* It does not have knowledge of any private keys, unlike the builder.
|
|
*/
|
|
export class EncryptionSetupOperation {
|
|
/**
|
|
* @param {Map<String, Object>} accountData
|
|
* @param {Object} crossSigningKeys
|
|
* @param {Object} keyBackupInfo
|
|
* @param {Object} keySignatures
|
|
*/
|
|
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
|
|
this._accountData = accountData;
|
|
this._crossSigningKeys = crossSigningKeys;
|
|
this._keyBackupInfo = keyBackupInfo;
|
|
this._keySignatures = keySignatures;
|
|
}
|
|
|
|
/**
|
|
* Runs the (remaining part of, in the future) operation by sending requests to the server.
|
|
* @param {Crypto} crypto
|
|
*/
|
|
async apply(crypto) {
|
|
const baseApis = crypto._baseApis;
|
|
// upload cross-signing keys
|
|
if (this._crossSigningKeys) {
|
|
const keys = {};
|
|
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
|
keys[name + "_key"] = key;
|
|
}
|
|
|
|
// We must only call `uploadDeviceSigningKeys` from inside this auth
|
|
// helper to ensure we properly handle auth errors.
|
|
await this._crossSigningKeys.authUpload(authDict => {
|
|
return baseApis.uploadDeviceSigningKeys(authDict, keys);
|
|
});
|
|
|
|
// pass the new keys to the main instance of our own CrossSigningInfo.
|
|
crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
|
}
|
|
// set account data
|
|
if (this._accountData) {
|
|
for (const [type, content] of this._accountData) {
|
|
await baseApis.setAccountData(type, content);
|
|
}
|
|
}
|
|
// upload first cross-signing signatures with the new key
|
|
// (e.g. signing our own device)
|
|
if (this._keySignatures) {
|
|
await baseApis.uploadKeySignatures(this._keySignatures);
|
|
}
|
|
// need to create/update key backup info
|
|
if (this._keyBackupInfo) {
|
|
if (this._keyBackupInfo.version) {
|
|
// session backup signature
|
|
// The backup is trusted because the user provided the private key.
|
|
// Sign the backup with the cross signing key so the key backup can
|
|
// be trusted via cross-signing.
|
|
await baseApis.http.authedRequest(
|
|
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
|
undefined, {
|
|
algorithm: this._keyBackupInfo.algorithm,
|
|
auth_data: this._keyBackupInfo.auth_data,
|
|
},
|
|
{ prefix: PREFIX_UNSTABLE },
|
|
);
|
|
} else {
|
|
// add new key backup
|
|
await baseApis.http.authedRequest(
|
|
undefined, "POST", "/room_keys/version",
|
|
undefined, this._keyBackupInfo,
|
|
{ prefix: PREFIX_UNSTABLE },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Catches account data set by SecretStorage during bootstrapping by
|
|
* implementing the methods related to account data in MatrixClient
|
|
*/
|
|
class AccountDataClientAdapter extends EventEmitter {
|
|
/**
|
|
* @param {Object.<String, MatrixEvent>} accountData existing account data
|
|
*/
|
|
constructor(accountData) {
|
|
super();
|
|
this._existingValues = accountData;
|
|
this._values = new Map();
|
|
}
|
|
|
|
/**
|
|
* @param {String} type
|
|
* @return {Promise<Object>} the content of the account data
|
|
*/
|
|
getAccountDataFromServer(type) {
|
|
return Promise.resolve(this.getAccountData(type));
|
|
}
|
|
|
|
/**
|
|
* @param {String} type
|
|
* @return {Object} the content of the account data
|
|
*/
|
|
getAccountData(type) {
|
|
const modifiedValue = this._values.get(type);
|
|
if (modifiedValue) {
|
|
return modifiedValue;
|
|
}
|
|
const existingValue = this._existingValues[type];
|
|
if (existingValue) {
|
|
return existingValue.getContent();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {String} type
|
|
* @param {Object} content
|
|
* @return {Promise}
|
|
*/
|
|
setAccountData(type, content) {
|
|
const lastEvent = this._values.get(type);
|
|
this._values.set(type, content);
|
|
// ensure accountData is emitted on the next tick,
|
|
// as SecretStorage listens for it while calling this method
|
|
// and it seems to rely on this.
|
|
return Promise.resolve().then(() => {
|
|
const event = new MatrixEvent({ type, content });
|
|
this.emit("accountData", event, lastEvent);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Catches the private cross-signing keys set during bootstrapping
|
|
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
|
|
* See CrossSigningInfo constructor
|
|
*/
|
|
class CrossSigningCallbacks {
|
|
constructor() {
|
|
this.privateKeys = new Map();
|
|
}
|
|
|
|
// cache callbacks
|
|
getCrossSigningKeyCache(type, expectedPublicKey) {
|
|
return this.getCrossSigningKey(type, expectedPublicKey);
|
|
}
|
|
|
|
storeCrossSigningKeyCache(type, key) {
|
|
this.privateKeys.set(type, key);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// non-cache callbacks
|
|
getCrossSigningKey(type, _expectedPubkey) {
|
|
return Promise.resolve(this.privateKeys.get(type));
|
|
}
|
|
|
|
saveCrossSigningKeys(privateKeys) {
|
|
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
|
this.privateKeys.set(type, privateKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Catches the 4S private key set during bootstrapping by implementing
|
|
* the SecretStorage crypto callbacks
|
|
*/
|
|
class SSSSCryptoCallbacks {
|
|
constructor(delegateCryptoCallbacks) {
|
|
this._privateKeys = new Map();
|
|
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
|
|
}
|
|
|
|
async getSecretStorageKey({ keys }, name) {
|
|
for (const keyId of Object.keys(keys)) {
|
|
const privateKey = this._privateKeys.get(keyId);
|
|
if (privateKey) {
|
|
return [keyId, privateKey];
|
|
}
|
|
}
|
|
// if we don't have the key cached yet, ask
|
|
// for it to the general crypto callbacks and cache it
|
|
if (this._delegateCryptoCallbacks) {
|
|
const result = await this._delegateCryptoCallbacks.
|
|
getSecretStorageKey({ keys }, name);
|
|
if (result) {
|
|
const [keyId, privateKey] = result;
|
|
this._privateKeys.set(keyId, privateKey);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
addPrivateKey(keyId, keyInfo, privKey) {
|
|
this._privateKeys.set(keyId, privKey);
|
|
// Also pass along to application to cache if it wishes
|
|
if (
|
|
this._delegateCryptoCallbacks &&
|
|
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
|
) {
|
|
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
|
|
}
|
|
}
|
|
}
|