src/shared/utils/encryption/encryption.service.ts

Description

Service for encrypting and decrypting sensitive data at rest. Uses AES-256-GCM for authenticated encryption.

The encryption key is fetched at runtime from the configured provider:

  • env: Derived from MASTER_SECRET (development only)
  • vault: Fetched from HashiCorp Vault (production)
  • aws: Fetched from AWS Secrets Manager (production)
  • azure: Fetched from Azure Key Vault (production)

Security: When using vault/aws/azure, the key is only in RAM, not exposed via environment variables.

Relationships

Depends on

No results matching.

Index

Properties
Methods

Constructor

constructor(keyProvider: EncryptionKeyProvider)
Parameters :
Name Type Optional
keyProvider EncryptionKeyProvider No

Methods

decrypt
decrypt(encryptedValue: string)

Decrypt a value that was encrypted with encrypt(). Expects format: base64(iv):base64(authTag):base64(ciphertext)

Parameters :
Name Type Optional
encryptedValue string No
Returns : string
decryptJson
decryptJson<T>(encryptedValue: string)
Type parameters :
  • T

Decrypt a JSON value.

Parameters :
Name Type Optional
encryptedValue string No
Returns : T
encrypt
encrypt(plaintext: string)

Encrypt a string value using AES-256-GCM. Returns format: base64(iv):base64(authTag):base64(ciphertext)

Parameters :
Name Type Optional
plaintext string No
Returns : string
encryptJson
encryptJson<T>(value: T)
Type parameters :
  • T

Encrypt a JSON-serializable value.

Parameters :
Name Type Optional
value T No
Returns : string
Private ensureInitialized
ensureInitialized()

Ensure the service is initialized before operations.

Returns : Buffer
Async initialize
initialize()

Initialize the encryption service by fetching the key from the provider. Must be called during module initialization before any encrypt/decrypt operations.

Returns : Promise<void>
isEncrypted
isEncrypted(value: string)

Check if a value appears to be encrypted (has the expected format).

Parameters :
Name Type Optional
value string No
Returns : boolean

Properties

Private Readonly algorithm
Type : unknown
Default value : "aes-256-gcm" as const
Private Readonly authTagLength
Type : number
Default value : 16
Private encryptionKey
Type : Buffer | null
Default value : null
Private initialized
Type : unknown
Default value : false
Private Readonly ivLength
Type : number
Default value : 12
Private Readonly logger
Type : unknown
Default value : new Logger(EncryptionService.name)
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { Inject, Injectable, Logger } from "@nestjs/common";
import {
    ENCRYPTION_KEY_PROVIDER,
    EncryptionKeyProvider,
} from "./providers/encryption-key-provider.interface";

/**
 * Service for encrypting and decrypting sensitive data at rest.
 * Uses AES-256-GCM for authenticated encryption.
 *
 * The encryption key is fetched at runtime from the configured provider:
 * - env: Derived from MASTER_SECRET (development only)
 * - vault: Fetched from HashiCorp Vault (production)
 * - aws: Fetched from AWS Secrets Manager (production)
 * - azure: Fetched from Azure Key Vault (production)
 *
 * Security: When using vault/aws/azure, the key is only in RAM,
 * not exposed via environment variables.
 */
@Injectable()
export class EncryptionService {
    private encryptionKey: Buffer | null = null;
    private initialized = false;
    private readonly algorithm = "aes-256-gcm" as const;
    private readonly ivLength = 12; // 96 bits for GCM
    private readonly authTagLength = 16; // 128 bits
    private readonly logger = new Logger(EncryptionService.name);

    constructor(
        @Inject(ENCRYPTION_KEY_PROVIDER)
        private readonly keyProvider: EncryptionKeyProvider,
    ) {}

    /**
     * Initialize the encryption service by fetching the key from the provider.
     * Must be called during module initialization before any encrypt/decrypt operations.
     */
    async initialize(): Promise<void> {
        if (this.initialized) {
            return;
        }

        this.logger.log(
            `Initializing encryption with key provider: ${this.keyProvider.name}`,
        );
        this.encryptionKey = await this.keyProvider.getKey();
        this.initialized = true;
        this.logger.log("Encryption service initialized successfully");
    }

    /**
     * Ensure the service is initialized before operations.
     */
    private ensureInitialized(): Buffer {
        if (!this.initialized || !this.encryptionKey) {
            throw new Error(
                "Encryption service not initialized. Call initialize() first.",
            );
        }
        return this.encryptionKey;
    }

    /**
     * Encrypt a string value using AES-256-GCM.
     * Returns format: base64(iv):base64(authTag):base64(ciphertext)
     */
    encrypt(plaintext: string): string {
        const key = this.ensureInitialized();

        const iv = randomBytes(this.ivLength);
        const cipher = createCipheriv(this.algorithm, key, iv, {
            authTagLength: this.authTagLength,
        });

        const encrypted = Buffer.concat([
            cipher.update(plaintext, "utf8"),
            cipher.final(),
        ]);

        const authTag = cipher.getAuthTag();

        // Format: iv:authTag:ciphertext (all base64)
        return [
            iv.toString("base64"),
            authTag.toString("base64"),
            encrypted.toString("base64"),
        ].join(":");
    }

    /**
     * Decrypt a value that was encrypted with encrypt().
     * Expects format: base64(iv):base64(authTag):base64(ciphertext)
     */
    decrypt(encryptedValue: string): string {
        const key = this.ensureInitialized();

        const parts = encryptedValue.split(":");
        if (parts.length !== 3) {
            throw new Error("Invalid encrypted value format");
        }

        const [ivBase64, authTagBase64, ciphertextBase64] = parts;
        const iv = Buffer.from(ivBase64, "base64");
        const authTag = Buffer.from(authTagBase64, "base64");
        const ciphertext = Buffer.from(ciphertextBase64, "base64");

        const decipher = createDecipheriv(this.algorithm, key, iv, {
            authTagLength: this.authTagLength,
        });

        decipher.setAuthTag(authTag);

        const decrypted = Buffer.concat([
            decipher.update(ciphertext),
            decipher.final(),
        ]);

        return decrypted.toString("utf8");
    }

    /**
     * Encrypt a JSON-serializable value.
     */
    encryptJson<T>(value: T): string {
        return this.encrypt(JSON.stringify(value));
    }

    /**
     * Decrypt a JSON value.
     */
    decryptJson<T>(encryptedValue: string): T {
        return JSON.parse(this.decrypt(encryptedValue));
    }

    /**
     * Check if a value appears to be encrypted (has the expected format).
     */
    isEncrypted(value: string): boolean {
        if (typeof value !== "string") return false;
        const parts = value.split(":");
        if (parts.length !== 3) return false;

        // Check if all parts look like base64
        const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
        return parts.every((part) => base64Regex.test(part));
    }
}

results matching ""

    No results matching ""