942 lines
40 KiB
JavaScript
942 lines
40 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.RustCrypto = void 0;
|
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-wasm"));
|
|
var _logger = require("../logger");
|
|
var _httpApi = require("../http-api");
|
|
var _CrossSigning = require("../crypto/CrossSigning");
|
|
var _RoomEncryptor = require("./RoomEncryptor");
|
|
var _OutgoingRequestProcessor = require("./OutgoingRequestProcessor");
|
|
var _KeyClaimManager = require("./KeyClaimManager");
|
|
var _utils = require("../utils");
|
|
var _cryptoApi = require("../crypto-api");
|
|
var _deviceConverter = require("./device-converter");
|
|
var _secretStorage = require("../secret-storage");
|
|
var _CrossSigningIdentity = require("./CrossSigningIdentity");
|
|
var _secretStorage2 = require("./secret-storage");
|
|
var _key_passphrase = require("../crypto/key_passphrase");
|
|
var _recoverykey = require("../crypto/recoverykey");
|
|
var _crypto = require("../crypto/crypto");
|
|
var _verification = require("./verification");
|
|
var _event = require("../@types/event");
|
|
var _crypto2 = require("../crypto");
|
|
var _typedEventEmitter = require("../models/typed-event-emitter");
|
|
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; }
|
|
/*
|
|
Copyright 2022-2023 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.
|
|
*/
|
|
|
|
const ALL_VERIFICATION_METHODS = ["m.sas.v1", "m.qr_code.scan.v1", "m.qr_code.show.v1", "m.reciprocate.v1"];
|
|
|
|
/**
|
|
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
|
*
|
|
* @internal
|
|
*/
|
|
class RustCrypto extends _typedEventEmitter.TypedEventEmitter {
|
|
constructor( /** The `OlmMachine` from the underlying rust crypto sdk. */
|
|
olmMachine,
|
|
/**
|
|
* Low-level HTTP interface: used to make outgoing requests required by the rust SDK.
|
|
*
|
|
* We expect it to set the access token, etc.
|
|
*/
|
|
http, /** The local user's User ID. */
|
|
userId, /** The local user's Device ID. */
|
|
_deviceId, /** Interface to server-side secret storage */
|
|
secretStorage, /** Crypto callbacks provided by the application */
|
|
cryptoCallbacks) {
|
|
super();
|
|
this.olmMachine = olmMachine;
|
|
this.http = http;
|
|
this.userId = userId;
|
|
this.secretStorage = secretStorage;
|
|
this.cryptoCallbacks = cryptoCallbacks;
|
|
(0, _defineProperty2.default)(this, "globalErrorOnUnknownDevices", false);
|
|
(0, _defineProperty2.default)(this, "_trustCrossSignedDevices", true);
|
|
/** whether {@link stop} has been called */
|
|
(0, _defineProperty2.default)(this, "stopped", false);
|
|
/** whether {@link outgoingRequestLoop} is currently running */
|
|
(0, _defineProperty2.default)(this, "outgoingRequestLoopRunning", false);
|
|
/** mapping of roomId → encryptor class */
|
|
(0, _defineProperty2.default)(this, "roomEncryptors", {});
|
|
(0, _defineProperty2.default)(this, "eventDecryptor", void 0);
|
|
(0, _defineProperty2.default)(this, "keyClaimManager", void 0);
|
|
(0, _defineProperty2.default)(this, "outgoingRequestProcessor", void 0);
|
|
(0, _defineProperty2.default)(this, "crossSigningIdentity", void 0);
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CryptoApi implementation
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
(0, _defineProperty2.default)(this, "globalBlacklistUnverifiedDevices", false);
|
|
/**
|
|
* The verification methods we offer to the other side during an interactive verification.
|
|
*/
|
|
(0, _defineProperty2.default)(this, "_supportedVerificationMethods", ALL_VERIFICATION_METHODS);
|
|
this.outgoingRequestProcessor = new _OutgoingRequestProcessor.OutgoingRequestProcessor(olmMachine, http);
|
|
this.keyClaimManager = new _KeyClaimManager.KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
|
this.eventDecryptor = new EventDecryptor(olmMachine);
|
|
|
|
// Fire if the cross signing keys are imported from the secret storage
|
|
const onCrossSigningKeysImport = () => {
|
|
this.emit(_crypto2.CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(this.userId));
|
|
};
|
|
this.crossSigningIdentity = new _CrossSigningIdentity.CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor, secretStorage, onCrossSigningKeysImport);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CryptoBackend implementation
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
stop() {
|
|
// stop() may be called multiple times, but attempting to close() the OlmMachine twice
|
|
// will cause an error.
|
|
if (this.stopped) {
|
|
return;
|
|
}
|
|
this.stopped = true;
|
|
this.keyClaimManager.stop();
|
|
|
|
// make sure we close() the OlmMachine; doing so means that all the Rust objects will be
|
|
// cleaned up; in particular, the indexeddb connections will be closed, which means they
|
|
// can then be deleted.
|
|
this.olmMachine.close();
|
|
}
|
|
async encryptEvent(event, _room) {
|
|
const roomId = event.getRoomId();
|
|
const encryptor = this.roomEncryptors[roomId];
|
|
if (!encryptor) {
|
|
throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
|
|
}
|
|
await encryptor.encryptEvent(event);
|
|
}
|
|
async decryptEvent(event) {
|
|
const roomId = event.getRoomId();
|
|
if (!roomId) {
|
|
// presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages
|
|
// so the fact it has come back here suggests that decryption failed.
|
|
//
|
|
// once we drop support for the libolm crypto implementation, we can stop passing to-device messages
|
|
// through decryptEvent and hence get rid of this case.
|
|
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
|
|
}
|
|
return await this.eventDecryptor.attemptEventDecryption(event);
|
|
}
|
|
getEventEncryptionInfo(event) {
|
|
var _event$getSenderKey;
|
|
// TODO: make this work properly. Or better, replace it.
|
|
|
|
const ret = {};
|
|
ret.senderKey = (_event$getSenderKey = event.getSenderKey()) !== null && _event$getSenderKey !== void 0 ? _event$getSenderKey : undefined;
|
|
ret.algorithm = event.getWireContent().algorithm;
|
|
if (!ret.senderKey || !ret.algorithm) {
|
|
ret.encrypted = false;
|
|
return ret;
|
|
}
|
|
ret.encrypted = true;
|
|
ret.authenticated = true;
|
|
ret.mismatchedSender = true;
|
|
return ret;
|
|
}
|
|
checkUserTrust(userId) {
|
|
// TODO
|
|
return new _CrossSigning.UserTrustLevel(false, false, false);
|
|
}
|
|
|
|
/**
|
|
* Get the cross signing information for a given user.
|
|
*
|
|
* The cross-signing API is currently UNSTABLE and may change without notice.
|
|
*
|
|
* @param userId - the user ID to get the cross-signing info for.
|
|
*
|
|
* @returns the cross signing information for the user.
|
|
*/
|
|
getStoredCrossSigningForUser(userId) {
|
|
// TODO
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* This function is unneeded for the rust-crypto.
|
|
* The cross signing key import and the device verification are done in {@link CryptoApi#bootstrapCrossSigning}
|
|
*
|
|
* The function is stub to keep the compatibility with the old crypto.
|
|
* More information: https://github.com/vector-im/element-web/issues/25648
|
|
*
|
|
*
|
|
* Implementation of {@link CryptoBackend#checkOwnCrossSigningTrust}
|
|
*/
|
|
async checkOwnCrossSigningTrust() {
|
|
return;
|
|
}
|
|
/**
|
|
* Implementation of {@link CryptoApi.userHasCrossSigningKeys}.
|
|
*/
|
|
async userHasCrossSigningKeys() {
|
|
const userId = new RustSdkCryptoJs.UserId(this.userId);
|
|
/* make sure we have an *up-to-date* idea of the user's cross-signing keys. This is important, because if we
|
|
* return "false" here, we will end up generating new cross-signing keys and replacing the existing ones.
|
|
*/
|
|
const request = this.olmMachine.queryKeysForUsers([userId]);
|
|
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
|
const userIdentity = await this.olmMachine.getIdentity(userId);
|
|
return userIdentity !== undefined;
|
|
}
|
|
prepareToEncrypt(room) {
|
|
const encryptor = this.roomEncryptors[room.roomId];
|
|
if (encryptor) {
|
|
encryptor.ensureEncryptionSession();
|
|
}
|
|
}
|
|
forceDiscardSession(roomId) {
|
|
var _this$roomEncryptors$;
|
|
return (_this$roomEncryptors$ = this.roomEncryptors[roomId]) === null || _this$roomEncryptors$ === void 0 ? void 0 : _this$roomEncryptors$.forceDiscardSession();
|
|
}
|
|
async exportRoomKeys() {
|
|
const raw = await this.olmMachine.exportRoomKeys(() => true);
|
|
return JSON.parse(raw);
|
|
}
|
|
async importRoomKeys(keys, opts) {
|
|
// TODO when backup support will be added we would need to expose the `from_backup` flag in the bindings
|
|
const jsonKeys = JSON.stringify(keys);
|
|
await this.olmMachine.importRoomKeys(jsonKeys, (progress, total) => {
|
|
var _opts$progressCallbac;
|
|
const importOpt = {
|
|
total: Number(total),
|
|
successes: Number(progress),
|
|
stage: "load_keys",
|
|
failures: 0
|
|
};
|
|
opts === null || opts === void 0 || (_opts$progressCallbac = opts.progressCallback) === null || _opts$progressCallbac === void 0 ? void 0 : _opts$progressCallbac.call(opts, importOpt);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the device information for the given list of users.
|
|
*
|
|
* @param userIds - The users to fetch.
|
|
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
|
* currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
|
|
*
|
|
* @returns A map `{@link DeviceMap}`.
|
|
*/
|
|
async getUserDeviceInfo(userIds, downloadUncached = false) {
|
|
const deviceMapByUserId = new Map();
|
|
const rustTrackedUsers = await this.olmMachine.trackedUsers();
|
|
|
|
// Convert RustSdkCryptoJs.UserId to a `Set<string>`
|
|
const trackedUsers = new Set();
|
|
rustTrackedUsers.forEach(rustUserId => trackedUsers.add(rustUserId.toString()));
|
|
|
|
// Keep untracked user to download their keys after
|
|
const untrackedUsers = new Set();
|
|
for (const userId of userIds) {
|
|
// if this is a tracked user, we can just fetch the device list from the rust-sdk
|
|
// (NB: this is probably ok even if we race with a leave event such that we stop tracking the user's
|
|
// devices: the rust-sdk will return the last-known device list, which will be good enough.)
|
|
if (trackedUsers.has(userId)) {
|
|
deviceMapByUserId.set(userId, await this.getUserDevices(userId));
|
|
} else {
|
|
untrackedUsers.add(userId);
|
|
}
|
|
}
|
|
|
|
// for any users whose device lists we are not tracking, fall back to downloading the device list
|
|
// over HTTP.
|
|
if (downloadUncached && untrackedUsers.size >= 1) {
|
|
const queryResult = await this.downloadDeviceList(untrackedUsers);
|
|
Object.entries(queryResult.device_keys).forEach(([userId, deviceKeys]) => deviceMapByUserId.set(userId, (0, _deviceConverter.deviceKeysToDeviceMap)(deviceKeys)));
|
|
}
|
|
return deviceMapByUserId;
|
|
}
|
|
|
|
/**
|
|
* Get the device list for the given user from the olm machine
|
|
* @param userId - Rust SDK UserId
|
|
*/
|
|
async getUserDevices(userId) {
|
|
const rustUserId = new RustSdkCryptoJs.UserId(userId);
|
|
|
|
// For reasons I don't really understand, the Javascript FinalizationRegistry doesn't seem to run the
|
|
// registered callbacks when `userDevices` goes out of scope, nor when the individual devices in the array
|
|
// returned by `userDevices.devices` do so.
|
|
//
|
|
// This is particularly problematic, because each of those structures holds a reference to the
|
|
// VerificationMachine, which in turn holds a reference to the IndexeddbCryptoStore. Hence, we end up leaking
|
|
// open connections to the crypto store, which means the store can't be deleted on logout.
|
|
//
|
|
// To fix this, we explicitly call `.free` on each of the objects, which tells the rust code to drop the
|
|
// allocated memory and decrement the refcounts for the crypto store.
|
|
|
|
const userDevices = await this.olmMachine.getUserDevices(rustUserId);
|
|
try {
|
|
const deviceArray = userDevices.devices();
|
|
try {
|
|
return new Map(deviceArray.map(device => [device.deviceId.toString(), (0, _deviceConverter.rustDeviceToJsDevice)(device, rustUserId)]));
|
|
} finally {
|
|
deviceArray.forEach(d => d.free());
|
|
}
|
|
} finally {
|
|
userDevices.free();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download the given user keys by calling `/keys/query` request
|
|
* @param untrackedUsers - download keys of these users
|
|
*/
|
|
async downloadDeviceList(untrackedUsers) {
|
|
const queryBody = {
|
|
device_keys: {}
|
|
};
|
|
untrackedUsers.forEach(user => queryBody.device_keys[user] = []);
|
|
return await this.http.authedRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/query", undefined, queryBody, {
|
|
prefix: ""
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#getTrustCrossSignedDevices}.
|
|
*/
|
|
getTrustCrossSignedDevices() {
|
|
return this._trustCrossSignedDevices;
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#setTrustCrossSignedDevices}.
|
|
*/
|
|
setTrustCrossSignedDevices(val) {
|
|
this._trustCrossSignedDevices = val;
|
|
// TODO: legacy crypto goes through the list of known devices and emits DeviceVerificationChanged
|
|
// events. Maybe we need to do the same?
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#getDeviceVerificationStatus}.
|
|
*/
|
|
async getDeviceVerificationStatus(userId, deviceId) {
|
|
const device = await this.olmMachine.getDevice(new RustSdkCryptoJs.UserId(userId), new RustSdkCryptoJs.DeviceId(deviceId));
|
|
if (!device) return null;
|
|
return new _cryptoApi.DeviceVerificationStatus({
|
|
signedByOwner: device.isCrossSignedByOwner(),
|
|
crossSigningVerified: device.isCrossSigningTrusted(),
|
|
localVerified: device.isLocallyTrusted(),
|
|
trustCrossSignedDevices: this._trustCrossSignedDevices
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#isCrossSigningReady}
|
|
*/
|
|
async isCrossSigningReady() {
|
|
const {
|
|
publicKeysOnDevice,
|
|
privateKeysInSecretStorage,
|
|
privateKeysCachedLocally
|
|
} = await this.getCrossSigningStatus();
|
|
const hasKeysInCache = Boolean(privateKeysCachedLocally.masterKey) && Boolean(privateKeysCachedLocally.selfSigningKey) && Boolean(privateKeysCachedLocally.userSigningKey);
|
|
|
|
// The cross signing is ready if the public and private keys are available
|
|
return publicKeysOnDevice && (hasKeysInCache || privateKeysInSecretStorage);
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
|
|
*/
|
|
async getCrossSigningKeyId(type = _cryptoApi.CrossSigningKey.Master) {
|
|
const userIdentity = await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
|
|
const crossSigningStatus = await this.olmMachine.crossSigningStatus();
|
|
const privateKeysOnDevice = crossSigningStatus.hasMaster && crossSigningStatus.hasUserSigning && crossSigningStatus.hasSelfSigning;
|
|
if (!userIdentity || !privateKeysOnDevice) {
|
|
// The public or private keys are not available on this device
|
|
return null;
|
|
}
|
|
if (!userIdentity.isVerified()) {
|
|
// We have both public and private keys, but they don't match!
|
|
return null;
|
|
}
|
|
let key;
|
|
switch (type) {
|
|
case _cryptoApi.CrossSigningKey.Master:
|
|
key = userIdentity.masterKey;
|
|
break;
|
|
case _cryptoApi.CrossSigningKey.SelfSigning:
|
|
key = userIdentity.selfSigningKey;
|
|
break;
|
|
case _cryptoApi.CrossSigningKey.UserSigning:
|
|
key = userIdentity.userSigningKey;
|
|
break;
|
|
default:
|
|
// Unknown type
|
|
return null;
|
|
}
|
|
const parsedKey = JSON.parse(key);
|
|
// `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(parsedKey.keys)[0];
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#boostrapCrossSigning}
|
|
*/
|
|
async bootstrapCrossSigning(opts) {
|
|
await this.crossSigningIdentity.bootstrapCrossSigning(opts);
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#isSecretStorageReady}
|
|
*/
|
|
async isSecretStorageReady() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#bootstrapSecretStorage}
|
|
*/
|
|
async bootstrapSecretStorage({
|
|
createSecretStorageKey,
|
|
setupNewSecretStorage
|
|
} = {}) {
|
|
// If an AES Key is already stored in the secret storage and setupNewSecretStorage is not set
|
|
// we don't want to create a new key
|
|
const isNewSecretStorageKeyNeeded = setupNewSecretStorage || !(await this.secretStorageHasAESKey());
|
|
if (isNewSecretStorageKeyNeeded) {
|
|
if (!createSecretStorageKey) {
|
|
throw new Error("unable to create a new secret storage key, createSecretStorageKey is not set");
|
|
}
|
|
|
|
// Create a new storage key and add it to secret storage
|
|
const recoveryKey = await createSecretStorageKey();
|
|
await this.addSecretStorageKeyToSecretStorage(recoveryKey);
|
|
}
|
|
const crossSigningStatus = await this.olmMachine.crossSigningStatus();
|
|
const hasPrivateKeys = crossSigningStatus.hasMaster && crossSigningStatus.hasSelfSigning && crossSigningStatus.hasUserSigning;
|
|
|
|
// If we have cross-signing private keys cached, store them in secret
|
|
// storage if they are not there already.
|
|
if (hasPrivateKeys && (isNewSecretStorageKeyNeeded || !(await (0, _secretStorage2.secretStorageContainsCrossSigningKeys)(this.secretStorage)))) {
|
|
const crossSigningPrivateKeys = await this.olmMachine.exportCrossSigningKeys();
|
|
if (!crossSigningPrivateKeys.masterKey) {
|
|
throw new Error("missing master key in cross signing private keys");
|
|
}
|
|
if (!crossSigningPrivateKeys.userSigningKey) {
|
|
throw new Error("missing user signing key in cross signing private keys");
|
|
}
|
|
if (!crossSigningPrivateKeys.self_signing_key) {
|
|
throw new Error("missing self signing key in cross signing private keys");
|
|
}
|
|
await this.secretStorage.store("m.cross_signing.master", crossSigningPrivateKeys.masterKey);
|
|
await this.secretStorage.store("m.cross_signing.user_signing", crossSigningPrivateKeys.userSigningKey);
|
|
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the secretStorage key to the secret storage
|
|
* - The secret storage key must have the `keyInfo` field filled
|
|
* - The secret storage key is set as the default key of the secret storage
|
|
* - Call `cryptoCallbacks.cacheSecretStorageKey` when done
|
|
*
|
|
* @param secretStorageKey - The secret storage key to add in the secret storage.
|
|
*/
|
|
async addSecretStorageKeyToSecretStorage(secretStorageKey) {
|
|
var _this$cryptoCallbacks, _this$cryptoCallbacks2;
|
|
// keyInfo is required to continue
|
|
if (!secretStorageKey.keyInfo) {
|
|
throw new Error("missing keyInfo field in the secret storage key");
|
|
}
|
|
const secretStorageKeyObject = await this.secretStorage.addKey(_secretStorage.SECRET_STORAGE_ALGORITHM_V1_AES, secretStorageKey.keyInfo);
|
|
await this.secretStorage.setDefaultKeyId(secretStorageKeyObject.keyId);
|
|
(_this$cryptoCallbacks = (_this$cryptoCallbacks2 = this.cryptoCallbacks).cacheSecretStorageKey) === null || _this$cryptoCallbacks === void 0 ? void 0 : _this$cryptoCallbacks.call(_this$cryptoCallbacks2, secretStorageKeyObject.keyId, secretStorageKeyObject.keyInfo, secretStorageKey.privateKey);
|
|
}
|
|
|
|
/**
|
|
* Check if a secret storage AES Key is already added in secret storage
|
|
*
|
|
* @returns True if an AES key is in the secret storage
|
|
*/
|
|
async secretStorageHasAESKey() {
|
|
// See if we already have an AES secret-storage key.
|
|
const secretStorageKeyTuple = await this.secretStorage.getKey();
|
|
if (!secretStorageKeyTuple) return false;
|
|
const [, keyInfo] = secretStorageKeyTuple;
|
|
|
|
// Check if the key is an AES key
|
|
return keyInfo.algorithm === _secretStorage.SECRET_STORAGE_ALGORITHM_V1_AES;
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
|
*/
|
|
async getCrossSigningStatus() {
|
|
const userIdentity = await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
|
|
const publicKeysOnDevice = Boolean(userIdentity === null || userIdentity === void 0 ? void 0 : userIdentity.masterKey) && Boolean(userIdentity === null || userIdentity === void 0 ? void 0 : userIdentity.selfSigningKey) && Boolean(userIdentity === null || userIdentity === void 0 ? void 0 : userIdentity.userSigningKey);
|
|
const privateKeysInSecretStorage = await (0, _secretStorage2.secretStorageContainsCrossSigningKeys)(this.secretStorage);
|
|
const crossSigningStatus = await this.olmMachine.crossSigningStatus();
|
|
return {
|
|
publicKeysOnDevice,
|
|
privateKeysInSecretStorage,
|
|
privateKeysCachedLocally: {
|
|
masterKey: Boolean(crossSigningStatus === null || crossSigningStatus === void 0 ? void 0 : crossSigningStatus.hasMaster),
|
|
userSigningKey: Boolean(crossSigningStatus === null || crossSigningStatus === void 0 ? void 0 : crossSigningStatus.hasUserSigning),
|
|
selfSigningKey: Boolean(crossSigningStatus === null || crossSigningStatus === void 0 ? void 0 : crossSigningStatus.hasSelfSigning)
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link CryptoApi#createRecoveryKeyFromPassphrase}
|
|
*/
|
|
async createRecoveryKeyFromPassphrase(password) {
|
|
let key;
|
|
const keyInfo = {};
|
|
if (password) {
|
|
// Generate the key from the passphrase
|
|
const derivation = await (0, _key_passphrase.keyFromPassphrase)(password);
|
|
keyInfo.passphrase = {
|
|
algorithm: "m.pbkdf2",
|
|
iterations: derivation.iterations,
|
|
salt: derivation.salt
|
|
};
|
|
key = derivation.key;
|
|
} else {
|
|
// Using the navigator crypto API to generate the private key
|
|
key = new Uint8Array(32);
|
|
_crypto.crypto.getRandomValues(key);
|
|
}
|
|
const encodedPrivateKey = (0, _recoverykey.encodeRecoveryKey)(key);
|
|
return {
|
|
keyInfo,
|
|
encodedPrivateKey,
|
|
privateKey: key
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns to-device verification requests that are already in progress for the given user id.
|
|
*
|
|
* Implementation of {@link CryptoApi#getVerificationRequestsToDeviceInProgress}
|
|
*
|
|
* @param userId - the ID of the user to query
|
|
*
|
|
* @returns the VerificationRequests that are in progress
|
|
*/
|
|
getVerificationRequestsToDeviceInProgress(userId) {
|
|
const requests = this.olmMachine.getVerificationRequests(new RustSdkCryptoJs.UserId(userId));
|
|
return requests.filter(request => request.roomId === undefined).map(request => new _verification.RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods));
|
|
}
|
|
|
|
/**
|
|
* Finds a DM verification request that is already in progress for the given room id
|
|
*
|
|
* Implementation of {@link CryptoApi#findVerificationRequestDMInProgress}
|
|
*
|
|
* @param roomId - the room to use for verification
|
|
*
|
|
* @returns the VerificationRequest that is in progress, if any
|
|
*
|
|
*/
|
|
findVerificationRequestDMInProgress(roomId) {
|
|
// TODO
|
|
return;
|
|
}
|
|
/**
|
|
* Set the verification methods we offer to the other side during an interactive verification.
|
|
*
|
|
* If `undefined`, we will offer all the methods supported by the Rust SDK.
|
|
*/
|
|
setSupportedVerificationMethods(methods) {
|
|
// by default, the Rust SDK does not offer `m.qr_code.scan.v1`, but we do want to offer that.
|
|
this._supportedVerificationMethods = methods !== null && methods !== void 0 ? methods : ALL_VERIFICATION_METHODS;
|
|
}
|
|
|
|
/**
|
|
* Send a verification request to our other devices.
|
|
*
|
|
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
|
*
|
|
* Implementation of {@link CryptoApi#requestOwnUserVerification}.
|
|
*
|
|
* @returns a VerificationRequest when the request has been sent to the other party.
|
|
*/
|
|
async requestOwnUserVerification() {
|
|
const userIdentity = await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
|
|
if (userIdentity === undefined) {
|
|
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
|
|
}
|
|
const [request, outgoingRequest] = await userIdentity.requestVerification(this._supportedVerificationMethods.map(_verification.verificationMethodIdentifierToMethod));
|
|
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
|
return new _verification.RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
|
}
|
|
|
|
/**
|
|
* Request an interactive verification with the given device.
|
|
*
|
|
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
|
*
|
|
* Implementation of {@link CryptoApi#requestDeviceVerification}.
|
|
*
|
|
* @param userId - ID of the owner of the device to verify
|
|
* @param deviceId - ID of the device to verify
|
|
*
|
|
* @returns a VerificationRequest when the request has been sent to the other party.
|
|
*/
|
|
async requestDeviceVerification(userId, deviceId) {
|
|
const device = await this.olmMachine.getDevice(new RustSdkCryptoJs.UserId(userId), new RustSdkCryptoJs.DeviceId(deviceId));
|
|
if (!device) {
|
|
throw new Error("Not a known device");
|
|
}
|
|
const [request, outgoingRequest] = await device.requestVerification(this._supportedVerificationMethods.map(_verification.verificationMethodIdentifierToMethod));
|
|
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
|
return new _verification.RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods);
|
|
}
|
|
|
|
/**
|
|
* Fetch the backup decryption key we have saved in our store.
|
|
*
|
|
* Implementation of {@link CryptoApi#getSessionBackupPrivateKey}.
|
|
*
|
|
* @returns the key, if any, or null
|
|
*/
|
|
async getSessionBackupPrivateKey() {
|
|
const backupKeys = await this.olmMachine.getBackupKeys();
|
|
if (!backupKeys.decryptionKeyBase64) return null;
|
|
return Buffer.from(backupKeys.decryptionKeyBase64, "base64");
|
|
}
|
|
|
|
/**
|
|
* Store the backup decryption key.
|
|
*
|
|
* Implementation of {@link CryptoApi#storeSessionBackupPrivateKey}.
|
|
*
|
|
* @param key - the backup decryption key
|
|
*/
|
|
async storeSessionBackupPrivateKey(key) {
|
|
const base64Key = Buffer.from(key).toString("base64");
|
|
|
|
// TODO get version from backupManager
|
|
await this.olmMachine.saveBackupDecryptionKey(RustSdkCryptoJs.BackupDecryptionKey.fromBase64(base64Key), "");
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// SyncCryptoCallbacks implementation
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Apply sync changes to the olm machine
|
|
* @param events - the received to-device messages
|
|
* @param oneTimeKeysCounts - the received one time key counts
|
|
* @param unusedFallbackKeys - the received unused fallback keys
|
|
* @param devices - the received device list updates
|
|
* @returns A list of preprocessed to-device messages.
|
|
*/
|
|
async receiveSyncChanges({
|
|
events,
|
|
oneTimeKeysCounts = new Map(),
|
|
unusedFallbackKeys,
|
|
devices = new RustSdkCryptoJs.DeviceLists()
|
|
}) {
|
|
const result = await this.olmMachine.receiveSyncChanges(events ? JSON.stringify(events) : "[]", devices, oneTimeKeysCounts, unusedFallbackKeys);
|
|
|
|
// receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages.
|
|
return JSON.parse(result);
|
|
}
|
|
|
|
/** called by the sync loop to preprocess incoming to-device messages
|
|
*
|
|
* @param events - the received to-device messages
|
|
* @returns A list of preprocessed to-device messages.
|
|
*/
|
|
async preprocessToDeviceMessages(events) {
|
|
// send the received to-device messages into receiveSyncChanges. We have no info on device-list changes,
|
|
// one-time-keys, or fallback keys, so just pass empty data.
|
|
const processed = await this.receiveSyncChanges({
|
|
events
|
|
});
|
|
|
|
// look for interesting to-device messages
|
|
for (const message of processed) {
|
|
if (message.type === _event.EventType.KeyVerificationRequest) {
|
|
this.onIncomingKeyVerificationRequest(message.sender, message.content);
|
|
}
|
|
}
|
|
return processed;
|
|
}
|
|
|
|
/** called by the sync loop to process one time key counts and unused fallback keys
|
|
*
|
|
* @param oneTimeKeysCounts - the received one time key counts
|
|
* @param unusedFallbackKeys - the received unused fallback keys
|
|
*/
|
|
async processKeyCounts(oneTimeKeysCounts, unusedFallbackKeys) {
|
|
const mapOneTimeKeysCount = oneTimeKeysCounts && new Map(Object.entries(oneTimeKeysCounts));
|
|
const setUnusedFallbackKeys = unusedFallbackKeys && new Set(unusedFallbackKeys);
|
|
if (mapOneTimeKeysCount !== undefined || setUnusedFallbackKeys !== undefined) {
|
|
await this.receiveSyncChanges({
|
|
oneTimeKeysCounts: mapOneTimeKeysCount,
|
|
unusedFallbackKeys: setUnusedFallbackKeys
|
|
});
|
|
}
|
|
}
|
|
|
|
/** called by the sync loop to process the notification that device lists have
|
|
* been changed.
|
|
*
|
|
* @param deviceLists - device_lists field from /sync
|
|
*/
|
|
async processDeviceLists(deviceLists) {
|
|
var _deviceLists$changed, _deviceLists$left;
|
|
const devices = new RustSdkCryptoJs.DeviceLists((_deviceLists$changed = deviceLists.changed) === null || _deviceLists$changed === void 0 ? void 0 : _deviceLists$changed.map(userId => new RustSdkCryptoJs.UserId(userId)), (_deviceLists$left = deviceLists.left) === null || _deviceLists$left === void 0 ? void 0 : _deviceLists$left.map(userId => new RustSdkCryptoJs.UserId(userId)));
|
|
await this.receiveSyncChanges({
|
|
devices
|
|
});
|
|
}
|
|
|
|
/** called by the sync loop on m.room.encrypted events
|
|
*
|
|
* @param room - in which the event was received
|
|
* @param event - encryption event to be processed
|
|
*/
|
|
async onCryptoEvent(room, event) {
|
|
const config = event.getContent();
|
|
const existingEncryptor = this.roomEncryptors[room.roomId];
|
|
if (existingEncryptor) {
|
|
existingEncryptor.onCryptoEvent(config);
|
|
} else {
|
|
this.roomEncryptors[room.roomId] = new _RoomEncryptor.RoomEncryptor(this.olmMachine, this.keyClaimManager, this.outgoingRequestProcessor, room, config);
|
|
}
|
|
|
|
// start tracking devices for any users already known to be in this room.
|
|
const members = await room.getEncryptionTargetMembers();
|
|
_logger.logger.debug(`[${room.roomId} encryption] starting to track devices for: `, members.map(u => `${u.userId} (${u.membership})`));
|
|
await this.olmMachine.updateTrackedUsers(members.map(u => new RustSdkCryptoJs.UserId(u.userId)));
|
|
}
|
|
|
|
/** called by the sync loop after processing each sync.
|
|
*
|
|
* TODO: figure out something equivalent for sliding sync.
|
|
*
|
|
* @param syncState - information on the completed sync.
|
|
*/
|
|
onSyncCompleted(syncState) {
|
|
// Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing
|
|
// request loop, if it's not already running.
|
|
this.outgoingRequestLoop();
|
|
}
|
|
|
|
/**
|
|
* Handle an incoming m.key.verification request event
|
|
*
|
|
* @param sender - the sender of the event
|
|
* @param content - the content of the event
|
|
*/
|
|
onIncomingKeyVerificationRequest(sender, content) {
|
|
const transactionId = content.transaction_id;
|
|
if (!transactionId || !sender) {
|
|
// not a valid request: ignore
|
|
return;
|
|
}
|
|
const request = this.olmMachine.getVerificationRequest(new RustSdkCryptoJs.UserId(sender), transactionId);
|
|
if (request) {
|
|
this.emit(_crypto2.CryptoEvent.VerificationRequestReceived, new _verification.RustVerificationRequest(request, this.outgoingRequestProcessor, this._supportedVerificationMethods));
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Other public functions
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/** called by the MatrixClient on a room membership event
|
|
*
|
|
* @param event - The matrix event which caused this event to fire.
|
|
* @param member - The member whose RoomMember.membership changed.
|
|
* @param oldMembership - The previous membership state. Null if it's a new member.
|
|
*/
|
|
onRoomMembership(event, member, oldMembership) {
|
|
const enc = this.roomEncryptors[event.getRoomId()];
|
|
if (!enc) {
|
|
// not encrypting in this room
|
|
return;
|
|
}
|
|
enc.onRoomMembership(member);
|
|
}
|
|
|
|
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback
|
|
*
|
|
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We
|
|
* check if we have any events waiting for the given keys, and schedule them for
|
|
* a decryption retry if so.
|
|
*
|
|
* @param keys - details of the updated keys
|
|
*/
|
|
async onRoomKeysUpdated(keys) {
|
|
for (const key of keys) {
|
|
this.onRoomKeyUpdated(key);
|
|
}
|
|
}
|
|
onRoomKeyUpdated(key) {
|
|
_logger.logger.debug(`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`);
|
|
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
|
|
if (pendingList.length === 0) return;
|
|
_logger.logger.debug("Retrying decryption on events:", pendingList.map(e => `${e.getId()}`));
|
|
|
|
// Have another go at decrypting events with this key.
|
|
//
|
|
// We don't want to end up blocking the callback from Rust, which could otherwise end up dropping updates,
|
|
// so we don't wait for the decryption to complete. In any case, there is no need to wait:
|
|
// MatrixEvent.attemptDecryption ensures that there is only one decryption attempt happening at once,
|
|
// and deduplicates repeated attempts for the same event.
|
|
for (const ev of pendingList) {
|
|
ev.attemptDecryption(this, {
|
|
isRetry: true
|
|
}).catch(_e => {
|
|
_logger.logger.info(`Still unable to decrypt event ${ev.getId()} after receiving key`);
|
|
});
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Outgoing requests
|
|
//
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
async outgoingRequestLoop() {
|
|
if (this.outgoingRequestLoopRunning) {
|
|
return;
|
|
}
|
|
this.outgoingRequestLoopRunning = true;
|
|
try {
|
|
while (!this.stopped) {
|
|
const outgoingRequests = await this.olmMachine.outgoingRequests();
|
|
if (outgoingRequests.length == 0 || this.stopped) {
|
|
// no more messages to send (or we have been told to stop): exit the loop
|
|
return;
|
|
}
|
|
for (const msg of outgoingRequests) {
|
|
await this.outgoingRequestProcessor.makeOutgoingRequest(msg);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
_logger.logger.error("Error processing outgoing-message requests from rust crypto-sdk", e);
|
|
} finally {
|
|
this.outgoingRequestLoopRunning = false;
|
|
}
|
|
}
|
|
}
|
|
exports.RustCrypto = RustCrypto;
|
|
class EventDecryptor {
|
|
constructor(olmMachine) {
|
|
this.olmMachine = olmMachine;
|
|
/**
|
|
* Events which we couldn't decrypt due to unknown sessions / indexes.
|
|
*
|
|
* Map from senderKey to sessionId to Set of MatrixEvents
|
|
*/
|
|
(0, _defineProperty2.default)(this, "eventsPendingKey", new _utils.MapWithDefault(() => new _utils.MapWithDefault(() => new Set())));
|
|
}
|
|
async attemptEventDecryption(event) {
|
|
_logger.logger.info("Attempting decryption of event", event);
|
|
// add the event to the pending list *before* attempting to decrypt.
|
|
// then, if the key turns up while decryption is in progress (and
|
|
// decryption fails), we will schedule a retry.
|
|
// (fixes https://github.com/vector-im/element-web/issues/5001)
|
|
this.addEventToPendingList(event);
|
|
const res = await this.olmMachine.decryptRoomEvent(JSON.stringify({
|
|
event_id: event.getId(),
|
|
type: event.getWireType(),
|
|
sender: event.getSender(),
|
|
state_key: event.getStateKey(),
|
|
content: event.getWireContent(),
|
|
origin_server_ts: event.getTs()
|
|
}), new RustSdkCryptoJs.RoomId(event.getRoomId()));
|
|
|
|
// Success. We can remove the event from the pending list, if
|
|
// that hasn't already happened.
|
|
this.removeEventFromPendingList(event);
|
|
return {
|
|
clearEvent: JSON.parse(res.event),
|
|
claimedEd25519Key: res.senderClaimedEd25519Key,
|
|
senderCurve25519Key: res.senderCurve25519Key,
|
|
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Look for events which are waiting for a given megolm session
|
|
*
|
|
* Returns a list of events which were encrypted by `session` and could not be decrypted
|
|
*
|
|
* @param session -
|
|
*/
|
|
getEventsPendingRoomKey(session) {
|
|
const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64());
|
|
if (!senderPendingEvents) return [];
|
|
const sessionPendingEvents = senderPendingEvents.get(session.sessionId);
|
|
if (!sessionPendingEvents) return [];
|
|
const roomId = session.roomId.toString();
|
|
return [...sessionPendingEvents].filter(ev => ev.getRoomId() === roomId);
|
|
}
|
|
|
|
/**
|
|
* Add an event to the list of those awaiting their session keys.
|
|
*/
|
|
addEventToPendingList(event) {
|
|
const content = event.getWireContent();
|
|
const senderKey = content.sender_key;
|
|
const sessionId = content.session_id;
|
|
const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey);
|
|
const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId);
|
|
sessionPendingEvents.add(event);
|
|
}
|
|
|
|
/**
|
|
* Remove an event from the list of those awaiting their session keys.
|
|
*/
|
|
removeEventFromPendingList(event) {
|
|
const content = event.getWireContent();
|
|
const senderKey = content.sender_key;
|
|
const sessionId = content.session_id;
|
|
const senderPendingEvents = this.eventsPendingKey.get(senderKey);
|
|
if (!senderPendingEvents) return;
|
|
const sessionPendingEvents = senderPendingEvents.get(sessionId);
|
|
if (!sessionPendingEvents) return;
|
|
sessionPendingEvents.delete(event);
|
|
|
|
// also clean up the higher-level maps if they are now empty
|
|
if (sessionPendingEvents.size === 0) {
|
|
senderPendingEvents.delete(sessionId);
|
|
if (senderPendingEvents.size === 0) {
|
|
this.eventsPendingKey.delete(senderKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=rust-crypto.js.map
|