matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
1,054 lines (1,008 loc) • 136 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.IncomingRoomKeyRequest = exports.CryptoEvent = exports.Crypto = void 0;
exports.fixBackupKey = fixBackupKey;
exports.isCryptoAvailable = isCryptoAvailable;
exports.verificationMethods = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _anotherJson = _interopRequireDefault(require("another-json"));
var _uuid = require("uuid");
var _event = require("../@types/event");
var _ReEmitter = require("../ReEmitter");
var _logger = require("../logger");
var _OlmDevice = require("./OlmDevice");
var olmlib = _interopRequireWildcard(require("./olmlib"));
var _DeviceList = require("./DeviceList");
var _deviceinfo = require("./deviceinfo");
var algorithms = _interopRequireWildcard(require("./algorithms"));
var _CrossSigning = require("./CrossSigning");
var _EncryptionSetup = require("./EncryptionSetup");
var _SecretStorage = require("./SecretStorage");
var _OutgoingRoomKeyRequestManager = require("./OutgoingRoomKeyRequestManager");
var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store");
var _QRCode = require("./verification/QRCode");
var _SAS = require("./verification/SAS");
var _key_passphrase = require("./key_passphrase");
var _recoverykey = require("./recoverykey");
var _VerificationRequest = require("./verification/request/VerificationRequest");
var _InRoomChannel = require("./verification/request/InRoomChannel");
var _ToDeviceChannel = require("./verification/request/ToDeviceChannel");
var _IllegalMethod = require("./verification/IllegalMethod");
var _errors = require("../errors");
var _aes = require("./aes");
var _dehydration = require("./dehydration");
var _backup = require("./backup");
var _room = require("../models/room");
var _roomMember = require("../models/room-member");
var _event2 = require("../models/event");
var _client = require("../client");
var _typedEventEmitter = require("../models/typed-event-emitter");
var _roomState = require("../models/room-state");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification;
const defaultVerificationMethods = {
[_QRCode.ReciprocateQRCode.NAME]: _QRCode.ReciprocateQRCode,
[_SAS.SAS.NAME]: _SAS.SAS,
// These two can't be used for actual verification, but we do
// need to be able to define them here for the verification flows
// to start.
[_QRCode.SHOW_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod,
[_QRCode.SCAN_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod
};
/**
* verification method names
*/
// legacy export identifier
const verificationMethods = {
RECIPROCATE_QR_CODE: _QRCode.ReciprocateQRCode.NAME,
SAS: _SAS.SAS.NAME
};
exports.verificationMethods = verificationMethods;
function isCryptoAvailable() {
return Boolean(global.Olm);
}
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
let CryptoEvent;
exports.CryptoEvent = CryptoEvent;
(function (CryptoEvent) {
CryptoEvent["DeviceVerificationChanged"] = "deviceVerificationChanged";
CryptoEvent["UserTrustStatusChanged"] = "userTrustStatusChanged";
CryptoEvent["UserCrossSigningUpdated"] = "userCrossSigningUpdated";
CryptoEvent["RoomKeyRequest"] = "crypto.roomKeyRequest";
CryptoEvent["RoomKeyRequestCancellation"] = "crypto.roomKeyRequestCancellation";
CryptoEvent["KeyBackupStatus"] = "crypto.keyBackupStatus";
CryptoEvent["KeyBackupFailed"] = "crypto.keyBackupFailed";
CryptoEvent["KeyBackupSessionsRemaining"] = "crypto.keyBackupSessionsRemaining";
CryptoEvent["KeySignatureUploadFailure"] = "crypto.keySignatureUploadFailure";
CryptoEvent["VerificationRequest"] = "crypto.verification.request";
CryptoEvent["Warning"] = "crypto.warning";
CryptoEvent["WillUpdateDevices"] = "crypto.willUpdateDevices";
CryptoEvent["DevicesUpdated"] = "crypto.devicesUpdated";
CryptoEvent["KeysChanged"] = "crossSigning.keysChanged";
})(CryptoEvent || (exports.CryptoEvent = CryptoEvent = {}));
class Crypto extends _typedEventEmitter.TypedEventEmitter {
/**
* @returns The version of Olm.
*/
static getOlmVersion() {
return _OlmDevice.OlmDevice.getOlmVersion();
}
/**
* Cryptography bits
*
* This module is internal to the js-sdk; the public API is via MatrixClient.
*
* @internal
*
* @param baseApis - base matrix api interface
*
* @param userId - The user ID for the local user
*
* @param deviceId - The identifier for this device.
*
* @param clientStore - the MatrixClient data store.
*
* @param cryptoStore - storage for the crypto layer.
*
* @param roomList - An initialised RoomList object
*
* @param verificationMethods - Array of verification methods to use.
* Each element can either be a string from MatrixClient.verificationMethods
* or a class that implements a verification method.
*/
constructor(baseApis, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) {
super();
this.baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.clientStore = clientStore;
this.cryptoStore = cryptoStore;
this.roomList = roomList;
(0, _defineProperty2.default)(this, "backupManager", void 0);
(0, _defineProperty2.default)(this, "crossSigningInfo", void 0);
(0, _defineProperty2.default)(this, "olmDevice", void 0);
(0, _defineProperty2.default)(this, "deviceList", void 0);
(0, _defineProperty2.default)(this, "dehydrationManager", void 0);
(0, _defineProperty2.default)(this, "secretStorage", void 0);
(0, _defineProperty2.default)(this, "reEmitter", void 0);
(0, _defineProperty2.default)(this, "verificationMethods", void 0);
(0, _defineProperty2.default)(this, "supportedAlgorithms", void 0);
(0, _defineProperty2.default)(this, "outgoingRoomKeyRequestManager", void 0);
(0, _defineProperty2.default)(this, "toDeviceVerificationRequests", void 0);
(0, _defineProperty2.default)(this, "inRoomVerificationRequests", void 0);
(0, _defineProperty2.default)(this, "trustCrossSignedDevices", true);
(0, _defineProperty2.default)(this, "lastOneTimeKeyCheck", null);
(0, _defineProperty2.default)(this, "oneTimeKeyCheckInProgress", false);
(0, _defineProperty2.default)(this, "roomEncryptors", new Map());
(0, _defineProperty2.default)(this, "roomDecryptors", new Map());
(0, _defineProperty2.default)(this, "deviceKeys", {});
(0, _defineProperty2.default)(this, "globalBlacklistUnverifiedDevices", false);
(0, _defineProperty2.default)(this, "globalErrorOnUnknownDevices", true);
(0, _defineProperty2.default)(this, "receivedRoomKeyRequests", []);
(0, _defineProperty2.default)(this, "receivedRoomKeyRequestCancellations", []);
(0, _defineProperty2.default)(this, "processingRoomKeyRequests", false);
(0, _defineProperty2.default)(this, "lazyLoadMembers", false);
(0, _defineProperty2.default)(this, "roomDeviceTrackingState", {});
(0, _defineProperty2.default)(this, "lastNewSessionForced", {});
(0, _defineProperty2.default)(this, "sendKeyRequestsImmediately", false);
(0, _defineProperty2.default)(this, "oneTimeKeyCount", void 0);
(0, _defineProperty2.default)(this, "needsNewFallback", void 0);
(0, _defineProperty2.default)(this, "fallbackCleanup", void 0);
(0, _defineProperty2.default)(this, "onDeviceListUserCrossSigningUpdated", async userId => {
if (userId === this.userId) {
// An update to our own cross-signing key.
// Get the new key first:
const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null;
const currentPubkey = this.crossSigningInfo.getId();
const changed = currentPubkey !== seenPubkey;
if (currentPubkey && seenPubkey && !changed) {
// If it's not changed, just make sure everything is up to date
await this.checkOwnCrossSigningTrust();
} else {
// We'll now be in a state where cross-signing on the account is not trusted
// because our locally stored cross-signing keys will not match the ones
// on the server for our account. So we clear our own stored cross-signing keys,
// effectively disabling cross-signing until the user gets verified by the device
// that reset the keys
this.storeTrustedSelfKeys(null);
// emit cross-signing has been disabled
this.emit(CryptoEvent.KeysChanged, {});
// as the trust for our own user has changed,
// also emit an event for this
this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId));
}
} else {
await this.checkDeviceVerifications(userId);
// Update verified before latch using the current state and save the new
// latch value in the device list store.
const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigning) {
crossSigning.updateCrossSigningVerifiedBefore(this.checkUserTrust(userId).isCrossSigningVerified());
this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage());
}
this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId));
}
});
(0, _defineProperty2.default)(this, "onMembership", (event, member, oldMembership) => {
try {
this.onRoomMembership(event, member, oldMembership);
} catch (e) {
_logger.logger.error("Error handling membership change:", e);
}
});
(0, _defineProperty2.default)(this, "onToDeviceEvent", event => {
try {
_logger.logger.log(`received to-device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getContent()[_event.ToDeviceMessageId]}`);
if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") {
this.onRoomKeyEvent(event);
} else if (event.getType() == "m.room_key_request") {
this.onRoomKeyRequestEvent(event);
} else if (event.getType() === "m.secret.request") {
this.secretStorage.onRequestReceived(event);
} else if (event.getType() === "m.secret.send") {
this.secretStorage.onSecretReceived(event);
} else if (event.getType() === "m.room_key.withheld") {
this.onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) {
this.onKeyVerificationMessage(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") {
this.onToDeviceBadEncrypted(event);
} else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
if (!event.isBeingDecrypted()) {
event.attemptDecryption(this);
}
// once the event has been decrypted, try again
event.once(_event2.MatrixEventEvent.Decrypted, ev => {
this.onToDeviceEvent(ev);
});
}
} catch (e) {
_logger.logger.error("Error handling toDeviceEvent:", e);
}
});
(0, _defineProperty2.default)(this, "onTimelineEvent", (event, room, atStart, removed, {
liveEvent = true
} = {}) => {
if (!_InRoomChannel.InRoomChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = event => {
const channel = new _InRoomChannel.InRoomChannel(this.baseApis, event.getRoomId());
return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis);
};
this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent);
});
this.reEmitter = new _ReEmitter.TypedReEmitter(this);
if (verificationMethods) {
this.verificationMethods = new Map();
for (const method of verificationMethods) {
if (typeof method === "string") {
if (defaultVerificationMethods[method]) {
this.verificationMethods.set(method, defaultVerificationMethods[method]);
}
} else if (method["NAME"]) {
this.verificationMethods.set(method["NAME"], method);
} else {
_logger.logger.warn(`Excluding unknown verification method ${method}`);
}
}
} else {
this.verificationMethods = new Map(Object.entries(defaultVerificationMethods));
}
this.backupManager = new _backup.BackupManager(baseApis, async () => {
// try to get key from cache
const cachedKey = await this.getSessionBackupPrivateKey();
if (cachedKey) {
return cachedKey;
}
// try to get key from secret storage
const storedKey = await this.getSecret("m.megolm_backup.v1");
if (storedKey) {
// ensure that the key is in the right format. If not, fix the key and
// store the fixed version
const fixedKey = fixBackupKey(storedKey);
if (fixedKey) {
const keys = await this.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys[0]]);
}
return olmlib.decodeBase64(fixedKey || storedKey);
}
// try to get key from app
if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) {
return this.baseApis.cryptoCallbacks.getBackupKey();
}
throw new Error("Unable to get private key");
});
this.olmDevice = new _OlmDevice.OlmDevice(cryptoStore);
this.deviceList = new _DeviceList.DeviceList(baseApis, cryptoStore, this.olmDevice);
// XXX: This isn't removed at any point, but then none of the event listeners
// this class sets seem to be removed at any point... :/
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
this.outgoingRoomKeyRequestManager = new _OutgoingRoomKeyRequestManager.OutgoingRoomKeyRequestManager(baseApis, this.deviceId, this.cryptoStore);
this.toDeviceVerificationRequests = new _ToDeviceChannel.ToDeviceRequests();
this.inRoomVerificationRequests = new _InRoomChannel.InRoomRequests();
const cryptoCallbacks = this.baseApis.cryptoCallbacks || {};
const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(cryptoStore, this.olmDevice);
this.crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
// Yes, we pass the client twice here: see SecretStorage
this.secretStorage = new _SecretStorage.SecretStorage(baseApis, cryptoCallbacks, baseApis);
this.dehydrationManager = new _dehydration.DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS.
if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
cryptoCallbacks.getCrossSigningKey = async type => {
return _CrossSigning.CrossSigningInfo.getFromSecretStorage(type, this.secretStorage);
};
}
}
/**
* Initialise the crypto module so that it is ready for use
*
* Returns a promise which resolves once the crypto module is ready for use.
*
* @param exportedOlmDevice - (Optional) data from exported device
* that must be re-created.
*/
async init({
exportedOlmDevice,
pickleKey
} = {}) {
_logger.logger.log("Crypto: initialising Olm...");
await global.Olm.init();
_logger.logger.log(exportedOlmDevice ? "Crypto: initialising Olm device from exported device..." : "Crypto: initialising Olm device...");
await this.olmDevice.init({
fromExportedDevice: exportedOlmDevice,
pickleKey
});
_logger.logger.log("Crypto: loading device list...");
await this.deviceList.load();
// build our device keys: these will later be uploaded
this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key;
this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key;
_logger.logger.log("Crypto: fetching own devices...");
let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId);
if (!myDevices) {
myDevices = {};
}
if (!myDevices[this.deviceId]) {
// add our own deviceinfo to the cryptoStore
_logger.logger.log("Crypto: adding this device to the store...");
const deviceInfo = {
keys: this.deviceKeys,
algorithms: this.supportedAlgorithms,
verified: DeviceVerification.VERIFIED,
known: true
};
myDevices[this.deviceId] = deviceInfo;
this.deviceList.storeDevicesForUser(this.userId, myDevices);
this.deviceList.saveIfDirty();
}
await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
this.cryptoStore.getCrossSigningKeys(txn, keys => {
// can be an empty object after resetting cross-signing keys, see storeTrustedSelfKeys
if (keys && Object.keys(keys).length !== 0) {
_logger.logger.log("Loaded cross-signing public keys from crypto store");
this.crossSigningInfo.setKeys(keys);
}
});
});
// make sure we are keeping track of our own devices
// (this is important for key backups & things)
this.deviceList.startTrackingDeviceList(this.userId);
_logger.logger.log("Crypto: checking for key backup...");
this.backupManager.checkAndStart();
}
/**
* Whether to trust a others users signatures of their devices.
* If false, devices will only be considered 'verified' if we have
* verified that device individually (effectively disabling cross-signing).
*
* Default: true
*
* @returns True if trusting cross-signed devices
*/
getCryptoTrustCrossSignedDevices() {
return this.trustCrossSignedDevices;
}
/**
* See getCryptoTrustCrossSignedDevices
* This may be set before initCrypto() is called to ensure no races occur.
*
* @param val - True to trust cross-signed devices
*/
setCryptoTrustCrossSignedDevices(val) {
this.trustCrossSignedDevices = val;
for (const userId of this.deviceList.getKnownUserIds()) {
const devices = this.deviceList.getRawStoredDevicesForUser(userId);
for (const deviceId of Object.keys(devices)) {
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
// If the device is locally verified then isVerified() is always true,
// so this will only have caused the value to change if the device is
// cross-signing verified but not locally verified
if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) {
const deviceObj = this.deviceList.getStoredDevice(userId, deviceId);
this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj);
}
}
}
}
/**
* Create a recovery key from a user-supplied passphrase.
*
* @param password - Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key.
* Optional.
* @returns Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
async createRecoveryKeyFromPassphrase(password) {
const decryption = new global.Olm.PkDecryption();
try {
const keyInfo = {};
if (password) {
const derivation = await (0, _key_passphrase.keyFromPassphrase)(password);
keyInfo.passphrase = {
algorithm: "m.pbkdf2",
iterations: derivation.iterations,
salt: derivation.salt
};
keyInfo.pubkey = decryption.init_with_private_key(derivation.key);
} else {
keyInfo.pubkey = decryption.generate_key();
}
const privateKey = decryption.get_private_key();
const encodedPrivateKey = (0, _recoverykey.encodeRecoveryKey)(privateKey);
return {
keyInfo: keyInfo,
encodedPrivateKey,
privateKey
};
} finally {
decryption === null || decryption === void 0 ? void 0 : decryption.free();
}
}
/**
* Checks if the user has previously published cross-signing keys
*
* This means downloading the devicelist for the user and checking if the list includes
* the cross-signing pseudo-device.
*
* @internal
*/
async userHasCrossSigningKeys() {
await this.downloadKeys([this.userId]);
return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null;
}
/**
* Checks whether cross signing:
* - is enabled on this account and trusted by this device
* - has private keys either cached locally or stored in secret storage
*
* If this function returns false, bootstrapCrossSigning() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapCrossSigning() completes successfully, this function should
* return true.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @returns True if cross-signing is ready to be used on this device
*/
async isCrossSigningReady() {
const publicKeysOnDevice = this.crossSigningInfo.getId();
const privateKeysExistSomewhere = (await this.crossSigningInfo.isStoredInKeyCache()) || (await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage));
return !!(publicKeysOnDevice && privateKeysExistSomewhere);
}
/**
* Checks whether secret storage:
* - is enabled on this account
* - is storing cross-signing private keys
* - is storing session backup key (if enabled)
*
* If this function returns false, bootstrapSecretStorage() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes successfully, this function should
* return true.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @returns True if secret storage is ready to be used on this device
*/
async isSecretStorageReady() {
const secretStorageKeyInAccount = await this.secretStorage.hasKey();
const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
const sessionBackupInStorage = !this.backupManager.getKeyBackupEnabled() || (await this.baseApis.isKeyBackupKeyStored());
return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
}
/**
* Bootstrap cross-signing by creating keys if needed. If everything is already
* set up, then no changes are made, so this is safe to run to ensure
* cross-signing is ready for use.
*
* This function:
* - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been setup)
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param authUploadDeviceSigningKeys - Function
* called to await an interactive auth flow when uploading device signing keys.
* @param setupNewCrossSigning - Optional. Reset even if keys
* already exist.
* Args:
* 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.
*/
async bootstrapCrossSigning({
authUploadDeviceSigningKeys,
setupNewCrossSigning
} = {}) {
_logger.logger.log("Bootstrapping cross-signing");
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this.userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks);
// Reset the cross-signing keys
const resetCrossSigning = async () => {
crossSigningInfo.resetKeys();
// Sign master key with device key
await this.signObject(crossSigningInfo.keys.master);
// Store auth flow helper function, as we need to call it when uploading
// to ensure we handle auth errors properly.
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
// Cross-sign own device
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
const deviceSignature = await crossSigningInfo.signDevice(this.userId, device);
builder.addKeySignature(this.userId, this.deviceId, deviceSignature);
// Sign message key backup with cross-signing master key
if (this.backupManager.backupInfo) {
await crossSigningInfo.signObject(this.backupManager.backupInfo.auth_data, "master");
builder.addSessionBackup(this.backupManager.backupInfo);
}
};
const publicKeysOnDevice = this.crossSigningInfo.getId();
const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache();
const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage);
const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage;
// Log all relevant state for easier parsing of debug logs.
_logger.logger.log({
setupNewCrossSigning,
publicKeysOnDevice,
privateKeysInCache,
privateKeysInStorage,
privateKeysExistSomewhere
});
if (!privateKeysExistSomewhere || setupNewCrossSigning) {
_logger.logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys");
// If a user has multiple devices, it important to only call bootstrap
// as part of some UI flow (and not silently during startup), as they
// may have setup cross-signing on a platform which has not saved keys
// to secret storage, and this would reset them. In such a case, you
// should prompt the user to verify any existing devices first (and
// request private keys from those devices) before calling bootstrap.
await resetCrossSigning();
} else if (publicKeysOnDevice && privateKeysInCache) {
_logger.logger.log("Cross-signing public keys trusted and private keys found locally");
} else if (privateKeysInStorage) {
_logger.logger.log("Cross-signing private keys not found locally, but they are available " + "in secret storage, reading storage and caching locally");
await this.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true
});
}
// Assuming no app-supplied callback, default to storing new private keys in
// secret storage if it exists. If it does not, it is assumed this will be
// done as part of setting up secret storage later.
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
if (crossSigningPrivateKeys.size && !this.baseApis.cryptoCallbacks.saveCrossSigningKeys) {
const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined);
if (await secretStorage.hasKey()) {
_logger.logger.log("Storing new cross-signing private keys in secret storage");
// This is writing to in-memory account data in
// builder.accountDataClientAdapter so won't fail
await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
}
}
const operation = builder.buildOperation();
await operation.apply(this);
// This persists private keys and public keys as trusted,
// only do this if apply succeeded for now as retry isn't in place yet
await builder.persist(this);
_logger.logger.log("Cross-signing ready");
}
/**
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
* already set up, then no changes are made, so this is safe to run to ensure secret
* storage is ready for use.
*
* This function
* - creates a new Secure Secret Storage key if no default key exists
* - if a key backup exists, it is migrated to store the key in the Secret
* Storage
* - creates a backup if none exists, and one is requested
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @param createSecretStorageKey - Optional. Function
* called to await a secret storage key creation flow.
* Returns a Promise which resolves to an object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
* @param keyBackupInfo - The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
* @param setupNewKeyBackup - If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
* @param setupNewSecretStorage - Optional. Reset even if keys already exist.
* @param getKeyBackupPassphrase - Optional. Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Buffer
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` etc fields.
*/
// TODO this does not resolve with what it says it does
async bootstrapSecretStorage({
createSecretStorageKey = async () => ({}),
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
getKeyBackupPassphrase
} = {}) {
_logger.logger.log("Bootstrapping Secure Secret Storage");
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks);
const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined);
// the ID of the new SSSS key, if we create one
let newKeyId = null;
// create a new SSSS key and set it as default
const createSSSS = async (opts, privateKey) => {
if (privateKey) {
opts.key = privateKey;
}
const {
keyId,
keyInfo
} = await secretStorage.addKey(_SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES, opts);
if (privateKey) {
// make the private key available to encrypt 4S secrets
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
}
await secretStorage.setDefaultKeyId(keyId);
return keyId;
};
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
if (!keyInfo.mac) {
var _this$baseApis$crypto, _this$baseApis$crypto2;
const key = await ((_this$baseApis$crypto = (_this$baseApis$crypto2 = this.baseApis.cryptoCallbacks).getSecretStorageKey) === null || _this$baseApis$crypto === void 0 ? void 0 : _this$baseApis$crypto.call(_this$baseApis$crypto2, {
keys: {
[keyId]: keyInfo
}
}, ""));
if (key) {
const privateKey = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
const {
iv,
mac
} = await (0, _aes.calculateKeyCheck)(privateKey);
keyInfo.iv = iv;
keyInfo.mac = mac;
await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo);
}
}
};
const signKeyBackupWithCrossSigning = async keyBackupAuthData => {
if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) {
try {
_logger.logger.log("Adding cross-signing signature to key backup");
await this.crossSigningInfo.signObject(keyBackupAuthData, "master");
} catch (e) {
// This step is not critical (just helpful), so we catch here
// and continue if it fails.
_logger.logger.error("Signing key backup with cross-signing keys failed", e);
}
} else {
_logger.logger.warn("Cross-signing keys not available, skipping signature on key backup");
}
};
const oldSSSSKey = await this.getSecretStorageKey();
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
const storageExists = !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES;
// Log all relevant state for easier parsing of debug logs.
_logger.logger.log({
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
storageExists,
oldKeyInfo
});
if (!storageExists && !keyBackupInfo) {
// either we don't have anything, or we've been asked to restart
// from scratch
_logger.logger.log("Secret storage does not exist, creating new storage key");
// if we already have a usable default SSSS key and aren't resetting
// SSSS just use it. otherwise, create a new one
// Note: we leave the old SSSS key in place: there could be other
// secrets using it, in theory. We could move them to the new key but a)
// that would mean we'd need to prompt for the old passphrase, and b)
// it's not clear that would be the right thing to do anyway.
const {
keyInfo = {},
privateKey
} = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
} else if (!storageExists && keyBackupInfo) {
// we have an existing backup, but no SSSS
_logger.logger.log("Secret storage does not exist, using key backup key");
// if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key
const backupKey = (await this.getSessionBackupPrivateKey()) || (await (getKeyBackupPassphrase === null || getKeyBackupPassphrase === void 0 ? void 0 : getKeyBackupPassphrase()));
// create a new SSSS key and use the backup key as the new SSSS key
const opts = {};
if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) {
// FIXME: ???
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256
};
}
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]);
// 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 signKeyBackupWithCrossSigning(keyBackupInfo.auth_data);
builder.addSessionBackup(keyBackupInfo);
} else {
// 4S is already set up
_logger.logger.log("Secret storage exists");
if (oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES) {
// make sure that the default key has the information needed to
// check the passphrase
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
}
// If we have cross-signing private keys cached, store them in secret
// storage if they are not there already.
if (!this.baseApis.cryptoCallbacks.saveCrossSigningKeys && (await this.isCrossSigningReady()) && (newKeyId || !(await this.crossSigningInfo.isStoredInSecretStorage(secretStorage)))) {
_logger.logger.log("Copying cross-signing private keys from cache to secret storage");
const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache();
// This is writing to in-memory account data in
// builder.accountDataClientAdapter so won't fail
await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage);
}
if (setupNewKeyBackup && !keyBackupInfo) {
_logger.logger.log("Creating new message key backup version");
const info = await this.baseApis.prepareKeyBackupVersion(null /* random key */,
// don't write to secret storage, as it will write to this.secretStorage.
// Here, we want to capture all the side-effects of bootstrapping,
// and want to write to the local secretStorage object
{
secureSecretStorage: false
});
// write the key ourselves to 4S
const privateKey = (0, _recoverykey.decodeRecoveryKey)(info.recovery_key);
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
// create keyBackupInfo object to add to builder
const data = {
algorithm: info.algorithm,
auth_data: info.auth_data
};
// Sign with cross-signing master key
await signKeyBackupWithCrossSigning(data.auth_data);
// sign with the device fingerprint
await this.signObject(data.auth_data);
builder.addSessionBackup(data);
}
// Cache the session backup key
const sessionBackupKey = await secretStorage.get("m.megolm_backup.v1");
if (sessionBackupKey) {
_logger.logger.info("Got session backup key from secret storage: caching");
// fix up the backup key if it's in the wrong format, and replace
// in secret storage
const fixedBackupKey = fixBackupKey(sessionBackupKey);
if (fixedBackupKey) {
const keyId = newKeyId || oldKeyId;
await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null);
}
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey));
builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
} else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
// the cache or the user can provide one, and if so, write it to SSSS
const backupKey = (await this.getSessionBackupPrivateKey()) || (await (getKeyBackupPassphrase === null || getKeyBackupPassphrase === void 0 ? void 0 : getKeyBackupPassphrase()));
if (!backupKey) {
// This will require user intervention to recover from since we don't have the key
// backup key anywhere. The user should probably just set up a new key backup and
// the key for the new backup will be stored. If we hit this scenario in the wild
// with any frequency, we should do more than just log an error.
_logger.logger.error("Key backup is enabled but couldn't get key backup key!");
return;
}
_logger.logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
}
const operation = builder.buildOperation();
await operation.apply(this);
// this persists private keys and public keys as trusted,
// only do this if apply succeeded for now as retry isn't in place yet
await builder.persist(this);
_logger.logger.log("Secure Secret Storage ready");
}
addSecretStorageKey(algorithm, opts, keyID) {
return this.secretStorage.addKey(algorithm, opts, keyID);
}
hasSecretStorageKey(keyID) {
return this.secretStorage.hasKey(keyID);
}
getSecretStorageKey(keyID) {
return this.secretStorage.getKey(keyID);
}
storeSecret(name, secret, keys) {
return this.secretStorage.store(name, secret, keys);
}
getSecret(name) {
return this.secretStorage.get(name);
}
isSecretStored(name) {
return this.secretStorage.isStored(name);
}
requestSecret(name, devices) {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
}
return this.secretStorage.request(name, devices);
}
getDefaultSecretStorageKeyId() {
return this.secretStorage.getDefaultKeyId();
}
setDefaultSecretStorageKeyId(k) {
return this.secretStorage.setDefaultKeyId(k);
}
checkSecretStorageKey(key, info) {
return this.secretStorage.checkKey(key, info);
}
/**
* Checks that a given secret storage private key matches a given public key.
* This can be used by the getSecretStorageKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* @param privateKey - The private key
* @param expectedPublicKey - The public key
* @returns true if the key matches, otherwise false
*/
checkSecretStoragePrivateKey(privateKey, expectedPublicKey) {
let decryption = null;
try {
decryption = new global.Olm.PkDecryption();
const gotPubkey = decryption.init_with_private_key(privateKey);
// make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey;
} finally {
var _decryption;
(_decryption = decryption) === null || _decryption === void 0 ? void 0 : _decryption.free();
}
}
/**
* Fetches the backup private key, if cached
* @returns the key, if any, or null
*/
async getSessionBackupPrivateKey() {
let key = await new Promise(resolve => {
// TODO types
this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1");
});
});
// make sure we have a Uint8Array, rather than a string
if (key && typeof key === "string") {
key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key));
await this.storeSessionBackupPrivateKey(key);
}
if (key && key.ciphertext) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await (0, _aes.decryptAES)(key, pickleKey, "m.megolm_backup.v1");
key = olmlib.decodeBase64(decrypted);
}
return key;
}
/**
* Stores the session backup key to the cache
* @param key - the private key
* @returns a promise so you can catch failures
*/
async storeSessionBackupPrivateKey(key) {
if (!(key instanceof Uint8Array)) {
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const encryptedKey = await (0, _aes.encryptAES)(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => {
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
});
}
/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* @param privateKey - The private key
* @param expectedPublicKey - The public key
* @returns true if the key matches, otherwise false
*/
checkCrossSigningPrivateKey(privateKey, expectedPublicKey) {
let signing = null;
try {
signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privateKey);
// make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey;
} finally {
var _signing;
(_signing = signing) === null || _signing === void 0 ? void 0 : _signing.free();
}
}
/**
* Run various follow-up actions after cross-signing keys have changed locally
* (either by resetting the keys for the account or by getting them from secret
* storage), such as signing the current device, upgrading device
* verifications, etc.
*/
async afterCrossSigningLocalKeyChange() {
_logger.logger.info("Starting cross-signing key change post-processing");
// sign the current device with the new key, and upload to the server
const device = this.deviceList.getStoredDevice(this.userId, this.deviceId);
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
_logger.logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({
shouldEmit = false
}) => {
return this.baseApis.uploadKeySignatures({
[this.userId]: {
[this.deviceId]: signedDevice
}
}).then(response => {
const {
failures
} = response || {};
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this.baseApis.emit(CryptoEvent.KeySignatureUploadFailure, failures, "afterCrossSigningLocalKeyChange", upload // continuation
);
}
throw new _errors.KeySignatureUploadError("Key upload failed", {
failures
});
}
_logger.logger.info(`Finished background key sig upload for ${this.deviceId}`);
}).catch(e => {
_logger.logger.error(`Error during background key sig upload for ${this.deviceId}`, e);
});
};
upload({
shouldEmit: true
});
const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications;
if (shouldUpgradeCb) {
_logger.logger.info("Starting device verification upgrade");
// Check all users for signatures if upgrade callback present
// FIXME: do this in batches
const users = {};
for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) {
const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, _CrossSigning.CrossSigningInfo.fromStorage(crossSigningInfo, userId));
if (upgradeInfo) {
users[userId] = upgradeInfo;
}
}
if (Object.keys(users).length > 0) {
_logger.logger.info(`Found ${Object.keys(users).length} verif users to upgrade`);
try {
const usersToUpgrade = await shouldUpgradeCb({
users: users
});
if (usersToUpgrade) {
for (const userId of usersToUpgrade) {
if (userId in users) {
await this.baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId());
}
}
}
} catch (e) {
_logger.logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e);
}
}
_logger.logger.info("Finished device verification upgrade");
}
_logger.logger.info("Finished cross-signing key change post-processing");
}
/**
* Check if a user's cross-signing key is a candidate for upgrading from device
* verification.
*
* @param userId - the user whose cross-signing information is to be checked
* @param crossSigningInfo - the cross-signing information to check
*/
async checkForDeviceVerificationUpgrade(userId, crossSigningInfo) {
// only upgrade if this is the first cross-signing key that we've seen for
// them, and if their cross-signing key isn't already verified
const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo);
if (crossSigningInfo.firstUse && !trustLevel.isVerified()) {
const devices = this.deviceList.getRawStoredDevicesForUser(userId);
const deviceIds = await this.checkForValidDeviceSignature(userId, crossSigningInfo.keys.master, devices);
if (deviceIds.length) {
return {
devices: deviceIds.map(deviceId => _deviceinfo.DeviceInfo.fromStorage(devices[deviceId], deviceId)),
cros