"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.OLM_ALGORITHM = exports.MEGOLM_BACKUP_ALGORITHM = exports.MEGOLM_ALGORITHM = void 0; exports.decodeBase64 = decodeBase64; exports.encodeBase64 = encodeBase64; exports.encodeUnpaddedBase64 = encodeUnpaddedBase64; exports.encryptMessageForDevice = encryptMessageForDevice; exports.ensureOlmSessionsForDevices = ensureOlmSessionsForDevices; exports.getExistingOlmSessions = getExistingOlmSessions; exports.isOlmEncrypted = isOlmEncrypted; exports.pkSign = pkSign; exports.pkVerify = pkVerify; exports.verifySignature = verifySignature; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _anotherJson = _interopRequireDefault(require("another-json")); var _logger = require("../logger"); var _event = require("../@types/event"); var _utils = require("../utils"); 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; } var Algorithm; /** * matrix algorithm tag for olm */ (function (Algorithm) { Algorithm["Olm"] = "m.olm.v1.curve25519-aes-sha2"; Algorithm["Megolm"] = "m.megolm.v1.aes-sha2"; Algorithm["MegolmBackup"] = "m.megolm_backup.v1.curve25519-aes-sha2"; })(Algorithm || (Algorithm = {})); const OLM_ALGORITHM = Algorithm.Olm; /** * matrix algorithm tag for megolm */ exports.OLM_ALGORITHM = OLM_ALGORITHM; const MEGOLM_ALGORITHM = Algorithm.Megolm; /** * matrix algorithm tag for megolm backups */ exports.MEGOLM_ALGORITHM = MEGOLM_ALGORITHM; const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup; exports.MEGOLM_BACKUP_ALGORITHM = MEGOLM_BACKUP_ALGORITHM; /** * 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` */ async function encryptMessageForDevice(resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields) { 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.logger.log(`[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` + `${recipientUserId}:${recipientDevice.deviceId}`); return; } _logger.logger.log(`[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` + `${recipientUserId}:${recipientDevice.deviceId}`); const payload = _objectSpread({ 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)); } /** * 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} */ async function getExistingOlmSessions(olmDevice, baseApis, devicesByUser) { // map user Id → DeviceInfo[] const devicesWithoutSession = new _utils.MapWithDefault(() => []); // map user Id → device Id → IExistingOlmSession const sessions = new _utils.MapWithDefault(() => new Map()); const promises = []; for (const [userId, devices] of Object.entries(devicesByUser)) { for (const deviceInfo of devices) { const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); promises.push((async () => { 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} */ async function ensureOlmSessionsForDevices(olmDevice, baseApis, devicesByUser, force = false, otkTimeout, failedServers, log = _logger.logger) { const devicesWithoutSession = [ // [userId, deviceId], ... ]; // map user Id → device Id → IExistingOlmSession const result = new Map(); // map device key → resolve session fn const resolveSession = 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 => { 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; 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 || {}; const promises = []; for (const [userId, devices] of devicesByUser) { const userRes = otkResult[userId] || {}; for (const deviceInfo of devices) { var _result$get, _result$get$get; 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 = result.get(userId)) !== null && _result$get !== void 0 && (_result$get$get = _result$get.get(deviceId)) !== null && _result$get$get !== void 0 && _result$get$get.sessionId && !force) { // we already have a result for this device continue; } const deviceRes = userRes[deviceId] || {}; let oneTimeKey = null; for (const keyId in deviceRes) { if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { oneTimeKey = deviceRes[keyId]; } } if (!oneTimeKey) { var _resolveSession$get; log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`); (_resolveSession$get = resolveSession.get(key)) === null || _resolveSession$get === void 0 ? void 0 : _resolveSession$get(); continue; } promises.push(_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(sid => { var _resolveSession$get2, _result$get2; (_resolveSession$get2 = resolveSession.get(key)) === null || _resolveSession$get2 === void 0 ? void 0 : _resolveSession$get2(sid !== null && sid !== void 0 ? sid : undefined); const deviceInfo = (_result$get2 = result.get(userId)) === null || _result$get2 === void 0 ? void 0 : _result$get2.get(deviceId); if (deviceInfo) deviceInfo.sessionId = sid; }, e => { var _resolveSession$get3; (_resolveSession$get3 = resolveSession.get(key)) === null || _resolveSession$get3 === void 0 ? void 0 : _resolveSession$get3(); 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, oneTimeKey, userId, deviceInfo) { const deviceId = deviceInfo.deviceId; try { await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint()); } catch (e) { _logger.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.logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e); return null; } _logger.logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId); return sid; } /** * 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. */ async function verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) { 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.default.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 */ function pkSign(obj, key, userId, pubKey) { 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.default.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 */ function pkVerify(obj, pubKey, userId) { 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.default.stringify(obj), signature); } finally { obj.signatures = sigs; if (unsigned) obj.unsigned = unsigned; util.free(); } } /** * Check that an event was encrypted using olm. */ function isOlmEncrypted(event) { if (!event.getSenderKey()) { _logger.logger.error("Event has no sender key (not encrypted?)"); return false; } if (event.getWireType() !== _event.EventType.RoomMessageEncrypted || !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)) { _logger.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. */ function encodeBase64(uint8Array) { 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. */ function encodeUnpaddedBase64(uint8Array) { 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. */ function decodeBase64(base64) { return Buffer.from(base64, "base64"); } //# sourceMappingURL=olmlib.js.map