Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions packages/backend/src/nest/storage/channels/channels.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { jest } from '@jest/globals'

import { Test, TestingModule } from '@nestjs/testing'
import { getBaseTypesFactory } from '@quiet/state-manager'
import { generateDmChannelId } from '@quiet/common'
import {
ChannelMessage,
ChannelType,
Community,
DeleteChannelResponse,
FileMetadata,
Expand All @@ -14,6 +16,7 @@ import {

import path from 'path'
import { type PeerId } from '@libp2p/interface'
import { type LogEntry } from '@orbitdb/core'
import waitForExpect from 'wait-for-expect'
import { TestModule } from '../../common/test.module'
import { createArbitraryFile, libp2pInstanceParams } from '../../common/utils'
Expand All @@ -33,6 +36,11 @@ import { createLogger } from '../../common/logger'
import { ChannelsService } from './channels.service'
import { SigChainService } from '../../auth/sigchain.service'
import { CID } from 'multiformats/cid'
import { SigChain } from '../../auth/sigchain'
import { RoleName } from '../../auth/services/roles/roles'
import { EncryptedAndSignedPayload, EncryptionScopeType } from '../../auth/services/crypto/types'
import { InviteService } from '../../auth/services/invites/invite.service'
import { UserService } from '../../auth/services/members/user.service'

const logger = createLogger('channelsService:test')

Expand Down Expand Up @@ -107,6 +115,51 @@ describe('ChannelsService', () => {
})
})

const createNonAdminMemberChain = (username: string): SigChain => {
const adminChain = sigChainService.getActiveChain()
const invite = adminChain.invites.createUserInvite()
const salt = `${username}-metadata-validation-salt`

adminChain.lockbox.createInviteLockboxes(invite.seed, salt, RoleName.MEMBER)

const invitedChain = SigChain.createFromInvite(username, invite.seed)
adminChain.invites.admitMemberFromInvite(
InviteService.generateProof(invite.seed),
invitedChain.user.userName,
invitedChain.user.userId,
UserService.redactUser(invitedChain.user).keys
)

const joinedChain = SigChain.joinForTesting(
{
user: invitedChain.user,
device: invitedChain.device,
},
adminChain.save(),
adminChain.team!.teamKeyring()
)
joinedChain.roles.addSelf(RoleName.MEMBER, invite.seed, salt)

expect(joinedChain.roles.amIAdmin()).toBe(false)
expect(joinedChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true)

return joinedChain
}

const channelPutEntry = (
key: string,
value: EncryptedAndSignedPayload,
hash: string = 'test-channel-metadata-entry'
): LogEntry<EncryptedAndSignedPayload> =>
({
hash,
payload: {
op: 'PUT',
key,
value,
},
}) as unknown as LogEntry<EncryptedAndSignedPayload>

afterEach(async () => {
await storageService.stop()
await libp2pService.close()
Expand Down Expand Up @@ -305,6 +358,170 @@ describe('ChannelsService', () => {
})
})

