Skip to content
Merged
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
155 changes: 155 additions & 0 deletions src/lib/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY_SIZE = 32;
const IV_SIZE = 16;
const TAG_SIZE = 16;
const SALT_SIZE = 16;

interface EncryptedData {
iv: string;
encryptedData: string;
authTag: string;
salt: string;
}

function deriveKey(password: string, salt: Buffer): Buffer {
return crypto.pbkdf2Sync(password, salt, 100000, KEY_SIZE, 'sha256');
}

export function encryptData(data: string, password: string): EncryptedData {
const salt = crypto.randomBytes(SALT_SIZE);
const key = deriveKey(password, salt);
const iv = crypto.randomBytes(IV_SIZE);

const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');

const authTag = cipher.getAuthTag();

return {
iv: iv.toString('hex'),
encryptedData: encrypted,
authTag: authTag.toString('hex'),
salt: salt.toString('hex'),
};
}

export function decryptData(encryptedData: EncryptedData, password: string): string {
const salt = Buffer.from(encryptedData.salt, 'hex');
const key = deriveKey(password, salt);
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');

const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);

let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}

export function hashData(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}

export function generateApiKey(): string {
return crypto.randomBytes(32).toString('hex');
}

export interface EncryptionConfig {
enabled: boolean;
algorithm: string;
keyRotationInterval: number; // ms
encryptPII: boolean;
encryptPayments: boolean;
}

export const defaultEncryptionConfig: EncryptionConfig = {
enabled: true,
algorithm: ALGORITHM,
keyRotationInterval: 90 * 24 * 60 * 60 * 1000, // 90 days
encryptPII: true,
encryptPayments: true,
};

export class KeyRotationManager {
private keys: Map<string, { key: Buffer; createdAt: number }> = new Map();
private rotationInterval: number;
private currentKeyId: string;

constructor(rotationIntervalMs: number = defaultEncryptionConfig.keyRotationInterval) {
this.rotationInterval = rotationIntervalMs;
this.currentKeyId = this.generateNewKeyId();
this.keys.set(this.currentKeyId, {
key: crypto.randomBytes(KEY_SIZE),
createdAt: Date.now(),
});
}

private generateNewKeyId(): string {
return crypto.randomBytes(8).toString('hex');
}

rotateKey(): string {
this.currentKeyId = this.generateNewKeyId();
this.keys.set(this.currentKeyId, {
key: crypto.randomBytes(KEY_SIZE),
createdAt: Date.now(),
});

const expiredKeys = Array.from(this.keys.entries())
.filter(([_, keyData]) => Date.now() - keyData.createdAt > this.rotationInterval * 2);

expiredKeys.forEach(([keyId]) => this.keys.delete(keyId));

return this.currentKeyId;
}

getCurrentKey(): Buffer {
const keyData = this.keys.get(this.currentKeyId);
if (!keyData) throw new Error('No current key available');
return keyData.key;
}

getKeyById(keyId: string): Buffer | undefined {
return this.keys.get(keyId)?.key;
}

getCurrentKeyId(): string {
return this.currentKeyId;
}

shouldRotate(): boolean {
const keyData = this.keys.get(this.currentKeyId);
if (!keyData) return false;
return Date.now() - keyData.createdAt > this.rotationInterval;
}
}

export class EncryptionValidator {
static validateEncryptedData(data: EncryptedData): boolean {
return (
data.iv && data.iv.length === IV_SIZE * 2 &&
data.encryptedData && data.encryptedData.length > 0 &&
data.authTag && data.authTag.length === TAG_SIZE * 2 &&
data.salt && data.salt.length === SALT_SIZE * 2
);
}

static validatePassword(password: string): boolean {
return password && password.length >= 12;
}

static validateEncryptionConfig(config: EncryptionConfig): boolean {
return (
typeof config.enabled === 'boolean' &&
config.algorithm === ALGORITHM &&
config.keyRotationInterval > 0 &&
typeof config.encryptPII === 'boolean' &&
typeof config.encryptPayments === 'boolean'
);
}
}
215 changes: 215 additions & 0 deletions src/lib/gdpr-compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
export enum DataResidency {
EU = 'EU',
US = 'US',
APAC = 'APAC',
AUTO = 'AUTO',
}

export enum ConsentType {
MARKETING = 'marketing',
ANALYTICS = 'analytics',
NECESSARY = 'necessary',
FUNCTIONAL = 'functional',
PERFORMANCE = 'performance',
}

export interface UserConsent {
userId: string;
consentTypes: Map<ConsentType, boolean>;
givenAt: Date;
expiresAt?: Date;
ipAddress: string;
}

export interface DataPortabilityRequest {
userId: string;
format: 'json' | 'csv' | 'xml';
requestedAt: Date;
completedAt?: Date;
dataUrl?: string;
expiresAt: Date;
}

export interface RightToBeForgettenRequest {
userId: string;
requestedAt: Date;
reason: string;
completedAt?: Date;
verificationToken: string;
verified: boolean;
}

