diff --git a/CHANGELOG.md b/CHANGELOG.md index c304c01bda..e4544e01f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Tighten controls on channel metadata DB operations, move private channels to separate metadata DB [#3329](https://github.com/TryQuiet/quiet/issues/3329) * Use chain permission checks to gate channel creation, deletion and membership [#3344](https://github.com/TryQuiet/quiet/issues/3344) +* Use randomly generated role names for private channels [#3354](https://github.com/TryQuiet/quiet/issues/3354) * Tighten user profile store access control and validations [#3340](https://github.com/TryQuiet/quiet/issues/3340) ## [8.0.0] diff --git a/packages/backend/src/nest/auth/services/members/user.service.spec.ts b/packages/backend/src/nest/auth/services/members/user.service.spec.ts index 396d55a03f..0f747ff206 100644 --- a/packages/backend/src/nest/auth/services/members/user.service.spec.ts +++ b/packages/backend/src/nest/auth/services/members/user.service.spec.ts @@ -38,7 +38,7 @@ describe('users', () => { }) it('get admin member by name', () => { const user = adminSigChain.users.getUserById(adminSigChain.user.userId) - expect(user!.userName).toEqual(adminSigChain.user.userName) + expect(user.userName).toEqual(adminSigChain.user.userName) }) it('should redact user', () => { const redactedUser = UserService.redactUser(adminSigChain.user) diff --git a/packages/backend/src/nest/auth/services/members/user.service.ts b/packages/backend/src/nest/auth/services/members/user.service.ts index bbbed62ce1..c7d7168263 100644 --- a/packages/backend/src/nest/auth/services/members/user.service.ts +++ b/packages/backend/src/nest/auth/services/members/user.service.ts @@ -80,12 +80,17 @@ class UserService extends ChainServiceBase { return this.sigChain.team!.members(memberIds, options) } + public getUserById(memberId: string): Member + public getUserById(memberId: string, options: MemberSearchOptions & { throwOnMissing: true }): Member + public getUserById(memberId: string, options: MemberSearchOptions & { throwOnMissing: false }): Member | undefined + public getUserById(memberId: string, options: MemberSearchOptions): Member | undefined public getUserById(memberId: string, options: MemberSearchOptions = DEFAULT_SEARCH_OPTIONS): Member | undefined { const users = this.getUsersById([memberId], options) if (users.length === 0) { if (options.throwOnMissing) { throw new Error(`No user found for ID ${memberId}`) } + return undefined } if (users.length > 1) { throw new Error(`Expected 1 user for ID but found ${users.length} user records`) @@ -93,6 +98,14 @@ class UserService extends ChainServiceBase { return users[0] } + public getMyUser(): Member + public getMyUser(options: MemberSearchOptions & { throwOnMissing: true }): Member + public getMyUser(options: MemberSearchOptions & { throwOnMissing: false }): Member | undefined + public getMyUser(options: MemberSearchOptions): Member | undefined + public getMyUser(options: MemberSearchOptions = DEFAULT_SEARCH_OPTIONS): Member | undefined { + return this.getUserById(this.sigChain.user.userId, options) + } + public static redactUser(user: UserWithSecrets): User { return SigChain.lfa.redactUser(user) } diff --git a/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts b/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts index aae4a59b4a..46937c7ceb 100644 --- a/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts +++ b/packages/backend/src/nest/auth/services/roles/channel.service.spec.ts @@ -1,6 +1,6 @@ import { SigChain } from '../../sigchain' import { createLogger } from '../../../common/logger' -import { RoleName } from './roles' +import { DEFAULT_CHANNEL_ROLE_NAME_LENGTH, RoleName } from './roles' import { base58, hash, randomBytes } from '@localfirst/crypto' import * as uint8arrays from 'uint8arrays' import { generateProof, InviteResult, MemberContext, redactKeys, Team } from '@localfirst/auth' @@ -19,7 +19,7 @@ describe('channels', () => { let seed: string let salt: string let generatedKeys: InviteLockboxMetadata - const channelId = 'foobar' + let channelRoleName: string it('should initialize a new sigchain and be admin', () => { adminSigChain = SigChain.create() @@ -37,13 +37,13 @@ describe('channels', () => { expect(adminSigChain.channels.canIDeletePublicChannel()).toBe(true) }) it('should create channel and admin should be added as member', () => { - const channel = adminSigChain.channels.create(channelId) - expect(channel).toBeDefined() - expect(channel).toBe(adminSigChain.channels.generateChannelRoleName(channelId)) - expect(adminSigChain.channels.amIMemberOfChannel(channelId)).toBe(true) - expect(adminSigChain.channels.canIAddMembersToPrivateChannel(channelId)).toBe(true) - expect(adminSigChain.channels.canIRemoveMembersFromPrivateChannel(channelId)).toBe(true) - expect(adminSigChain.channels.canIDeletePrivateChannel(channelId)).toBe(true) + channelRoleName = adminSigChain.channels.create() + expect(channelRoleName).toBeDefined() + expect(channelRoleName).toHaveLength(DEFAULT_CHANNEL_ROLE_NAME_LENGTH) + expect(adminSigChain.channels.amIMemberOfChannel(channelRoleName)).toBe(true) + expect(adminSigChain.channels.canIAddMembersToPrivateChannel(channelRoleName)).toBe(true) + expect(adminSigChain.channels.canIRemoveMembersFromPrivateChannel(channelRoleName)).toBe(true) + expect(adminSigChain.channels.canIDeletePrivateChannel(channelRoleName)).toBe(true) }) it('should create an invite', () => { invite = adminSigChain.invites.createUserInvite() @@ -79,7 +79,7 @@ describe('channels', () => { secondSigChain.context.user.userId, redactKeys(secondSigChain.context.user.keys) ) - expect(adminSigChain.users.getUserById(secondSigChain.user.userId)).toBeDefined() + expect(() => adminSigChain.users.getUserById(secondSigChain.user.userId)).not.toThrow() const teamBytes = adminSigChain.save() const teamKeyring = adminSigChain.team!.teamKeyring() @@ -105,22 +105,24 @@ describe('channels', () => { expect(secondSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true) }) it('should fail to self-assign channel role on second user', () => { - const failedSelfAssign = () => - secondSigChain.roles.addSelf(secondSigChain.channels.generateChannelRoleName(channelId), seed, salt) + const failedSelfAssign = () => secondSigChain.roles.addSelf(channelRoleName, seed, salt) expect(failedSelfAssign).toThrow() }) it('should add second user to channel', () => { - adminSigChain.channels.addMember(secondSigChain.context.user.userId, channelId) - expect(adminSigChain.channels.memberInChannel(secondSigChain.context.user.userId, channelId)).toBe(true) + adminSigChain.channels.addMember(secondSigChain.context.user.userId, channelRoleName) + expect(adminSigChain.channels.memberInChannel(secondSigChain.context.user.userId, channelRoleName)).toBe(true) expect( - adminSigChain.channels.canMemberAddMembersToPrivateChannel(secondSigChain.context.user.userId, channelId) + adminSigChain.channels.canMemberAddMembersToPrivateChannel(secondSigChain.context.user.userId, channelRoleName) ).toBe(false) expect( - adminSigChain.channels.canMemberRemoveMembersFromPrivateChannel(secondSigChain.context.user.userId, channelId) + adminSigChain.channels.canMemberRemoveMembersFromPrivateChannel( + secondSigChain.context.user.userId, + channelRoleName + ) + ).toBe(false) + expect( + adminSigChain.channels.canMemberDeletePrivateChannel(secondSigChain.context.user.userId, channelRoleName) ).toBe(false) - expect(adminSigChain.channels.canMemberDeletePrivateChannel(secondSigChain.context.user.userId, channelId)).toBe( - false - ) }) it('should fail to create channel on second user', () => { expect(secondSigChain.roles.amIAdmin()).toBe(false) @@ -128,7 +130,7 @@ describe('channels', () => { expect(secondSigChain.channels.canICreatePrivateChannel()).toBe(false) expect(secondSigChain.channels.canIDeletePublicChannel()).toBe(false) - const failedToCreateChannel = () => secondSigChain.channels.create(randomUUID()) + const failedToCreateChannel = () => secondSigChain.channels.create() expect(failedToCreateChannel).toThrow() }) }) diff --git a/packages/backend/src/nest/auth/services/roles/channel.service.ts b/packages/backend/src/nest/auth/services/roles/channel.service.ts index 1c369cf642..1fd3348d22 100644 --- a/packages/backend/src/nest/auth/services/roles/channel.service.ts +++ b/packages/backend/src/nest/auth/services/roles/channel.service.ts @@ -6,8 +6,8 @@ import { SigChain } from '../../sigchain' import { ChainServiceBase } from '../chainServiceBase' import { Member } from '@localfirst/auth' import { createLogger } from '../../../common/logger' -import { hash } from '@localfirst/crypto' -import { PUBLIC_CHANNEL_MODIFICATION_ROLES } from './roles' +import { hash, randomKey } from '@localfirst/crypto' +import { DEFAULT_CHANNEL_ROLE_NAME_LENGTH, PUBLIC_CHANNEL_MODIFICATION_ROLES } from './roles' const logger = createLogger('auth:channelService') @@ -16,52 +16,46 @@ class ChannelService extends ChainServiceBase { super(sigChain) } - public create(channelId: string): string { - const roleName = this.generateChannelRoleName(channelId) + public create(): string { + const roleName = this.generateChannelRoleName() logger.info(`Adding new channel role with name ${roleName}`) this.sigChain.roles.create(roleName) return roleName } - public createWithMembers(channelId: string, memberIdsForChannel: string[]): string { - const roleName = this.create(channelId) + public createWithMembers(memberIdsForChannel: string[]): string { + const roleName = this.create() for (const memberId of memberIdsForChannel) { - this.addMember(memberId, channelId) + this.addMember(memberId, roleName) } return roleName } - public addMember(memberId: string, channelId: string) { - logger.info(`Adding member with ID ${memberId} to channel ${channelId}`) - const roleName = this.generateChannelRoleName(channelId) - this.sigChain.roles!.addMember(memberId, roleName) + public addMember(memberId: string, channelRoleName: string) { + logger.info(`Adding member with ID ${memberId} to channel ${channelRoleName}`) + this.sigChain.roles!.addMember(memberId, channelRoleName) } - public memberInChannel(memberId: string, channelId: string): boolean { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.memberHasRole(memberId, roleName) + public memberInChannel(memberId: string, channelRoleName: string): boolean { + return this.sigChain.roles.memberHasRole(memberId, channelRoleName) } - public amIMemberOfChannel(channelId: string): boolean { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.amIMemberOfRole(roleName) + public amIMemberOfChannel(channelRoleName: string): boolean { + return this.sigChain.roles.amIMemberOfRole(channelRoleName) } - public getMembersInChannel(channelId: string): Member[] { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.getMembersForRole(roleName) + public getMembersInChannel(channelRoleName: string): Member[] { + return this.sigChain.roles.getMembersForRole(channelRoleName) } - public revokeMembership(memberId: string, channelId: string) { - logger.info(`Revoking membership of channel ${channelId} for member with ID ${memberId}`) - const roleName = this.generateChannelRoleName(channelId) - this.sigChain.roles.revokeMembership(memberId, roleName) + public revokeMembership(memberId: string, channelRoleName: string) { + logger.info(`Revoking membership of channel for member with ID ${memberId}`) + this.sigChain.roles.revokeMembership(memberId, channelRoleName) } - public delete(channelId: string) { - logger.info(`Removing role for channel ${channelId}`) - const roleName = this.generateChannelRoleName(channelId) - this.sigChain.roles.delete(roleName) + public delete(channelRoleName: string) { + logger.info(`Removing role for channel`) + this.sigChain.roles.delete(channelRoleName) } public canMemberCreatePublicChannel(memberId: string): boolean { @@ -98,35 +92,32 @@ class ChannelService extends ChainServiceBase { return this.canMemberCreatePrivateChannel(this.sigChain.user.userId) } - public canMemberAddMembersToPrivateChannel(memberId: string, channelId: string): boolean { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.canMemberAddMembersRole(memberId, roleName) + public canMemberAddMembersToPrivateChannel(memberId: string, channelRoleName: string): boolean { + return this.sigChain.roles.canMemberAddMembersRole(memberId, channelRoleName) } - public canIAddMembersToPrivateChannel(channelId: string): boolean { - return this.canMemberAddMembersToPrivateChannel(this.sigChain.user.userId, channelId) + public canIAddMembersToPrivateChannel(channelRoleName: string): boolean { + return this.canMemberAddMembersToPrivateChannel(this.sigChain.user.userId, channelRoleName) } - public canMemberRemoveMembersFromPrivateChannel(memberId: string, channelId: string): boolean { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.canMemberRemoveMembersFromRole(memberId, roleName) + public canMemberRemoveMembersFromPrivateChannel(memberId: string, channelRoleName: string): boolean { + return this.sigChain.roles.canMemberRemoveMembersFromRole(memberId, channelRoleName) } - public canIRemoveMembersFromPrivateChannel(channelId: string): boolean { - return this.canMemberRemoveMembersFromPrivateChannel(this.sigChain.user.userId, channelId) + public canIRemoveMembersFromPrivateChannel(channelRoleName: string): boolean { + return this.canMemberRemoveMembersFromPrivateChannel(this.sigChain.user.userId, channelRoleName) } - public canMemberDeletePrivateChannel(memberId: string, channelId: string): boolean { - const roleName = this.generateChannelRoleName(channelId) - return this.sigChain.roles.canMemberDeleteRole(memberId, roleName) + public canMemberDeletePrivateChannel(memberId: string, channelRoleName: string): boolean { + return this.sigChain.roles.canMemberDeleteRole(memberId, channelRoleName) } - public canIDeletePrivateChannel(channelId: string): boolean { - return this.canMemberDeletePrivateChannel(this.sigChain.user.userId, channelId) + public canIDeletePrivateChannel(channelRoleName: string): boolean { + return this.canMemberDeletePrivateChannel(this.sigChain.user.userId, channelRoleName) } - public generateChannelRoleName(channelId: string): string { - return hash(this.sigChain.team!.id, `private_channel_${channelId}`) + public generateChannelRoleName(roleNameLength: number = DEFAULT_CHANNEL_ROLE_NAME_LENGTH): string { + return randomKey(roleNameLength) } } diff --git a/packages/backend/src/nest/auth/services/roles/roles.service.spec.ts b/packages/backend/src/nest/auth/services/roles/roles.service.spec.ts index 86871f954c..9c62979cbe 100644 --- a/packages/backend/src/nest/auth/services/roles/roles.service.spec.ts +++ b/packages/backend/src/nest/auth/services/roles/roles.service.spec.ts @@ -69,7 +69,7 @@ describe('roles', () => { secondSigChain.context.user.userId, redactKeys(secondSigChain.context.user.keys) ) - expect(adminSigChain.users.getUserById(secondSigChain.user.userId)).toBeDefined() + expect(() => adminSigChain.users.getUserById(secondSigChain.user.userId)).not.toThrow() const teamBytes = adminSigChain.save() const teamKeyring = adminSigChain.team!.teamKeyring() diff --git a/packages/backend/src/nest/auth/services/roles/roles.ts b/packages/backend/src/nest/auth/services/roles/roles.ts index 949dfc4e38..514aed467e 100644 --- a/packages/backend/src/nest/auth/services/roles/roles.ts +++ b/packages/backend/src/nest/auth/services/roles/roles.ts @@ -45,3 +45,5 @@ export class NotAdminError extends Error { super('User is not an admin on this community') } } + +export const DEFAULT_CHANNEL_ROLE_NAME_LENGTH = 64 diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index 18da5ed157..45eb862fab 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -84,6 +84,7 @@ import { QPSService } from '../qps/qps.service' import { CaptchaService } from '../captcha/captcha.service' import { SigChain } from '../auth/sigchain' import { Member } from '@localfirst/auth' +import type { PrivateChannelMappings } from '../storage/channels/channels.types' /** * A monolith service that handles lots of events received from the state-manager. @@ -975,7 +976,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } // handle chain updates - let channelMapping: Record = {} + const sigChain = this.sigChainService.getChain(teamId) + let channelMapping: PrivateChannelMappings = { + roleNameToChannel: {}, + idToRoleName: {}, + } if (!this.storageService || !this.storageService.initialized || !this.storageService.channels.initialized) { this.logger.warn(`StorageService hasn't been initialized, skipping channel mappings...`) } else { @@ -983,18 +988,21 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } const _handleUser = (member: Member, sigChain: SigChain): User => { - const privateChannelIds = + const privateChannelIds: string[] = channelMapping != null - ? member.roles.filter(roleName => roleName in channelMapping).map(roleName => channelMapping[roleName].id) + ? member.roles + .filter(roleName => roleName in channelMapping.roleNameToChannel) + .map(roleName => channelMapping.roleNameToChannel[roleName].id) : [] if (member.userId === sigChain.user.userId) { const channelSpecificPermissions: PrivateChannelPermissions[] = [] for (const channelId of privateChannelIds) { + const roleName = channelMapping.idToRoleName[channelId] channelSpecificPermissions.push({ channelId, - addMembers: sigChain.channels.canMemberAddMembersToPrivateChannel(member.userId, channelId), - removeMembers: sigChain.channels.canMemberRemoveMembersFromPrivateChannel(member.userId, channelId), - delete: sigChain.channels.canMemberDeletePrivateChannel(member.userId, channelId), + addMembers: sigChain.channels.canMemberAddMembersToPrivateChannel(member.userId, roleName), + removeMembers: sigChain.channels.canMemberRemoveMembersFromPrivateChannel(member.userId, roleName), + delete: sigChain.channels.canMemberDeletePrivateChannel(member.userId, roleName), }) } const payload: SetChannelPermissionsPayload = { @@ -1025,7 +1033,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI * * (Can we base these updates on the graph itself vs pulling directly from the Team object?) */ - const sigChain = this.sigChainService.getChain(teamId) const users = sigChain.team?.members().map((member): User => _handleUser(member, sigChain)) this.serverIoProvider.io.emit(SocketEvents.USERS_UPDATED, { users }) } diff --git a/packages/backend/src/nest/libp2p/integration-tests/orbitdb-message-fanout.spec.ts b/packages/backend/src/nest/libp2p/integration-tests/orbitdb-message-fanout.spec.ts index 5077266e7a..a44a3163a1 100644 --- a/packages/backend/src/nest/libp2p/integration-tests/orbitdb-message-fanout.spec.ts +++ b/packages/backend/src/nest/libp2p/integration-tests/orbitdb-message-fanout.spec.ts @@ -595,7 +595,7 @@ describe(`OrbitDB Syncing with ${N_PEERS} peers`, () => { ...userContext, team: loadedTeam, } - const newUser = sigchain.users.getUserById(sigchain.user.userId) + const newUser = sigchain.users.getUserById(sigchain.user.userId, { includeRemoved: false, throwOnMissing: false }) expect(newUser).toBeDefined() expect(newUser!.keys.encryption).toBe(sigchain.context.user.keys.encryption.publicKey) diff --git a/packages/backend/src/nest/qss/qss.service.spec.ts b/packages/backend/src/nest/qss/qss.service.spec.ts index b1bd0229f0..883060e594 100644 --- a/packages/backend/src/nest/qss/qss.service.spec.ts +++ b/packages/backend/src/nest/qss/qss.service.spec.ts @@ -22,7 +22,7 @@ import { QSSEvents, } from './qss.types' import { createLogger } from '../common/logger' -import { Community, Identity, ChannelMessage, SocketActions, SocketEvents } from '@quiet/types' +import { Community, Identity, ChannelMessage, SocketActions, SocketEvents, PublicChannel } from '@quiet/types' import { getReduxStoreFactory, getBaseTypesFactory, prepareStore, Store } from '@quiet/state-manager' import { FactoryGirl } from 'factory-girl' import { DateTime } from 'luxon' @@ -1078,8 +1078,9 @@ describe('QSSService', () => { AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }), sync: true, }) + const channel = await baseFactory.create('PublicChannel') const channelMessage = await baseFactory.create('ChannelMessage') - const hash = await db.add(await publicMessagesService.onSend(channelMessage)) + const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel)) const entry = await db.log.get(hash) expect(hash).toBeDefined() expect(entry).toBeDefined() @@ -1323,8 +1324,9 @@ describe('QSSService', () => { AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }), sync: true, }) + const channel = await baseFactory.create('PublicChannel') const channelMessage = await baseFactory.create('ChannelMessage') - const hash = await db.add(await publicMessagesService.onSend(channelMessage)) + const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel)) expect(hash).toBeDefined() const entry = await db.log.get(hash) expect(entry).toBeDefined() @@ -1849,8 +1851,9 @@ describe('QSSService', () => { AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }), sync: true, }) + const channel = await baseFactory.create('PublicChannel') const channelMessage = await baseFactory.create('ChannelMessage') - const hash = await db.add(await publicMessagesService.onSend(channelMessage)) + const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel)) const entry = await db.log.get(hash) expect(hash).toBeDefined() expect(entry).toBeDefined() @@ -1879,8 +1882,9 @@ describe('QSSService', () => { AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }), sync: true, }) + const channel = await baseFactory.create('PublicChannel') const channelMessage = await baseFactory.create('ChannelMessage') - const hash = await db.add(await publicMessagesService.onSend(channelMessage)) + const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel)) const entry = await db.log.get(hash) expect(hash).toBeDefined() expect(entry).toBeDefined() @@ -1909,8 +1913,9 @@ describe('QSSService', () => { AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }), sync: true, }) + const channel = await baseFactory.create('PublicChannel') const channelMessage = await baseFactory.create('ChannelMessage') - const hash = await db.add(await publicMessagesService.onSend(channelMessage)) + const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel)) const entry = await db.log.get(hash) expect(hash).toBeDefined() expect(entry).toBeDefined() diff --git a/packages/backend/src/nest/storage/channels/channel.store.ts b/packages/backend/src/nest/storage/channels/channel.store.ts index ef4ded53f6..31006a901d 100644 --- a/packages/backend/src/nest/storage/channels/channel.store.ts +++ b/packages/backend/src/nest/storage/channels/channel.store.ts @@ -97,11 +97,15 @@ export class ChannelStore extends EventStoreBase { this.logger.info('Adding message to database') try { - const encryptedMessage = await this.messagesService.onSend(message) + const encryptedMessage = await this.messagesService.onSend(message, this.channelData) return await this.getStore().add(encryptedMessage) } catch (e) { throw new CompoundError(`Could not append message (entry not allowed to write to the log)`, e) @@ -328,7 +332,7 @@ export class ChannelStore extends EventStoreBase { public: false, teamId: community.teamId!, } - privateChannel.roleName = sigChainService.getActiveChain().channels.create(privateChannel.id) + privateChannel.roleName = sigChainService.getActiveChain().channels.create() await channelsService.createChannel(privateChannel) @@ -1049,7 +1049,7 @@ describe('ChannelsService', () => { public: false, teamId: community.teamId!, } - privateChannel.roleName = activeChain.channels.create(privateChannel.id) + privateChannel.roleName = activeChain.channels.create() const encryptedEntry = channelsService.encryptChannelEntry(privateChannel) await expectChannelEntryValidation( @@ -1071,7 +1071,7 @@ describe('ChannelsService', () => { public: false, teamId: community.teamId!, } - privateChannel.roleName = activeChain.channels.create(privateChannel.id) + privateChannel.roleName = activeChain.channels.create() const encryptedEntry = channelsService.encryptChannelEntry(privateChannel) await expectChannelEntryValidation( @@ -1140,7 +1140,7 @@ describe('ChannelsService', () => { public: false, teamId: community.teamId!, } - privateChannel.roleName = activeChain.channels.create(privateChannel.id) + privateChannel.roleName = activeChain.channels.create() const teamScopedEntry = malloryChain.crypto.encryptAndSign(privateChannel, { type: EncryptionScopeType.TEAM, }) @@ -1157,7 +1157,7 @@ describe('ChannelsService', () => { const activeChain = sigChainService.getActiveChain() const malloryChain = createNonAdminMemberChain('mallory') const privateChannelId = 'downgraded-private-channel-id' - activeChain.channels.create(privateChannelId) + activeChain.channels.create() const forgedPublicChannel: PublicChannel = { id: privateChannelId, diff --git a/packages/backend/src/nest/storage/channels/channels.service.ts b/packages/backend/src/nest/storage/channels/channels.service.ts index 48e9c36db5..9e24ab6e17 100644 --- a/packages/backend/src/nest/storage/channels/channels.service.ts +++ b/packages/backend/src/nest/storage/channels/channels.service.ts @@ -45,6 +45,7 @@ import { NotAMemberError } from './channels.errors' import { SigchainEvents } from '../../auth/types' import { ChannelMetadataAccessController } from './orbitdb/ChannelMetadataAccessController' import crypto from 'crypto' +import type { PrivateChannelMappings } from './channels.types' /** * Manages storage-level logic for all channels in Quiet @@ -281,9 +282,12 @@ export class ChannelsService extends EventEmitter { name: RoleName.MEMBER, } if (!(payload.public ?? true)) { + if (payload.roleName == null) { + throw new Error(`Private channel metadata entry had a nullish role name: ${payload.id}`) + } scope = { type: EncryptionScopeType.ROLE, - name: chain.channels.generateChannelRoleName(payload.id), + name: payload.roleName, } } const encryptedPayload = chain.crypto.encryptAndSign(payload, scope) @@ -410,19 +414,6 @@ export class ChannelsService extends EventEmitter { return false } - const expectedPrivateRoleName = chain.channels.generateChannelRoleName(decEntry.id) - if (chain.roles.getAllRoles().some(role => role.roleName === expectedPrivateRoleName)) { - this.logger.error( - 'Failed to validate public channel entry: channel id already has a private channel role:', - entry.hash, - { - channelId: decEntry.id, - roleName: expectedPrivateRoleName, - } - ) - return false - } - return this.validateChannelEncryptionScope(entry, encPayload, RoleName.MEMBER) } @@ -441,24 +432,27 @@ export class ChannelsService extends EventEmitter { } const chain = this.sigchainService.getActiveChain() - const expectedRoleName = chain.channels.generateChannelRoleName(decEntry.id) - if (decEntry.roleName !== expectedRoleName) { - this.logger.error('Failed to validate private channel entry: roleName must match channel id:', entry.hash, { + if (decEntry.roleName == null) { + this.logger.error('Private channel metadata entry had a nullish role name', entry.hash) + return false + } + + if (decEntry.roleName === RoleName.MEMBER || decEntry.roleName === RoleName.ADMIN) { + this.logger.error('Failed to validate private channel entry: roleName must not be MEMBER or ADMIN', entry.hash, { roleName: decEntry.roleName, - expectedRoleName, }) return false } - if (!chain.roles.memberHasRole(sigAuthor, expectedRoleName)) { + if (!chain.roles.memberHasRole(sigAuthor, decEntry.roleName)) { this.logger.error('Failed to validate private channel entry: signer must have the channel role:', entry.hash, { sigAuthor, - roleName: expectedRoleName, + roleName: decEntry.roleName, }) return false } - return this.validateChannelEncryptionScope(entry, encPayload, expectedRoleName) + return this.validateChannelEncryptionScope(entry, encPayload, decEntry.roleName) } private validateChannelEncryptionScope( @@ -856,15 +850,19 @@ export class ChannelsService extends EventEmitter { * @returns Map of private channels to their role names * @throws Error */ - public async getPrivateChannelsByRolename(): Promise> { + public async getPrivateChannelsByRolename(): Promise { if (!this.channels || !this.privateChannels) { throw new Error('Channels have not been initialized!') } const channels = await this.getChannels() - const channelMapping: { [channelRoleName: string]: PublicChannel } = {} + const channelMapping: PrivateChannelMappings = { + roleNameToChannel: {}, + idToRoleName: {}, + } channels.forEach((channel: PublicChannel) => { if (!(channel.public ?? true) && channel.roleName != null) { - channelMapping[channel.roleName] = channel + channelMapping.roleNameToChannel[channel.roleName] = channel + channelMapping.idToRoleName[channel.id] = channel.roleName } }) return channelMapping @@ -977,7 +975,7 @@ export class ChannelsService extends EventEmitter { if (!sigChain.channels.canICreatePrivateChannel()) { throw new Error('User is missing permissions to create private channels') } - roleName = sigChain.channels.create(channelData.id) + roleName = sigChain.channels.create() channelData.roleName = roleName } else { if (!sigChain.channels.canICreatePublicChannel()) { @@ -1170,13 +1168,18 @@ export class ChannelsService extends EventEmitter { return { channelId, status: AddMembersChannelStatus.INVALID_CHANNEL_TYPE } } - const isMemberOfChannel = this.sigchainService.activeChain.channels.amIMemberOfChannel(channelId) + if (channel.roleName == null) { + this.logger.error(`Channel is marked private but has a nullish rolename, can't add members!`) + return { channelId, status: AddMembersChannelStatus.MISSING_ROLE } + } + + const isMemberOfChannel = this.sigchainService.activeChain.channels.amIMemberOfChannel(channel.roleName) if (!isMemberOfChannel) { this.logger.error(`You are not a member of private channel ${channelId}, cannot add members!`) return { channelId, status: AddMembersChannelStatus.NOT_MEMBER } } - const canAddMembers = this.sigchainService.activeChain.channels.canIAddMembersToPrivateChannel(channelId) + const canAddMembers = this.sigchainService.activeChain.channels.canIAddMembersToPrivateChannel(channel.roleName) if (!canAddMembers) { this.logger.error(`You don't have permission to add members to private channel ${channelId}!`) return { channelId, status: AddMembersChannelStatus.NOT_PERMITTED } @@ -1191,11 +1194,11 @@ export class ChannelsService extends EventEmitter { this.logger.info(`Updating private channel membership`, channelId) for (const memberId of memberIds) { - if (this.sigchainService.activeChain.channels.memberInChannel(memberId, channelId)) { + if (this.sigchainService.activeChain.channels.memberInChannel(memberId, channel.roleName)) { this.logger.debug('User already in channel', memberId, channelId) continue } - this.sigchainService.activeChain.channels.addMember(memberId, channelId) + this.sigchainService.activeChain.channels.addMember(memberId, channel.roleName) } this.logger.info(`Private channel membership updated`, channelId) return { channelId, status: AddMembersChannelStatus.SUCCESS } diff --git a/packages/backend/src/nest/storage/channels/channels.types.ts b/packages/backend/src/nest/storage/channels/channels.types.ts index 0978b056f1..7a9b6e4d30 100644 --- a/packages/backend/src/nest/storage/channels/channels.types.ts +++ b/packages/backend/src/nest/storage/channels/channels.types.ts @@ -1,4 +1,11 @@ +import type { PublicChannel } from '@quiet/types' + export interface GetChannelsFilters { public: boolean private: boolean } + +export interface PrivateChannelMappings { + roleNameToChannel: { [roleName: string]: PublicChannel } + idToRoleName: { [channelId: string]: string } +} diff --git a/packages/backend/src/nest/storage/channels/messages/base-messages.service.ts b/packages/backend/src/nest/storage/channels/messages/base-messages.service.ts index e0177286e2..d2280434e0 100644 --- a/packages/backend/src/nest/storage/channels/messages/base-messages.service.ts +++ b/packages/backend/src/nest/storage/channels/messages/base-messages.service.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events' -import { ChannelMessage, ConsumedChannelMessage } from '@quiet/types' +import { ChannelMessage, ConsumedChannelMessage, type PublicChannel } from '@quiet/types' import { createLogger } from '../../../common/logger' import { SigChainService } from '../../../auth/sigchain.service' @@ -18,20 +18,25 @@ export class BaseMessagesService extends EventEmitter { /** * Handle processing of message to be added to OrbitDB and sent to peers * - * @param message Message to send + * @param rawMessage Message to send + * @param channel The metadata for the channel this message is being sent on * @returns Processed message */ - public async onSend(message: ChannelMessage): Promise { + public async onSend(rawMessage: ChannelMessage, channel: PublicChannel): Promise { throw new NotImplementedException('onSend is not implemented') } /** * Handle processing of message consumed from OrbitDB * - * @param message Message consumed from OrbitDB + * @param encryptedMessage Message consumed from OrbitDB + * @param channel The metadata for the channel this message is being received on * @returns Processed message if decryptable, undefined if undecryptable and false if intentionally skip decryption */ - public async onConsume(message: EncryptedMessage): Promise { + public async onConsume( + encryptedMessage: EncryptedMessage, + channel: PublicChannel + ): Promise { throw new NotImplementedException('onSend is not implemented') } @@ -44,9 +49,14 @@ export class BaseMessagesService extends EventEmitter { * * @param decryptedMessage Message to validate * @param encryptedMessage Encrypted message to validate against + * @param channel The metadata for the channel this message is being sent/received on * @returns True if the message is valid, false otherwise */ - public validateMessage(decryptedMessage: ConsumedChannelMessage, encryptedMessage: EncryptedMessage): boolean { + public validateMessage( + decryptedMessage: ConsumedChannelMessage, + encryptedMessage: EncryptedMessage, + channel: PublicChannel + ): boolean { if (decryptedMessage.id !== encryptedMessage.id) { this.logger.warn(`Cannot validate msg ${decryptedMessage.id}: IDs do not match`) return false @@ -55,6 +65,14 @@ export class BaseMessagesService extends EventEmitter { this.logger.warn(`Cannot validate msg ${decryptedMessage.id}: message shape is not valid`) return false } + if (!channel.public && channel.roleName == null) { + this.logger.warn(`Channel role name was nullish but channel is private`) + return false + } + if (!channel.public && channel.roleName !== encryptedMessage.contents.scope.name) { + this.logger.warn(`Channel role name didn't match the role name that encrypted the message`) + return false + } // ensure that the fields we write to the message unencrypted match the ones inside the encrypted blob const mismatchedFields: typeof SHARED_FIELDS = [] for (const sharedField of SHARED_FIELDS) { diff --git a/packages/backend/src/nest/storage/channels/messages/orbitdb/PrivateMessagesAccessController.ts b/packages/backend/src/nest/storage/channels/messages/orbitdb/PrivateMessagesAccessController.ts index e309160ec7..f8d32070a1 100644 --- a/packages/backend/src/nest/storage/channels/messages/orbitdb/PrivateMessagesAccessController.ts +++ b/packages/backend/src/nest/storage/channels/messages/orbitdb/PrivateMessagesAccessController.ts @@ -15,6 +15,7 @@ const TYPE = 'privatemessagesaccess' export interface PrivateAccessControllerConfig extends AccessControllerConfig { channelId: string teamId: string + roleName: string } @Injectable() @@ -67,7 +68,7 @@ export class PrivateMessagesAccessController extends BaseMessagesAccessControlle return false } - if (!sigchain.channels.memberInChannel(id, config.channelId)) { + if (!sigchain.channels.memberInChannel(id, config.roleName)) { this.logger.warn( `User is not a member of the channel, skipping log append`, id, @@ -77,6 +78,11 @@ export class PrivateMessagesAccessController extends BaseMessagesAccessControlle return false } + if (config.roleName !== entry.payload.value.contents.scope.name) { + this.logger.warn(`Message was encrypted to a different scope than the one configured on the channel`) + return false + } + return true } } diff --git a/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.spec.ts b/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.spec.ts index 605307b027..ed73f0045a 100644 --- a/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.spec.ts +++ b/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.spec.ts @@ -2,7 +2,7 @@ import { jest } from '@jest/globals' import { Test, TestingModule } from '@nestjs/testing' import { getBaseTypesFactory } from '@quiet/state-manager' -import { ChannelMessage } from '@quiet/types' +import { ChannelMessage, type PublicChannel } from '@quiet/types' import { FactoryGirl } from 'factory-girl' import { isUint8Array } from 'util/types' import { EncryptionScopeType } from '../../../auth/services/crypto/types' @@ -20,9 +20,11 @@ describe('PrivateChannelMessagesService', () => { let module: TestingModule let messagesService: PrivateChannelMessagesService let sigChainService: SigChainService + let channelRoleName: string let factory: FactoryGirl let message: ChannelMessage + let channel: PublicChannel let handleChainUpdateSpy: jest.SpiedFunction @@ -42,7 +44,12 @@ describe('PrivateChannelMessagesService', () => { sigChainService = await module.resolve(SigChainService) await sigChainService.createChain(true) message = await factory.create('ChannelMessage', { userId: sigChainService.getActiveChain().user.userId }) - sigChainService.activeChain.channels.create(message.channelId) + channelRoleName = sigChainService.activeChain.channels.create() + channel = await factory.create('PublicChannel', { + id: message.channelId, + public: false, + roleName: channelRoleName, + }) messagesService = await module.resolve(PrivateChannelMessagesService) handleChainUpdateSpy = jest.spyOn(sigChainService as any, 'handleChainUpdate').mockImplementation(() => { logger.debug('MOCK: handling chain update') @@ -56,7 +63,7 @@ describe('PrivateChannelMessagesService', () => { describe('onSend', () => { it('encrypts message correctly', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) expect(isEncryptedMessage(encryptedMessage)).toBeTruthy() expect(encryptedMessage).toEqual( expect.objectContaining({ @@ -69,7 +76,7 @@ describe('PrivateChannelMessagesService', () => { scope: { generation: 0, type: EncryptionScopeType.ROLE, - name: sigChainService.activeChain.channels.generateChannelRoleName(message.channelId), + name: expect.any(String), }, }), encSignature: expect.objectContaining({ @@ -88,8 +95,8 @@ describe('PrivateChannelMessagesService', () => { describe('onConsume', () => { it('decrypts an encrypted message correctly', async () => { - const encryptedMessage = await messagesService.onSend(message) - expect(await messagesService.onConsume(encryptedMessage)).toEqual({ + const encryptedMessage = await messagesService.onSend(message, channel) + expect(await messagesService.onConsume(encryptedMessage, channel)).toEqual({ ...message, verified: true, encSignature: encryptedMessage.encSignature, @@ -99,32 +106,32 @@ describe('PrivateChannelMessagesService', () => { // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched createdAt', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, createdAt: 1234, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched team ID', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, teamId: INVALID_FIELD_VALUE, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched channel ID', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, channelId: INVALID_FIELD_VALUE, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3334 @@ -133,12 +140,12 @@ describe('PrivateChannelMessagesService', () => { ...message, userId: INVALID_FIELD_VALUE, } - const encryptedMessage = await messagesService.onSend(messageWithBadUserId) - expect(await messagesService.onConsume(encryptedMessage)).toBeFalsy() + const encryptedMessage = await messagesService.onSend(messageWithBadUserId, channel) + expect(await messagesService.onConsume(encryptedMessage, channel)).toBeFalsy() }) it('returns undefined when the signature is invalid', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const invalidEncryptedMessage: EncryptedMessage = { ...encryptedMessage, encSignature: { @@ -151,7 +158,7 @@ describe('PrivateChannelMessagesService', () => { }, } - expect(await messagesService.onConsume(invalidEncryptedMessage)).toBeUndefined() + expect(await messagesService.onConsume(invalidEncryptedMessage, channel)).toBeUndefined() }) }) }) diff --git a/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.ts b/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.ts index 1af7d5c7f9..e36d4e7799 100644 --- a/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.ts +++ b/packages/backend/src/nest/storage/channels/messages/private-channel-messages.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common' -import { ChannelMessage, CompoundError, ConsumedChannelMessage } from '@quiet/types' +import { ChannelMessage, CompoundError, ConsumedChannelMessage, type PublicChannel } from '@quiet/types' import { createLogger } from '../../../common/logger' import { EncryptionScopeType } from '../../../auth/services/crypto/types' @@ -8,6 +8,7 @@ import { SigChainService } from '../../../auth/sigchain.service' import { EncryptableMessageComponents, EncryptedMessage } from './messages.types' import { BaseMessagesService } from './base-messages.service' import { SigChain } from '../../../auth/sigchain' +import { RoleName } from '../../../auth/services/roles/roles' @Injectable() export class PrivateChannelMessagesService extends BaseMessagesService { @@ -20,37 +21,38 @@ export class PrivateChannelMessagesService extends BaseMessagesService { /** * Handle processing of message to be added to OrbitDB and sent to peers * - * @param message Message to send + * @param rawMessage Message to send + * @param channel The metadata for the channel this message is being sent on * @returns Processed message */ - public async onSend(message: ChannelMessage): Promise { + public async onSend(rawMessage: ChannelMessage, channel: PublicChannel): Promise { this.logger.debug('Sending private channel message') - return this._encryptPrivateChannelMessage(message) + return this._encryptPrivateChannelMessage(rawMessage, channel) } /** * Handle processing of message consumed from OrbitDB * - * @param message Message consumed from OrbitDB + * @param encryptedMessage Message consumed from OrbitDB + * @param channel The metadata for the channel this message is being received on on * @returns Processed message if decryptable, undefined if undecryptable and false if intentionally skip decryption */ - public async onConsume(message: EncryptedMessage): Promise { + public async onConsume( + encryptedMessage: EncryptedMessage, + channel: PublicChannel + ): Promise { this.logger.debug('Received private channel message') - const chain = this.sigChainService.getChain(message.teamId, false) + const chain = this.sigChainService.getChain(encryptedMessage.teamId, false) if (chain == null) { this.logger.warn( - `Chain doesn't exist or hasn't been initialized, can't consume messages for ${message.channelId}` + `Chain doesn't exist or hasn't been initialized, can't consume messages for ${encryptedMessage.channelId}` ) return false } - if (!chain.channels.amIMemberOfChannel(message.channelId)) { - this.logger.warn(`Not a member of channel ${message.channelId} on team ${message.teamId}`) - return false - } try { - const decryptedMessage = this._decryptPrivateChannelMessage(message, chain) - if (!this.validateMessage(decryptedMessage, message)) { + const decryptedMessage = this._decryptPrivateChannelMessage(encryptedMessage, chain, channel) + if (!this.validateMessage(decryptedMessage, encryptedMessage, channel)) { return } return decryptedMessage @@ -60,8 +62,12 @@ export class PrivateChannelMessagesService extends BaseMessagesService { } } - private _encryptPrivateChannelMessage(rawMessage: ChannelMessage): EncryptedMessage { + private _encryptPrivateChannelMessage(rawMessage: ChannelMessage, channel: PublicChannel): EncryptedMessage { try { + if (channel.roleName == null) { + throw new Error('Private channel role name was nullish') + } + const chain = this.sigChainService.getActiveChain() const encryptable: EncryptableMessageComponents = { id: rawMessage.id, @@ -73,10 +79,9 @@ export class PrivateChannelMessagesService extends BaseMessagesService { teamId: chain.team!.id, createdAt: rawMessage.createdAt, } - const roleName = chain.channels.generateChannelRoleName(rawMessage.channelId) const encryptedMessage = chain.crypto.encryptAndSign(encryptable, { type: EncryptionScopeType.ROLE, - name: roleName, + name: channel.roleName, }) return { id: rawMessage.id, @@ -91,8 +96,23 @@ export class PrivateChannelMessagesService extends BaseMessagesService { } } - private _decryptPrivateChannelMessage(encryptedMessage: EncryptedMessage, chain: SigChain): ConsumedChannelMessage { + private _decryptPrivateChannelMessage( + encryptedMessage: EncryptedMessage, + chain: SigChain, + channel: PublicChannel + ): ConsumedChannelMessage { try { + if ( + encryptedMessage.contents.scope.name === RoleName.MEMBER || + encryptedMessage.contents.scope.name === RoleName.ADMIN + ) { + throw new Error('Private channel role name cannot be MEMBER or ADMIN') + } + + if (channel.roleName !== encryptedMessage.contents.scope.name) { + throw new Error('Private channel metadata role name did not match the role name on the message') + } + const decryptedMessage = chain.crypto.decryptAndVerify( encryptedMessage.contents, encryptedMessage.encSignature, diff --git a/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.spec.ts b/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.spec.ts index 1c25a2c2c4..de0e896794 100644 --- a/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.spec.ts +++ b/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.spec.ts @@ -2,7 +2,7 @@ import { jest } from '@jest/globals' import { Test, TestingModule } from '@nestjs/testing' import { getBaseTypesFactory } from '@quiet/state-manager' -import { ChannelMessage } from '@quiet/types' +import { ChannelMessage, type PublicChannel } from '@quiet/types' import { FactoryGirl } from 'factory-girl' import { isUint8Array } from 'util/types' import { EncryptionScopeType } from '../../../auth/services/crypto/types' @@ -24,6 +24,7 @@ describe('PublicChannelMessagesService', () => { let factory: FactoryGirl let message: ChannelMessage + let channel: PublicChannel const INVALID_FIELD_VALUE = 'THIS IS INVALID' @@ -40,13 +41,18 @@ describe('PublicChannelMessagesService', () => { sigChainService = await module.resolve(SigChainService) await sigChainService.createChain(true) - message = await factory.create('ChannelMessage', { userId: sigChainService.getActiveChain().user.userId }) + + channel = await factory.create('PublicChannel') + message = await factory.create('ChannelMessage', { + channelId: channel.id, + userId: sigChainService.getActiveChain().user.userId, + }) messagesService = await module.resolve(PublicChannelMessagesService) }) describe('onSend', () => { it('encrypts message correctly', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) expect(isEncryptedMessage(encryptedMessage)).toBeTruthy() expect(encryptedMessage).toEqual( expect.objectContaining({ @@ -78,8 +84,8 @@ describe('PublicChannelMessagesService', () => { describe('onConsume', () => { it('decrypts an encrypted message correctly', async () => { - const encryptedMessage = await messagesService.onSend(message) - expect(await messagesService.onConsume(encryptedMessage)).toEqual({ + const encryptedMessage = await messagesService.onSend(message, channel) + expect(await messagesService.onConsume(encryptedMessage, channel)).toEqual({ ...message, verified: true, encSignature: encryptedMessage.encSignature, @@ -89,32 +95,32 @@ describe('PublicChannelMessagesService', () => { // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched createdAt', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, createdAt: 1234, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched team ID', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, teamId: INVALID_FIELD_VALUE, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3304 it('fails to consume message with mismatched channel ID', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const mismatchedEncryptedMessage: EncryptedMessage = { ...encryptedMessage, channelId: INVALID_FIELD_VALUE, } - expect(await messagesService.onConsume(mismatchedEncryptedMessage)).toBeFalsy() + expect(await messagesService.onConsume(mismatchedEncryptedMessage, channel)).toBeFalsy() }) // https://github.com/TryQuiet/quiet/issues/3334 @@ -123,12 +129,12 @@ describe('PublicChannelMessagesService', () => { ...message, userId: INVALID_FIELD_VALUE, } - const encryptedMessage = await messagesService.onSend(messageWithBadUserId) - expect(await messagesService.onConsume(encryptedMessage)).toBeFalsy() + const encryptedMessage = await messagesService.onSend(messageWithBadUserId, channel) + expect(await messagesService.onConsume(encryptedMessage, channel)).toBeFalsy() }) it('returns undefined when the signature is invalid', async () => { - const encryptedMessage = await messagesService.onSend(message) + const encryptedMessage = await messagesService.onSend(message, channel) const invalidEncryptedMessage: EncryptedMessage = { ...encryptedMessage, encSignature: { @@ -141,7 +147,7 @@ describe('PublicChannelMessagesService', () => { }, } - expect(await messagesService.onConsume(invalidEncryptedMessage)).toBeUndefined() + expect(await messagesService.onConsume(invalidEncryptedMessage, channel)).toBeUndefined() }) }) }) diff --git a/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.ts b/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.ts index 23a8d0b23f..d8391cfe90 100644 --- a/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.ts +++ b/packages/backend/src/nest/storage/channels/messages/public-channel-messages.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common' -import { ChannelMessage, CompoundError, ConsumedChannelMessage } from '@quiet/types' +import { ChannelMessage, CompoundError, ConsumedChannelMessage, type PublicChannel } from '@quiet/types' import { createLogger } from '../../../common/logger' import { EncryptionScopeType } from '../../../auth/services/crypto/types' @@ -21,37 +21,50 @@ export class PublicChannelMessagesService extends BaseMessagesService { /** * Handle processing of message to be added to OrbitDB and sent to peers * - * @param message Message to send + * @param rawMessage Message to send + * @param channel The metadata for the channel this message is being sent on * @returns Processed message */ - public async onSend(message: ChannelMessage): Promise { + public async onSend(rawMessage: ChannelMessage, channel: PublicChannel): Promise { this.logger.debug('Sending public channel message') - return this._encryptPublicChannelMessage(message) + if (channel.roleName != null) { + this.logger.warn('onSend: Public channel metadata contained a non-null role name') + } + return this._encryptPublicChannelMessage(rawMessage) } /** * Handle processing of message consumed from OrbitDB * - * @param message Message consumed from OrbitDB + * @param encryptedMessage Message consumed from OrbitDB + * @param channel The metadata for the channel this message is being received on * @returns Processed message if decryptable, undefined if undecryptable and false if intentionally skip decryption */ - public async onConsume(message: EncryptedMessage): Promise { + public async onConsume( + encryptedMessage: EncryptedMessage, + channel: PublicChannel + ): Promise { this.logger.debug('Received public channel message') - const chain = this.sigChainService.getChain(message.teamId, false) + const chain = this.sigChainService.getChain(encryptedMessage.teamId, false) if (chain == null) { this.logger.warn( - `Chain doesn't exist or hasn't been initialized, can't consume messages for ${message.channelId}` + `onConsume: Chain doesn't exist or hasn't been initialized, can't consume messages for ${encryptedMessage.channelId}` ) return false } if (!chain.roles.amIMemberOfRole(RoleName.MEMBER)) { - this.logger.warn(`Not a member of team ${message.teamId}, can't consume messages for ${message.channelId}`) + this.logger.warn( + `onConsume: Not a member of team ${encryptedMessage.teamId}, can't consume messages for ${encryptedMessage.channelId}` + ) return false } + if (channel.roleName != null) { + this.logger.warn('onConsume: Public channel metadata contained a non-null role name') + } try { - const decryptedMessage = this._decryptPublicChannelMessage(message, chain) - if (!this.validateMessage(decryptedMessage, message)) { + const decryptedMessage = this._decryptPublicChannelMessage(encryptedMessage, chain) + if (!this.validateMessage(decryptedMessage, encryptedMessage, channel)) { return } return decryptedMessage diff --git a/packages/backend/src/nest/storage/orbitDb/identity/lfa/lfa-identity.provider.ts b/packages/backend/src/nest/storage/orbitDb/identity/lfa/lfa-identity.provider.ts index c83b6ab054..cf7e9ed2cd 100644 --- a/packages/backend/src/nest/storage/orbitDb/identity/lfa/lfa-identity.provider.ts +++ b/packages/backend/src/nest/storage/orbitDb/identity/lfa/lfa-identity.provider.ts @@ -115,7 +115,7 @@ class LFAIdentityProvider implements IdentityProvider { const sigchain = this.sigchainService.getChain(teamId, true) const user = sigchain.users.getUserById(userId, { includeRemoved, throwOnMissing: true }) return { - user: user!, + user, sigchain, } } diff --git a/packages/types/src/channel.ts b/packages/types/src/channel.ts index 7babd68838..b14de9f5ab 100644 --- a/packages/types/src/channel.ts +++ b/packages/types/src/channel.ts @@ -197,6 +197,7 @@ export enum AddMembersChannelStatus { NOT_ADMIN = 'NOT_ADMIN', NOT_CHANNEL_OWNER = 'NOT_CHANNEL_OWNER', NOT_PERMITTED = 'NOT_PERMITTED', + MISSING_ROLE = 'MISSING_ROLE', } export interface AddMembersChannelResponse {