File Encryption Example
This example demonstrates how to encrypt and decrypt files using the AES service in NestJS.
Use Case
Encrypt sensitive files before storing them on disk or in cloud storage (S3, Azure Blob, etc.) to ensure data at rest is protected.
Implementation
1. File Encryption Service
typescript
import { Injectable, Logger } from '@nestjs/common';
import { AesService } from 'nestjs-crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { createCipheriv, createDecipheriv } from 'crypto';
interface EncryptedFileMetadata {
originalName: string;
encryptedPath: string;
iv: string;
keyVersion: number;
size: number;
mimeType: string;
encryptedAt: Date;
}
@Injectable()
export class FileEncryptionService {
private readonly logger = new Logger(FileEncryptionService.name);
constructor(private readonly aesService: AesService) {}
/**
* Encrypt a file (for small files)
*/
async encryptFile(filePath: string): Promise<EncryptedFileMetadata> {
try {
// Read file content
const content = await fs.readFile(filePath);
const contentString = content.toString('base64');
// Encrypt
const key = this.getEncryptionKey();
const iv = this.aesService.generateIv();
const result = this.aesService.encrypt(contentString, key, iv);
// Generate encrypted file path
const encryptedPath = `${filePath}.encrypted`;
// Save encrypted content
await fs.writeFile(encryptedPath, result.encrypted);
// Get file stats
const stats = await fs.stat(filePath);
return {
originalName: path.basename(filePath),
encryptedPath,
iv: result.iv,
keyVersion: 1,
size: stats.size,
mimeType: this.getMimeType(filePath),
encryptedAt: new Date(),
};
} catch (error) {
this.logger.error(`Failed to encrypt file: ${error.message}`);
throw error;
}
}
/**
* Decrypt a file (for small files)
*/
async decryptFile(
encryptedPath: string,
iv: string,
outputPath?: string,
): Promise<string> {
try {
// Read encrypted content
const encryptedContent = await fs.readFile(encryptedPath, 'utf8');
// Decrypt
const key = this.getEncryptionKey();
const decrypted = this.aesService.decrypt(encryptedContent, key, iv);
// Convert from base64
const content = Buffer.from(decrypted, 'base64');
// Determine output path
const finalPath = outputPath || encryptedPath.replace('.encrypted', '');
// Save decrypted file
await fs.writeFile(finalPath, content);
return finalPath;
} catch (error) {
this.logger.error(`Failed to decrypt file: ${error.message}`);
throw error;
}
}
/**
* Encrypt large file using streams
*/
async encryptLargeFile(
inputPath: string,
outputPath?: string,
): Promise<EncryptedFileMetadata> {
const encryptedPath = outputPath || `${inputPath}.encrypted`;
// Generate key and IV
const key = Buffer.from(this.getEncryptionKey(), 'hex');
const iv = this.aesService.generateIv();
// Create cipher
const cipher = createCipheriv('aes-256-cbc', key, iv);
// Stream encryption
const input = createReadStream(inputPath);
const output = createWriteStream(encryptedPath);
await pipeline(input, cipher, output);
const stats = await fs.stat(inputPath);
return {
originalName: path.basename(inputPath),
encryptedPath,
iv: iv.toString('hex'),
keyVersion: 1,
size: stats.size,
mimeType: this.getMimeType(inputPath),
encryptedAt: new Date(),
};
}
/**
* Decrypt large file using streams
*/
async decryptLargeFile(
encryptedPath: string,
iv: string,
outputPath?: string,
): Promise<string> {
const finalPath = outputPath || encryptedPath.replace('.encrypted', '');
// Get key
const key = Buffer.from(this.getEncryptionKey(), 'hex');
const ivBuffer = Buffer.from(iv, 'hex');
// Create decipher
const decipher = createDecipheriv('aes-256-cbc', key, ivBuffer);
// Stream decryption
const input = createReadStream(encryptedPath);
const output = createWriteStream(finalPath);
await pipeline(input, decipher, output);
return finalPath;
}
/**
* Get encryption key from environment or KMS
*/
private getEncryptionKey(): string {
const key = process.env.FILE_ENCRYPTION_KEY;
if (!key) {
throw new Error('FILE_ENCRYPTION_KEY not configured');
}
if (key.length !== 64) {
throw new Error('FILE_ENCRYPTION_KEY must be 64 hex characters');
}
return key;
}
/**
* Determine MIME type from file extension
*/
private getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.txt': 'text/plain',
'.json': 'application/json',
'.xml': 'application/xml',
};
return mimeTypes[ext] || 'application/octet-stream';
}
}2. File Storage Entity
typescript
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity()
export class EncryptedFile {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@Column()
originalName: string;
@Column()
encryptedPath: string;
@Column()
iv: string;
@Column()
keyVersion: number;
@Column()
size: number;
@Column()
mimeType: string;
@CreateDateColumn()
uploadedAt: Date;
@Column({ nullable: true })
expiresAt?: Date;
}3. File Upload Controller
typescript
import {
Controller,
Post,
Get,
Param,
UseInterceptors,
UploadedFile,
UseGuards,
Request,
Res,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { Response } from 'express';
import { diskStorage } from 'multer';
import { v4 as uuid } from 'uuid';
@Controller('files')
@UseGuards(JwtAuthGuard)
export class FileController {
constructor(
private readonly fileEncryptionService: FileEncryptionService,
private readonly fileService: FileService,
) {}
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const filename = `${uuid()}-${file.originalname}`;
cb(null, filename);
},
}),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
}),
)
async uploadFile(
@UploadedFile() file: Express.Multer.File,
@Request() req,
) {
// Encrypt file
const metadata = await this.fileEncryptionService.encryptFile(file.path);
// Save metadata to database
const record = await this.fileService.create({
userId: req.user.id,
...metadata,
});
// Delete original file
await fs.unlink(file.path);
return {
id: record.id,
originalName: file.originalname,
size: file.size,
mimeType: file.mimetype,
};
}
@Get(':id/download')
async downloadFile(@Param('id') id: string, @Request() req, @Res() res: Response) {
// Get file metadata
const file = await this.fileService.findOne(id, req.user.id);
if (!file) {
throw new NotFoundException('File not found');
}
// Decrypt to temporary location
const tempPath = `./temp/${uuid()}-${file.originalName}`;
await this.fileEncryptionService.decryptFile(
file.encryptedPath,
file.iv,
tempPath,
);
// Send file
res.download(tempPath, file.originalName, async (err) => {
// Clean up temp file
await fs.unlink(tempPath);
if (err) {
this.logger.error(`Download failed: ${err.message}`);
}
});
}
}4. File Service
typescript
@Injectable()
export class FileService {
constructor(
@InjectRepository(EncryptedFile)
private readonly fileRepository: Repository<EncryptedFile>,
) {}
async create(data: Partial<EncryptedFile>): Promise<EncryptedFile> {
const file = this.fileRepository.create(data);
return this.fileRepository.save(file);
}
async findOne(id: string, userId: string): Promise<EncryptedFile> {
return this.fileRepository.findOne({
where: { id, userId },
});
}
async findAllByUser(userId: string): Promise<EncryptedFile[]> {
return this.fileRepository.find({
where: { userId },
order: { uploadedAt: 'DESC' },
});
}
async delete(id: string, userId: string): Promise<void> {
const file = await this.findOne(id, userId);
if (!file) {
throw new NotFoundException('File not found');
}
// Delete encrypted file
await fs.unlink(file.encryptedPath);
// Delete database record
await this.fileRepository.remove(file);
}
}5. Module Configuration
typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MulterModule } from '@nestjs/platform-express';
import { CryptoModule } from 'nestjs-crypto';
import { FileEncryptionService } from './file-encryption.service';
import { FileService } from './file.service';
import { FileController } from './file.controller';
import { EncryptedFile } from './entities/encrypted-file.entity';
@Module({
imports: [
TypeOrmModule.forFeature([EncryptedFile]),
MulterModule.register({
dest: './uploads',
}),
CryptoModule.forRoot(),
],
controllers: [FileController],
providers: [FileEncryptionService, FileService],
exports: [FileEncryptionService, FileService],
})
export class FileEncryptionModule {}Usage Examples
Upload and Encrypt
bash
curl -X POST http://localhost:3000/files/upload \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf"Download and Decrypt
bash
curl -X GET http://localhost:3000/files/FILE_ID/download \
-H "Authorization: Bearer YOUR_TOKEN" \
-o decrypted-document.pdfEnvironment Configuration
bash
# .env
FILE_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefBest Practices
✅ Do
- Use streams for large files (>10MB)
- Delete unencrypted files after encryption
- Implement file size limits
- Validate file types
- Use temporary files for decryption
- Clean up temporary files
- Store IV with file metadata
- Implement access control
- Log file access
❌ Don't
- Keep unencrypted files on disk
- Encrypt files in memory for large files
- Store encryption keys in database
- Expose file paths to clients
- Allow unlimited file sizes
- Skip virus scanning
- Log file contents
Security Considerations
- Virus Scanning: Scan files before encryption
- File Validation: Validate file types and content
- Access Control: Verify user owns the file
- Audit Logging: Log all file access
- Temporary Files: Clean up after operations
- Key Rotation: Support re-encrypting with new keys