UNPKG

@signalapp/mock-server

Version:
304 lines (303 loc) 14.9 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 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, groupChange: null, }, ], groupSendEndorsementsResponse: null, }; } 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), 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, }; assert_1.default.ok(actions.version, 'Actions should have a new version'); const timestamp = BigInt(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 addMembersPendingProfileKey = actions.addMembersPendingProfileKey ?? []; for (const { added } of addMembersPendingProfileKey) { assert_1.default.ok(added, 'Missing addPendingMember.added'); const { member } = added; assert_1.default.ok(member, 'Missing addMembersPendingProfileKey.added.member'); const { userId, role } = member; assert_1.default.ok(userId, 'Missing addMembersPendingProfileKey.added.member.userId'); assert_1.default.ok(role != null, 'Missing addMembersPendingProfileKey.added.member.role'); this.verifyAccess('pendingMembers', authMember, accessControl?.members ?? AccessRequired.UNKNOWN); const newPendingMember = { member: { userId, role, profileKey: null, presentation: null, joinedAtVersion: null, labelEmoji: null, labelString: null, }, addedByUserId: sourceAci.serialize(), timestamp, }; newState.membersPendingProfileKey = [ ...(newState.membersPendingProfileKey ?? []), newPendingMember, ]; appliedActions.addMembersPendingProfileKey = [ ...(appliedActions.addMembersPendingProfileKey ?? []), { added: newPendingMember }, ]; } const deleteMembersPendingProfileKey = actions.deleteMembersPendingProfileKey ?? []; for (const { deletedUserId } of deleteMembersPendingProfileKey) { 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.deleteMembersPendingProfileKey = [ ...(appliedActions.deleteMembersPendingProfileKey ?? []), { deletedUserId }, ]; } const promoteMembersPendingProfileKey = actions.promoteMembersPendingProfileKey ?? []; for (const { presentation } of promoteMembersPendingProfileKey) { assert_1.default.ok(presentation, 'Missing presentation in deleteMembersPendingProfileKey'); const presentationFFI = new zkgroup_1.ProfileKeyCredentialPresentation(Buffer.from(presentation)); this.profileOps.verifyProfileKeyCredentialPresentation(this.publicParams, presentationFFI); assert_1.default.ok(Buffer.from(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, presentation: null, joinedAtVersion: null, labelEmoji: null, labelString: null, }, ]; appliedActions.promoteMembersPendingProfileKey = [ ...(appliedActions.promoteMembersPendingProfileKey ?? []), { userId, profileKey, presentation: null }, ]; } 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(Buffer.from(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(), presentation: null, joinedAtVersion: null, labelEmoji: null, labelString: null, }, ]; changeEpoch = Math.max(changeEpoch, 5); appliedActions.sourceUserId = sourcePni.serialize(); appliedActions.promoteMembersPendingPniAciProfileKey = [ ...(appliedActions.promoteMembersPendingPniAciProfileKey ?? []), { userId: aci.serialize(), pni: pni.serialize(), profileKey: profileKey.serialize(), presentation: null, }, ]; } if (actions.terminateGroup !== null) { this.verifyAccess('terminated', authMember, AccessRequired.ADMINISTRATOR); appliedActions.terminateGroup = {}; newState.terminated = true; } if (actions.modifyDisappearingMessageTimer !== null) { this.verifyAccess('disappearingMessagesTimer', authMember, accessControl?.attributes ?? AccessRequired.UNKNOWN); appliedActions.modifyDisappearingMessageTimer = actions.modifyDisappearingMessageTimer; newState.disappearingMessagesTimer = actions.modifyDisappearingMessageTimer.timer; } 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); const serverSignature = this.zkSecret .sign(Buffer.from(encodedActions)) .serialize(); const groupChange = { actions: encodedActions, changeEpoch, serverSignature: null, }; 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(), presentation: null, joinedAtVersion: null, labelEmoji: null, labelString: null, }; } } exports.ServerGroup = ServerGroup;