@signalapp/mock-server
Version:
Mock Signal Server for writing tests
1,229 lines (1,228 loc) • 63.2 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 () {
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,
};