UNPKG

@signalapp/mock-server

Version:
1,229 lines (1,228 loc) 63.2 kB
"use strict"; // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PrimaryDevice = exports.SenderKeyStore = exports.SessionStore = exports.EMPTY_DATA_MESSAGE = exports.EMPTY_GROUP_ACTIONS = exports.ReceiptType = void 0; const assert_1 = __importDefault(require("assert")); const crypto_1 = __importDefault(require("crypto")); const libsignal_client_1 = require("@signalapp/libsignal-client"); const SignalClient = __importStar(require("@signalapp/libsignal-client")); const AccountKeys_1 = require("@signalapp/libsignal-client/dist/AccountKeys"); const debug_1 = __importDefault(require("debug")); const zkgroup_1 = require("@signalapp/libsignal-client/zkgroup"); const compiled_1 = require("../../protos/compiled"); const types_1 = require("../types"); const crypto_2 = require("../crypto"); const base_1 = require("../server/base"); const util_1 = require("../util"); const group_1 = require("./group"); const storage_state_1 = require("./storage-state"); const debug = (0, debug_1.default)('mock:primary-device'); var ReceiptType; (function (ReceiptType) { ReceiptType["Delivery"] = "Delivery"; ReceiptType["Read"] = "Read"; })(ReceiptType || (exports.ReceiptType = ReceiptType = {})); var SyncState; (function (SyncState) { SyncState[SyncState["Empty"] = 0] = "Empty"; SyncState[SyncState["Contacts"] = 1] = "Contacts"; SyncState[SyncState["Groups"] = 2] = "Groups"; SyncState[SyncState["Blocked"] = 4] = "Blocked"; SyncState[SyncState["Configuration"] = 8] = "Configuration"; SyncState[SyncState["Keys"] = 16] = "Keys"; SyncState[SyncState["Complete"] = 13] = "Complete"; })(SyncState || (SyncState = {})); exports.EMPTY_GROUP_ACTIONS = { sourceUserId: null, version: null, groupId: null, addMembers: null, deleteMembers: null, modifyMemberRoles: null, modifyMemberProfileKeys: null, addMembersPendingProfileKey: null, deleteMembersPendingProfileKey: null, promoteMembersPendingProfileKey: null, modifyTitle: null, modifyAvatar: null, modifyDisappearingMessageTimer: null, modifyAttributesAccess: null, modifyMemberAccess: null, modifyAddFromInviteLinkAccess: null, addMembersPendingAdminApproval: null, deleteMembersPendingAdminApproval: null, promoteMembersPendingAdminApproval: null, modifyInviteLinkPassword: null, modifyDescription: null, modifyAnnouncementsOnly: null, addMembersBanned: null, deleteMembersBanned: null, promoteMembersPendingPniAciProfileKey: null, modifyMemberLabels: null, modifyMemberLabelAccess: null, terminateGroup: null, }; exports.EMPTY_DATA_MESSAGE = { body: null, attachments: null, groupV2: null, flags: null, expireTimer: null, expireTimerVersion: null, profileKey: null, timestamp: null, quote: null, contact: null, preview: null, sticker: null, requiredProtocolVersion: null, isViewOnce: null, reaction: null, delete: null, bodyRanges: null, groupCallUpdate: null, payment: null, storyContext: null, giftBadge: null, pollCreate: null, pollTerminate: null, pollVote: null, pinMessage: null, unpinMessage: null, adminDelete: null, }; class SignedPreKeyStore extends libsignal_client_1.SignedPreKeyStore { lastId = 0; records = new Map(); async saveSignedPreKey(id, record) { this.records.set(id, record); } async getSignedPreKey(id) { const result = this.records.get(id); if (!result) { throw new Error(`Signed pre key not found: ${id}`); } return result; } getNextId() { this.lastId += 1; // Note: intentionally starting from 1 return this.lastId; } } class PreKeyStore extends libsignal_client_1.PreKeyStore { lastId = 0; records = new Map(); async savePreKey(id, record) { this.records.set(id, record); } async getPreKey(id) { const record = this.records.get(id); if (!record) { throw new Error(`Pre key not found: ${id}`); } return record; } async removePreKey(id) { this.records.delete(id); } getNextId() { this.lastId += 1; // Note: intentionally starting from 1 return this.lastId; } } class KyberPreKeyStore extends libsignal_client_1.KyberPreKeyStore { lastId = 0; records = new Map(); async saveKyberPreKey(id, record) { if (this.records.get(id)) { throw new Error(`saveKyberPreKey: id ${id} has already been used`); } this.records.set(id, { isLastResort: false, record }); } async getKyberPreKey(id) { const item = this.records.get(id); if (!item?.record) { throw new Error(`Kyber pre key not found: ${id}`); } return item.record; } async markKyberPreKeyUsed(id) { const item = this.records.get(id); if (!item || item.isLastResort) { return; } this.records.delete(id); } async saveLastResortKey(id, record) { if (this.records.get(id)) { throw new Error(`saveLastResortKey: id ${id} has already been used`); } this.records.set(id, { isLastResort: true, record }); } getNextId() { this.lastId += 1; // Note: intentionally starting from 1 return this.lastId; } } class IdentityStore extends libsignal_client_1.IdentityKeyStore { privateKey; registrationId; knownIdentities = new Map(); constructor(privateKey, registrationId) { super(); this.privateKey = privateKey; this.registrationId = registrationId; } async getIdentityKey() { return this.privateKey; } async getLocalRegistrationId() { return this.registrationId; } async saveIdentity(name, key) { this.knownIdentities.set((0, util_1.addressToString)(name), key); return SignalClient.IdentityChange.ReplacedExisting; } async isTrustedIdentity() { // We trust everyone return true; } async getIdentity(name) { return this.knownIdentities.get((0, util_1.addressToString)(name)) ?? null; } // Not part of IdentityKeyStore API async updateIdentityKey(privateKey) { this.privateKey = privateKey; } async updateLocalRegistrationId(registrationId) { this.registrationId = registrationId; } } class SessionStore extends libsignal_client_1.SessionStore { sessions = new Map(); async saveSession(name, record) { this.sessions.set((0, util_1.addressToString)(name), record); } async getSession(name) { return this.sessions.get((0, util_1.addressToString)(name)) ?? null; } async getExistingSessions(addresses) { return addresses.map((name) => { const existing = this.sessions.get((0, util_1.addressToString)(name)); if (!existing) { throw new Error('Existing session not found'); } return existing; }); } } exports.SessionStore = SessionStore; class SenderKeyStore extends libsignal_client_1.SenderKeyStore { keys = new Map(); async saveSenderKey(sender, distributionId, record) { this.keys.set(`${sender.toString()}.${distributionId}`, record); } async getSenderKey(sender, distributionId) { const key = this.keys.get(`${sender.toString()}.${distributionId}`); return key ?? null; } } exports.SenderKeyStore = SenderKeyStore; class PrimaryDevice { device; config; isInitialized = false; lockPromise; syncStates = new WeakMap(); storageKey; privateKey = libsignal_client_1.PrivateKey.generate(); pniPrivateKey = libsignal_client_1.PrivateKey.generate(); contactsBlob; privSenderCertificate; decryptionErrorQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.decryptionErrorQueue', }); messageQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.messageQueue', }); receiptQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.receiptQueue', }); storyQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.storyQueue', }); editMessageQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.editMessageQueue', }); syncMessageQueue = new util_1.PromiseQueue({ name: 'PrimaryDevice.syncMessageQueue', }); privPniPublicKey = this.pniPrivateKey.getPublicKey(); // Various stores signedPreKeys = new Map(); preKeys = new Map(); kyberPreKeys = new Map(); sessions = new SessionStore(); senderKeys = new Map(); identity = new Map(); publicKey = this.privateKey.getPublicKey(); profileKey; profileName; secondaryDevices = new Array(); accountEntropyPool = AccountKeys_1.AccountEntropyPool.generate(); masterKey = (0, crypto_2.deriveMasterKey)(this.accountEntropyPool); mediaRootBackupKey = crypto_1.default.randomBytes(32); // Forwarded in provisioning envelope ephemeralBackupKey; // Overridable to test legacy encryption modes storageRecordIkm = crypto_1.default.randomBytes(32); // TODO(indutny): make primary device type configurable userAgent = 'OWI'; constructor(device, config) { this.device = device; this.config = config; for (const serviceIdKind of [types_1.ServiceIdKind.ACI, types_1.ServiceIdKind.PNI]) { this.identity.set(serviceIdKind, new IdentityStore(serviceIdKind === types_1.ServiceIdKind.ACI ? this.privateKey : this.pniPrivateKey, this.device.getRegistrationId(serviceIdKind))); this.preKeys.set(serviceIdKind, new PreKeyStore()); this.kyberPreKeys.set(serviceIdKind, new KyberPreKeyStore()); this.signedPreKeys.set(serviceIdKind, new SignedPreKeyStore()); this.senderKeys.set(serviceIdKind, new SenderKeyStore()); } this.contactsBlob = this.config.contacts; this.profileName = config.profileName; this.profileKey = new zkgroup_1.ProfileKey(crypto_1.default.randomBytes(32)); this.storageKey = (0, crypto_2.deriveStorageKey)(this.masterKey); this.device.profileName = (0, crypto_2.encryptProfileName)(this.profileKey.serialize(), this.profileName); } async init() { if (this.isInitialized) { throw new Error('Already initialized'); } for (const serviceIdKind of [types_1.ServiceIdKind.ACI, types_1.ServiceIdKind.PNI]) { const identity = this.identity.get(serviceIdKind); assert_1.default.ok(identity); await identity.saveIdentity(this.device.getAddressByKind(serviceIdKind), this.getPublicKey(serviceIdKind)); await this.device.setKeys(serviceIdKind, await this.generateKeys(this.device, serviceIdKind)); } this.privSenderCertificate = await this.config.getSenderCertificate(); this.device.profileKeyCommitment = this.profileKey.getCommitment(libsignal_client_1.Aci.parseFromServiceIdString(this.device.aci)); this.device.accessKey = (0, crypto_2.deriveAccessKey)(this.profileKey.serialize()); this.isInitialized = true; } toContact() { return { aciBinary: this.device.aciRawUuid, number: this.device.number, profileName: this.profileName, }; } addSecondaryDevice(device) { this.secondaryDevices.push(device); device.profileName = this.device.profileName; device.profileKeyCommitment = this.device.profileKeyCommitment; device.accessKey = this.device.accessKey; } // // Keys // async generateKeys(device, serviceIdKind) { const isPrimary = device === this.device; const signedPreKey = libsignal_client_1.PrivateKey.generate(); const signedPreKeySig = this.getPrivateKey(serviceIdKind).sign(signedPreKey.getPublicKey().serialize()); const signedPreKeyId = this.signedPreKeys.get(serviceIdKind)?.getNextId() ?? 1; const signedPreKeyRecord = libsignal_client_1.SignedPreKeyRecord.new(signedPreKeyId, Date.now(), signedPreKey.getPublicKey(), signedPreKey, signedPreKeySig); if (isPrimary) { await this.signedPreKeys .get(serviceIdKind) ?.saveSignedPreKey(signedPreKeyId, signedPreKeyRecord); } const lastResortKeyId = this.kyberPreKeys.get(serviceIdKind)?.getNextId() ?? 1; const lastResortKeyRecord = this.generateKyberPreKey(lastResortKeyId, serviceIdKind); if (isPrimary) { await this.kyberPreKeys .get(serviceIdKind) ?.saveLastResortKey(lastResortKeyId, lastResortKeyRecord); } return { identityKey: this.getPublicKey(serviceIdKind), signedPreKey: { keyId: signedPreKeyId, publicKey: signedPreKey.getPublicKey(), signature: Buffer.from(signedPreKeySig), }, lastResortKey: { keyId: lastResortKeyId, publicKey: lastResortKeyRecord.publicKey(), signature: Buffer.from(lastResortKeyRecord.signature()), }, preKeyIterator: this.getPreKeyIterator(device, serviceIdKind), kyberPreKeyIterator: this.getKyberPreKeyIterator(device, serviceIdKind), signedPreKeyRecord, lastResortKeyRecord, }; } async *getPreKeyIterator(device, serviceIdKind) { const preKeyStore = this.preKeys.get(serviceIdKind); assert_1.default.ok(preKeyStore, 'Missing preKey store'); const isPrimary = device === this.device; if (!isPrimary) { return; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { const preKey = libsignal_client_1.PrivateKey.generate(); const publicKey = preKey.getPublicKey(); const keyId = preKeyStore.getNextId(); const record = libsignal_client_1.PreKeyRecord.new(keyId, publicKey, preKey); await preKeyStore.savePreKey(keyId, record); yield { keyId, publicKey }; } } generateKyberPreKey(keyId, serviceIdKind) { const kyberPreKey = libsignal_client_1.KEMKeyPair.generate(); const kyberPreKeySig = this.getPrivateKey(serviceIdKind).sign(kyberPreKey.getPublicKey().serialize()); const kyberPreKeyRecord = libsignal_client_1.KyberPreKeyRecord.new(keyId, Date.now(), kyberPreKey, kyberPreKeySig); return kyberPreKeyRecord; } async *getKyberPreKeyIterator(device, serviceIdKind) { const kyberPreKeyStore = this.kyberPreKeys.get(serviceIdKind); assert_1.default.ok(kyberPreKeyStore, 'Missing kyberPreKeyStore store'); const isPrimary = device === this.device; if (!isPrimary) { return; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { const keyId = kyberPreKeyStore.getNextId(); const record = this.generateKyberPreKey(keyId, serviceIdKind); await kyberPreKeyStore.saveKyberPreKey(keyId, record); yield { keyId, publicKey: record.publicKey(), signature: Buffer.from(record.signature()), }; } } async getIdentityKey(serviceIdKind) { const identity = this.identity.get(serviceIdKind); assert_1.default.ok(identity); return identity.getIdentityKey(); } getPublicKey(serviceIdKind) { switch (serviceIdKind) { case types_1.ServiceIdKind.ACI: return this.publicKey; case types_1.ServiceIdKind.PNI: return this.privPniPublicKey; } } async addSingleUseKey(target, key, serviceIdKind = types_1.ServiceIdKind.ACI) { assert_1.default.ok(this.isInitialized, 'Not initialized'); debug('adding singleUseKey for', target.debugId); // Outgoing stores const identity = this.identity.get(types_1.ServiceIdKind.ACI); (0, assert_1.default)(identity, 'Should have an ACI identity'); await identity.saveIdentity(target.getAddressByKind(serviceIdKind), key.identityKey); const bundle = libsignal_client_1.PreKeyBundle.new(target.getRegistrationId(serviceIdKind), target.deviceId, key.preKey === undefined ? null : key.preKey.keyId, key.preKey === undefined ? null : key.preKey.publicKey, key.signedPreKey.keyId, key.signedPreKey.publicKey, key.signedPreKey.signature, key.identityKey, key.pqPreKey.keyId, key.pqPreKey.publicKey, key.pqPreKey.signature); await SignalClient.processPreKeyBundle(bundle, target.getAddressByKind(serviceIdKind), this.sessions, identity); } // // Groups // async getAllGroups(storage) { const records = storage.getAllGroupRecords(); return Promise.all(records.map(async ({ record }) => { const { groupV2 } = record; const { masterKey } = groupV2; assert_1.default.ok(masterKey, 'Group v2 record without master key'); const secretParams = zkgroup_1.GroupSecretParams.deriveFromMasterKey(new zkgroup_1.GroupMasterKey(Buffer.from(masterKey))); const publicParams = secretParams.getPublicParams().serialize(); const serverGroup = await this.config.getGroup(publicParams); assert_1.default.ok(serverGroup, `Group not found: ${Buffer.from(publicParams).toString('base64')}`); return new group_1.Group({ secretParams, groupState: serverGroup.state, }); })); } async createGroup({ title, members: memberDevices, }) { const groupParams = zkgroup_1.GroupSecretParams.generate(); const members = await Promise.all(memberDevices.map(async (member) => { const presentation = await member.getProfileKeyPresentation(groupParams); return { aci: member.device.aci, profileKey: member.profileKey, presentation, joinedAtVersion: 0n, }; })); const clientGroup = group_1.Group.fromConfig({ secretParams: groupParams, title, members, }); await this.config.createGroup(clientGroup.state); return clientGroup; } async waitForGroupUpdate(group) { await this.config.waitForGroupUpdate(group); const publicParams = group.publicParams.serialize(); const serverGroup = await this.config.getGroup(publicParams); assert_1.default.ok(serverGroup, `Group not found: ${group.id}`); return new group_1.Group({ secretParams: group.secretParams, groupState: serverGroup.state, }); } async #modifyGroup(options) { const { group, actions, timestamp, sendUpdateTo } = options; const serverGroup = await this.config.getGroup(group.publicParams.serialize()); (0, assert_1.default)(serverGroup !== undefined, 'Group does not exist on server'); const modifyResult = await this.config.modifyGroup({ group: serverGroup, actions: { ...actions, version: group.revision + 1, }, aciCiphertext: group.encryptServiceId(this.device.aci), pniCiphertext: group.encryptServiceId(this.device.pni), }); (0, assert_1.default)(!modifyResult.conflict, 'Group update conflict!'); const updatedGroup = new group_1.Group({ secretParams: group.secretParams, groupState: serverGroup.state, }); if (sendUpdateTo.length) { const groupV2 = { ...updatedGroup.toContext(), groupChange: compiled_1.signalservice.GroupChange.encode(modifyResult.signedChange), }; await Promise.all(sendUpdateTo.map(async ({ device, options }) => { const sync = device.aci === this.device.aci; const encryptOptions = { timestamp, ...options, }; const dataMessage = { ...exports.EMPTY_DATA_MESSAGE, groupV2, timestamp: BigInt(encryptOptions.timestamp), }; const syncMessage = { content: { sent: { timestamp: BigInt(timestamp), message: dataMessage, destinationServiceIdBinary: device.aciBinary, destinationE164: null, destinationServiceId: null, expirationStartTimestamp: null, unidentifiedStatus: null, isRecipientUpdate: null, storyMessage: null, storyMessageRecipients: null, editMessage: null, }, }, read: null, stickerPackOperation: null, viewed: null, padding: null, }; const content = { content: sync ? { syncMessage } : { dataMessage }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; const envelope = await this.encryptContent(device, content, encryptOptions); await this.config.send(device, envelope); })); } return updatedGroup; } async inviteToGroup(group, invitee, { timestamp = Date.now(), serviceIdKind = types_1.ServiceIdKind.ACI, sendUpdateTo = [{ device: invitee, options: { serviceIdKind } }], } = {}) { const targetServiceId = invitee.getServiceIdByKind(serviceIdKind); const userId = group.encryptServiceId(targetServiceId); return this.#modifyGroup({ group, actions: { ...exports.EMPTY_GROUP_ACTIONS, addMembersPendingProfileKey: [ { added: { member: { userId, role: compiled_1.signalservice.Member.Role.DEFAULT, profileKey: null, presentation: null, joinedAtVersion: null, labelEmoji: null, labelString: null, }, addedByUserId: null, timestamp: null, }, }, ], }, timestamp, sendUpdateTo, }); } async acceptPniInvite(group, { timestamp = Date.now(), sendUpdateTo = [] } = {}) { const presentation = await this.getProfileKeyPresentation(group.secretParams); return this.#modifyGroup({ group, actions: { ...exports.EMPTY_GROUP_ACTIONS, promoteMembersPendingPniAciProfileKey: [ { presentation: presentation.serialize(), userId: null, pni: null, profileKey: null, }, ], }, timestamp, sendUpdateTo, }); } async modifyGroupDisappearingMessageTimer(group, disappearingMessagesDuration, { timestamp = Date.now(), sendUpdateTo = [] } = {}) { return this.#modifyGroup({ group, actions: { ...exports.EMPTY_GROUP_ACTIONS, modifyDisappearingMessageTimer: { timer: group.encryptBlob({ content: { disappearingMessagesDuration }, }), }, }, timestamp, sendUpdateTo, }); } // // Storage Service // async waitForStorageState({ after, } = {}) { debug('waiting for storage manifest for device=%s after version=%d', this.device.debugId, after?.version); await this.config.waitForStorageManifest(after?.version); const state = await this.getStorageState(); (0, assert_1.default)(state, 'Missing storage state'); debug('got storage manifest for device=%s version=%d', this.device.debugId, state.version); return state; } async getStorageState() { const manifest = await this.config.getStorageManifest(); if (!manifest) { return undefined; } return this.convertManifestToStorageState(manifest); } async expectStorageState(reason) { const state = await this.getStorageState(); if (!state) { throw new Error(`expectStorageState: no storage state, ${reason}`); } return state; } async setStorageState(state, previousState) { const writeOperation = state.createWriteOperation({ storageKey: this.storageKey, recordIkm: this.storageRecordIkm, previous: previousState, }); (0, assert_1.default)(writeOperation.manifest, 'write operation without manifest'); const { updated, error } = await this.config.applyStorageWrite(writeOperation, false); if (!updated) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`setStorageState: failed to update, ${error}`); } return this.convertManifestToStorageState(writeOperation.manifest); } async getOrphanedStorageKeys() { const manifest = await this.config.getStorageManifest(); if (!manifest) { return []; } const state = await this.convertManifestToStorageState(manifest); const keys = await this.config.getAllStorageKeys(); return keys.filter((key) => !state.hasKey(key)); } // // Sync // // TODO(indutny): timeout async waitForSync(secondaryDevice) { debug('waiting for sync with %s', secondaryDevice.debugId); const { onComplete } = this.getSyncState(secondaryDevice); await onComplete; } resetSyncState(secondaryDevice) { this.syncStates.delete(secondaryDevice); } // // Receive/Send // async handleEnvelope(source, serviceIdKind, envelopeType, encrypted) { const { unsealedSource, content, envelopeType: unsealedType, } = await this.lock(async () => { return this.decrypt(source, serviceIdKind, envelopeType, encrypted); }); let handled = true; if (content.content?.decryptionErrorMessage) { assert_1.default.strictEqual(serviceIdKind, types_1.ServiceIdKind.ACI, 'Got sync message on PNI'); this.handleResendRequest(unsealedSource, serviceIdKind, unsealedType, content, content.content.decryptionErrorMessage); } else if (content.content?.syncMessage) { assert_1.default.strictEqual(serviceIdKind, types_1.ServiceIdKind.ACI, 'Got sync message on PNI'); await this.handleSync(unsealedSource, content.content.syncMessage); } else if (content.content?.dataMessage) { this.handleDataMessage(unsealedSource, serviceIdKind, unsealedType, content, content.content.dataMessage); } else if (content.content?.storyMessage) { this.handleStoryMessage(unsealedSource, serviceIdKind, unsealedType, content, content.content.storyMessage); } else if (content.content?.editMessage) { this.handleEditMessage(unsealedSource, serviceIdKind, unsealedType, content, content.content.editMessage); } else if (content.content?.receiptMessage) { this.handleReceiptMessage(unsealedSource, serviceIdKind, unsealedType, content, content.content.receiptMessage); } else { handled = false; } const { senderKeyDistributionMessage } = content; if (senderKeyDistributionMessage != null && senderKeyDistributionMessage.length > 0) { handled = true; await this.processSenderKeyDistribution(unsealedSource, senderKeyDistributionMessage); } if (!handled) { debug('unsupported message', content); } } async encryptText(target, text, options = {}) { const encryptOptions = { timestamp: Date.now(), ...options, }; let pniSignatureMessage = null; if (options.withPniSignature) { const pniPrivate = this.getPrivateKey(types_1.ServiceIdKind.PNI); const pniPublic = this.getPublicKey(types_1.ServiceIdKind.PNI); const aciPublic = this.getPublicKey(types_1.ServiceIdKind.ACI); const pniIdentity = new libsignal_client_1.IdentityKeyPair(pniPublic, pniPrivate); const signature = pniIdentity.signAlternateIdentity(aciPublic); pniSignatureMessage = { pni: libsignal_client_1.Pni.parseFromServiceIdString(this.device.pni).getRawUuidBytes(), signature, }; } const content = { content: { dataMessage: { ...exports.EMPTY_DATA_MESSAGE, groupV2: options.group?.toContext() ?? null, body: text, profileKey: options.withProfileKey ? this.profileKey.serialize() : null, timestamp: BigInt(encryptOptions.timestamp), }, }, pniSignatureMessage, senderKeyDistributionMessage: null, }; return this.encryptContent(target, content, encryptOptions); } async encryptSyncSent(target, text, options) { const dataMessage = { ...exports.EMPTY_DATA_MESSAGE, body: text, timestamp: BigInt(options.timestamp), }; const content = { content: { syncMessage: { content: { sent: { destinationServiceIdBinary: libsignal_client_1.ServiceId.parseFromServiceIdString(options.destinationServiceId).getServiceIdBinary(), timestamp: BigInt(options.timestamp), message: dataMessage, destinationE164: null, unidentifiedStatus: null, isRecipientUpdate: null, expirationStartTimestamp: null, storyMessage: null, storyMessageRecipients: null, editMessage: null, destinationServiceId: null, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; return this.encryptContent(target, content, options); } async encryptSyncRead(target, options) { const content = { content: { syncMessage: { content: null, stickerPackOperation: null, read: options.messages.map(({ senderAci, timestamp }) => { return { senderAciBinary: libsignal_client_1.Aci.parseFromServiceIdString(senderAci).getRawUuidBytes(), timestamp: BigInt(timestamp), // Deprecated string field senderAci: null, }; }), viewed: null, padding: null, }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; return this.encryptContent(target, content, options); } async sendFetchStorage(options) { const content = { content: { syncMessage: { content: { fetchLatest: { type: compiled_1.signalservice.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; return this.broadcast('fetch storage', content, options); } async sendStickerPackSync(options) { const Type = compiled_1.signalservice.SyncMessage.StickerPackOperation.Type; const content = { content: { syncMessage: { content: null, stickerPackOperation: [ { packId: options.packId, packKey: options.packKey, type: options.type === 'install' ? Type.INSTALL : Type.REMOVE, }, ], read: null, viewed: null, padding: null, }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; return this.broadcast('sticker pack sync', content, options); } async encryptReceipt(target, options) { let type; if (options.type === ReceiptType.Delivery) { type = compiled_1.signalservice.ReceiptMessage.Type.DELIVERY; } else { assert_1.default.strictEqual(options.type, ReceiptType.Read); type = compiled_1.signalservice.ReceiptMessage.Type.READ; } const content = { content: { receiptMessage: { type, timestamp: options.messageTimestamps.map((timestamp) => BigInt(timestamp)), }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; return this.encryptContent(target, content, options); } async sendReceipt(target, options) { const receipt = await this.encryptReceipt(target, options); return this.config.send(target, receipt); } async sendUnencryptedReceipt(target, { messageTimestamp, timestamp = Date.now() }) { const envelope = { type: compiled_1.signalservice.Envelope.Type.SERVER_DELIVERY_RECEIPT, clientTimestamp: BigInt(messageTimestamp), serverTimestamp: BigInt(timestamp), sourceServiceIdBinary: this.device.aciBinary, sourceDeviceId: this.device.deviceId, destinationServiceIdBinary: target.aciBinary, serverGuid: null, ephemeral: null, urgent: null, story: null, reportSpamToken: null, serverGuidBinary: null, content: null, updatedPniBinary: null, // Deprecated string fields sourceServiceId: null, destinationServiceId: null, updatedPni: null, }; return this.config.send(target, Buffer.from(compiled_1.signalservice.Envelope.encode(envelope))); } async prepareChangeNumber(options = {}) { const { timestamp = Date.now() } = options; const newNumber = await this.config.generateNumber(); const newPni = await this.config.generatePni(); const newPniRegistrationId = (0, util_1.generateRegistrationId)(); const newPniIdentity = libsignal_client_1.IdentityKeyPair.generate(); debug('sending change number to %d linked devices timestamp=%d newPni=%s', this.secondaryDevices.length, timestamp, newPni); this.pniPrivateKey = newPniIdentity.privateKey; this.privPniPublicKey = newPniIdentity.publicKey; const allDevices = [this.device, ...this.secondaryDevices]; // Update PNI await Promise.all(allDevices.map(async (device) => { await this.config.changeDeviceNumber(device, { pni: newPni, number: newNumber, pniRegistrationId: newPniRegistrationId, }); })); const identity = this.identity.get(types_1.ServiceIdKind.PNI); (0, assert_1.default)(identity, 'Should have a PNI identity'); await identity.updateIdentityKey(newPniIdentity.privateKey); await identity.updateLocalRegistrationId(newPniRegistrationId); await identity.saveIdentity(this.device.getAddressByKind(types_1.ServiceIdKind.PNI), this.getPublicKey(types_1.ServiceIdKind.PNI)); // Update all keys and prepare sync message const results = await Promise.all(allDevices.map(async (device) => { const isPrimary = device === this.device; const keys = await this.generateKeys(device, types_1.ServiceIdKind.PNI); await device.setKeys(types_1.ServiceIdKind.PNI, keys); if (isPrimary) { return; } // Send sync message const { signedPreKeyRecord, lastResortKeyRecord } = keys; const content = { content: { syncMessage: { content: { pniChangeNumber: { identityKeyPair: newPniIdentity.serialize(), lastResortKyberPreKey: lastResortKeyRecord.serialize(), signedPreKey: signedPreKeyRecord.serialize(), registrationId: newPniRegistrationId, newE164: newNumber, }, }, read: null, stickerPackOperation: null, viewed: null, padding: null, }, }, pniSignatureMessage: null, senderKeyDistributionMessage: null, }; const envelope = await this.encryptContent(device, content, { ...options, timestamp, updatedPni: this.device.pni, }); return { device, envelope }; })); return results.filter((entry) => { return entry !== undefined; }); } async sendChangeNumber(result) { await Promise.all(result.map(({ device, envelope }) => { return this.config.send(device, envelope); })); } async changeNumber(options = {}) { const result = await this.prepareChangeNumber(options); await this.sendChangeNumber(result); } async sendText(target, text, options) { await this.config.send(target, await this.encryptText(target, text, options)); } async sendRaw(target, content, options) { await this.config.send(target, await this.encryptContent(target, content, options)); } async sendSenderKey(target, options) { const distributionId = crypto_1.default.randomUUID(); const senderKeys = this.senderKeys.get(types_1.ServiceIdKind.ACI); (0, assert_1.default)(senderKeys, 'Should have a sender key store'); const skdm = await libsignal_client_1.SenderKeyDistributionMessage.create(target.address, distributionId, senderKeys); if (!options?.skipSkdmSend) { void this.sendRaw(target, { content: null, pniSignatureMessage: null, senderKeyDistributionMessage: skdm.serialize(), }, options); } return distributionId; } async unlink(device) { const index = this.secondaryDevices.indexOf(device); if (index === -1) { throw new Error('Device was not linked'); } this.secondaryDevices.splice(index, 1); } async receive(source, encrypted) { const envelope = compiled_1.signalservice.Envelope.decode(encrypted); if (envelope.sourceServiceIdBinary != null && source.getServiceIdBinaryKind(envelope.sourceServiceIdBinary) !== types_1.ServiceIdKind.ACI) { throw new Error(`Invalid envelope source. Expected: ${source.aci}, got PNI`); } let envelopeType; if (envelope.type === compiled_1.signalservice.Envelope.Type.DOUBLE_RATCHET) { envelopeType = base_1.EnvelopeType.CipherText; } else if (envelope.type === compiled_1.signalservice.Envelope.Type.PREKEY_MESSAGE) { envelopeType = base_1.EnvelopeType.PreKey; } else if (envelope.type === compiled_1.signalservice.Envelope.Type.UNIDENTIFIED_SENDER) { envelopeType = base_1.EnvelopeType.SealedSender; } else { throw new Error('Unsupported envelope type'); } const serviceIdKind = envelope.destinationServiceIdBinary?.length ? this.device.getServiceIdBinaryKind(envelope.destinationServiceIdBinary) : types_1.ServiceIdKind.ACI; return this.handleEnvelope(source, serviceIdKind, envelopeType, envelope.content ? Buffer.from(envelope.content) : Buffer.alloc(0)); } async waitForMessage() { return this.messageQueue.shift(); } getMessageQueueSize() { return this.messageQueue.size; } async waitForDecryptionError() { return this.decryptionErrorQueue.shift(); } getDecryptionErrorQueueSize() { return this.decryptionErrorQueue.size; } async waitForReceipt() { return this.receiptQueue.shift(); } getReceiptQueueSize() { return this.receiptQueue.size; } async waitForStory() { return this.storyQueue.shift(); } getStoryQueueSize() { return this.storyQueue.size; } async waitForEditMessage() { return this.editMessageQueue.shift(); } getEditQueueSize() { return this.editMessageQueue.size; } async waitForSyncMessage(predicate = () => true) { for (;;) { const entry = await this.syncMessageQueue.shift(); if (!predicate(entry)) { continue; } return entry; } } // // Private // async getProfileKeyPresentation(groupParams) { const ops = new zkgroup_1.ClientZkProfileOperations(this.config.serverPublicParams); const ctx = ops.createProfileKeyCredentialRequestContext(libsignal_client_1.Aci.parseFromServiceIdString(this.device.aci), this.profileKey); const response = await this.config.issueExpiringProfileKeyCredential(this.device, ctx.getRequest()); assert_1.default.ok(response, `Member device ${this.device.aci} not initialized`); const credential = ops.receiveExpiringProfileKeyCredential(ctx, new zkgroup_1.ExpiringProfileKeyCredentialResponse(response)); return ops.createExpiringProfileKeyCredentialPresentation(groupParams, credential); } getPrivateKey(serviceIdKind) { switch (serviceIdKind) { case types_1.ServiceIdKind.ACI: return this.privateKey; case types_1.ServiceIdKind.PNI: return this.pniPrivateKey; } } async encryptContent(target, content, options) { const encoded = Buffer.from(compiled_1.signalservice.Content.encode(content)); return this.lock(async () => { return this.encrypt(target, encoded, options); }); } async broadcast(type, content, options) { debug('broadcasting %s to %d linked devices', type, this.secondaryDevices.length); await Promise.all(this.secondaryDevices.map(async (device) => { const envelope = await this.encryptContent(device, content, options); await this.config.send(device, envelope); })); } getSyncState(secondaryDevice) { const existing = this.syncStates.get(secondaryDevice); if (existing) { return existing; } let complete; const onComplete = new Promise((resolve) => { complete = resolve; }); if (!complete) { throw new Error('Failed to obtain resolve callback'); } const entry = { state: SyncState.Empty, onComplete, complete, }; this.syncStates.set(secondaryDevice, entry); return entry; } async handleSync(source, sync) { if (sync.content?.request == null) { debug('got generic sync message'); this.syncMessageQueue.push({ source, syncMessage: sync, }); return; } const { content: { request }, } = sync; let stateChange; let response; if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.CONTACTS) { debug('got sync contacts request'); response = { content: { contacts: { blob: this.contactsBlob, complete: true, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, }; stateChange = SyncState.Contacts; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.BLOCKED) { debug('got sync blocked request'); response = { content: { blocked: { numbers: null, groupIds: null, acisBinary: null, // Deprecated string field acis: null, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, }; stateChange = SyncState.Blocked; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.CONFIGURATION) { debug('got sync configuration request'); response = { content: { configuration: { readReceipts: true, unidentifiedDeliveryIndicators: false, typingIndicators: false, linkPreviews: false, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, }; stateChange = SyncState.Configuration; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.KEYS) { debug('got sync keys request'); response = { content: { keys: { master: this.masterKey, mediaRootBackupKey: this.mediaRootBackupKey, accountEntropyPool: this.accountEntropyPool, }, }, stickerPackOperation: null, read: null, viewed: null, padding: null, };