UNPKG

@signalapp/mock-server

Version:
1,160 lines (1,159 loc) 54.7 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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.ReceiptType = void 0; const assert_1 = __importDefault(require("assert")); const crypto_1 = __importDefault(require("crypto")); const long_1 = __importDefault(require("long")); 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 = {})); 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 true; } 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.serviceId}.${distributionId}`, record); } async getSenderKey(sender, distributionId) { const key = this.keys.get(`${sender.serviceId}.${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(); messageQueue = new util_1.PromiseQueue(); receiptQueue = new util_1.PromiseQueue(); storyQueue = new util_1.PromiseQueue(); editMessageQueue = new util_1.PromiseQueue(); syncMessageQueue = new util_1.PromiseQueue(); 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 { aci: this.device.aci, 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: signedPreKeySig, }, lastResortKey: { keyId: lastResortKeyId, publicKey: lastResortKeyRecord.publicKey(), signature: 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; } 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; } while (true) { const keyId = kyberPreKeyStore.getNextId(); const record = this.generateKyberPreKey(keyId, serviceIdKind); await kyberPreKeyStore.saveKyberPreKey(keyId, record); yield { keyId, publicKey: record.publicKey(), signature: 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 await Promise.all(records.map(async ({ record }) => { const { groupV2 } = record; assert_1.default.ok(groupV2, 'Not a group v2 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: ${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: long_1.default.fromNumber(0), }; })); 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 inviteToGroup(group, invitee, { timestamp = Date.now(), serviceIdKind = types_1.ServiceIdKind.ACI, sendUpdateTo = [{ device: invitee, options: { serviceIdKind } }], } = {}) { const serverGroup = await this.config.getGroup(group.publicParams.serialize()); (0, assert_1.default)(serverGroup !== undefined, 'Group does not exist on server'); const targetServiceId = invitee.getServiceIdByKind(serviceIdKind); const userId = group.encryptServiceId(targetServiceId); const modifyResult = await this.config.modifyGroup({ group: serverGroup, actions: { version: group.revision + 1, addPendingMembers: [ { added: { member: { userId, role: compiled_1.signalservice.Member.Role.DEFAULT }, }, }, ], }, 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).finish(), }; await Promise.all(sendUpdateTo.map(async ({ device, options }) => { // Send the invitation const encryptOptions = { timestamp, ...options, }; const envelope = await this.encryptContent(device, { dataMessage: { groupV2, timestamp: long_1.default.fromNumber(encryptOptions.timestamp), }, }, encryptOptions); await this.config.send(device, envelope); })); } return updatedGroup; } async acceptPniInvite(group, { timestamp = Date.now(), sendUpdateTo = [] } = {}) { const serverGroup = await this.config.getGroup(group.publicParams.serialize()); (0, assert_1.default)(serverGroup !== undefined, 'Group does not exist on server'); const aciCiphertext = group.encryptServiceId(this.device.aci); const pniCiphertext = group.encryptServiceId(this.device.pni); const presentation = await this.getProfileKeyPresentation(group.secretParams); const modifyResult = await this.config.modifyGroup({ group: serverGroup, actions: { version: group.revision + 1, promoteMembersPendingPniAciProfileKey: [ { presentation: presentation.serialize(), }, ], }, aciCiphertext, pniCiphertext, }); (0, assert_1.default)(!modifyResult.conflict, 'Group update conflict!'); const updatedGroup = new group_1.Group({ secretParams: group.secretParams, groupState: serverGroup.state, }); const groupV2 = { ...updatedGroup.toContext(), groupChange: compiled_1.signalservice.GroupChange.encode(modifyResult.signedChange).finish(), }; await Promise.all(sendUpdateTo.map(async ({ device, options }) => { // Send the accepted invite const encryptOptions = { timestamp, ...options, }; const content = { dataMessage: { groupV2, timestamp: long_1.default.fromNumber(encryptOptions.timestamp), }, }; const envelope = await this.encryptContent(device, content, encryptOptions); await this.config.send(device, envelope); })); return updatedGroup; } // // 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) { 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 await this.decrypt(source, serviceIdKind, envelopeType, encrypted); }); let handled = true; if (content.decryptionErrorMessage && content.decryptionErrorMessage.length > 0) { assert_1.default.strictEqual(serviceIdKind, types_1.ServiceIdKind.ACI, 'Got sync message on PNI'); await this.handleResendRequest(unsealedSource, serviceIdKind, unsealedType, content); } else if (content.syncMessage) { assert_1.default.strictEqual(serviceIdKind, types_1.ServiceIdKind.ACI, 'Got sync message on PNI'); await this.handleSync(unsealedSource, content.syncMessage); } else if (content.dataMessage) { this.handleDataMessage(unsealedSource, serviceIdKind, unsealedType, content); } else if (content.storyMessage) { this.handleStoryMessage(unsealedSource, serviceIdKind, unsealedType, content); } else if (content.editMessage) { this.handleEditMessage(unsealedSource, serviceIdKind, unsealedType, content); } else if (content.receiptMessage) { this.handleReceiptMessage(unsealedSource, serviceIdKind, unsealedType, content); } else { handled = false; } const { senderKeyDistributionMessage } = content; if (senderKeyDistributionMessage && 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; 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 = { dataMessage: { groupV2: options.group?.toContext(), body: text, profileKey: options.withProfileKey ? this.profileKey.serialize() : undefined, timestamp: long_1.default.fromNumber(encryptOptions.timestamp), }, pniSignatureMessage, }; return await this.encryptContent(target, content, encryptOptions); } async encryptSyncSent(target, text, options) { const dataMessage = { body: text, timestamp: long_1.default.fromNumber(options.timestamp), }; const content = { syncMessage: { sent: { destinationServiceId: options.destinationServiceId, timestamp: long_1.default.fromNumber(options.timestamp), message: dataMessage, }, }, }; return await this.encryptContent(target, content, options); } async encryptSyncRead(target, options) { const content = { syncMessage: { read: options.messages.map(({ senderAci, timestamp }) => { return { senderAci, timestamp: long_1.default.fromNumber(timestamp), }; }), }, }; return await this.encryptContent(target, content, options); } async sendFetchStorage(options) { const content = { syncMessage: { fetchLatest: { type: compiled_1.signalservice.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST, }, }, }; return this.broadcast('fetch storage', content, options); } async sendStickerPackSync(options) { const Type = compiled_1.signalservice.SyncMessage.StickerPackOperation.Type; const content = { syncMessage: { stickerPackOperation: [ { packId: options.packId, packKey: options.packKey, type: options.type === 'install' ? Type.INSTALL : Type.REMOVE, }, ], }, }; 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 = { receiptMessage: { type, timestamp: options.messageTimestamps.map((timestamp) => long_1.default.fromNumber(timestamp)), }, }; return await 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, timestamp: long_1.default.fromNumber(messageTimestamp), serverTimestamp: long_1.default.fromNumber(timestamp), sourceServiceId: this.device.aci, sourceDevice: this.device.deviceId, destinationServiceId: target.aci, }; return this.config.send(target, Buffer.from(compiled_1.signalservice.Envelope.encode(envelope).finish())); } 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 = { syncMessage: { pniChangeNumber: { identityKeyPair: newPniIdentity.serialize(), lastResortKyberPreKey: lastResortKeyRecord.serialize(), signedPreKey: signedPreKeyRecord.serialize(), registrationId: newPniRegistrationId, newE164: newNumber, }, }, }; const envelope = await this.encryptContent(device, content, { ...options, timestamp, updatedPni: (0, types_1.untagPni)(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) { this.sendRaw(target, { 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 (source.aci !== envelope.sourceServiceId) { throw new Error(`Invalid envelope source. Expected: ${source.aci}, ` + `Got: ${envelope.sourceServiceId}`); } let envelopeType; if (envelope.type === compiled_1.signalservice.Envelope.Type.CIPHERTEXT) { envelopeType = base_1.EnvelopeType.CipherText; } else if (envelope.type === compiled_1.signalservice.Envelope.Type.PREKEY_BUNDLE) { 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.destinationServiceId ? this.device.getServiceIdKind(envelope.destinationServiceId) : types_1.ServiceIdKind.ACI; return await this.handleEnvelope(source, serviceIdKind, envelopeType, Buffer.from(envelope.content)); } 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).finish()); return await this.lock(async () => { return await 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) { const { request } = sync; if (!request) { debug('got generic sync message'); this.syncMessageQueue.push({ source, syncMessage: sync, }); return; } let stateChange; let response; if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.CONTACTS) { debug('got sync contacts request'); response = { contacts: { blob: this.contactsBlob, complete: true, }, }; stateChange = SyncState.Contacts; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.BLOCKED) { debug('got sync blocked request'); response = { blocked: {}, }; stateChange = SyncState.Blocked; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.CONFIGURATION) { debug('got sync configuration request'); response = { configuration: { readReceipts: true, unidentifiedDeliveryIndicators: false, typingIndicators: false, linkPreviews: false, }, }; stateChange = SyncState.Configuration; } else if (request.type === compiled_1.signalservice.SyncMessage.Request.Type.KEYS) { debug('got sync keys request'); response = { keys: { master: this.masterKey, mediaRootBackupKey: this.mediaRootBackupKey, accountEntropyPool: this.accountEntropyPool, }, }; stateChange = SyncState.Keys; } else { debug('Unsupported sync request', request); return; } const encrypted = await this.encryptContent(source, { syncMessage: response, }); // Intentionally not awaiting since the device might be offline or // not responding. void this.config.send(source, encrypted); const syncEntry = this.getSyncState(source); syncEntry.state |= stateChange; if ((syncEntry.state & SyncState.Complete) === SyncState.Complete) { debug('sync with %s complete', source.debugId); syncEntry.complete(); } } handleResendRequest(source, serviceIdKind, envelopeType, content) { const { decryptionErrorMessage } = content; assert_1.default.ok(decryptionErrorMessage, 'decryptionErrorMessage must be present'); const request = libsignal_client_1.DecryptionErrorMessage.deserialize(Buffer.from(decryptionErrorMessage)); this.decryptionErrorQueue.push({ source, serviceIdKind, envelopeType, content, timestamp: request.timestamp(), ratchetKey: request.ratchetKey(), senderDevice: request.deviceId(), }); } handleDataMessage(source, serviceIdKind, envelopeType, content) { const { dataMessage } = content; assert_1.default.ok(dataMessage, 'dataMessage must be present'); const { body } = dataMessage; this.messageQueue.push({ source, serviceIdKind, body: body ?? '', envelopeType, dataMessage, content, }); } handleReceiptMessage(source, serviceIdKind, envelopeType, content) { const { receiptMessage } = content; assert_1.default.ok(receiptMessage, 'receiptMessage must be present'); this.receiptQueue.push({ source, serviceIdKind, envelopeType, receiptMessage, content, }); } handleStoryMessage(source, serviceIdKind, envelopeType, content) { const { storyMessage } = content; assert_1.default.ok(storyMessage, 'storyMessage must be present'); this.storyQueue.push({ source, serviceIdKind, envelopeType, storyMessage, content, }); } handleEditMessage(source, serviceIdKind, envelopeType, content) { const { editMessage } = content; assert_1.default.ok(editMessage, 'editMessage must be present'); this.editMessageQueue.push({ source, serviceIdKind, envelopeType, editMessage, content, }); } async encrypt(target, message, { timestamp = Date.now(), sealed = false, serviceIdKind = types_1.ServiceIdKind.ACI, updatedPni, distributionId, group, } = {}) { assert_1.default.ok(this.isInitialized, 'Not initialized'); // "Pad" const paddedMessage = Buffer.concat([message, Buffer.from([0x80])]); let envelopeType; let content; // Outgoing stores const identity = this.identity.get(types_1.ServiceIdKind.ACI); (0, assert_1.default)(identity, 'Should have an ACI identity'); if (sealed) { (0, assert_1.default)(serviceIdKind === types_1.ServiceIdKind.ACI, "Can't send sealed sender to PNI"); if (distributionId) { const senderKey = this.senderKeys.get(types_1.ServiceIdKind.ACI); (0, assert_1.default)(senderKey, 'Should have an ACI sender keys'); const ciphertext = await SignalClient.groupEncrypt(this.device.address, distributionId, senderKey, paddedMessage); const usmc = SignalClient.UnidentifiedSenderMessageContent.new(ciphertext, this.senderCertificate, SignalClient.ContentHint.Implicit, group?.publicParams.getGroupIdentifier().serialize() ?? null); const multiRecipient = await SignalClient.sealedSenderMultiRecipientEncrypt(usmc, [target.getAddressByKind(serviceIdKind)], identity, this.sessions); content = SignalClient.sealedSenderMultiRecipientMessageForSingleRecipient(multiRecipient); } else { content = await SignalClient.sealedSenderEncryptMessage(paddedMessage, target.getAddressByKind(serviceIdKind), this.senderCertificate, this.sessions, identity); } envelopeType = compiled_1.signalservice.Envelope.Type.UNIDENTIFIED_SENDER; } else { const ciphertext = await SignalClient.signalEncrypt(paddedMessage, target.getAddressByKind(serviceIdKind), this.sessions, identity); content = ciphertext.serialize(); if (ciphertext.type() === libsignal_client_1.CiphertextMessageType.Whisper) { (0, assert_1.default)(serviceIdKind === types_1.ServiceIdKind.ACI, "Can't send non-prekey messages to PNI"); envelopeType = compiled_1.signalservice.Envelope.Type.CIPHERTEXT; debug('encrypting ciphertext envelope'); } else { assert_1.default.strictEqual(ciphertext.type(), libsignal_client_1.CiphertextMessageType.PreKey); envelopeType = compiled_1.signalservice.Envelope.Type.PREKEY_BUNDLE; debug('encrypting prekeyBundle envelope'); } } const envelope = Buffer.from(compiled_1.signalservice.Envelope.encode({ type: envelopeType, sourceServiceId: this.device.aci, sourceDevice: this.device.deviceId, destinationServiceId: target.getServiceIdByKind(serviceIdKind), updatedPni, serverTimestamp: long_1.default.fromNumber(timestamp), timestamp: long_1.default.fromNumber(timestamp), content, }).finish()); debug('encrypting envelope finish'); return envelope; } async decrypt(source, serviceIdKind, envelopeType, encrypted) { debug('decrypting envelope type=%s start', envelopeType); const identity = this.identity.get(serviceIdKind); const preKeys = this.preKeys.get(serviceIdKind); const kyberPreKeys = this.kyberPreKeys.get(serviceIdKind); const signedPreKeys = this.signedPreKeys.get(serviceIdKind); const senderKeys = this.senderKeys.get(serviceIdKind); (0, assert_1.default)(identity && preKeys && signedPreKeys && kyberPreKeys && senderKeys, 'Should have identity, prekey/kyber/signed/senderkey stores'); let decrypted; if (envelopeType === base_1.EnvelopeType.Plaintext) { (0, assert_1.default)(source !== undefined, 'Plaintext must have source'); const plaintext = libsignal_client_1.PlaintextContent.deserialize(encrypted); decrypted = plaintext.body(); } else if (envelopeType === base_1.EnvelopeType.CipherText) { (0, assert_1.default)(source !== undefined, 'CipherText must have source'); decrypted = await SignalClient.signalDecrypt(libsignal_client_1.SignalMessage.deserialize(encrypted), source.getAddressByKind(serviceIdKind), this.sessions, identity); } else if (envelopeType === base_1.EnvelopeType.PreKey) { (0, assert_1.default)(source !== undefined, 'PreKey must have source'); decrypted = await SignalClient.signalDecryptPreKey(libsignal_client_1.PreKeySignalMessage.deserialize(encrypted), source.getAddressByKind(serviceIdKind), this.sessions, identity, preKeys, signedPreKeys, kyberPreKeys); } else if (envelopeType === base_1.EnvelopeType.SenderKey) { (0, assert_1.default)(source !== undefined, 'SenderKey must have source'); decrypted = await SignalClient.groupDecrypt(source.getAddressByKind(serviceIdKind), senderKeys, encrypted); } else if (envelopeType === base_1.EnvelopeType.SealedSender) { (0, assert_1.default)(source === undefined, 'Sealed sender must have no source');