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 { Module } from '@nestjs/common';
|
||||||
import { AltchaController } from './controllers/AltchaController';
|
import { AltchaController } from './controllers/AltchaController';
|
||||||
|
import { AltchaGuard } from './guards/AltchaGuard';
|
||||||
import { AltchaService } from './services/AltchaService';
|
import { AltchaService } from './services/AltchaService';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [AltchaController],
|
controllers: [AltchaController],
|
||||||
providers: [AltchaService],
|
providers: [AltchaService, AltchaGuard],
|
||||||
exports: [AltchaService]
|
exports: [AltchaGuard]
|
||||||
})
|
})
|
||||||
export class AltchaModule {}
|
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 { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
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 type { Challenge } from 'altcha-lib/v1/types';
|
||||||
import { Config } from '../../../types/Config';
|
import { Config } from '../../../types/Config';
|
||||||
|
|
||||||
@@ -16,4 +16,10 @@ export class AltchaService {
|
|||||||
maxNumber
|
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';
|
import { StorageService } from './services/StorageService';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [StorageService],
|
providers: [StorageService]
|
||||||
exports: [StorageService]
|
|
||||||
})
|
})
|
||||||
export class StorageModule {}
|
export class StorageModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user