import { Inject, Injectable } from '@nestjs/common'; import { createCipheriv, createDecipheriv, createHmac, randomBytes } from 'node:crypto'; import { AppConfigService } from '../config/app-config.service'; @Injectable() export class SensitiveDataService { constructor(@Inject(AppConfigService) private readonly config: AppConfigService) {} encrypt(value: string): string { const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', this.config.fieldEncryptionKey, iv); const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return [ 'v1', iv.toString('base64url'), tag.toString('base64url'), ciphertext.toString('base64url'), ].join('.'); } decrypt(value: string): string { const [version, ivValue, tagValue, ciphertextValue] = value.split('.'); if (version !== 'v1' || !ivValue || !tagValue || !ciphertextValue) { throw new Error('Unsupported encrypted value format'); } const decipher = createDecipheriv( 'aes-256-gcm', this.config.fieldEncryptionKey, Buffer.from(ivValue, 'base64url'), ); decipher.setAuthTag(Buffer.from(tagValue, 'base64url')); return Buffer.concat([ decipher.update(Buffer.from(ciphertextValue, 'base64url')), decipher.final(), ]).toString('utf8'); } lookupHash(domain: string, value: string): string { return createHmac('sha256', this.config.fieldEncryptionKey) .update(`${domain}\0${value}`) .digest('hex'); } sessionTokenHash(token: string): string { return createHmac('sha256', this.config.sessionTokenPepper).update(token).digest('hex'); } auditIpHash(ip: string): string { return createHmac('sha256', this.config.auditIpHashKey).update(ip).digest('hex'); } }