Data Encryption Example
This example demonstrates how to implement secure data encryption in a NestJS application using the AES service.
Use Case
Encrypt sensitive user data (PII, financial data, health records) at rest in your database while maintaining the ability to decrypt when needed.
Implementation
1. Data Encryption Service
typescript
import { Injectable, Logger } from '@nestjs/common';
import { AesService } from 'nestjs-crypto';
interface EncryptedField {
encrypted: string;
iv: string;
keyVersion: number;
}
@Injectable()
export class DataEncryptionService {
private readonly logger = new Logger(DataEncryptionService.name);
private readonly currentKeyVersion = 1;
constructor(private readonly aesService: AesService) {}
/**
* Encrypt a single field value
*/
encryptField(value: string): EncryptedField {
try {
const key = this.getEncryptionKey();
const { encrypted, iv } = this.aesService.encrypt(value, key);
return {
encrypted,
iv,
keyVersion: this.currentKeyVersion,
};
} catch (error) {
this.logger.error(`Encryption failed: ${error.message}`);
throw error;
}
}
/**
* Decrypt a single field value
*/
decryptField(field: EncryptedField): string {
try {
const key = this.getEncryptionKey(field.keyVersion);
return this.aesService.decrypt(field.encrypted, key, field.iv);
} catch (error) {
this.logger.error(`Decryption failed: ${error.message}`);
throw error;
}
}
/**
* Encrypt multiple fields in an object
*/
encryptFields<T extends Record<string, any>>(
data: T,
fieldsToEncrypt: (keyof T)[],
): Partial<T> {
const encrypted: any = { ...data };
for (const field of fieldsToEncrypt) {
if (data[field] !== undefined && data[field] !== null) {
encrypted[field] = this.encryptField(String(data[field]));
}
}
return encrypted;
}
/**
* Decrypt multiple fields in an object
*/
decryptFields<T extends Record<string, any>>(
data: T,
fieldsToDecrypt: (keyof T)[],
): Partial<T> {
const decrypted: any = { ...data };
for (const field of fieldsToDecrypt) {
if (data[field] && typeof data[field] === 'object') {
decrypted[field] = this.decryptField(data[field] as EncryptedField);
}
}
return decrypted;
}
/**
* Get encryption key based on version
*/
private getEncryptionKey(version: number = this.currentKeyVersion): string {
// In production, retrieve from AWS KMS, Azure Key Vault, etc.
const keyMap: Record<number, string> = {
1: process.env.AES_KEY_V1!,
2: process.env.AES_KEY_V2!,
};
const key = keyMap[version];
if (!key) {
throw new Error(`Encryption key version ${version} not found`);
}
return key;
}
}2. User Entity with Encrypted Fields
typescript
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
interface EncryptedField {
encrypted: string;
iv: string;
keyVersion: number;
}
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column()
password: string; // Hashed with bcrypt
// Encrypted fields stored as JSON
@Column('jsonb')
encryptedSsn: EncryptedField;
@Column('jsonb', { nullable: true })
encryptedPhone: EncryptedField | null;
@Column('jsonb', { nullable: true })
encryptedAddress: EncryptedField | null;
@Column('jsonb', { nullable: true })
encryptedCreditCard: EncryptedField | null;
@CreateDateColumn()
createdAt: Date;
// Virtual properties for decrypted values
ssn?: string;
phone?: string;
address?: string;
creditCard?: string;
}3. User Service
typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { DataEncryptionService } from '../encryption/data-encryption.service';
import { BcryptService } from 'nestjs-crypto';
interface CreateUserDto {
email: string;
password: string;
ssn: string;
phone?: string;
address?: string;
creditCard?: string;
}
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly encryptionService: DataEncryptionService,
private readonly bcryptService: BcryptService,
) {}
async createUser(dto: CreateUserDto): Promise<User> {
// Hash password with bcrypt
const password = await this.bcryptService.hash(dto.password);
// Encrypt sensitive fields
const encryptedSsn = this.encryptionService.encryptField(dto.ssn);
const encryptedPhone = dto.phone
? this.encryptionService.encryptField(dto.phone)
: null;
const encryptedAddress = dto.address
? this.encryptionService.encryptField(dto.address)
: null;
const encryptedCreditCard = dto.creditCard
? this.encryptionService.encryptField(dto.creditCard)
: null;
// Create user entity
const user = this.userRepository.create({
email: dto.email,
password,
encryptedSsn,
encryptedPhone,
encryptedAddress,
encryptedCreditCard,
});
return this.userRepository.save(user);
}
async findById(id: string, decrypt = false): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new Error('User not found');
}
if (decrypt) {
return this.decryptUser(user);
}
return user;
}
async updateSensitiveData(
userId: string,
data: {
phone?: string;
address?: string;
creditCard?: string;
},
): Promise<User> {
const user = await this.findById(userId);
if (data.phone) {
user.encryptedPhone = this.encryptionService.encryptField(data.phone);
}
if (data.address) {
user.encryptedAddress = this.encryptionService.encryptField(data.address);
}
if (data.creditCard) {
user.encryptedCreditCard = this.encryptionService.encryptField(
data.creditCard,
);
}
return this.userRepository.save(user);
}
/**
* Decrypt all encrypted fields in a user
*/
private decryptUser(user: User): User {
return {
...user,
ssn: this.encryptionService.decryptField(user.encryptedSsn),
phone: user.encryptedPhone
? this.encryptionService.decryptField(user.encryptedPhone)
: undefined,
address: user.encryptedAddress
? this.encryptionService.decryptField(user.encryptedAddress)
: undefined,
creditCard: user.encryptedCreditCard
? this.encryptionService.decryptField(user.encryptedCreditCard)
: undefined,
};
}
/**
* Re-encrypt data with a new key version (for key rotation)
*/
async rotateEncryptionKeys(): Promise<void> {
const users = await this.userRepository.find();
for (const user of users) {
// Decrypt with old key
const decrypted = this.decryptUser(user);
// Re-encrypt with new key
user.encryptedSsn = this.encryptionService.encryptField(decrypted.ssn!);
if (decrypted.phone) {
user.encryptedPhone = this.encryptionService.encryptField(decrypted.phone);
}
if (decrypted.address) {
user.encryptedAddress = this.encryptionService.encryptField(
decrypted.address,
);
}
if (decrypted.creditCard) {
user.encryptedCreditCard = this.encryptionService.encryptField(
decrypted.creditCard,
);
}
await this.userRepository.save(user);
}
}
}4. User Module
typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CryptoModule } from 'nestjs-crypto';
import { User } from './user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { DataEncryptionService } from '../encryption/data-encryption.service';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
CryptoModule.forRoot({
bcrypt: { saltRounds: 10 },
}),
],
providers: [UserService, DataEncryptionService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}5. User Controller
typescript
import {
Controller,
Post,
Get,
Patch,
Body,
Param,
UseGuards,
Request,
} from '@nestjs/common';
import { UserService } from './user.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async register(@Body() dto: CreateUserDto) {
const user = await this.userService.createUser(dto);
// Don't return encrypted fields
const { encryptedSsn, encryptedPhone, encryptedAddress, encryptedCreditCard, ...safeUser } = user;
return {
message: 'User registered successfully',
user: safeUser,
};
}
@UseGuards(JwtAuthGuard)
@Get('me')
async getProfile(@Request() req) {
// Return decrypted data only for authenticated user
return this.userService.findById(req.user.id, true);
}
@UseGuards(JwtAuthGuard)
@Patch('me/sensitive')
async updateSensitiveData(
@Request() req,
@Body() data: { phone?: string; address?: string; creditCard?: string },
) {
const user = await this.userService.updateSensitiveData(req.user.id, data);
return {
message: 'Sensitive data updated successfully',
user: this.sanitizeUser(user),
};
}
private sanitizeUser(user: any) {
const { encryptedSsn, encryptedPhone, encryptedAddress, encryptedCreditCard, ...safe } = user;
return safe;
}
}Testing
typescript
import { Test } from '@nestjs/testing';
import { AesService } from 'nestjs-crypto';
import { DataEncryptionService } from './data-encryption.service';
describe('DataEncryptionService', () => {
let service: DataEncryptionService;
let aesService: AesService;
beforeEach(async () => {
process.env.AES_KEY_V1 = 'a'.repeat(64); // 64 hex characters
const module = await Test.createTestingModule({
providers: [DataEncryptionService, AesService],
}).compile();
service = module.get<DataEncryptionService>(DataEncryptionService);
aesService = module.get<AesService>(AesService);
});
describe('encryptField', () => {
it('should encrypt a field value', () => {
const value = '123-45-6789';
const encrypted = service.encryptField(value);
expect(encrypted.encrypted).toBeDefined();
expect(encrypted.iv).toBeDefined();
expect(encrypted.keyVersion).toBe(1);
});
it('should produce different ciphertexts for same value', () => {
const value = '123-45-6789';
const encrypted1 = service.encryptField(value);
const encrypted2 = service.encryptField(value);
expect(encrypted1.encrypted).not.toBe(encrypted2.encrypted);
expect(encrypted1.iv).not.toBe(encrypted2.iv);
});
});
describe('decryptField', () => {
it('should decrypt an encrypted field', () => {
const value = '123-45-6789';
const encrypted = service.encryptField(value);
const decrypted = service.decryptField(encrypted);
expect(decrypted).toBe(value);
});
});
describe('encryptFields', () => {
it('should encrypt multiple fields in an object', () => {
const data = {
id: '123',
ssn: '123-45-6789',
phone: '555-1234',
email: 'user@example.com',
};
const encrypted = service.encryptFields(data, ['ssn', 'phone']);
expect(encrypted.id).toBe('123');
expect(encrypted.email).toBe('user@example.com');
expect(typeof encrypted.ssn).toBe('object');
expect(typeof encrypted.phone).toBe('object');
});
});
describe('decryptFields', () => {
it('should decrypt multiple encrypted fields', () => {
const data = {
id: '123',
ssn: '123-45-6789',
phone: '555-1234',
email: 'user@example.com',
};
const encrypted = service.encryptFields(data, ['ssn', 'phone']);
const decrypted = service.decryptFields(encrypted, ['ssn', 'phone']);
expect(decrypted.ssn).toBe('123-45-6789');
expect(decrypted.phone).toBe('555-1234');
});
});
});Environment Configuration
bash
# .env
AES_KEY_V1=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
AES_KEY_V2=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210Key Rotation Strategy
typescript
@Injectable()
export class KeyRotationService {
constructor(
private readonly userService: UserService,
private readonly encryptionService: DataEncryptionService,
) {}
/**
* Rotate encryption keys for all users
* Run this as a scheduled task or manual operation
*/
async rotateKeys(): Promise<void> {
await this.userService.rotateEncryptionKeys();
// Update current key version in environment/config
// This ensures new data uses the new key
}
}Best Practices
✅ Do
- Store keys in KMS (AWS KMS, Azure Key Vault, HashiCorp Vault)
- Use different IVs for each encryption operation
- Implement key rotation with version tracking
- Encrypt at application layer before saving to database
- Use JSONB columns for encrypted field storage (PostgreSQL)
- Log access to sensitive data (audit trail)
- Sanitize responses - never return encrypted fields to clients
- Use authentication before allowing decryption
- Validate input before encryption
❌ Don't
- Store keys in code or version control
- Reuse IVs with the same key
- Return encrypted data in API responses
- Allow unauthenticated decryption
- Encrypt searchable fields (consider tokenization instead)
- Log decrypted values
- Share keys between environments
Performance Considerations
typescript
// Bad: Decrypt all users in a list
async getAllUsers(): Promise<User[]> {
const users = await this.userRepository.find();
return users.map(user => this.decryptUser(user)); // Slow!
}
// Good: Only decrypt when needed
async getAllUsers(): Promise<User[]> {
// Return without decryption for list views
return this.userRepository.find({
select: ['id', 'email', 'createdAt'], // Don't select encrypted fields
});
}
// Good: Decrypt single user
async getUser(id: string): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
return this.decryptUser(user);
}Compliance Considerations
- GDPR: Right to be forgotten - ensure encrypted data can be deleted
- HIPAA: Encrypt PHI (Protected Health Information)
- PCI DSS: Encrypt credit card data, implement key rotation
- SOC 2: Document encryption practices and key management