From 84f8a852149d24074c9da71630a2da8ee78d3106 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Tue, 6 Oct 2026 15:04:31 +0200 Subject: [PATCH] add ALTCHA verification guard for uploads. Verify body.altcha when ALTCHA_ENABLED; export AltchaGuard only. Drop unused StorageModule export. --- backend/src/modules/altcha/AltchaModule.ts | 5 ++- .../src/modules/altcha/guards/AltchaGuard.ts | 36 +++++++++++++++++ .../altcha/services/AltchaService.spec.ts | 39 +++++++++++++++++++ .../modules/altcha/services/AltchaService.ts | 8 +++- backend/src/modules/storage/StorageModule.ts | 3 +- 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 backend/src/modules/altcha/guards/AltchaGuard.ts create mode 100644 backend/src/modules/altcha/services/AltchaService.spec.ts diff --git a/backend/src/modules/altcha/AltchaModule.ts b/backend/src/modules/altcha/AltchaModule.ts index b2744b5..0037b45 100644 --- a/backend/src/modules/altcha/AltchaModule.ts +++ b/backend/src/modules/altcha/AltchaModule.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { AltchaController } from './controllers/AltchaController'; +import { AltchaGuard } from './guards/AltchaGuard'; import { AltchaService } from './services/AltchaService'; @Module({ controllers: [AltchaController], - providers: [AltchaService], - exports: [AltchaService] + providers: [AltchaService, AltchaGuard], + exports: [AltchaGuard] }) export class AltchaModule {} diff --git a/backend/src/modules/altcha/guards/AltchaGuard.ts b/backend/src/modules/altcha/guards/AltchaGuard.ts new file mode 100644 index 0000000..d213756 --- /dev/null +++ b/backend/src/modules/altcha/guards/AltchaGuard.ts @@ -0,0 +1,36 @@ +import { BadRequestException, CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import { AltchaService } from '../services/AltchaService'; +import { Config } from '../../../types/Config'; + +@Injectable() +export class AltchaGuard implements CanActivate { + constructor( + private readonly altchaService: AltchaService, + private readonly configService: ConfigService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const { enabled } = this.configService.get('altcha') as Config['altcha']; + + if (!enabled) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const altcha = request.body?.altcha; + + if (typeof altcha !== 'string' || altcha.length === 0) { + throw new BadRequestException('ALTCHA verification required'); + } + + const verified = await this.altchaService.verifyPayload(altcha); + + if (!verified) { + throw new BadRequestException('ALTCHA verification failed'); + } + + return true; + } +} diff --git a/backend/src/modules/altcha/services/AltchaService.spec.ts b/backend/src/modules/altcha/services/AltchaService.spec.ts new file mode 100644 index 0000000..de6c458 --- /dev/null +++ b/backend/src/modules/altcha/services/AltchaService.spec.ts @@ -0,0 +1,39 @@ +import { ConfigService } from '@nestjs/config'; +import { createChallenge, solveChallenge } from 'altcha-lib/v1'; +import type { Payload } from 'altcha-lib/v1/types'; +import { AltchaService } from './AltchaService'; + +const hmacKey = 'test-hmac-key'; + +const encodePayload = (payload: Payload): string => + Buffer.from(JSON.stringify(payload), 'utf8').toString('base64'); + +describe('AltchaService', () => { + const configService = { + get: jest.fn(() => ({ enabled: true, hmacKey, maxNumber: 100_000 })) + } as unknown as ConfigService; + + const service = new AltchaService(configService); + + it('verifies a valid ALTCHA payload', async () => { + const challenge = await createChallenge({ hmacKey, number: 42, maxNumber: 100_000 }); + const { promise } = solveChallenge(challenge.challenge, challenge.salt, challenge.algorithm, 100_000); + const solution = await promise; + + expect(solution).not.toBeNull(); + + const payload: Payload = { + algorithm: challenge.algorithm, + challenge: challenge.challenge, + number: solution!.number, + salt: challenge.salt, + signature: challenge.signature + }; + + await expect(service.verifyPayload(encodePayload(payload))).resolves.toBe(true); + }); + + it('rejects an invalid ALTCHA payload', async () => { + await expect(service.verifyPayload('not-valid-base64-json')).resolves.toBe(false); + }); +}); diff --git a/backend/src/modules/altcha/services/AltchaService.ts b/backend/src/modules/altcha/services/AltchaService.ts index 1266fd9..137cafb 100644 --- a/backend/src/modules/altcha/services/AltchaService.ts +++ b/backend/src/modules/altcha/services/AltchaService.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { createChallenge } from 'altcha-lib/v1'; +import { createChallenge, verifySolution } from 'altcha-lib/v1'; import type { Challenge } from 'altcha-lib/v1/types'; import { Config } from '../../../types/Config'; @@ -16,4 +16,10 @@ export class AltchaService { maxNumber }); } + + async verifyPayload(altcha: string): Promise { + const { hmacKey } = this.configService.get('altcha') as Config['altcha']; + + return verifySolution(altcha, hmacKey); + } } diff --git a/backend/src/modules/storage/StorageModule.ts b/backend/src/modules/storage/StorageModule.ts index 1ea11c8..9497dcd 100644 --- a/backend/src/modules/storage/StorageModule.ts +++ b/backend/src/modules/storage/StorageModule.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { StorageService } from './services/StorageService'; @Module({ - providers: [StorageService], - exports: [StorageService] + providers: [StorageService] }) export class StorageModule {}