Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/common/utils/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { encryptBuffer, decryptBuffer } from './crypto';

describe('Document Cryptographic Round-Trip Integrity Matrix', () => {
beforeAll(() => {
// Set mock local environment key variables for isolation context
process.env.TRADEFLOW_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
});

it('should successfully encrypt and decrypt raw buffers natively in system memory without layout data leakage', () => {
const rawSecretMessage = 'TradeFlow Confidential Real World Asset (RWA) KYC Data Payload.';
const bufferData = Buffer.from(rawSecretMessage, 'utf-8');

// 1. Run server-side memory encryption
const encrypted = encryptBuffer(bufferData);

expect(encrypted.iv).toBeDefined();
expect(encrypted.authTag).toBeDefined();
expect(encrypted.ciphertext.toString('utf-8')).not.toBe(rawSecretMessage); // Data is safely obfuscated

// 2. Run matching decryption
const decryptedBuffer = decryptBuffer(encrypted.ciphertext, encrypted.iv, encrypted.authTag);
const decryptedMessage = decryptedBuffer.toString('utf-8');

// 3. Assert full data fidelity recovery
expect(decryptedMessage).toBe(rawSecretMessage);
});
});
50 changes: 50 additions & 0 deletions src/common/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as nodeCrypto from 'crypto'; // Changed to namespace import to bypass Jest resolver stubs

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;

// Strict environment key validation guardrail, checked once at module load.
const MASTER_KEY_HEX = process.env.TRADEFLOW_ENCRYPTION_KEY;
if (!MASTER_KEY_HEX || Buffer.from(MASTER_KEY_HEX, 'hex').length !== 32) {
throw new Error('CRITICAL: TRADEFLOW_ENCRYPTION_KEY must be a valid 32-byte hex string.');
}
const MASTER_KEY = Buffer.from(MASTER_KEY_HEX, 'hex');

export interface EncryptedArtifact {
ciphertext: Buffer;
iv: string;
authTag: string;
}

/**
* Encrypts a Buffer using AES-256-GCM.
* @param buffer The data to encrypt.
* @returns An object containing the ciphertext, IV, and authentication tag.
*/
export function encryptBuffer(buffer: Buffer): EncryptedArtifact {
const iv = nodeCrypto.randomBytes(IV_LENGTH);
const cipher = nodeCrypto.createCipheriv(ALGORITHM, MASTER_KEY, iv);
const ciphertext = Buffer.concat([cipher.update(buffer), cipher.final()]);
const authTag = cipher.getAuthTag();
return {
ciphertext,
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
};
}
/**
* Decrypts a Buffer using AES-256-GCM.
* @param ciphertext The encrypted data.
* @param ivHex The Initialization Vector in hex format.
* @param authTagHex The authentication tag in hex format.
* @returns The decrypted data as a Buffer.
*/
export function decryptBuffer(ciphertext: Buffer, ivHex: string, authTagHex: string): Buffer {
const decipher = nodeCrypto.createDecipheriv(
ALGORITHM,
MASTER_KEY,
Buffer.from(ivHex, 'hex'),
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}
72 changes: 72 additions & 0 deletions src/controllers/documents.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Controller, Post, Get, Param, UseInterceptors, UploadedFile, Res, BadRequestException, NotFoundException } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import { encryptBuffer, decryptBuffer } from '../common/utils/crypto';
import axios from 'axios';

@Controller('api/v1/documents')
export class DocumentsController {
// Replace this with your actual Prisma service injection if available
private prisma = {
documentRegistry: {
create: async (args: any) => true,
findUnique: async (args: any) => ({
iv: 'mock-iv',
authTag: 'mock-tag',
mimeType: 'application/pdf',
name: 'invoice.pdf'
})
}
};

@Post('upload')
@UseInterceptors(FileInterceptor('document', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: (req, file, callback) => {
if (!['application/pdf', 'image/jpeg', 'image/png'].includes(file.mimetype)) {
return callback(new BadRequestException('Invalid format. Allowed: PDF, JPG, PNG.'), false);
}
callback(null, true);
},
}))
async uploadDocument(@UploadedFile() file: Express.Multer.File) {
if (!file) throw new BadRequestException('No file uploaded.');

// Encrypt raw buffer directly in system memory
const { ciphertext, iv, authTag } = encryptBuffer(file.buffer);

// Pin encrypted binary payload to Pinata API
const formData = new FormData();
const blob = new Blob([ciphertext], { type: 'application/octet-stream' });
formData.append('file', blob, file.originalname);

const ipfsRes = await axios.post('https://api.pinata.cloud/pinning/pinFileToIPFS', formData, {
headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` },
});

const cid = ipfsRes.data.IpfsHash;

// Persist layout pointers to DB
await this.prisma.documentRegistry.create({
data: { cid, iv, authTag, mimeType: file.mimetype, name: file.originalname },
});

return { success: true, cid };
}

@Get(':cid')
async getDocument(@Param('cid') cid: string, @Res() res: Response) {
const record = await this.prisma.documentRegistry.findUnique({ where: { cid } });
if (!record) throw new NotFoundException('Document mapping not found.');

const gatewayRes = await axios.get(`https://gateway.pinata.cloud/ipfs/${cid}`, {
responseType: 'arraybuffer',
});

const decrypted = decryptBuffer(Buffer.from(gatewayRes.data), record.iv, record.authTag);

res.setHeader('Content-Type', record.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${record.name}"`);
return res.send(decrypted);
}
}