UNPKG

@signalapp/mock-server

Version:
404 lines (403 loc) 13.5 kB
"use strict"; // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StorageState = void 0; const assert_1 = __importDefault(require("assert")); const crypto_1 = __importDefault(require("crypto")); const long_1 = __importDefault(require("long")); const compiled_1 = require("../../protos/compiled"); const crypto_2 = require("../crypto"); const types_1 = require("../types"); const KEY_SIZE = 16; const IdentifierType = compiled_1.signalservice.ManifestRecord.Identifier.Type; class StorageStateItem { type; key; record; constructor({ type, key, record }) { this.type = type; this.key = key; this.record = record; } getKeyString() { return this.key.toString('base64'); } toStorageItem({ storageKey, recordIkm, }) { return (0, crypto_2.encryptStorageItem)({ storageKey, recordIkm, key: this.key, record: this.record, }); } toIdentifier() { return { type: this.type, raw: this.key, }; } isAccount() { return this.type === IdentifierType.ACCOUNT && Boolean(this.record.account); } isGroup(group) { if (this.type !== IdentifierType.GROUPV2) { return false; } const masterKey = this.record?.groupV2?.masterKey; if (!masterKey) { return false; } return group.masterKey.equals(masterKey); } isContact(device, serviceIdKind) { if (this.type !== IdentifierType.CONTACT) { return false; } if (serviceIdKind === types_1.ServiceIdKind.ACI) { return this.record?.contact?.aci === device.aci; } const untaggedPni = this.record?.contact?.pni; if (!untaggedPni) { return false; } const pni = (0, types_1.tagPni)(untaggedPni); return pni === device.pni; } inspect() { return [ `type: ${this.type}`, `key: ${this.key.toString('base64')}`, ...JSON.stringify(this.record, null, 2).split(/\n/g), ] .map((line) => ` ${line}`) .join('\n'); } toRecord() { return { type: this.type, key: this.key, record: this.record, }; } } class StorageState { version; items; constructor(version, items) { this.version = version; this.items = items.map((options) => new StorageStateItem(options)); } static getEmpty() { return new StorageState(0, [ new StorageStateItem({ key: StorageState.createStorageID(), type: IdentifierType.ACCOUNT, record: { account: {}, }, }), ]); } // // Account // getAccountRecord() { const item = this.items.find((item) => item.isAccount()); if (!item) { return undefined; } const { account } = item.record; (0, assert_1.default)(account, 'consistency check'); return account; } updateAccount(diff) { return this.updateItem((item) => item.isAccount(), ({ account }) => ({ account: { ...account, ...diff, }, })); } // // Group // getGroup(group) { const item = this.items.find((item) => item.isGroup(group)); if (!item) { return undefined; } const { groupV2 } = item.record; (0, assert_1.default)(groupV2, 'consistency check'); return groupV2; } addGroup(group, diff = {}) { return this.addItem({ type: IdentifierType.GROUPV2, record: { groupV2: { ...diff, masterKey: group.masterKey, }, }, }); } updateGroup(group, diff) { return this.updateItem((item) => item.isGroup(group), ({ groupV2 }) => ({ groupV2: { ...groupV2, ...diff, }, })); } pinGroup(group) { return this.changeGroupPin(group, true); } unpinGroup(group) { return this.changeGroupPin(group, false); } isGroupPinned(group) { const account = this.getAccountRecord(); (0, assert_1.default)(account, 'No account record found'); return (account.pinnedConversations || []).some((convo) => { if (!convo.groupMasterKey) { return false; } return group.masterKey.equals(convo.groupMasterKey); }); } // // Contacts // addContact({ device }, diff = {}, serviceIdKind = types_1.ServiceIdKind.ACI) { return this.addItem({ type: IdentifierType.CONTACT, record: { contact: { aci: serviceIdKind === types_1.ServiceIdKind.ACI ? device.aci : undefined, pni: serviceIdKind === types_1.ServiceIdKind.PNI ? (0, types_1.untagPni)(device.pni) : undefined, e164: device.number, ...diff, }, }, }); } updateContact({ device }, diff, serviceIdKind = types_1.ServiceIdKind.ACI) { return this.updateItem((item) => item.isContact(device, serviceIdKind), ({ contact }) => ({ contact: { ...contact, ...diff, }, })); } getContact({ device }, serviceIdKind = types_1.ServiceIdKind.ACI) { const item = this.items.find((item) => item.isContact(device, serviceIdKind)); if (!item) { return undefined; } const { contact } = item.record; (0, assert_1.default)(contact, 'consistency check'); return contact; } removeContact({ device }, serviceIdKind = types_1.ServiceIdKind.ACI) { return this.removeItem((item) => item.isContact(device, serviceIdKind)); } mergeContact(primary, diff) { const { device } = primary; return this.removeItem((item) => item.isContact(device, types_1.ServiceIdKind.ACI)) .removeItem((item) => item.isContact(device, types_1.ServiceIdKind.PNI)) .addContact(primary, { pni: (0, types_1.untagPni)(device.pni), ...diff, }) .unpin(primary, types_1.ServiceIdKind.PNI); } pin(primary, serviceIdKind = types_1.ServiceIdKind.ACI) { return this.changePin(primary, serviceIdKind, true); } unpin(primary, serviceIdKind = types_1.ServiceIdKind.ACI) { return this.changePin(primary, serviceIdKind, false); } isPinned({ device }) { const account = this.getAccountRecord(); (0, assert_1.default)(account, 'No account record found'); return (account.pinnedConversations || []).some((convo) => { return convo?.contact?.serviceId === device.aci; }); } // // Raw record access // addRecord(newRecord) { return this.addItem(newRecord); } findRecord(find) { const item = this.items.find((item) => find(item.toRecord())); return item?.toRecord(); } hasRecord(find) { return this.findRecord(find) !== undefined; } updateRecord(find, map) { return this.updateItem((item) => find(item.toRecord()), map); } removeRecord(find) { return this.removeItem((item) => find(item.toRecord())); } getAllGroupRecords() { return this.items .filter((item) => item.type === IdentifierType.GROUPV2) .map((item) => item.toRecord()); } hasKey(storageKey) { return this.hasRecord(({ key }) => key?.equals(storageKey)); } // // General // createWriteOperation({ storageKey, recordIkm, previous, }) { const newVersion = long_1.default.fromNumber(previous ? previous.version + 1 : this.version + 1); const keysToDelete = new Set((previous?.items ?? []).map((item) => { return item.getKeyString(); })); const insertItem = new Array(); for (const item of this.items) { if (!keysToDelete.delete(item.getKeyString())) { insertItem.push(item.toStorageItem({ storageKey, recordIkm })); } } const manifest = (0, crypto_2.encryptStorageManifest)(storageKey, { version: newVersion, identifiers: this.items.map((item) => item.toIdentifier()), recordIkm, }); return { manifest, insertItem, deleteKey: Array.from(keysToDelete).map((key) => { return Buffer.from(key, 'base64'); }), }; } inspect() { return [ `version: ${this.version}`, ...this.items.map((item) => item.inspect()), ].join('\n'); } diff(oldState) { const addedIds = new Map(); const removedIds = new Map(); for (const item of this.items) { addedIds.set(item.key.toString('base64'), item.record); } for (const item of oldState.items) { const keyString = item.key.toString('base64'); if (!addedIds.delete(keyString)) { removedIds.set(keyString, item.record); } } return { added: Array.from(addedIds.values()), removed: Array.from(removedIds.values()), }; } // // Private // addItem(newRecord) { return this.replaceItem(this.items.length, newRecord); } updateItem(find, map) { const itemIndex = this.items.findIndex(find); if (itemIndex === -1) { throw new Error('Item not found'); } const item = this.items[itemIndex]; (0, assert_1.default)(item, 'consistency check'); return this.replaceItem(itemIndex, { type: item.type, record: map(item.record), }); } replaceItem(index, { type, record, key = StorageState.createStorageID(), }) { const newItems = [ ...this.items.slice(0, index), new StorageStateItem({ type, key, record }), ...this.items.slice(index + 1), ]; return new StorageState(this.version, newItems); } removeItem(find) { const itemIndex = this.items.findIndex((item) => find(item)); if (itemIndex === -1) { throw new Error('Record not found'); } const newItems = [ ...this.items.slice(0, itemIndex), ...this.items.slice(itemIndex + 1), ]; return new StorageState(this.version, newItems); } changePin({ device }, serviceIdKind, isPinned) { const deviceServiceId = device.getServiceIdByKind(serviceIdKind); return this.updateItem((item) => item.isAccount(), ({ account }) => { (0, assert_1.default)(account, 'consistency check'); const { pinnedConversations } = account; const newPinnedConversations = pinnedConversations?.slice() || []; const existingIndex = newPinnedConversations.findIndex((convo) => { return convo?.contact?.serviceId === deviceServiceId; }); if (isPinned && existingIndex === -1) { newPinnedConversations.push({ contact: { serviceId: deviceServiceId }, }); } else if (!isPinned && existingIndex !== -1) { newPinnedConversations.splice(existingIndex, 1); } return { account: { ...account, pinnedConversations: newPinnedConversations, }, }; }); } changeGroupPin(group, isPinned) { return this.updateItem((item) => item.isAccount(), ({ account }) => { (0, assert_1.default)(account, 'consistency check'); const { pinnedConversations } = account; const newPinnedConversations = pinnedConversations?.slice() || []; const existingIndex = newPinnedConversations.findIndex((convo) => { if (!convo.groupMasterKey) { return false; } return group.masterKey.equals(convo.groupMasterKey); }); if (isPinned && existingIndex === -1) { newPinnedConversations.push({ groupMasterKey: group.masterKey, }); } else if (!isPinned && existingIndex !== -1) { newPinnedConversations.splice(existingIndex, 1); } return { account: { ...account, pinnedConversations: newPinnedConversations, }, }; }); } static createStorageID() { return crypto_1.default.randomBytes(KEY_SIZE); } } exports.StorageState = StorageState;