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.
This commit is contained in:
8
backend/src/crypto/crypto.module.ts
Normal file
8
backend/src/crypto/crypto.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { CryptoService } from "./crypto.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [CryptoService],
|
||||||
|
exports: [CryptoService],
|
||||||
|
})
|
||||||
|
export class CryptoModule {}
|
||||||
117
backend/src/crypto/crypto.service.spec.ts
Normal file
117
backend/src/crypto/crypto.service.spec.ts
Normal file
@@ -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>(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
126
backend/src/crypto/crypto.service.ts
Normal file
126
backend/src/crypto/crypto.service.ts
Normal file
@@ -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<string>("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<string>("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<string> {
|
||||||
|
return hash(password, {
|
||||||
|
algorithm: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return verify(hash, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- JWT Operations via jose ---
|
||||||
|
|
||||||
|
async generateJwt(
|
||||||
|
payload: jose.JWTPayload,
|
||||||
|
expiresIn = "2h",
|
||||||
|
): Promise<string> {
|
||||||
|
return new jose.SignJWT(payload)
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(expiresIn)
|
||||||
|
.sign(this.jwtSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyJwt<T extends jose.JWTPayload>(token: string): Promise<T> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user