Skip to content

Commit

Permalink
feat(2631): Encrypt all message contents and reduce size of encryption (
Browse files Browse the repository at this point in the history
#2749)

* Encrypt all message contents and reduce size of encryption

* Sign raw message contents

* We don't need verifyMessage anymore
  • Loading branch information
islathehut authored Feb 19, 2025
1 parent 13e231a commit 87fdb68
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 154 deletions.
32 changes: 22 additions & 10 deletions packages/backend/src/nest/auth/services/crypto/crypto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EncryptedPayload,
EncryptionScope,
EncryptionScopeType,
Signature,
} from './types'
import { ChainServiceBase } from '../chainServiceBase'
import { SigChain } from '../../sigchain'
Expand Down Expand Up @@ -61,14 +62,17 @@ class CryptoService extends ChainServiceBase {
throw new Error(`Unknown encryption type ${scope.type} provided!`)
}

const signature = this.sigChain.team!.sign(encryptedPayload.contents)
const signature = this.sigChain.team!.sign(message)

return {
encrypted: encryptedPayload,
signature,
signature: {
author: signature.author,
signature: signature.signature,
},
ts: Date.now(),
username: context.user.userName,
}
} as EncryptedAndSignedPayload
}

private symEncrypt(message: any, scope: EncryptionScope): EncryptedPayload {
Expand Down Expand Up @@ -113,15 +117,10 @@ class CryptoService extends ChainServiceBase {

public decryptAndVerify<T>(
encrypted: EncryptedPayload,
signature: SignedEnvelope,
signature: Signature,
context: LocalUserContext,
failOnInvalid = true
): DecryptedPayload<T> {
const isValid = this.verifyMessage(signature)
if (!isValid && failOnInvalid) {
throw new Error(`Couldn't verify signature on message`)
}

let contents: T
switch (encrypted.scope.type) {
// Symmetrical Encryption Types
Expand All @@ -139,6 +138,15 @@ class CryptoService extends ChainServiceBase {
throw new Error(`Unknown encryption scope type ${encrypted.scope.type}`)
}

const fullSig: SignedEnvelope = {
...signature,
contents,
}
const isValid = this.verifyMessage(fullSig)
if (!isValid && failOnInvalid) {
throw new Error(`Couldn't verify signature on message`)
}

return {
contents,
isValid,
Expand All @@ -164,7 +172,11 @@ class CryptoService extends ChainServiceBase {
}) as T
}

private asymUserDecrypt<T>(encrypted: EncryptedPayload, signature: SignedEnvelope, context: LocalUserContext): T {
private asymUserDecrypt<T>(
encrypted: EncryptedPayload,
signature: Signature | SignedEnvelope,
context: LocalUserContext
): T {
if (encrypted.scope.name == null) {
throw new Error(`Must provide a user ID when encryption scope is set to ${encrypted.scope.type}`)
}
Expand Down
10 changes: 8 additions & 2 deletions packages/backend/src/nest/auth/services/crypto/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Base58, SignedEnvelope } from '@localfirst/auth'
import { KeyMetadata } from '3rd-party/auth/packages/crdx/dist'
import { Base58 } from '@localfirst/auth'

export enum EncryptionScopeType {
ROLE = 'ROLE',
Expand All @@ -23,7 +24,7 @@ export type EncryptedPayload = {

export type EncryptedAndSignedPayload = {
encrypted: EncryptedPayload
signature: SignedEnvelope
signature: Signature
ts: number
username: string
}
Expand All @@ -32,3 +33,8 @@ export type DecryptedPayload<T> = {
contents: T
isValid: boolean
}

export type Signature = {
signature: Base58
author: KeyMetadata
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class ChannelStore extends EventStoreBase<EncryptedMessage, ConsumedChann
{
type: 'events',
Database: EventsWithStorage(),
AccessController: MessagesAccessController({ write: ['*'], messagesService: this.messagesService }),
AccessController: MessagesAccessController({ write: ['*'] }),
sync: options.sync,
}
)
Expand Down
27 changes: 12 additions & 15 deletions packages/backend/src/nest/storage/channels/channels.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,18 @@ describe('ChannelsService', () => {
expect(eventSpy).toHaveBeenCalled()
const savedMessages = await channelsService.getMessages(channelio.id)
expect(savedMessages?.messages.length).toBe(1)
expect(savedMessages?.messages[0]).toEqual(
expect.objectContaining({
...messageCopy,
verified: true,
encSignature: expect.objectContaining({
author: {
generation: 0,
type: 'USER',
name: sigChainService.getActiveChain().localUserContext.user.userId,
},
contents: expect.any(Uint8Array),
signature: expect.any(String),
}),
})
)
expect(savedMessages?.messages[0]).toEqual({
...messageCopy,
verified: true,
encSignature: expect.objectContaining({
author: {
generation: 0,
type: 'USER',
name: sigChainService.getActiveChain().localUserContext.user.userId,
},
signature: expect.any(String),
}),
})
})

// TODO: figure out a good way to spoof the signature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Store,
} from '@quiet/state-manager'
import { ChannelMessage, Community, Identity, PublicChannel, TestMessage } from '@quiet/types'
import { isBase58 } from 'class-validator'
import { FactoryGirl } from 'factory-girl'
import { isUint8Array } from 'util/types'
import { EncryptionScopeType } from '../../../auth/services/crypto/types'
Expand Down Expand Up @@ -62,50 +61,33 @@ describe('MessagesService', () => {
messagesService = await module.resolve(MessagesService)
})

describe('verifyMessage', () => {
it('message with valid signature is verified', async () => {
const encryptedMessage = await messagesService.onSend(message)
expect(messagesService.verifyMessage(encryptedMessage)).toBeTruthy()
})

it('message with invalid signature is not verified', async () => {
const encryptedMessage = await messagesService.onSend(message)
let err: Error | undefined = undefined
try {
messagesService.verifyMessage({
...encryptedMessage,
encSignature: {
...encryptedMessage.encSignature,
author: {
generation: 1,
name: 'foobar',
type: '',
},
},
})
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
})

describe('onSend', () => {
it('encrypts message correctly', async () => {
const encryptedMessage = await messagesService.onSend(message)
expect(encryptedMessage).toEqual(
expect.objectContaining({
...message,
message: expect.objectContaining({
id: message.id,
createdAt: message.createdAt,
channelId: message.channelId,
contents: expect.objectContaining({
contents: expect.any(Uint8Array),
scope: {
generation: 0,
type: EncryptionScopeType.ROLE,
name: RoleName.MEMBER,
},
}),
encSignature: expect.objectContaining({
author: expect.objectContaining({
generation: 0,
type: EncryptionScopeType.USER,
name: sigChainService.getActiveChain().localUserContext.user.userId,
}),
signature: expect.any(String),
}),
})
)
expect(isUint8Array(encryptedMessage.message.contents)).toBeTruthy()
expect(isUint8Array(encryptedMessage.contents.contents)).toBeTruthy()
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,16 @@
import { Injectable } from '@nestjs/common'
import { stringToArrayBuffer } from 'pvutils'
import EventEmitter from 'events'
import { getCrypto, ICryptoEngine } from 'pkijs'

import { keyObjectFromString, verifySignature } from '@quiet/identity'
import {
ChannelMessage,
CompoundError,
ConsumedChannelMessage,
EncryptionSignature,
NoCryptoEngineError,
} from '@quiet/types'
import { ChannelMessage, CompoundError, ConsumedChannelMessage } from '@quiet/types'

import { createLogger } from '../../../common/logger'
import { EncryptionScopeType } from '../../../auth/services/crypto/types'
import { SigChainService } from '../../../auth/sigchain.service'
import { EncryptedMessage } from './messages.types'
import { SignedEnvelope } from '3rd-party/auth/packages/auth/dist'
import { EncryptableMessageComponents, EncryptedMessage } from './messages.types'
import { RoleName } from '../../../auth/services/roles/roles'

@Injectable()
export class MessagesService extends EventEmitter {
/**
* Map of signing keys used on messages
*
* Maps public key string -> CryptoKey
*/
private publicKeysMap: Map<string, CryptoKey> = new Map()

private readonly logger = createLogger(`storage:channels:messagesService`)

constructor(private readonly sigChainService: SigChainService) {
Expand Down Expand Up @@ -59,33 +42,27 @@ export class MessagesService extends EventEmitter {
}
}

/**
* Verify encryption signature on message
*
* @param message Message to verify
* @returns True if message is valid
*/
public verifyMessage(message: EncryptedMessage): boolean {
try {
const chain = this.sigChainService.getActiveChain()
return chain.crypto.verifyMessage(message.encSignature)
} catch (e) {
throw new CompoundError(`Failed to verify message signature`, e)
}
}

private _encryptPublicChannelMessage(rawMessage: ChannelMessage): EncryptedMessage {
try {
const chain = this.sigChainService.getActiveChain()
const encryptable: EncryptableMessageComponents = {
type: rawMessage.type,
message: rawMessage.message,
signature: rawMessage.signature,
pubKey: rawMessage.pubKey,
media: rawMessage.media,
}
const encryptedMessage = chain.crypto.encryptAndSign(
rawMessage.message,
encryptable,
{ type: EncryptionScopeType.ROLE, name: RoleName.MEMBER },
chain.localUserContext
)
return {
...rawMessage,
id: rawMessage.id,
channelId: rawMessage.channelId,
createdAt: rawMessage.createdAt,
encSignature: encryptedMessage.signature,
message: encryptedMessage.encrypted,
contents: encryptedMessage.encrypted,
}
} catch (e) {
throw new CompoundError(`Failed to encrypt message with error`, e)
Expand All @@ -95,43 +72,22 @@ export class MessagesService extends EventEmitter {
private _decryptPublicChannelMessage(encryptedMessage: EncryptedMessage): ConsumedChannelMessage {
try {
const chain = this.sigChainService.getActiveChain()
const decryptedMessage = chain.crypto.decryptAndVerify<string>(
encryptedMessage.message,
const decryptedMessage = chain.crypto.decryptAndVerify<EncryptableMessageComponents>(
encryptedMessage.contents,
encryptedMessage.encSignature,
chain.localUserContext,
false
)
return {
...encryptedMessage,
message: decryptedMessage.contents,
...decryptedMessage.contents,
id: encryptedMessage.id,
channelId: encryptedMessage.channelId,
createdAt: encryptedMessage.createdAt,
encSignature: encryptedMessage.encSignature,
verified: decryptedMessage.isValid,
}
} catch (e) {
throw new CompoundError(`Failed to decrypt message with error`, e)
}
}

/**
* Get crypto engine that was initialized previously
*
* @returns Crypto engine
* @throws NoCryptoEngineError
*/
private getCrypto(): ICryptoEngine {
const crypto = getCrypto()
if (crypto == null) {
throw new NoCryptoEngineError()
}

return crypto
}

/**
* Clean service
*
* NOTE: Does NOT affect data stored in IPFS
*/
public async clean(): Promise<void> {
this.publicKeysMap = new Map()
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { SignedEnvelope } from '3rd-party/auth/packages/auth/dist'
import { EncryptedPayload } from '../../../auth/services/crypto/types'

import { FileMetadata } from '@quiet/types'
import { EncryptedPayload, Signature } from '../../../auth/services/crypto/types'

export interface EncryptedMessage {
id: string
export interface EncryptableMessageComponents {
type: number
message: EncryptedPayload
createdAt: number
channelId: string
encSignature: SignedEnvelope
message: string
signature: string
pubKey: string
media?: FileMetadata
}

export interface EncryptedMessage {
id: string
contents: EncryptedPayload
createdAt: number
channelId: string
encSignature: Signature
}
Loading

0 comments on commit 87fdb68

Please sign in to comment.