Compare commits

...

4 Commits

Author SHA1 Message Date
Mathis HERRIOT
c1bc68e3e3 feat: add dependencies for cryptographic utilities
Some checks failed
Lint / lint (push) Failing after 4m59s
Added `@noble/post-quantum`, `@node-rs/argon2`, and `jose` to backend dependencies to support advanced cryptographic operations, including post-quantum algorithms, Argon2 hashing, and JWT/JWE handling.
2026-01-06 12:09:57 +01:00
Mathis HERRIOT
810acd8ed4 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.
2026-01-06 12:09:44 +01:00
Mathis HERRIOT
adceada1b6 feat: add CryptoModule to app imports 2026-01-06 12:09:28 +01:00
Mathis HERRIOT
dfba0c0adb feat: integrate ConfigModule and DatabaseModule into app initialization 2026-01-06 11:42:44 +01:00
6 changed files with 448 additions and 1 deletions

View File

@@ -28,8 +28,11 @@
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@noble/post-quantum": "^0.5.4",
"@node-rs/argon2": "^2.0.2",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.1",
"jose": "^6.1.3",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"

View File

@@ -1,9 +1,17 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import {DatabaseModule} from "./database/database.module";
import {ConfigModule} from "@nestjs/config";
import {CryptoModule} from "./crypto/crypto.module";
@Module({
imports: [],
imports: [
DatabaseModule,
CryptoModule,
ConfigModule.forRoot({
isGlobal: true,
})],
controllers: [AppController],
providers: [AppService],
})

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { CryptoService } from "./crypto.service";
@Module({
providers: [CryptoService],
exports: [CryptoService],
})
export class CryptoModule {}

View 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);
});
});
});

View 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);
}
}

185
pnpm-lock.yaml generated
View File

@@ -26,12 +26,21 @@ importers:
'@nestjs/platform-express':
specifier: ^11.0.1
version: 11.1.11(@nestjs/common@11.1.11(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)
'@noble/post-quantum':
specifier: ^0.5.4
version: 0.5.4
'@node-rs/argon2':
specifier: ^2.0.2
version: 2.0.2
dotenv:
specifier: ^17.2.3
version: 17.2.3
drizzle-orm:
specifier: ^0.45.1
version: 0.45.1(@types/pg@8.16.0)(pg@8.16.3)
jose:
specifier: ^6.1.3
version: 6.1.3
pg:
specifier: ^8.16.3
version: 8.16.3
@@ -1588,10 +1597,109 @@ packages:
cpu: [x64]
os: [win32]
'@noble/curves@2.0.1':
resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==}
engines: {node: '>= 20.19.0'}
'@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
'@noble/post-quantum@0.5.4':
resolution: {integrity: sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==}
engines: {node: '>= 20.19.0'}
'@node-rs/argon2-android-arm-eabi@2.0.2':
resolution: {integrity: sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@node-rs/argon2-android-arm64@2.0.2':
resolution: {integrity: sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@node-rs/argon2-darwin-arm64@2.0.2':
resolution: {integrity: sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@node-rs/argon2-darwin-x64@2.0.2':
resolution: {integrity: sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@node-rs/argon2-freebsd-x64@2.0.2':
resolution: {integrity: sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@node-rs/argon2-linux-arm-gnueabihf@2.0.2':
resolution: {integrity: sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@node-rs/argon2-linux-arm64-gnu@2.0.2':
resolution: {integrity: sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@node-rs/argon2-linux-arm64-musl@2.0.2':
resolution: {integrity: sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@node-rs/argon2-linux-x64-gnu@2.0.2':
resolution: {integrity: sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@node-rs/argon2-linux-x64-musl@2.0.2':
resolution: {integrity: sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@node-rs/argon2-wasm32-wasi@2.0.2':
resolution: {integrity: sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@node-rs/argon2-win32-arm64-msvc@2.0.2':
resolution: {integrity: sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@node-rs/argon2-win32-ia32-msvc@2.0.2':
resolution: {integrity: sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@node-rs/argon2-win32-x64-msvc@2.0.2':
resolution: {integrity: sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@node-rs/argon2@2.0.2':
resolution: {integrity: sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==}
engines: {node: '>= 10'}
'@nuxt/opencollective@0.4.1':
resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==}
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
@@ -4122,6 +4230,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@6.1.3:
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -6994,8 +7105,80 @@ snapshots:
'@next/swc-win32-x64-msvc@16.1.1':
optional: true
'@noble/curves@2.0.1':
dependencies:
'@noble/hashes': 2.0.1
'@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {}
'@noble/post-quantum@0.5.4':
dependencies:
'@noble/curves': 2.0.1
'@noble/hashes': 2.0.1
'@node-rs/argon2-android-arm-eabi@2.0.2':
optional: true
'@node-rs/argon2-android-arm64@2.0.2':
optional: true
'@node-rs/argon2-darwin-arm64@2.0.2':
optional: true
'@node-rs/argon2-darwin-x64@2.0.2':
optional: true
'@node-rs/argon2-freebsd-x64@2.0.2':
optional: true
'@node-rs/argon2-linux-arm-gnueabihf@2.0.2':
optional: true
'@node-rs/argon2-linux-arm64-gnu@2.0.2':
optional: true
'@node-rs/argon2-linux-arm64-musl@2.0.2':
optional: true
'@node-rs/argon2-linux-x64-gnu@2.0.2':
optional: true
'@node-rs/argon2-linux-x64-musl@2.0.2':
optional: true
'@node-rs/argon2-wasm32-wasi@2.0.2':
dependencies:
'@napi-rs/wasm-runtime': 0.2.12
optional: true
'@node-rs/argon2-win32-arm64-msvc@2.0.2':
optional: true
'@node-rs/argon2-win32-ia32-msvc@2.0.2':
optional: true
'@node-rs/argon2-win32-x64-msvc@2.0.2':
optional: true
'@node-rs/argon2@2.0.2':
optionalDependencies:
'@node-rs/argon2-android-arm-eabi': 2.0.2
'@node-rs/argon2-android-arm64': 2.0.2
'@node-rs/argon2-darwin-arm64': 2.0.2
'@node-rs/argon2-darwin-x64': 2.0.2
'@node-rs/argon2-freebsd-x64': 2.0.2
'@node-rs/argon2-linux-arm-gnueabihf': 2.0.2
'@node-rs/argon2-linux-arm64-gnu': 2.0.2
'@node-rs/argon2-linux-arm64-musl': 2.0.2
'@node-rs/argon2-linux-x64-gnu': 2.0.2
'@node-rs/argon2-linux-x64-musl': 2.0.2
'@node-rs/argon2-wasm32-wasi': 2.0.2
'@node-rs/argon2-win32-arm64-msvc': 2.0.2
'@node-rs/argon2-win32-ia32-msvc': 2.0.2
'@node-rs/argon2-win32-x64-msvc': 2.0.2
'@nuxt/opencollective@0.4.1':
dependencies:
consola: 3.4.2
@@ -9869,6 +10052,8 @@ snapshots:
jiti@2.6.1: {}
jose@6.1.3: {}
js-tokens@4.0.0: {}
js-yaml@3.14.2: