UNPKG

@signalapp/mock-server

Version:
247 lines (246 loc) 12.2 kB
"use strict"; // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerGroup = void 0; const zkgroup_1 = require("@signalapp/libsignal-client/zkgroup"); const assert_1 = __importDefault(require("assert")); const long_1 = __importDefault(require("long")); const compiled_1 = require("../../protos/compiled"); const group_1 = require("../data/group"); const schemas_1 = require("../data/schemas"); const util_1 = require("../util"); const { AccessRequired } = compiled_1.signalservice.AccessControl; const { Role } = compiled_1.signalservice.Member; function getTodaysKey(zkSecret) { const startOfDay = (0, util_1.getTodayInSeconds)(); const expiration = startOfDay + (0, util_1.daysToSeconds)(2); return zkgroup_1.GroupSendDerivedKeyPair.forExpiration(new Date(1000 * expiration), zkSecret); } class ServerGroup extends group_1.Group { profileOps; zkSecret; constructor({ profileOps, zkSecret, state }) { super(); const parsedState = schemas_1.GroupStateSchema.parse(state); this.privPublicParams = new zkgroup_1.GroupPublicParams(Buffer.from(parsedState.publicKey)); this.profileOps = profileOps; this.zkSecret = zkSecret; const unrolledState = { ...state }; unrolledState.members = (state.members ?? []).map((member) => this.unrollMember(member)); this.privChanges = { groupChanges: [ { groupState: unrolledState, }, ], }; } getGroupSendEndorsementResponse(sourceAci) { const authMember = this.getMember(sourceAci); if (!authMember) { return null; } const members = this.state.members ?? []; const groupCiphertexts = members.map((member) => { (0, assert_1.default)(member.userId, 'Member must have a user ID'); return new zkgroup_1.UuidCiphertext(Buffer.from(member.userId)); }); const todaysKey = getTodaysKey(this.zkSecret); return zkgroup_1.GroupSendEndorsementsResponse.issue(groupCiphertexts, todaysKey).serialize(); } modify(sourceAci, sourcePni, actions) { const appliedActions = { version: actions.version, sourceUserId: sourceAci.serialize(), groupId: (0, util_1.fromBase64)(this.id), }; assert_1.default.ok(actions.version, 'Actions should have a new version'); const timestamp = long_1.default.fromNumber(Date.now()); const newState = { ...this.state, version: actions.version, }; const authMember = this.getMember(sourceAci); const { accessControl } = newState; let changeEpoch = 1; if (actions.modifyTitle) { this.verifyAccess('title', authMember, accessControl?.attributes ?? AccessRequired.UNKNOWN); appliedActions.modifyTitle = actions.modifyTitle; newState.title = actions.modifyTitle.title; } const deleteMembers = actions.deleteMembers ?? []; for (const { deletedUserId } of deleteMembers) { assert_1.default.ok(deletedUserId, 'Missing deletedUserId'); this.verifyAccess('members', authMember, accessControl?.members ?? AccessRequired.UNKNOWN, deletedUserId); const member = this.getMember(new zkgroup_1.UuidCiphertext(Buffer.from(deletedUserId))); assert_1.default.ok(member, 'Pending member not found for deletion'); newState.members = (newState.members ?? []).filter((entry) => entry !== member); appliedActions.deleteMembers = [ ...(appliedActions.deleteMembers ?? []), { deletedUserId }, ]; } const addPendingMembers = actions.addPendingMembers ?? []; for (const { added } of addPendingMembers) { assert_1.default.ok(added, 'Missing addPendingMember.added'); const { member } = added; assert_1.default.ok(member, 'Missing addPendingMembers.added.member'); const { userId, role } = member; assert_1.default.ok(userId, 'Missing addPendingMembers.added.member.userId'); assert_1.default.ok(role, 'Missing addPendingMembers.added.member.role'); this.verifyAccess('pendingMembers', authMember, accessControl?.members ?? AccessRequired.UNKNOWN); const newPendingMember = { member: { userId, role }, addedByUserId: sourceAci.serialize(), timestamp, }; newState.membersPendingProfileKey = [ ...(newState.membersPendingProfileKey ?? []), newPendingMember, ]; appliedActions.addPendingMembers = [ ...(appliedActions.addPendingMembers ?? []), { added: newPendingMember }, ]; } const deletePendingMembers = actions.deletePendingMembers ?? []; for (const { deletedUserId } of deletePendingMembers) { assert_1.default.ok(deletedUserId, 'Missing deletedUserId'); assert_1.default.ok(Buffer.from(deletedUserId).equals(sourceAci.serialize()) || Buffer.from(deletedUserId).equals(sourcePni.serialize()), 'Not a pending member'); const pendingMember = this.getPendingMember(new zkgroup_1.UuidCiphertext(Buffer.from(deletedUserId))); assert_1.default.ok(pendingMember, 'Pending member not found for deletion'); newState.membersPendingProfileKey = (newState.membersPendingProfileKey ?? []).filter((entry) => entry !== pendingMember); appliedActions.deletePendingMembers = [ ...(appliedActions.deletePendingMembers ?? []), { deletedUserId }, ]; } const promotePendingMembers = actions.promotePendingMembers ?? []; for (const { presentation } of promotePendingMembers) { assert_1.default.ok(presentation, 'Missing presentation in promotePendingMembers'); const presentationFFI = new zkgroup_1.ProfileKeyCredentialPresentation(Buffer.from(presentation)); this.profileOps.verifyProfileKeyCredentialPresentation(this.publicParams, presentationFFI); assert_1.default.ok(presentationFFI .getUuidCiphertext() .serialize() .equals(sourceAci.serialize()), 'Not a pending member'); const pendingMember = this.getPendingMember(presentationFFI.getUuidCiphertext()); assert_1.default.ok(pendingMember, 'No pending member'); assert_1.default.ok(!this.getMember(presentationFFI.getUuidCiphertext()), 'Member is both pending and active'); newState.membersPendingProfileKey = (newState.membersPendingProfileKey ?? []).filter((entry) => entry !== pendingMember); const userId = presentationFFI.getUuidCiphertext().serialize(); const profileKey = presentationFFI.getProfileKeyCiphertext().serialize(); newState.members = [ ...(newState.members ?? []), { role: Role.DEFAULT, userId, profileKey, }, ]; appliedActions.promotePendingMembers = [ ...(appliedActions.promotePendingMembers ?? []), { userId, profileKey }, ]; } const promotePNIMembers = actions.promoteMembersPendingPniAciProfileKey; for (const { presentation } of promotePNIMembers ?? []) { assert_1.default.ok(presentation, 'Missing presentation in promoteMembersPendingPniAciProfileKey'); const presentationFFI = new zkgroup_1.ProfileKeyCredentialPresentation(Buffer.from(presentation)); this.profileOps.verifyProfileKeyCredentialPresentation(this.publicParams, presentationFFI); const aci = presentationFFI.getUuidCiphertext(); const pni = sourcePni; const profileKey = presentationFFI.getProfileKeyCiphertext(); assert_1.default.ok(aci.serialize().equals(sourceAci.serialize()), 'Not a pending member'); const pendingMember = this.getPendingMember(pni); assert_1.default.ok(pendingMember, 'No pending pni member'); assert_1.default.ok(!this.getMember(aci), 'ACI is already a member'); newState.membersPendingProfileKey = (newState.membersPendingProfileKey ?? []).filter((entry) => entry !== pendingMember); newState.members = [ ...(newState.members ?? []), { role: Role.DEFAULT, userId: aci.serialize(), profileKey: profileKey.serialize(), }, ]; changeEpoch = Math.max(changeEpoch, 5); appliedActions.sourceUserId = sourcePni.serialize(); appliedActions.promoteMembersPendingPniAciProfileKey = [ ...(appliedActions.promoteMembersPendingPniAciProfileKey ?? []), { userId: aci.serialize(), pni: pni.serialize(), profileKey: profileKey.serialize(), }, ]; } const { version: oldVersion } = this.state; assert_1.default.ok(typeof oldVersion === 'number', 'Group must have existing version'); if (actions.version !== oldVersion + 1) { return { conflict: true, signedChange: undefined }; } const encodedActions = compiled_1.signalservice.GroupChange.Actions.encode(appliedActions).finish(); const serverSignature = this.zkSecret .sign(Buffer.from(encodedActions)) .serialize(); const groupChange = { actions: encodedActions, changeEpoch, }; assert_1.default.ok(this.privChanges?.groupChanges, 'Must be initialized'); this.privChanges.groupChanges.push({ groupChange, groupState: newState, }); return { conflict: false, signedChange: { ...groupChange, serverSignature, }, }; } // // Private // verifyAccess(attribute, member, access, affectedUserId) { // Changing something about ourselves is always allowed if (member?.userId && affectedUserId && Buffer.from(member.userId).equals(affectedUserId)) { return; } switch (access) { case AccessRequired.ANY: break; case AccessRequired.MEMBER: assert_1.default.ok(member, `Must be a member to access: ${attribute}`); break; case AccessRequired.ADMINISTRATOR: assert_1.default.strictEqual(member?.role, Role.ADMINISTRATOR, `Must be an administrator to modify: ${attribute}`); break; case AccessRequired.UNSATISFIABLE: throw new Error(`Unsatisfiable access attribute: ${attribute}`); case AccessRequired.UNKNOWN: throw new Error(`Unknown access for attribute: ${attribute}`); } } unrollMember({ role, presentation }) { assert_1.default.strictEqual(typeof role, 'number', 'Group member role is undefined'); assert_1.default.ok(presentation, 'Group member presentation is undefined'); const presentationFFI = new zkgroup_1.ProfileKeyCredentialPresentation(Buffer.from(presentation)); this.profileOps.verifyProfileKeyCredentialPresentation(this.publicParams, presentationFFI); return { role, userId: presentationFFI.getUuidCiphertext().serialize(), profileKey: presentationFFI.getProfileKeyCiphertext().serialize(), }; } } exports.ServerGroup = ServerGroup;