add ALTCHA verification guard for uploads.
Verify body.altcha when ALTCHA_ENABLED; export AltchaGuard only. Drop unused StorageModule export.
This commit is contained in:
@@ -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 {}
|
||||
|
||||
@@ -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<boolean> {
|
||||
const { enabled } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
if (!enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<boolean> {
|
||||
const { hmacKey } = this.configService.get('altcha') as Config['altcha'];
|
||||
|
||||
return verifySolution(altcha, hmacKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
|
||||
import { StorageService } from './services/StorageService';
|
||||
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
exports: [StorageService]
|
||||
providers: [StorageService]
|
||||
})
|
||||
export class StorageModule {}
|
||||
|
||||
Reference in New Issue
Block a user