summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
diff options
context:
space:
mode:
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts271
1 files changed, 271 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
new file mode 100644
index 0000000..373b236
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/dehydration.ts
@@ -0,0 +1,271 @@
+/*
+Copyright 2020-2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import anotherjson from "another-json";
+
+import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
+import { decodeBase64, encodeBase64 } from "./olmlib";
+import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
+import { decryptAES, encryptAES } from "./aes";
+import { logger } from "../logger";
+import { Crypto } from "./index";
+import { Method } from "../http-api";
+import { SecretStorageKeyDescription } from "../secret-storage";
+
+export interface IDehydratedDevice {
+ device_id: string; // eslint-disable-line camelcase
+ device_data: SecretStorageKeyDescription & {
+ // eslint-disable-line camelcase
+ algorithm: string;
+ account: string; // pickle
+ };
+}
+
+export interface IDehydratedDeviceKeyInfo {
+ passphrase?: string;
+}
+
+export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
+
+const oneweek = 7 * 24 * 60 * 60 * 1000;
+
+export class DehydrationManager {
+ private inProgress = false;
+ private timeoutId: any;
+ private key?: Uint8Array;
+ private keyInfo?: { [props: string]: any };
+ private deviceDisplayName?: string;
+
+ public constructor(private readonly crypto: Crypto) {
+ this.getDehydrationKeyFromCache();
+ }
+
+ public getDehydrationKeyFromCache(): Promise<void> {
+ return this.crypto.cryptoStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
+ this.crypto.cryptoStore.getSecretStorePrivateKey(
+ txn,
+ async (result) => {
+ if (result) {
+ const { key, keyInfo, deviceDisplayName, time } = result;
+ const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
+ const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
+ this.key = decodeBase64(decrypted);
+ this.keyInfo = keyInfo;
+ this.deviceDisplayName = deviceDisplayName;
+ const now = Date.now();
+ const delay = Math.max(1, time + oneweek - now);
+ this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), delay);
+ }
+ },
+ "dehydration",
+ );
+ });
+ }
+
+ /** set the key, and queue periodic dehydration to the server in the background */
+ public async setKeyAndQueueDehydration(
+ key: Uint8Array,
+ keyInfo: { [props: string]: any } = {},
+ deviceDisplayName?: string,
+ ): Promise<void> {
+ const matches = await this.setKey(key, keyInfo, deviceDisplayName);
+ if (!matches) {
+ // start dehydration in the background
+ this.dehydrateDevice();
+ }
+ }
+
+ public async setKey(
+ key: Uint8Array,
+ keyInfo: { [props: string]: any } = {},
+ deviceDisplayName?: string,
+ ): Promise<boolean | undefined> {
+ if (!key) {
+ // unsetting the key -- cancel any pending dehydration task
+ if (this.timeoutId) {
+ global.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ }
+ // clear storage
+ await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
+ this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", null);
+ });
+ this.key = undefined;
+ this.keyInfo = undefined;
+ return;
+ }
+
+ // Check to see if it's the same key as before. If it's different,
+ // dehydrate a new device. If it's the same, we can keep the same
+ // device. (Assume that keyInfo and deviceDisplayName will be the
+ // same if the key is the same.)
+ let matches: boolean = !!this.key && key.length == this.key.length;
+ for (let i = 0; matches && i < key.length; i++) {
+ if (key[i] != this.key![i]) {
+ matches = false;
+ }
+ }
+ if (!matches) {
+ this.key = key;
+ this.keyInfo = keyInfo;
+ this.deviceDisplayName = deviceDisplayName;
+ }
+ return matches;
+ }
+
+ /** returns the device id of the newly created dehydrated device */
+ public async dehydrateDevice(): Promise<string | undefined> {
+ if (this.inProgress) {
+ logger.log("Dehydration already in progress -- not starting new dehydration");
+ return;
+ }
+ this.inProgress = true;
+ if (this.timeoutId) {
+ global.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ }
+ try {
+ const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey);
+
+ // update the crypto store with the timestamp
+ const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM);
+ await this.crypto.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
+ this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", {
+ keyInfo: this.keyInfo,
+ key,
+ deviceDisplayName: this.deviceDisplayName!,
+ time: Date.now(),
+ });
+ });
+ logger.log("Attempting to dehydrate device");
+
+ logger.log("Creating account");
+ // create the account and all the necessary keys
+ const account = new global.Olm.Account();
+ account.create();
+ const e2eKeys = JSON.parse(account.identity_keys());
+
+ const maxKeys = account.max_number_of_one_time_keys();
+ // FIXME: generate in small batches?
+ account.generate_one_time_keys(maxKeys / 2);
+ account.generate_fallback_key();
+ const otks: Record<string, string> = JSON.parse(account.one_time_keys());
+ const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
+ account.mark_keys_as_published();
+
+ // dehydrate the account and store it on the server
+ const pickledAccount = account.pickle(new Uint8Array(this.key!));
+
+ const deviceData: { [props: string]: any } = {
+ algorithm: DEHYDRATION_ALGORITHM,
+ account: pickledAccount,
+ };
+ if (this.keyInfo!.passphrase) {
+ deviceData.passphrase = this.keyInfo!.passphrase;
+ }
+
+ logger.log("Uploading account to server");
+ // eslint-disable-next-line camelcase
+ const dehydrateResult = await this.crypto.baseApis.http.authedRequest<{ device_id: string }>(
+ Method.Put,
+ "/dehydrated_device",
+ undefined,
+ {
+ device_data: deviceData,
+ initial_device_display_name: this.deviceDisplayName,
+ },
+ {
+ prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
+ },
+ );
+
+ // send the keys to the server
+ const deviceId = dehydrateResult.device_id;
+ logger.log("Preparing device keys", deviceId);
+ const deviceKeys: IDeviceKeys = {
+ algorithms: this.crypto.supportedAlgorithms,
+ device_id: deviceId,
+ user_id: this.crypto.userId,
+ keys: {
+ [`ed25519:${deviceId}`]: e2eKeys.ed25519,
+ [`curve25519:${deviceId}`]: e2eKeys.curve25519,
+ },
+ };
+ const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
+ deviceKeys.signatures = {
+ [this.crypto.userId]: {
+ [`ed25519:${deviceId}`]: deviceSignature,
+ },
+ };
+ if (this.crypto.crossSigningInfo.getId("self_signing")) {
+ await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
+ }
+
+ logger.log("Preparing one-time keys");
+ const oneTimeKeys: Record<string, IOneTimeKey> = {};
+ for (const [keyId, key] of Object.entries(otks.curve25519)) {
+ const k: IOneTimeKey = { key };
+ const signature = account.sign(anotherjson.stringify(k));
+ k.signatures = {
+ [this.crypto.userId]: {
+ [`ed25519:${deviceId}`]: signature,
+ },
+ };
+ oneTimeKeys[`signed_curve25519:${keyId}`] = k;
+ }
+
+ logger.log("Preparing fallback keys");
+ const fallbackKeys: Record<string, IOneTimeKey> = {};
+ for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
+ const k: IOneTimeKey = { key, fallback: true };
+ const signature = account.sign(anotherjson.stringify(k));
+ k.signatures = {
+ [this.crypto.userId]: {
+ [`ed25519:${deviceId}`]: signature,
+ },
+ };
+ fallbackKeys[`signed_curve25519:${keyId}`] = k;
+ }
+
+ logger.log("Uploading keys to server");
+ await this.crypto.baseApis.http.authedRequest(
+ Method.Post,
+ "/keys/upload/" + encodeURI(deviceId),
+ undefined,
+ {
+ "device_keys": deviceKeys,
+ "one_time_keys": oneTimeKeys,
+ "org.matrix.msc2732.fallback_keys": fallbackKeys,
+ },
+ );
+ logger.log("Done dehydrating");
+
+ // dehydrate again in a week
+ this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek);
+
+ return deviceId;
+ } finally {
+ this.inProgress = false;
+ }
+ }
+
+ public stop(): void {
+ if (this.timeoutId) {
+ global.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ }
+ }
+}