"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceTracker = void 0; const LogService_1 = require("../logging/LogService"); const Crypto_1 = require("../models/Crypto"); /** * Tracks user devices for encryption operations. * @category Encryption */ class DeviceTracker { constructor(client) { this.client = client; this.deviceListUpdates = {}; } /** * Gets the device lists for the given user IDs. Outdated device lists will be updated before * returning. * @param {string[]} userIds The user IDs to get the device lists of. * @returns {Promise>} Resolves to a map of user ID to device list. * If a user has no devices, they may be excluded from the result or appear as an empty array. */ getDevicesFor(userIds) { return __awaiter(this, void 0, void 0, function* () { const outdatedUserIds = []; for (const userId of userIds) { const isOutdated = yield this.client.cryptoStore.isUserOutdated(userId); if (isOutdated) outdatedUserIds.push(userId); } yield this.updateUsersDeviceLists(outdatedUserIds); const userDeviceMap = {}; for (const userId of userIds) { userDeviceMap[userId] = yield this.client.cryptoStore.getActiveUserDevices(userId); } return userDeviceMap; }); } /** * Flags multiple user's device lists as outdated, optionally queuing an immediate update. * @param {string} userIds The user IDs to flag the device lists of. * @param {boolean} resync True (default) to queue an immediate update, false otherwise. * @returns {Promise} Resolves when the flagging has completed. Will wait for the resync * if requested too. */ flagUsersOutdated(userIds, resync = true) { return __awaiter(this, void 0, void 0, function* () { yield this.client.cryptoStore.flagUsersOutdated(userIds); if (resync) { yield this.updateUsersDeviceLists(userIds); } }); } /** * Updates multiple user's device lists regardless of outdated flag. * @param {string[]} userIds The user IDs to update. * @returns {Promise} Resolves when complete. */ updateUsersDeviceLists(userIds) { return __awaiter(this, void 0, void 0, function* () { // We wait for the lock, but still run through with our update just in case we are lagged. // This can happen if the server is slow to reply to device list queries, but a user is // changing information about their device a lot. const existingPromises = userIds.map(u => this.deviceListUpdates[u]).filter(p => !!p); if (existingPromises.length > 0) { yield Promise.all(existingPromises); } const promise = new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _a, _b; try { const resp = yield this.client.getUserDevices(userIds); for (const userId of Object.keys(resp.device_keys)) { if (!userIds.includes(userId)) { LogService_1.LogService.warn("DeviceTracker", `Server returned unexpected user ID: ${userId} - ignoring user`); continue; } const validated = []; for (const deviceId of Object.keys(resp.device_keys[userId])) { const device = resp.device_keys[userId][deviceId]; if (device.user_id !== userId || device.device_id !== deviceId) { LogService_1.LogService.warn("DeviceTracker", `Server appears to be lying about device lists: ${userId} ${deviceId} has unexpected device ${device.user_id} ${device.device_id} listed - ignoring device`); continue; } const ed25519 = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`]; const curve25519 = device.keys[`${Crypto_1.DeviceKeyAlgorithm.Curve25519}:${deviceId}`]; if (!ed25519 || !curve25519) { LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} is missing either an Ed25519 or Curve25519 key - ignoring device`); continue; } const currentDevices = yield this.client.cryptoStore.getAllUserDevices(userId); const existingDevice = currentDevices.find(d => d.device_id === deviceId); if (existingDevice) { const existingEd25519 = existingDevice.keys[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`]; if (existingEd25519 !== ed25519) { LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} appears compromised: Ed25519 key changed - ignoring device`); continue; } } const signature = (_b = (_a = device.signatures) === null || _a === void 0 ? void 0 : _a[userId]) === null || _b === void 0 ? void 0 : _b[`${Crypto_1.DeviceKeyAlgorithm.Ed25519}:${deviceId}`]; if (!signature) { LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} is missing a signature - ignoring device`); continue; } const validSignature = yield this.client.crypto.verifySignature(device, ed25519, signature); if (!validSignature) { LogService_1.LogService.warn("DeviceTracker", `Device ${userId} ${deviceId} has an invalid signature - ignoring device`); continue; } validated.push(device); } yield this.client.cryptoStore.setActiveUserDevices(userId, validated); } } catch (e) { LogService_1.LogService.error("DeviceTracker", "Error updating device lists:", e); // return reject(e); } resolve(); })); userIds.forEach(u => this.deviceListUpdates[u] = promise); yield promise; }); } } exports.DeviceTracker = DeviceTracker;