describe('Channel metadata validation', () => {
it('accepts legitimate public channel metadata encrypted to the member role', async () => {
const publicChannel = await factory.build<PublicChannel>('PublicChannel', {
owner: aliceUserId,
teamId: community.teamId!,
type: ChannelType.CHANNEL,
})
const encryptedEntry = channelsService.encryptChannelEntry(publicChannel)

await expect(channelsService.validateEntry(channelPutEntry(publicChannel.id, encryptedEntry))).resolves.toBe(true)
})

it('accepts legitimate DM metadata encrypted to the deterministic DM role', async () => {
const bobChain = createNonAdminMemberChain('bob')
const activeChain = sigChainService.getActiveChain()
const dmMemberIds = [aliceUserId, bobChain.user.userId].sort()
const legitimateDmRoleName = activeChain.dms.createWithMembers(dmMemberIds)
const dmChannelId = generateDmChannelId(dmMemberIds)

const dmChannel: PublicChannel = {
id: dmChannelId,
name: 'alice, bob',
description: 'legitimate DM metadata',
owner: aliceUserId,
timestamp: Date.now(),
public: false,
roleName: legitimateDmRoleName,
type: ChannelType.DM,
memberIds: dmMemberIds,
teamId: community.teamId!,
}
const encryptedEntry = channelsService.encryptChannelEntry(dmChannel)

await expect(channelsService.validateEntry(channelPutEntry(dmChannel.id, encryptedEntry))).resolves.toBe(true)
})

it('rejects forged DM metadata encrypted to the broad member role', async () => {
const bobChain = createNonAdminMemberChain('bob')
const malloryChain = createNonAdminMemberChain('mallory')
const activeChain = sigChainService.getActiveChain()
const dmMemberIds = [aliceUserId, bobChain.user.userId].sort()
activeChain.dms.createWithMembers(dmMemberIds)
const dmChannelId = generateDmChannelId(dmMemberIds)

const forgedDmChannel: PublicChannel = {
id: dmChannelId,
name: 'alice, bob',
description: 'forged DM metadata',
owner: malloryChain.user.userId,
timestamp: Date.now(),
public: false,
roleName: RoleName.MEMBER,
type: ChannelType.DM,
memberIds: dmMemberIds,
teamId: community.teamId!,
}
const forgedEntry = malloryChain.crypto.encryptAndSign(forgedDmChannel, {
type: EncryptionScopeType.ROLE,
name: RoleName.MEMBER,
})

await expect(
channelsService.validateEntry(channelPutEntry(forgedDmChannel.id, forgedEntry, 'forged-dm-metadata'))
).resolves.toBe(false)
})

it('rejects forged private channel metadata encrypted to the broad member role', async () => {
const malloryChain = createNonAdminMemberChain('mallory')
const forgedPrivateChannel: PublicChannel = {
id: 'private-channel-id',
name: 'private-channel',
description: 'forged private channel metadata',
owner: malloryChain.user.userId,
timestamp: Date.now(),
public: false,
roleName: RoleName.MEMBER,
type: ChannelType.CHANNEL,
teamId: community.teamId!,
}
const forgedEntry = malloryChain.crypto.encryptAndSign(forgedPrivateChannel, {
type: EncryptionScopeType.ROLE,
name: RoleName.MEMBER,
})

await expect(
channelsService.validateEntry(
channelPutEntry(forgedPrivateChannel.id, forgedEntry, 'forged-private-channel-metadata')
)
).resolves.toBe(false)
})

it('accepts legitimate private channel metadata encrypted to the channel role', async () => {
const activeChain = sigChainService.getActiveChain()
const privateChannel: PublicChannel = {
id: 'legitimate-private-channel-id',
name: 'legitimate-private-channel',
description: 'legitimate private channel metadata',
owner: aliceUserId,
timestamp: Date.now(),
public: false,
type: ChannelType.CHANNEL,
teamId: community.teamId!,
}
privateChannel.roleName = activeChain.channels.create(privateChannel.id)
const encryptedEntry = channelsService.encryptChannelEntry(privateChannel)

await expect(channelsService.validateEntry(channelPutEntry(privateChannel.id, encryptedEntry))).resolves.toBe(
true
)
})

it('rejects private channel metadata encrypted outside the channel role', async () => {
const activeChain = sigChainService.getActiveChain()
const privateChannel: PublicChannel = {
id: 'team-scoped-private-channel-id',
name: 'team-scoped-private-channel',
description: 'private channel metadata encrypted to team scope',
owner: aliceUserId,
timestamp: Date.now(),
public: false,
type: ChannelType.CHANNEL,
teamId: community.teamId!,
}
privateChannel.roleName = activeChain.channels.create(privateChannel.id)
const teamScopedEntry = activeChain.crypto.encryptAndSign(privateChannel, {
type: EncryptionScopeType.TEAM,
})

await expect(
channelsService.validateEntry(
channelPutEntry(privateChannel.id, teamScopedEntry, 'team-scoped-private-channel-metadata')
)
).resolves.toBe(false)
})

it('rejects public metadata forged for an existing private channel id', async () => {
const activeChain = sigChainService.getActiveChain()
const malloryChain = createNonAdminMemberChain('mallory')
const privateChannelId = 'downgraded-private-channel-id'
activeChain.channels.create(privateChannelId)

const forgedPublicChannel: PublicChannel = {
id: privateChannelId,
name: 'downgraded-private-channel',
description: 'forged public metadata for a private channel',
owner: malloryChain.user.userId,
timestamp: Date.now(),
public: true,
type: ChannelType.CHANNEL,
teamId: community.teamId!,
}
const forgedEntry = malloryChain.crypto.encryptAndSign(forgedPublicChannel, {
type: EncryptionScopeType.ROLE,
name: RoleName.MEMBER,
})

await expect(
channelsService.validateEntry(
channelPutEntry(forgedPublicChannel.id, forgedEntry, 'downgraded-private-channel-metadata')
)
).resolves.toBe(false)
})
})

describe('Files deletion', () => {
let realFilePath: string
let messages: {
Expand Down
Loading
Loading