summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
diff options
context:
space:
mode:
authorRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
committerRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
commit633c92eae865e957121e08de634aeee11a8b3992 (patch)
tree09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
parentc4657e4509733699c0f26a3c900bab47e915d5a0 (diff)
downloadpluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts566
1 files changed, 566 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
new file mode 100644
index 0000000..c37b7f0
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/olmlib.ts
@@ -0,0 +1,566 @@
+/*
+Copyright 2016 - 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.
+*/
+
+/**
+ * Utilities common to olm encryption algorithms
+ */
+
+import anotherjson from "another-json";
+
+import type { PkSigning } from "@matrix-org/olm";
+import type { IOneTimeKey } from "../@types/crypto";
+import { OlmDevice } from "./OlmDevice";
+import { DeviceInfo } from "./deviceinfo";
+import { logger } from "../logger";
+import { IClaimOTKsResult, MatrixClient } from "../client";
+import { ISignatures } from "../@types/signed";
+import { MatrixEvent } from "../models/event";
+import { EventType } from "../@types/event";
+import { IMessage } from "./algorithms/olm";
+import { MapWithDefault } from "../utils";
+
+enum Algorithm {
+ Olm = "m.olm.v1.curve25519-aes-sha2",
+ Megolm = "m.megolm.v1.aes-sha2",
+ MegolmBackup = "m.megolm_backup.v1.curve25519-aes-sha2",
+}
+
+/**
+ * matrix algorithm tag for olm
+ */
+export const OLM_ALGORITHM = Algorithm.Olm;
+
+/**
+ * matrix algorithm tag for megolm
+ */
+export const MEGOLM_ALGORITHM = Algorithm.Megolm;
+
+/**
+ * matrix algorithm tag for megolm backups
+ */
+export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup;
+
+export interface IOlmSessionResult {
+ /** device info */
+ device: DeviceInfo;
+ /** base64 olm session id; null if no session could be established */
+ sessionId: string | null;
+}
+
+/**
+ * Encrypt an event payload for an Olm device
+ *
+ * @param resultsObject - The `ciphertext` property
+ * of the m.room.encrypted event to which to add our result
+ *
+ * @param olmDevice - olm.js wrapper
+ * @param payloadFields - fields to include in the encrypted payload
+ *
+ * Returns a promise which resolves (to undefined) when the payload
+ * has been encrypted into `resultsObject`
+ */
+export async function encryptMessageForDevice(
+ resultsObject: Record<string, IMessage>,
+ ourUserId: string,
+ ourDeviceId: string | undefined,
+ olmDevice: OlmDevice,
+ recipientUserId: string,
+ recipientDevice: DeviceInfo,
+ payloadFields: Record<string, any>,
+): Promise<void> {
+ const deviceKey = recipientDevice.getIdentityKey();
+ const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
+ if (sessionId === null) {
+ // If we don't have a session for a device then
+ // we can't encrypt a message for it.
+ logger.log(
+ `[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` +
+ `${recipientUserId}:${recipientDevice.deviceId}`,
+ );
+ return;
+ }
+
+ logger.log(
+ `[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` +
+ `${recipientUserId}:${recipientDevice.deviceId}`,
+ );
+
+ const payload = {
+ sender: ourUserId,
+ // TODO this appears to no longer be used whatsoever
+ sender_device: ourDeviceId,
+
+ // Include the Ed25519 key so that the recipient knows what
+ // device this message came from.
+ // We don't need to include the curve25519 key since the
+ // recipient will already know this from the olm headers.
+ // When combined with the device keys retrieved from the
+ // homeserver signed by the ed25519 key this proves that
+ // the curve25519 key and the ed25519 key are owned by
+ // the same device.
+ keys: {
+ ed25519: olmDevice.deviceEd25519Key,
+ },
+
+ // include the recipient device details in the payload,
+ // to avoid unknown key attacks, per
+ // https://github.com/vector-im/vector-web/issues/2483
+ recipient: recipientUserId,
+ recipient_keys: {
+ ed25519: recipientDevice.getFingerprint(),
+ },
+ ...payloadFields,
+ };
+
+ // TODO: technically, a bunch of that stuff only needs to be included for
+ // pre-key messages: after that, both sides know exactly which devices are
+ // involved in the session. If we're looking to reduce data transfer in the
+ // future, we could elide them for subsequent messages.
+
+ resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload));
+}
+
+interface IExistingOlmSession {
+ device: DeviceInfo;
+ sessionId: string | null;
+}
+
+/**
+ * Get the existing olm sessions for the given devices, and the devices that
+ * don't have olm sessions.
+ *
+ *
+ *
+ * @param devicesByUser - map from userid to list of devices to ensure sessions for
+ *
+ * @returns resolves to an array. The first element of the array is a
+ * a map of user IDs to arrays of deviceInfo, representing the devices that
+ * don't have established olm sessions. The second element of the array is
+ * a map from userId to deviceId to {@link OlmSessionResult}
+ */
+export async function getExistingOlmSessions(
+ olmDevice: OlmDevice,
+ baseApis: MatrixClient,
+ devicesByUser: Record<string, DeviceInfo[]>,
+): Promise<[Map<string, DeviceInfo[]>, Map<string, Map<string, IExistingOlmSession>>]> {
+ // map user Id → DeviceInfo[]
+ const devicesWithoutSession: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
+ // map user Id → device Id → IExistingOlmSession
+ const sessions: MapWithDefault<string, Map<string, IExistingOlmSession>> = new MapWithDefault(() => new Map());
+
+ const promises: Promise<void>[] = [];
+
+ for (const [userId, devices] of Object.entries(devicesByUser)) {
+ for (const deviceInfo of devices) {
+ const deviceId = deviceInfo.deviceId;
+ const key = deviceInfo.getIdentityKey();
+ promises.push(
+ (async (): Promise<void> => {
+ const sessionId = await olmDevice.getSessionIdForDevice(key, true);
+ if (sessionId === null) {
+ devicesWithoutSession.getOrCreate(userId).push(deviceInfo);
+ } else {
+ sessions.getOrCreate(userId).set(deviceId, {
+ device: deviceInfo,
+ sessionId: sessionId,
+ });
+ }
+ })(),
+ );
+ }
+ }
+
+ await Promise.all(promises);
+
+ return [devicesWithoutSession, sessions];
+}
+
+/**
+ * Try to make sure we have established olm sessions for the given devices.
+ *
+ * @param devicesByUser - map from userid to list of devices to ensure sessions for
+ *
+ * @param force - If true, establish a new session even if one
+ * already exists.
+ *
+ * @param otkTimeout - The timeout in milliseconds when requesting
+ * one-time keys for establishing new olm sessions.
+ *
+ * @param failedServers - An array to fill with remote servers that
+ * failed to respond to one-time-key requests.
+ *
+ * @param log - A possibly customised log
+ *
+ * @returns resolves once the sessions are complete, to
+ * an Object mapping from userId to deviceId to
+ * {@link OlmSessionResult}
+ */
+export async function ensureOlmSessionsForDevices(
+ olmDevice: OlmDevice,
+ baseApis: MatrixClient,
+ devicesByUser: Map<string, DeviceInfo[]>,
+ force = false,
+ otkTimeout?: number,
+ failedServers?: string[],
+ log = logger,
+): Promise<Map<string, Map<string, IOlmSessionResult>>> {
+ const devicesWithoutSession: [string, string][] = [
+ // [userId, deviceId], ...
+ ];
+ // map user Id → device Id → IExistingOlmSession
+ const result: Map<string, Map<string, IExistingOlmSession>> = new Map();
+ // map device key → resolve session fn
+ const resolveSession: Map<string, (sessionId?: string) => void> = new Map();
+
+ // Mark all sessions this task intends to update as in progress. It is
+ // important to do this for all devices this task cares about in a single
+ // synchronous operation, as otherwise it is possible to have deadlocks
+ // where multiple tasks wait indefinitely on another task to update some set
+ // of common devices.
+ for (const devices of devicesByUser.values()) {
+ for (const deviceInfo of devices) {
+ const key = deviceInfo.getIdentityKey();
+
+ if (key === olmDevice.deviceCurve25519Key) {
+ // We don't start sessions with ourself, so there's no need to
+ // mark it in progress.
+ continue;
+ }
+
+ if (!olmDevice.sessionsInProgress[key]) {
+ // pre-emptively mark the session as in-progress to avoid race
+ // conditions. If we find that we already have a session, then
+ // we'll resolve
+ olmDevice.sessionsInProgress[key] = new Promise((resolve) => {
+ resolveSession.set(key, (v: any): void => {
+ delete olmDevice.sessionsInProgress[key];
+ resolve(v);
+ });
+ });
+ }
+ }
+ }
+
+ for (const [userId, devices] of devicesByUser) {
+ const resultDevices = new Map();
+ result.set(userId, resultDevices);
+
+ for (const deviceInfo of devices) {
+ const deviceId = deviceInfo.deviceId;
+ const key = deviceInfo.getIdentityKey();
+
+ if (key === olmDevice.deviceCurve25519Key) {
+ // We should never be trying to start a session with ourself.
+ // Apart from talking to yourself being the first sign of madness,
+ // olm sessions can't do this because they get confused when
+ // they get a message and see that the 'other side' has started a
+ // new chain when this side has an active sender chain.
+ // If you see this message being logged in the wild, we should find
+ // the thing that is trying to send Olm messages to itself and fix it.
+ log.info("Attempted to start session with ourself! Ignoring");
+ // We must fill in the section in the return value though, as callers
+ // expect it to be there.
+ resultDevices.set(deviceId, {
+ device: deviceInfo,
+ sessionId: null,
+ });
+ continue;
+ }
+
+ const forWhom = `for ${key} (${userId}:${deviceId})`;
+ const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log);
+ const resolveSessionFn = resolveSession.get(key);
+ if (sessionId !== null && resolveSessionFn) {
+ // we found a session, but we had marked the session as
+ // in-progress, so resolve it now, which will unmark it and
+ // unblock anything that was waiting
+ resolveSessionFn();
+ }
+ if (sessionId === null || force) {
+ if (force) {
+ log.info(`Forcing new Olm session ${forWhom}`);
+ } else {
+ log.info(`Making new Olm session ${forWhom}`);
+ }
+ devicesWithoutSession.push([userId, deviceId]);
+ }
+ resultDevices.set(deviceId, {
+ device: deviceInfo,
+ sessionId: sessionId,
+ });
+ }
+ }
+
+ if (devicesWithoutSession.length === 0) {
+ return result;
+ }
+
+ const oneTimeKeyAlgorithm = "signed_curve25519";
+ let res: IClaimOTKsResult;
+ let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
+ try {
+ log.debug(`Claiming ${taskDetail}`);
+ res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
+ log.debug(`Claimed ${taskDetail}`);
+ } catch (e) {
+ for (const resolver of resolveSession.values()) {
+ resolver();
+ }
+ log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
+ throw e;
+ }
+
+ if (failedServers && "failures" in res) {
+ failedServers.push(...Object.keys(res.failures));
+ }
+
+ const otkResult = res.one_time_keys || ({} as IClaimOTKsResult["one_time_keys"]);
+ const promises: Promise<void>[] = [];
+ for (const [userId, devices] of devicesByUser) {
+ const userRes = otkResult[userId] || {};
+ for (const deviceInfo of devices) {
+ const deviceId = deviceInfo.deviceId;
+ const key = deviceInfo.getIdentityKey();
+
+ if (key === olmDevice.deviceCurve25519Key) {
+ // We've already logged about this above. Skip here too
+ // otherwise we'll log saying there are no one-time keys
+ // which will be confusing.
+ continue;
+ }
+
+ if (result.get(userId)?.get(deviceId)?.sessionId && !force) {
+ // we already have a result for this device
+ continue;
+ }
+
+ const deviceRes = userRes[deviceId] || {};
+ let oneTimeKey: IOneTimeKey | null = null;
+ for (const keyId in deviceRes) {
+ if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
+ oneTimeKey = deviceRes[keyId];
+ }
+ }
+
+ if (!oneTimeKey) {
+ log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
+ resolveSession.get(key)?.();
+ continue;
+ }
+
+ promises.push(
+ _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(
+ (sid) => {
+ resolveSession.get(key)?.(sid ?? undefined);
+ const deviceInfo = result.get(userId)?.get(deviceId);
+ if (deviceInfo) deviceInfo.sessionId = sid;
+ },
+ (e) => {
+ resolveSession.get(key)?.();
+ throw e;
+ },
+ ),
+ );
+ }
+ }
+
+ taskDetail = `Olm sessions for ${promises.length} devices`;
+ log.debug(`Starting ${taskDetail}`);
+ await Promise.all(promises);
+ log.debug(`Started ${taskDetail}`);
+ return result;
+}
+
+async function _verifyKeyAndStartSession(
+ olmDevice: OlmDevice,
+ oneTimeKey: IOneTimeKey,
+ userId: string,
+ deviceInfo: DeviceInfo,
+): Promise<string | null> {
+ const deviceId = deviceInfo.deviceId;
+ try {
+ await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint());
+ } catch (e) {
+ logger.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e);
+ return null;
+ }
+
+ let sid;
+ try {
+ sid = await olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key);
+ } catch (e) {
+ // possibly a bad key
+ logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e);
+ return null;
+ }
+
+ logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId);
+ return sid;
+}
+
+export interface IObject {
+ unsigned?: object;
+ signatures?: ISignatures;
+}
+
+/**
+ * Verify the signature on an object
+ *
+ * @param olmDevice - olm wrapper to use for verify op
+ *
+ * @param obj - object to check signature on.
+ *
+ * @param signingUserId - ID of the user whose signature should be checked
+ *
+ * @param signingDeviceId - ID of the device whose signature should be checked
+ *
+ * @param signingKey - base64-ed ed25519 public key
+ *
+ * Returns a promise which resolves (to undefined) if the the signature is good,
+ * or rejects with an Error if it is bad.
+ */
+export async function verifySignature(
+ olmDevice: OlmDevice,
+ obj: IOneTimeKey | IObject,
+ signingUserId: string,
+ signingDeviceId: string,
+ signingKey: string,
+): Promise<void> {
+ const signKeyId = "ed25519:" + signingDeviceId;
+ const signatures = obj.signatures || {};
+ const userSigs = signatures[signingUserId] || {};
+ const signature = userSigs[signKeyId];
+ if (!signature) {
+ throw Error("No signature");
+ }
+
+ // prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson
+ const mangledObj = Object.assign({}, obj);
+ if ("unsigned" in mangledObj) {
+ delete mangledObj.unsigned;
+ }
+ delete mangledObj.signatures;
+ const json = anotherjson.stringify(mangledObj);
+
+ olmDevice.verifySignature(signingKey, json, signature);
+}
+
+/**
+ * Sign a JSON object using public key cryptography
+ * @param obj - Object to sign. The object will be modified to include
+ * the new signature
+ * @param key - the signing object or the private key
+ * seed
+ * @param userId - The user ID who owns the signing key
+ * @param pubKey - The public key (ignored if key is a seed)
+ * @returns the signature for the object
+ */
+export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
+ let createdKey = false;
+ if (key instanceof Uint8Array) {
+ const keyObj = new global.Olm.PkSigning();
+ pubKey = keyObj.init_with_seed(key);
+ key = keyObj;
+ createdKey = true;
+ }
+ const sigs = obj.signatures || {};
+ delete obj.signatures;
+ const unsigned = obj.unsigned;
+ if (obj.unsigned) delete obj.unsigned;
+ try {
+ const mysigs = sigs[userId] || {};
+ sigs[userId] = mysigs;
+
+ return (mysigs["ed25519:" + pubKey] = key.sign(anotherjson.stringify(obj)));
+ } finally {
+ obj.signatures = sigs;
+ if (unsigned) obj.unsigned = unsigned;
+ if (createdKey) {
+ key.free();
+ }
+ }
+}
+
+/**
+ * Verify a signed JSON object
+ * @param obj - Object to verify
+ * @param pubKey - The public key to use to verify
+ * @param userId - The user ID who signed the object
+ */
+export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
+ const keyId = "ed25519:" + pubKey;
+ if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
+ throw new Error("No signature");
+ }
+ const signature = obj.signatures[userId][keyId];
+ const util = new global.Olm.Utility();
+ const sigs = obj.signatures;
+ delete obj.signatures;
+ const unsigned = obj.unsigned;
+ if (obj.unsigned) delete obj.unsigned;
+ try {
+ util.ed25519_verify(pubKey, anotherjson.stringify(obj), signature);
+ } finally {
+ obj.signatures = sigs;
+ if (unsigned) obj.unsigned = unsigned;
+ util.free();
+ }
+}
+
+/**
+ * Check that an event was encrypted using olm.
+ */
+export function isOlmEncrypted(event: MatrixEvent): boolean {
+ if (!event.getSenderKey()) {
+ logger.error("Event has no sender key (not encrypted?)");
+ return false;
+ }
+ if (
+ event.getWireType() !== EventType.RoomMessageEncrypted ||
+ !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)
+ ) {
+ logger.error("Event was not encrypted using an appropriate algorithm");
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Encode a typed array of uint8 as base64.
+ * @param uint8Array - The data to encode.
+ * @returns The base64.
+ */
+export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
+ return Buffer.from(uint8Array).toString("base64");
+}
+
+/**
+ * Encode a typed array of uint8 as unpadded base64.
+ * @param uint8Array - The data to encode.
+ * @returns The unpadded base64.
+ */
+export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
+ return encodeBase64(uint8Array).replace(/=+$/g, "");
+}
+
+/**
+ * Decode a base64 string to a typed array of uint8.
+ * @param base64 - The base64 to decode.
+ * @returns The decoded data.
+ */
+export function decodeBase64(base64: string): Uint8Array {
+ return Buffer.from(base64, "base64");
+}