export interface ComplianceAuditLog {
id: string;
action: string;
userId?: string;
dataCategory: string;
timestamp: Date;
details: Record<string, unknown>;
ipAddress: string;
userAgent: string;
}

export class GDPRComplianceManager {
private consents: Map<string, UserConsent> = new Map();
private dataPortabilityRequests: Map<string, DataPortabilityRequest> = new Map();
private rtbfRequests: Map<string, RightToBeForgettenRequest> = new Map();
private auditLogs: ComplianceAuditLog[] = [];
private dataResidency: DataResidency;

constructor(residency: DataResidency = DataResidency.AUTO) {
this.dataResidency = residency;
}

recordConsent(userId: string, consents: Map<ConsentType, boolean>, ipAddress: string): void {
const userConsent: UserConsent = {
userId,
consentTypes: new Map(consents),
givenAt: new Date(),
ipAddress,
};

this.consents.set(userId, userConsent);
this.logAuditEvent({
action: 'CONSENT_GIVEN',
userId,
dataCategory: 'CONSENT',
details: { consents: Object.fromEntries(consents) },
ipAddress,
});
}

getConsent(userId: string, consentType: ConsentType): boolean {
const consent = this.consents.get(userId);
if (!consent) return false;
return consent.consentTypes.get(consentType) || false;
}

requestDataPortability(userId: string, format: 'json' | 'csv' | 'xml', ipAddress: string): string {
const requestId = this.generateRequestId();
const request: DataPortabilityRequest = {
userId,
format,
requestedAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
};

this.dataPortabilityRequests.set(requestId, request);
this.logAuditEvent({
action: 'DATA_PORTABILITY_REQUESTED',
userId,
dataCategory: 'DATA_EXPORT',
details: { format },
ipAddress,
});

return requestId;
}

completeDataPortability(requestId: string, dataUrl: string): void {
const request = this.dataPortabilityRequests.get(requestId);
if (!request) throw new Error('Request not found');

request.completedAt = new Date();
request.dataUrl = dataUrl;

this.logAuditEvent({
action: 'DATA_PORTABILITY_COMPLETED',
userId: request.userId,
dataCategory: 'DATA_EXPORT',
details: { requestId },
ipAddress: '',
});
}

requestRightToBeForgotten(userId: string, reason: string, ipAddress: string): string {
const verificationToken = this.generateVerificationToken();
const requestId = this.generateRequestId();

const rtbfRequest: RightToBeForgettenRequest = {
userId,
requestedAt: new Date(),
reason,
verificationToken,
verified: false,
};

this.rtbfRequests.set(requestId, rtbfRequest);
this.logAuditEvent({
action: 'RTBF_REQUESTED',
userId,
dataCategory: 'DATA_DELETION',
details: { reason },
ipAddress,
});

return requestId;
}

verifyRightToBeForgotten(requestId: string, token: string): boolean {
const request = this.rtbfRequests.get(requestId);
if (!request || request.verified) return false;
if (request.verificationToken !== token) return false;

request.verified = true;
request.completedAt = new Date();

this.logAuditEvent({
action: 'RTBF_VERIFIED',
userId: request.userId,
dataCategory: 'DATA_DELETION',
details: { requestId },
ipAddress: '',
});

return true;
}

getAuditLog(filters?: { userId?: string; action?: string; startDate?: Date; endDate?: Date }): ComplianceAuditLog[] {
return this.auditLogs.filter((log) => {
if (filters?.userId && log.userId !== filters.userId) return false;
if (filters?.action && log.action !== filters.action) return false;
if (filters?.startDate && log.timestamp < filters.startDate) return false;
if (filters?.endDate && log.timestamp > filters.endDate) return false;
return true;
});
}

generateComplianceReport(userId: string): Record<string, unknown> {
return {
userId,
consentStatus: this.consents.get(userId)?.consentTypes,
auditLog: this.getAuditLog({ userId }),
dataResidency: this.dataResidency,
reportGeneratedAt: new Date(),
};
}

private logAuditEvent(event: Omit<ComplianceAuditLog, 'id' | 'timestamp'>): void {
const log: ComplianceAuditLog = {
...event,
id: this.generateRequestId(),
timestamp: new Date(),
userAgent: '',
};
this.auditLogs.push(log);
}

private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

private generateVerificationToken(): string {
return Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

setDataResidency(residency: DataResidency): void {
this.dataResidency = residency;
}

getDataResidency(): DataResidency {
return this.dataResidency;
}
}
12 changes: 12 additions & 0 deletions src/lib/security-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Encryption at rest and in transit (#1017)
export * from './encryption';
export * from './tls-config';

// GDPR compliance and data residency (#1018)
export * from './gdpr-compliance';

// Vulnerability scanning and pen testing (#1019)
export * from './vulnerability-scanner';

// Zero-trust security model (#1020)
export * from './zero-trust-security';
Loading