matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
463 lines (449 loc) • 17.8 kB
JavaScript
;
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");
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) {
const devicesWithoutSession = {};
const sessions = {};
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[userId] = devicesWithoutSession[userId] || [];
devicesWithoutSession[userId].push(deviceInfo);
} else {
sessions[userId] = sessions[userId] || {};
sessions[userId][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], ...
];
const result = {};
const resolveSession = {};
// 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 Object.entries(devicesByUser)) {
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[key] = v => {
delete olmDevice.sessionsInProgress[key];
resolve(v);
};
});
}
}
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
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.
result[userId][deviceId] = {
device: deviceInfo,
sessionId: null
};
continue;
}
const forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
if (sessionId !== null && resolveSession[key]) {
// 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
resolveSession[key]();
}
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]);
}
result[userId][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 Object.values(resolveSession)) {
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 Object.entries(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[userId][deviceId].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) {
log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
if (resolveSession[key]) {
resolveSession[key]();
}
continue;
}
promises.push(_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(sid => {
if (resolveSession[key]) {
resolveSession[key](sid !== null && sid !== void 0 ? sid : undefined);
}
result[userId][deviceId].sessionId = sid;
}, e => {
if (resolveSession[key]) {
resolveSession[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, 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