@signalapp/mock-server
Version:
Mock Signal Server for writing tests
1,160 lines (1,159 loc) • 54.7 kB
JavaScript
"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');