recoder-security
Version:
Enterprise-grade security and compliance layer for CodeCraft CLI
520 lines • 21.4 kB
JavaScript
"use strict";
/**
* End-to-End Data Encryption System
* Provides comprehensive encryption for user data, API communications, and file storage
* with key management, rotation, and compliance features
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataEncryption = void 0;
const crypto_1 = require("crypto");
const util_1 = require("util");
const node_forge_1 = __importDefault(require("node-forge"));
const shared_1 = require("@recoder/shared");
class DataEncryption {
constructor(config) {
this.logger = new shared_1.Logger('DataEncryption');
this.masterKeys = new Map();
this.keyMetadata = new Map();
this.currentKeyVersion = '1';
this.config = {
algorithm: 'aes-256-gcm',
keyDerivation: 'pbkdf2',
keySize: 256,
ivSize: 12,
tagSize: 16,
iterations: 100000,
saltSize: 32,
compressionEnabled: true,
keyRotationDays: 90,
...config,
};
this.initializeMasterKey();
}
/**
* Encrypt data with context and metadata
*/
async encryptData(data, context, password) {
this.logger.debug(`Encrypting ${context.dataType} data`);
try {
// Get or derive encryption key
const key = password
? await this.deriveKeyFromPassword(password)
: await this.getCurrentEncryptionKey();
// Convert data to buffer
let dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
let compressed = false;
let originalSize = dataBuffer.length;
// Apply compression if enabled and beneficial
if (this.config.compressionEnabled && dataBuffer.length > 1024) {
const compressedData = await this.compressData(dataBuffer);
if (compressedData.length < dataBuffer.length * 0.9) {
dataBuffer = compressedData;
compressed = true;
}
}
// Generate IV and salt
const iv = (0, crypto_1.randomBytes)(this.config.ivSize);
const salt = (0, crypto_1.randomBytes)(this.config.saltSize);
// Create cipher
const cipher = (0, crypto_1.createCipheriv)(this.config.algorithm, key, iv);
// Encrypt data
const encrypted = Buffer.concat([
cipher.update(dataBuffer),
cipher.final(),
]);
// Get authentication tag (for GCM mode)
const tag = this.config.algorithm.includes('gcm')
? cipher.getAuthTag()
: undefined;
// Calculate checksum
const checksum = (0, crypto_1.createHash)('sha256').update(dataBuffer).digest('hex');
const result = {
ciphertext: encrypted.toString('base64'),
iv: iv.toString('base64'),
tag: tag?.toString('base64'),
salt: salt.toString('base64'),
algorithm: this.config.algorithm,
keyVersion: this.currentKeyVersion,
timestamp: Date.now(),
metadata: {
compressed,
originalSize: compressed ? originalSize : undefined,
checksum,
},
};
// Log encryption event (without sensitive data)
await this.logEncryptionEvent('encrypt', context, {
keyVersion: this.currentKeyVersion,
algorithm: this.config.algorithm,
dataSize: dataBuffer.length,
compressed,
});
return result;
}
catch (error) {
this.logger.error('Encryption failed:', error);
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Decrypt data with validation and integrity checks
*/
async decryptData(encryptedData, context, password) {
this.logger.debug(`Decrypting ${context.dataType} data`);
try {
// Get decryption key
const key = password
? await this.deriveKeyFromPassword(password, Buffer.from(encryptedData.salt, 'base64'))
: await this.getEncryptionKey(encryptedData.keyVersion);
// Decode encrypted components
const ciphertext = Buffer.from(encryptedData.ciphertext, 'base64');
const iv = Buffer.from(encryptedData.iv, 'base64');
const tag = encryptedData.tag ? Buffer.from(encryptedData.tag, 'base64') : undefined;
// Create decipher
const decipher = (0, crypto_1.createDecipheriv)(encryptedData.algorithm, key, iv);
// Set auth tag for GCM mode
if (tag && encryptedData.algorithm.includes('gcm')) {
decipher.setAuthTag(tag);
}
// Decrypt data
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
// Decompress if needed
let finalData = decrypted;
if (encryptedData.metadata?.compressed) {
finalData = await this.decompressData(decrypted);
}
// Verify checksum
if (encryptedData.metadata?.checksum) {
const calculatedChecksum = (0, crypto_1.createHash)('sha256').update(finalData).digest('hex');
if (calculatedChecksum !== encryptedData.metadata.checksum) {
throw new Error('Data integrity check failed - checksum mismatch');
}
}
// Log decryption event
await this.logEncryptionEvent('decrypt', context, {
keyVersion: encryptedData.keyVersion,
algorithm: encryptedData.algorithm,
dataSize: finalData.length,
compressed: encryptedData.metadata?.compressed || false,
});
// Update key usage
await this.updateKeyUsage(encryptedData.keyVersion);
return finalData;
}
catch (error) {
this.logger.error('Decryption failed:', error);
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Encrypt file with streaming support for large files
*/
async encryptFile(inputPath, outputPath, context) {
const fs = require('fs-extra');
const { pipeline } = require('stream');
const { createReadStream, createWriteStream } = require('fs');
this.logger.info(`Encrypting file: ${inputPath}`);
try {
const key = await this.getCurrentEncryptionKey();
const iv = (0, crypto_1.randomBytes)(this.config.ivSize);
const salt = (0, crypto_1.randomBytes)(this.config.saltSize);
// Get file stats
const stats = await fs.stat(inputPath);
const originalSize = stats.size;
// Create cipher stream
const cipher = (0, crypto_1.createCipheriv)(this.config.algorithm, key, iv);
// Setup file streams
const readStream = createReadStream(inputPath);
const writeStream = createWriteStream(outputPath);
// Calculate checksum while reading
const hashStream = (0, crypto_1.createHash)('sha256');
readStream.on('data', chunk => hashStream.update(chunk));
// Encrypt file
await (0, util_1.promisify)(pipeline)(readStream, cipher, writeStream);
// Get authentication tag
const tag = this.config.algorithm.includes('gcm')
? cipher.getAuthTag()
: undefined;
const checksum = hashStream.digest('hex');
const result = {
ciphertext: outputPath, // File path instead of base64
iv: iv.toString('base64'),
tag: tag?.toString('base64'),
salt: salt.toString('base64'),
algorithm: this.config.algorithm,
keyVersion: this.currentKeyVersion,
timestamp: Date.now(),
metadata: {
compressed: false,
originalSize,
checksum,
},
};
await this.logEncryptionEvent('encrypt_file', context, {
keyVersion: this.currentKeyVersion,
algorithm: this.config.algorithm,
dataSize: originalSize,
filePath: inputPath,
});
return result;
}
catch (error) {
this.logger.error('File encryption failed:', error);
throw new Error(`File encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Decrypt file with streaming support
*/
async decryptFile(encryptedData, outputPath, context) {
const { pipeline } = require('stream');
const { createReadStream, createWriteStream } = require('fs');
this.logger.info(`Decrypting file to: ${outputPath}`);
try {
const key = await this.getEncryptionKey(encryptedData.keyVersion);
const iv = Buffer.from(encryptedData.iv, 'base64');
const tag = encryptedData.tag ? Buffer.from(encryptedData.tag, 'base64') : undefined;
// Create decipher stream
const decipher = (0, crypto_1.createDecipheriv)(encryptedData.algorithm, key, iv);
if (tag && encryptedData.algorithm.includes('gcm')) {
decipher.setAuthTag(tag);
}
// Setup file streams
const readStream = createReadStream(encryptedData.ciphertext); // File path
const writeStream = createWriteStream(outputPath);
// Calculate checksum while writing
const hashStream = (0, crypto_1.createHash)('sha256');
writeStream.on('pipe', () => {
writeStream.on('data', chunk => hashStream.update(chunk));
});
// Decrypt file
await (0, util_1.promisify)(pipeline)(readStream, decipher, writeStream);
// Verify checksum
if (encryptedData.metadata?.checksum) {
const calculatedChecksum = hashStream.digest('hex');
if (calculatedChecksum !== encryptedData.metadata.checksum) {
throw new Error('File integrity check failed - checksum mismatch');
}
}
await this.logEncryptionEvent('decrypt_file', context, {
keyVersion: encryptedData.keyVersion,
algorithm: encryptedData.algorithm,
filePath: outputPath,
});
}
catch (error) {
this.logger.error('File decryption failed:', error);
throw new Error(`File decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Generate RSA key pair for asymmetric encryption
*/
async generateRSAKeyPair(keySize = 2048) {
this.logger.info(`Generating RSA key pair (${keySize} bits)`);
const keypair = node_forge_1.default.pki.rsa.generateKeyPair(keySize);
const publicKeyPem = node_forge_1.default.pki.publicKeyToPem(keypair.publicKey);
const privateKeyPem = node_forge_1.default.pki.privateKeyToPem(keypair.privateKey);
const keyId = this.generateKeyId();
// Store key metadata
const metadata = {
id: keyId,
version: this.currentKeyVersion,
algorithm: `rsa-${keySize}`,
created: Date.now(),
lastUsed: Date.now(),
rotationDue: Date.now() + (this.config.keyRotationDays * 24 * 60 * 60 * 1000),
purpose: 'encryption',
status: 'active',
};
this.keyMetadata.set(keyId, metadata);
return {
publicKey: publicKeyPem,
privateKey: privateKeyPem,
keyId,
};
}
/**
* Encrypt with RSA public key
*/
async encryptWithRSA(data, publicKeyPem) {
try {
const publicKey = node_forge_1.default.pki.publicKeyFromPem(publicKeyPem);
const encrypted = publicKey.encrypt(data, 'RSA-OAEP', {
md: node_forge_1.default.md.sha256.create(),
mgf1: node_forge_1.default.mgf.mgf1.create(node_forge_1.default.md.sha256.create()),
});
return node_forge_1.default.util.encode64(encrypted);
}
catch (error) {
this.logger.error('RSA encryption failed:', error);
throw new Error(`RSA encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Decrypt with RSA private key
*/
async decryptWithRSA(encryptedData, privateKeyPem) {
try {
const privateKey = node_forge_1.default.pki.privateKeyFromPem(privateKeyPem);
const encrypted = node_forge_1.default.util.decode64(encryptedData);
const decrypted = privateKey.decrypt(encrypted, 'RSA-OAEP', {
md: node_forge_1.default.md.sha256.create(),
mgf1: node_forge_1.default.mgf.mgf1.create(node_forge_1.default.md.sha256.create()),
});
return decrypted;
}
catch (error) {
this.logger.error('RSA decryption failed:', error);
throw new Error(`RSA decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Sign data with RSA private key
*/
async signData(data, privateKeyPem) {
try {
const privateKey = node_forge_1.default.pki.privateKeyFromPem(privateKeyPem);
const md = node_forge_1.default.md.sha256.create();
md.update(data, 'utf8');
const signature = privateKey.sign(md);
return node_forge_1.default.util.encode64(signature);
}
catch (error) {
this.logger.error('Data signing failed:', error);
throw new Error(`Data signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Verify signature with RSA public key
*/
async verifySignature(data, signature, publicKeyPem) {
try {
const publicKey = node_forge_1.default.pki.publicKeyFromPem(publicKeyPem);
const md = node_forge_1.default.md.sha256.create();
md.update(data, 'utf8');
const signatureBytes = node_forge_1.default.util.decode64(signature);
return publicKey.verify(md.digest().bytes(), signatureBytes);
}
catch (error) {
this.logger.error('Signature verification failed:', error);
return false;
}
}
/**
* Rotate encryption keys
*/
async rotateKeys() {
this.logger.info('Starting key rotation');
try {
// Create new key version
const newVersion = (parseInt(this.currentKeyVersion) + 1).toString();
// Generate new master key
const newMasterKey = (0, crypto_1.randomBytes)(this.config.keySize / 8);
this.masterKeys.set(newVersion, newMasterKey);
// Update metadata for old key
const oldMetadata = this.keyMetadata.get(this.currentKeyVersion);
if (oldMetadata) {
oldMetadata.status = 'deprecated';
this.keyMetadata.set(this.currentKeyVersion, oldMetadata);
}
// Create metadata for new key
const newMetadata = {
id: this.generateKeyId(),
version: newVersion,
algorithm: this.config.algorithm,
created: Date.now(),
lastUsed: Date.now(),
rotationDue: Date.now() + (this.config.keyRotationDays * 24 * 60 * 60 * 1000),
purpose: 'encryption',
status: 'active',
};
this.keyMetadata.set(newVersion, newMetadata);
this.currentKeyVersion = newVersion;
this.logger.info(`Key rotation completed. New version: ${newVersion}`);
// Schedule cleanup of old keys (after grace period)
setTimeout(() => {
this.cleanupOldKeys();
}, 30 * 24 * 60 * 60 * 1000); // 30 days
}
catch (error) {
this.logger.error('Key rotation failed:', error);
throw error;
}
}
/**
* Get encryption statistics
*/
getEncryptionStats() {
const activeKeys = Array.from(this.keyMetadata.values()).filter(k => k.status === 'active').length;
const deprecatedKeys = Array.from(this.keyMetadata.values()).filter(k => k.status === 'deprecated').length;
const currentMetadata = this.keyMetadata.get(this.currentKeyVersion);
const rotationDue = currentMetadata?.rotationDue || 0;
return {
currentKeyVersion: this.currentKeyVersion,
totalKeys: this.masterKeys.size,
algorithm: this.config.algorithm,
keyRotationDue: rotationDue,
activeKeys,
deprecatedKeys,
};
}
/**
* Initialize master encryption key
*/
initializeMasterKey() {
// In production, this would load from secure key management service
const masterKey = process.env.ENCRYPTION_MASTER_KEY
? Buffer.from(process.env.ENCRYPTION_MASTER_KEY, 'hex')
: (0, crypto_1.randomBytes)(this.config.keySize / 8);
this.masterKeys.set(this.currentKeyVersion, masterKey);
// Create initial metadata
const metadata = {
id: this.generateKeyId(),
version: this.currentKeyVersion,
algorithm: this.config.algorithm,
created: Date.now(),
lastUsed: Date.now(),
rotationDue: Date.now() + (this.config.keyRotationDays * 24 * 60 * 60 * 1000),
purpose: 'encryption',
status: 'active',
};
this.keyMetadata.set(this.currentKeyVersion, metadata);
}
/**
* Get current encryption key
*/
async getCurrentEncryptionKey() {
const key = this.masterKeys.get(this.currentKeyVersion);
if (!key) {
throw new Error('Master encryption key not found');
}
return key;
}
/**
* Get encryption key by version
*/
async getEncryptionKey(version) {
const key = this.masterKeys.get(version);
if (!key) {
throw new Error(`Encryption key version ${version} not found`);
}
return key;
}
/**
* Derive key from password using PBKDF2
*/
async deriveKeyFromPassword(password, salt) {
const derivationSalt = salt || (0, crypto_1.randomBytes)(this.config.saltSize);
return (0, crypto_1.pbkdf2Sync)(password, derivationSalt, this.config.iterations, this.config.keySize / 8, 'sha256');
}
/**
* Compress data using gzip
*/
async compressData(data) {
const zlib = require('zlib');
return (0, util_1.promisify)(zlib.gzip)(data);
}
/**
* Decompress data using gzip
*/
async decompressData(data) {
const zlib = require('zlib');
return (0, util_1.promisify)(zlib.gunzip)(data);
}
/**
* Generate unique key ID
*/
generateKeyId() {
return (0, crypto_1.createHash)('sha256')
.update((0, crypto_1.randomBytes)(32))
.digest('hex')
.substring(0, 16);
}
/**
* Log encryption events for audit trail
*/
async logEncryptionEvent(operation, context, metadata) {
const event = {
timestamp: new Date().toISOString(),
operation,
userId: context.userId,
dataType: context.dataType,
purpose: context.purpose,
complianceLevel: context.complianceLevel,
...metadata,
};
this.logger.info('Encryption event', event);
}
/**
* Update key usage timestamp
*/
async updateKeyUsage(version) {
const metadata = this.keyMetadata.get(version);
if (metadata) {
metadata.lastUsed = Date.now();
this.keyMetadata.set(version, metadata);
}
}
/**
* Cleanup old deprecated keys
*/
cleanupOldKeys() {
const cutoffTime = Date.now() - (90 * 24 * 60 * 60 * 1000); // 90 days ago
for (const [version, metadata] of this.keyMetadata) {
if (metadata.status === 'deprecated' && metadata.lastUsed < cutoffTime) {
this.masterKeys.delete(version);
this.keyMetadata.delete(version);
this.logger.info(`Cleaned up old key version: ${version}`);
}
}
}
}
exports.DataEncryption = DataEncryption;
//# sourceMappingURL=data-encryption.js.map