diff options
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.ts | 271 |
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; + } + } +} |