From 810acd8ed4817e943e4e9d56ecc51abcbe9032c9 Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:09:44 +0100 Subject: [PATCH] feat: implement CryptoModule with comprehensive cryptographic utilities and testing Added CryptoModule providing services for Argon2 hashing, JWT handling, JWE encryption/decryption, JWS signing/verification, and post-quantum cryptography (ML-KEM). Includes extensive unit tests for all features. --- backend/src/crypto/crypto.module.ts | 8 ++ backend/src/crypto/crypto.service.spec.ts | 117 ++++++++++++++++++++ backend/src/crypto/crypto.service.ts | 126 ++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 backend/src/crypto/crypto.module.ts create mode 100644 backend/src/crypto/crypto.service.spec.ts create mode 100644 backend/src/crypto/crypto.service.ts diff --git a/backend/src/crypto/crypto.module.ts b/backend/src/crypto/crypto.module.ts new file mode 100644 index 0000000..a17d37c --- /dev/null +++ b/backend/src/crypto/crypto.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { CryptoService } from "./crypto.service"; + +@Module({ + providers: [CryptoService], + exports: [CryptoService], +}) +export class CryptoModule {} diff --git a/backend/src/crypto/crypto.service.spec.ts b/backend/src/crypto/crypto.service.spec.ts new file mode 100644 index 0000000..d1d562e --- /dev/null +++ b/backend/src/crypto/crypto.service.spec.ts @@ -0,0 +1,117 @@ +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { CryptoService } from "./crypto.service"; + +describe("CryptoService", () => { + let service: CryptoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CryptoService, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue("test-secret"), + }, + }, + ], + }).compile(); + + service = module.get(CryptoService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("Argon2 Password Hashing", () => { + it("should hash and verify a password", async () => { + const password = "mySecurePassword123!"; + const hash = await service.hashPassword(password); + expect(hash).toBeDefined(); + expect(hash).not.toBe(password); + + const isValid = await service.verifyPassword(password, hash); + expect(isValid).toBe(true); + + const isInvalid = await service.verifyPassword("wrongPassword", hash); + expect(isInvalid).toBe(false); + }); + }); + + describe("JWT jose", () => { + it("should generate and verify a JWT", async () => { + const payload = { sub: "1234567890", name: "John Doe", admin: true }; + const token = await service.generateJwt(payload); + expect(token).toBeDefined(); + + const verifiedPayload = await service.verifyJwt(token); + expect(verifiedPayload.sub).toBe(payload.sub); + expect(verifiedPayload.name).toBe(payload.name); + expect(verifiedPayload.admin).toBe(payload.admin); + }); + + it("should throw for invalid token", async () => { + await expect(service.verifyJwt("invalid.token.here")).rejects.toThrow(); + }); + }); + + describe("Encryption/Decryption (JWE)", () => { + it("should encrypt and decrypt content", async () => { + const content = "This is a secret message 🤫"; + const jwe = await service.encryptContent(content); + expect(jwe).toBeDefined(); + expect(typeof jwe).toBe("string"); + expect(jwe.split(".").length).toBe(5); // JWE compact serialization has 5 parts + + const decrypted = await service.decryptContent(jwe); + expect(decrypted).toBe(content); + }); + + it("should fail to decrypt invalid content", async () => { + await expect( + service.decryptContent("invalid.jwe.content"), + ).rejects.toThrow(); + }); + }); + + describe("Signature (JWS)", () => { + it("should sign and verify content signature", async () => { + const content = "Important document content"; + const jws = await service.signContent(content); + expect(jws).toBeDefined(); + expect(typeof jws).toBe("string"); + expect(jws.split(".").length).toBe(3); // JWS compact serialization has 3 parts + + const verifiedContent = await service.verifyContentSignature(jws); + expect(verifiedContent).toBe(content); + }); + + it("should fail to verify tampered content", async () => { + const content = "Original content"; + const jws = await service.signContent(content); + const parts = jws.split("."); + // Tamper with the payload (middle part) + parts[1] = Buffer.from("Tampered content").toString("base64url"); + const tamperedJws = parts.join("."); + + await expect(service.verifyContentSignature(tamperedJws)).rejects.toThrow(); + }); + }); + + describe("Post-Quantum @noble/post-quantum", () => { + it("should generate keypair, encapsulate and decapsulate", () => { + const { publicKey, secretKey } = service.generatePostQuantumKeyPair(); + expect(publicKey).toBeDefined(); + expect(secretKey).toBeDefined(); + + const { cipherText, sharedSecret } = service.encapsulate(publicKey); + expect(cipherText).toBeDefined(); + expect(sharedSecret).toBeDefined(); + + const decapsulatedSecret = service.decapsulate(cipherText, secretKey); + expect(decapsulatedSecret).toEqual(sharedSecret); + }); + }); +}); diff --git a/backend/src/crypto/crypto.service.ts b/backend/src/crypto/crypto.service.ts new file mode 100644 index 0000000..ed01bfa --- /dev/null +++ b/backend/src/crypto/crypto.service.ts @@ -0,0 +1,126 @@ +import {Injectable, Logger} from "@nestjs/common"; +import {ConfigService} from "@nestjs/config"; +import {ml_kem768} from "@noble/post-quantum/ml-kem.js"; +import {hash, verify} from "@node-rs/argon2"; +import * as jose from "jose"; + +@Injectable() +export class CryptoService { + private readonly logger = new Logger(CryptoService.name); + private readonly jwtSecret: Uint8Array; + private readonly encryptionKey: Uint8Array; + + constructor(private configService: ConfigService) { + const secret = this.configService.get("JWT_SECRET"); + if (!secret) { + this.logger.warn( + "JWT_SECRET is not defined, using a default insecure secret for development", + ); + } + this.jwtSecret = new TextEncoder().encode( + secret || "default-secret-change-me-in-production", + ); + + const encKey = this.configService.get("ENCRYPTION_KEY"); + if (!encKey) { + this.logger.warn( + "ENCRYPTION_KEY is not defined, using a default insecure key for development", + ); + } + // Pour AES-GCM 256, on a besoin de 32 octets (256 bits) + const rawKey = encKey || "default-encryption-key-32-chars-"; + this.encryptionKey = new TextEncoder().encode( + rawKey.padEnd(32, "0").substring(0, 32), + ); + } + + // --- Argon2 Hashing --- + + async hashPassword(password: string): Promise { + return hash(password, { + algorithm: 2, + }); + } + + async verifyPassword(password: string, hash: string): Promise { + return verify(hash, password); + } + + // --- JWT Operations via jose --- + + async generateJwt( + payload: jose.JWTPayload, + expiresIn = "2h", + ): Promise { + return new jose.SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime(expiresIn) + .sign(this.jwtSecret); + } + + async verifyJwt(token: string): Promise { + const { payload } = await jose.jwtVerify(token, this.jwtSecret); + return payload as T; + } + + // --- Encryption & Decryption (JWE) --- + + /** + * Chiffre un contenu textuel en utilisant JWE (Compact Serialization) + * Algorithme: A256GCMKW pour la gestion des clés, A256GCM pour le chiffrement de contenu + */ + async encryptContent(content: string): Promise { + const data = new TextEncoder().encode(content); + return new jose.CompactEncrypt(data) + .setProtectedHeader({ alg: "dir", enc: "A256GCM" }) + .encrypt(this.encryptionKey); + } + + /** + * Déchiffre un contenu JWE + */ + async decryptContent(jwe: string): Promise { + const { plaintext } = await jose.compactDecrypt(jwe, this.encryptionKey); + return new TextDecoder().decode(plaintext); + } + + // --- Signature & Verification (JWS) --- + + /** + * Signe un contenu textuel en utilisant JWS (Compact Serialization) + * Algorithme: HS256 (HMAC-SHA256) + */ + async signContent(content: string): Promise { + const data = new TextEncoder().encode(content); + return new jose.CompactSign(data) + .setProtectedHeader({ alg: "HS256" }) + .sign(this.jwtSecret); + } + + /** + * Vérifie la signature JWS d'un contenu + */ + async verifyContentSignature(jws: string): Promise { + const { payload } = await jose.compactVerify(jws, this.jwtSecret); + return new TextDecoder().decode(payload); + } + + // --- Post-Quantum Cryptography via @noble/post-quantum --- + // Example: Kyber (ML-KEM) key encapsulation + + generatePostQuantumKeyPair() { + const seed = new Uint8Array(64); + crypto.getRandomValues(seed); + const { publicKey, secretKey } = ml_kem768.keygen(seed); + return { publicKey, secretKey }; + } + + encapsulate(publicKey: Uint8Array) { + return ml_kem768.encapsulate(publicKey); + } + + decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) { + return ml_kem768.decapsulate(cipherText, secretKey); + } +}