feat: add modular services and repositories for improved code organization
Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { CryptoService } from "./crypto.service";
|
||||
import { HashingService } from "./services/hashing.service";
|
||||
import { JwtService } from "./services/jwt.service";
|
||||
import { EncryptionService } from "./services/encryption.service";
|
||||
import { PostQuantumService } from "./services/post-quantum.service";
|
||||
|
||||
@Module({
|
||||
providers: [CryptoService],
|
||||
exports: [CryptoService],
|
||||
providers: [
|
||||
CryptoService,
|
||||
HashingService,
|
||||
JwtService,
|
||||
EncryptionService,
|
||||
PostQuantumService,
|
||||
],
|
||||
exports: [
|
||||
CryptoService,
|
||||
HashingService,
|
||||
JwtService,
|
||||
EncryptionService,
|
||||
PostQuantumService,
|
||||
],
|
||||
})
|
||||
export class CryptoModule {}
|
||||
|
||||
@@ -64,6 +64,10 @@ jest.mock("jose", () => ({
|
||||
}));
|
||||
|
||||
import { CryptoService } from "./crypto.service";
|
||||
import { HashingService } from "./services/hashing.service";
|
||||
import { JwtService } from "./services/jwt.service";
|
||||
import { EncryptionService } from "./services/encryption.service";
|
||||
import { PostQuantumService } from "./services/post-quantum.service";
|
||||
|
||||
describe("CryptoService", () => {
|
||||
let service: CryptoService;
|
||||
@@ -72,6 +76,10 @@ describe("CryptoService", () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CryptoService,
|
||||
HashingService,
|
||||
JwtService,
|
||||
EncryptionService,
|
||||
PostQuantumService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
|
||||
@@ -1,151 +1,79 @@
|
||||
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";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type * as jose from "jose";
|
||||
import { EncryptionService } from "./services/encryption.service";
|
||||
import { HashingService } from "./services/hashing.service";
|
||||
import { JwtService } from "./services/jwt.service";
|
||||
import { PostQuantumService } from "./services/post-quantum.service";
|
||||
|
||||
/**
|
||||
* @deprecated Use HashingService, JwtService, EncryptionService or PostQuantumService directly.
|
||||
* This service acts as a Facade for backward compatibility.
|
||||
*/
|
||||
@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),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Blind Indexing (for search on encrypted data) ---
|
||||
constructor(
|
||||
private readonly hashingService: HashingService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly encryptionService: EncryptionService,
|
||||
private readonly postQuantumService: PostQuantumService,
|
||||
) {}
|
||||
|
||||
async hashEmail(email: string): Promise<string> {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
const data = new TextEncoder().encode(normalizedEmail);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
return this.hashingService.hashEmail(email);
|
||||
}
|
||||
|
||||
async hashIp(ip: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(ip);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
return this.hashingService.hashIp(ip);
|
||||
}
|
||||
|
||||
getPgpEncryptionKey(): string {
|
||||
return (
|
||||
this.configService.get<string>("PGP_ENCRYPTION_KEY") || "default-pgp-key"
|
||||
);
|
||||
return this.encryptionService.getPgpEncryptionKey();
|
||||
}
|
||||
|
||||
// --- Argon2 Hashing ---
|
||||
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
return hash(password, {
|
||||
algorithm: 2,
|
||||
});
|
||||
return this.hashingService.hashPassword(password);
|
||||
}
|
||||
|
||||
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return verify(hash, password);
|
||||
return this.hashingService.verifyPassword(password, hash);
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
return this.jwtService.generateJwt(payload, expiresIn);
|
||||
}
|
||||
|
||||
async verifyJwt<T extends jose.JWTPayload>(token: string): Promise<T> {
|
||||
const { payload } = await jose.jwtVerify(token, this.jwtSecret);
|
||||
return payload as T;
|
||||
return this.jwtService.verifyJwt<T>(token);
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
return this.encryptionService.encryptContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Déchiffre un contenu JWE
|
||||
*/
|
||||
async decryptContent(jwe: string): Promise<string> {
|
||||
const { plaintext } = await jose.compactDecrypt(jwe, this.encryptionKey);
|
||||
return new TextDecoder().decode(plaintext);
|
||||
return this.encryptionService.decryptContent(jwe);
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
return this.encryptionService.signContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return this.encryptionService.verifyContentSignature(jws);
|
||||
}
|
||||
|
||||
// --- 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 };
|
||||
return this.postQuantumService.generatePostQuantumKeyPair();
|
||||
}
|
||||
|
||||
encapsulate(publicKey: Uint8Array) {
|
||||
return ml_kem768.encapsulate(publicKey);
|
||||
return this.postQuantumService.encapsulate(publicKey);
|
||||
}
|
||||
|
||||
decapsulate(cipherText: Uint8Array, secretKey: Uint8Array) {
|
||||
return ml_kem768.decapsulate(cipherText, secretKey);
|
||||
return this.postQuantumService.decapsulate(cipherText, secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
58
backend/src/crypto/services/encryption.service.ts
Normal file
58
backend/src/crypto/services/encryption.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as jose from "jose";
|
||||
|
||||
@Injectable()
|
||||
export class EncryptionService {
|
||||
private readonly logger = new Logger(EncryptionService.name);
|
||||
private readonly jwtSecret: Uint8Array;
|
||||
private readonly encryptionKey: Uint8Array;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const secret = this.configService.get<string>("JWT_SECRET");
|
||||
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",
|
||||
);
|
||||
}
|
||||
const rawKey = encKey || "default-encryption-key-32-chars-";
|
||||
this.encryptionKey = new TextEncoder().encode(
|
||||
rawKey.padEnd(32, "0").substring(0, 32),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async decryptContent(jwe: string): Promise<string> {
|
||||
const { plaintext } = await jose.compactDecrypt(jwe, this.encryptionKey);
|
||||
return new TextDecoder().decode(plaintext);
|
||||
}
|
||||
|
||||
async signContent(content: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(content);
|
||||
return new jose.CompactSign(data)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.sign(this.jwtSecret);
|
||||
}
|
||||
|
||||
async verifyContentSignature(jws: string): Promise<string> {
|
||||
const { payload } = await jose.compactVerify(jws, this.jwtSecret);
|
||||
return new TextDecoder().decode(payload);
|
||||
}
|
||||
|
||||
getPgpEncryptionKey(): string {
|
||||
return (
|
||||
this.configService.get<string>("PGP_ENCRYPTION_KEY") || "default-pgp-key"
|
||||
);
|
||||
}
|
||||
}
|
||||
32
backend/src/crypto/services/hashing.service.ts
Normal file
32
backend/src/crypto/services/hashing.service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { hash, verify } from "@node-rs/argon2";
|
||||
|
||||
@Injectable()
|
||||
export class HashingService {
|
||||
async hashEmail(email: string): Promise<string> {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
return this.hashSha256(normalizedEmail);
|
||||
}
|
||||
|
||||
async hashIp(ip: string): Promise<string> {
|
||||
return this.hashSha256(ip);
|
||||
}
|
||||
|
||||
async hashSha256(text: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(text);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
return hash(password, {
|
||||
algorithm: 2,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return verify(hash, password);
|
||||
}
|
||||
}
|
||||
37
backend/src/crypto/services/jwt.service.ts
Normal file
37
backend/src/crypto/services/jwt.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as jose from "jose";
|
||||
|
||||
@Injectable()
|
||||
export class JwtService {
|
||||
private readonly logger = new Logger(JwtService.name);
|
||||
private readonly jwtSecret: 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",
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
20
backend/src/crypto/services/post-quantum.service.ts
Normal file
20
backend/src/crypto/services/post-quantum.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
||||
|
||||
@Injectable()
|
||||
export class PostQuantumService {
|
||||
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