/** * HIPAA Compliance Layer * * Provides comprehensive HIPAA compliance features: * - Audit logging (access, modification, disclosure) * - Patient consent management * - Data encryption (at rest and in transit) * - Access control and authentication * - Break-the-glass emergency access * - Minimum necessary standard enforcement * - Business Associate Agreement tracking * * IMPORTANT: This module implements the technical safeguards required by HIPAA. * Administrative and physical safeguards must be implemented organizationally. */ import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; import { EventEmitter } from 'events'; // ═══════════════════════════════════════════════════════════════════════════════ // AUDIT LOG TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface AuditLogEntry { id: string; timestamp: Date; eventType: AuditEventType; action: AuditAction; outcome: 'success' | 'failure' | 'error'; // Actor information actor: { userId: string; userName?: string; role: string; organization?: string; ipAddress: string; userAgent?: string; sessionId?: string; }; // Resource information resource: { type: 'patient' | 'record' | 'report' | 'order' | 'document' | 'system' | 'configuration'; id?: string; patientId?: string; description?: string; }; // Additional context details?: { fieldsAccessed?: string[]; fieldsModified?: string[]; previousValues?: Record; newValues?: Record; query?: string; reason?: string; emergencyAccess?: boolean; consentId?: string; }; // Security metadata security: { authMethod: 'password' | 'mfa' | 'sso' | 'certificate' | 'api-key' | 'oauth'; encryptionUsed: boolean; integrityVerified: boolean; accessLevel: 'normal' | 'elevated' | 'emergency'; }; // Hash for tamper detection entryHash?: string; previousEntryHash?: string; } export type AuditEventType = | 'authentication' | 'authorization' | 'access' | 'modification' | 'disclosure' | 'deletion' | 'export' | 'print' | 'query' | 'emergency-access' | 'consent-change' | 'system-event'; export type AuditAction = | 'login' | 'logout' | 'login-failed' | 'session-timeout' | 'view' | 'search' | 'download' | 'print' | 'export' | 'create' | 'update' | 'delete' | 'restore' | 'share' | 'transmit' | 'receive' | 'grant-access' | 'revoke-access' | 'consent-obtained' | 'consent-revoked' | 'emergency-override' | 'break-the-glass'; // ═══════════════════════════════════════════════════════════════════════════════ // CONSENT MANAGEMENT TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface PatientConsent { id: string; patientId: string; consentType: ConsentType; status: 'active' | 'revoked' | 'expired' | 'pending'; scope: { dataCategories: DataCategory[]; purposes: ConsentPurpose[]; recipients?: string[]; excludedProviders?: string[]; excludedData?: string[]; }; validity: { effectiveDate: Date; expirationDate?: Date; autoRenew?: boolean; }; capture: { method: 'written' | 'electronic' | 'verbal' | 'implied'; documentId?: string; witnessName?: string; witnessDate?: Date; ipAddress?: string; deviceInfo?: string; }; audit: { createdAt: Date; createdBy: string; modifiedAt?: Date; modifiedBy?: string; revokedAt?: Date; revokedBy?: string; revocationReason?: string; }; } export type ConsentType = | 'treatment' // Consent for treatment | 'payment' // Consent for payment processing | 'healthcare-ops' // Consent for healthcare operations | 'research' // Consent for research use | 'marketing' // Consent for marketing communications | 'disclosure' // Consent for specific disclosure | 'genetic-testing' // Consent for genetic testing | 'clinical-trial' // Consent for clinical trial participation | 'data-sharing' // Consent for data sharing with third parties | 'hie' // Consent for Health Information Exchange | 'psychotherapy-notes' // Special consent for psychotherapy notes | 'substance-abuse'; // 42 CFR Part 2 consent for substance abuse records export type DataCategory = | 'demographics' | 'diagnoses' | 'medications' | 'lab-results' | 'imaging' | 'procedures' | 'genomics' | 'mental-health' | 'substance-abuse' | 'hiv-status' | 'sexual-health' | 'domestic-violence' | 'psychotherapy-notes' | 'billing'; export type ConsentPurpose = | 'treatment' | 'payment' | 'healthcare-operations' | 'research' | 'public-health' | 'legal' | 'insurance' | 'marketing' | 'fundraising' | 'care-coordination' | 'quality-improvement'; // ═══════════════════════════════════════════════════════════════════════════════ // ACCESS CONTROL TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface AccessPolicy { id: string; name: string; description?: string; subjects: { roles?: string[]; users?: string[]; organizations?: string[]; departments?: string[]; }; resources: { types?: string[]; patientIds?: string[]; dataCategories?: DataCategory[]; }; permissions: { actions: ('read' | 'write' | 'delete' | 'share' | 'print' | 'export')[]; conditions?: AccessCondition[]; }; priority: number; enabled: boolean; } export interface AccessCondition { type: 'time-of-day' | 'ip-range' | 'location' | 'mfa-required' | 'relationship' | 'purpose' | 'emergency-only'; parameters: Record; } export interface AccessDecision { allowed: boolean; policy?: string; reason: string; conditions?: string[]; requiresElevation?: boolean; auditRequired: boolean; } // ═══════════════════════════════════════════════════════════════════════════════ // ENCRYPTION TYPES // ═══════════════════════════════════════════════════════════════════════════════ export interface EncryptionConfig { algorithm: 'aes-256-gcm' | 'aes-256-cbc' | 'chacha20-poly1305'; keyDerivation: 'scrypt' | 'pbkdf2' | 'argon2'; keyRotationDays: number; fieldLevelEncryption: boolean; encryptedFields?: string[]; } export interface EncryptedData { ciphertext: string; iv: string; authTag?: string; keyId: string; algorithm: string; encryptedAt: Date; } export interface DataMaskingConfig { ssnPattern: 'full' | 'last4' | 'hidden'; dobPattern: 'full' | 'year-only' | 'age-only' | 'hidden'; phonePattern: 'full' | 'last4' | 'hidden'; addressPattern: 'full' | 'city-state' | 'zip-only' | 'hidden'; mrnPattern: 'full' | 'last4' | 'hidden'; genomicPattern: 'full' | 'summary' | 'hidden'; } // ═══════════════════════════════════════════════════════════════════════════════ // HIPAA COMPLIANCE SERVICE // ═══════════════════════════════════════════════════════════════════════════════ export class HIPAAComplianceService extends EventEmitter { private auditStore: AuditLogEntry[] = []; private consents: Map = new Map(); private accessPolicies: AccessPolicy[] = []; private encryptionKeys: Map = new Map(); private activeKeyId: string; private config: { encryption: EncryptionConfig; masking: DataMaskingConfig; auditRetentionDays: number; emergencyAccessEnabled: boolean; }; private lastEntryHash?: string; constructor(config?: Partial<{ encryption: Partial; masking: Partial; auditRetentionDays: number; emergencyAccessEnabled: boolean; }>) { super(); this.config = { encryption: { algorithm: 'aes-256-gcm', keyDerivation: 'scrypt', keyRotationDays: 90, fieldLevelEncryption: true, encryptedFields: ['ssn', 'dob', 'address', 'phone', 'genomics', 'mentalHealth'], ...config?.encryption }, masking: { ssnPattern: 'last4', dobPattern: 'age-only', phonePattern: 'last4', addressPattern: 'city-state', mrnPattern: 'full', genomicPattern: 'summary', ...config?.masking }, auditRetentionDays: config?.auditRetentionDays || 2190, // 6 years per HIPAA emergencyAccessEnabled: config?.emergencyAccessEnabled ?? true }; // Initialize encryption key this.activeKeyId = this.generateKeyId(); this.initializeEncryptionKey(); } // ═══════════════════════════════════════════════════════════════════════════════ // AUDIT LOGGING // ═══════════════════════════════════════════════════════════════════════════════ /** * Log an audit event */ async logAuditEvent(entry: Omit): Promise { const id = this.generateAuditId(); const timestamp = new Date(); // Create the entry const fullEntry: AuditLogEntry = { id, timestamp, ...entry, previousEntryHash: this.lastEntryHash }; // Calculate hash for tamper detection fullEntry.entryHash = this.calculateEntryHash(fullEntry); this.lastEntryHash = fullEntry.entryHash; // Store the entry this.auditStore.push(fullEntry); // Emit event for external handlers this.emit('audit-event', fullEntry); // Check for suspicious patterns await this.detectSuspiciousActivity(fullEntry); return id; } /** * Log PHI access event */ async logPHIAccess(params: { userId: string; userName?: string; role: string; ipAddress: string; patientId: string; action: 'view' | 'download' | 'print' | 'export'; resourceType: AuditLogEntry['resource']['type']; resourceId?: string; fieldsAccessed?: string[]; reason?: string; emergencyAccess?: boolean; consentId?: string; }): Promise { return this.logAuditEvent({ eventType: params.emergencyAccess ? 'emergency-access' : 'access', action: params.action, outcome: 'success', actor: { userId: params.userId, userName: params.userName, role: params.role, ipAddress: params.ipAddress }, resource: { type: params.resourceType, id: params.resourceId, patientId: params.patientId }, details: { fieldsAccessed: params.fieldsAccessed, reason: params.reason, emergencyAccess: params.emergencyAccess, consentId: params.consentId }, security: { authMethod: 'mfa', // Assuming MFA for PHI access encryptionUsed: true, integrityVerified: true, accessLevel: params.emergencyAccess ? 'emergency' : 'normal' } }); } /** * Log PHI modification event */ async logPHIModification(params: { userId: string; role: string; ipAddress: string; patientId: string; action: 'create' | 'update' | 'delete'; resourceType: AuditLogEntry['resource']['type']; resourceId?: string; fieldsModified?: string[]; previousValues?: Record; newValues?: Record; reason?: string; }): Promise { return this.logAuditEvent({ eventType: 'modification', action: params.action, outcome: 'success', actor: { userId: params.userId, role: params.role, ipAddress: params.ipAddress }, resource: { type: params.resourceType, id: params.resourceId, patientId: params.patientId }, details: { fieldsModified: params.fieldsModified, previousValues: params.previousValues, newValues: params.newValues, reason: params.reason }, security: { authMethod: 'mfa', encryptionUsed: true, integrityVerified: true, accessLevel: 'normal' } }); } /** * Log authentication event */ async logAuthentication(params: { userId: string; ipAddress: string; action: 'login' | 'logout' | 'login-failed' | 'session-timeout'; authMethod: AuditLogEntry['security']['authMethod']; success: boolean; failureReason?: string; }): Promise { return this.logAuditEvent({ eventType: 'authentication', action: params.action, outcome: params.success ? 'success' : 'failure', actor: { userId: params.userId, role: 'unknown', ipAddress: params.ipAddress }, resource: { type: 'system' }, details: params.failureReason ? { reason: params.failureReason } : undefined, security: { authMethod: params.authMethod, encryptionUsed: true, integrityVerified: true, accessLevel: 'normal' } }); } /** * Query audit logs */ queryAuditLogs(params: { startDate?: Date; endDate?: Date; patientId?: string; userId?: string; eventType?: AuditEventType; action?: AuditAction; outcome?: 'success' | 'failure'; limit?: number; offset?: number; }): { entries: AuditLogEntry[]; total: number } { let filtered = [...this.auditStore]; if (params.startDate) { filtered = filtered.filter(e => e.timestamp >= params.startDate!); } if (params.endDate) { filtered = filtered.filter(e => e.timestamp <= params.endDate!); } if (params.patientId) { filtered = filtered.filter(e => e.resource.patientId === params.patientId); } if (params.userId) { filtered = filtered.filter(e => e.actor.userId === params.userId); } if (params.eventType) { filtered = filtered.filter(e => e.eventType === params.eventType); } if (params.action) { filtered = filtered.filter(e => e.action === params.action); } if (params.outcome) { filtered = filtered.filter(e => e.outcome === params.outcome); } // Sort by timestamp descending filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); const total = filtered.length; const offset = params.offset || 0; const limit = params.limit || 100; return { entries: filtered.slice(offset, offset + limit), total }; } /** * Get accounting of disclosures for a patient (HIPAA requirement) */ getAccountingOfDisclosures(patientId: string, startDate?: Date, endDate?: Date): AuditLogEntry[] { return this.auditStore.filter(entry => entry.resource.patientId === patientId && entry.eventType === 'disclosure' && (!startDate || entry.timestamp >= startDate) && (!endDate || entry.timestamp <= endDate) ).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } /** * Verify audit log integrity */ verifyAuditLogIntegrity(): { valid: boolean; errors: string[] } { const errors: string[] = []; for (let i = 0; i < this.auditStore.length; i++) { const entry = this.auditStore[i]; // Verify hash const { entryHash: _hash, ...entryWithoutHash } = entry; const calculatedHash = this.calculateEntryHash(entryWithoutHash); if (calculatedHash !== entry.entryHash) { errors.push(`Entry ${entry.id} has been tampered with (hash mismatch)`); } // Verify chain if (i > 0 && entry.previousEntryHash !== this.auditStore[i - 1].entryHash) { errors.push(`Entry ${entry.id} has broken chain link`); } } return { valid: errors.length === 0, errors }; } // ═══════════════════════════════════════════════════════════════════════════════ // CONSENT MANAGEMENT // ═══════════════════════════════════════════════════════════════════════════════ /** * Record a patient consent */ async recordConsent(consent: Omit): Promise { const id = this.generateConsentId(); const now = new Date(); const fullConsent: PatientConsent = { id, ...consent, audit: { createdAt: now, createdBy: consent.capture.method === 'electronic' ? 'system' : 'staff' } }; // Store consent const patientConsents = this.consents.get(consent.patientId) || []; patientConsents.push(fullConsent); this.consents.set(consent.patientId, patientConsents); // Log the consent capture await this.logAuditEvent({ eventType: 'consent-change', action: 'consent-obtained', outcome: 'success', actor: { userId: fullConsent.audit.createdBy, role: 'system', ipAddress: consent.capture.ipAddress || 'unknown' }, resource: { type: 'patient', patientId: consent.patientId, description: `${consent.consentType} consent` }, details: { consentId: id }, security: { authMethod: 'password', encryptionUsed: true, integrityVerified: true, accessLevel: 'normal' } }); this.emit('consent-recorded', fullConsent); return id; } /** * Revoke a patient consent */ async revokeConsent(consentId: string, revokedBy: string, reason?: string): Promise { for (const [patientId, consents] of this.consents) { const consent = consents.find(c => c.id === consentId); if (consent) { consent.status = 'revoked'; consent.audit.revokedAt = new Date(); consent.audit.revokedBy = revokedBy; consent.audit.revocationReason = reason; // Log the revocation await this.logAuditEvent({ eventType: 'consent-change', action: 'consent-revoked', outcome: 'success', actor: { userId: revokedBy, role: 'patient', ipAddress: 'unknown' }, resource: { type: 'patient', patientId, description: `${consent.consentType} consent revoked` }, details: { consentId, reason }, security: { authMethod: 'password', encryptionUsed: true, integrityVerified: true, accessLevel: 'normal' } }); this.emit('consent-revoked', consent); return true; } } return false; } /** * Check if a patient has consented to a specific use */ checkConsent(patientId: string, purpose: ConsentPurpose, dataCategory?: DataCategory): { hasConsent: boolean; consentId?: string; restrictions?: string[]; } { const patientConsents = this.consents.get(patientId) || []; // Find active consent that covers the purpose const validConsent = patientConsents.find(c => c.status === 'active' && c.scope.purposes.includes(purpose) && (!dataCategory || c.scope.dataCategories.includes(dataCategory)) && (!c.validity.expirationDate || c.validity.expirationDate > new Date()) ); if (validConsent) { return { hasConsent: true, consentId: validConsent.id, restrictions: validConsent.scope.excludedData }; } // Check if TPO (Treatment, Payment, Operations) - may not require explicit consent if (['treatment', 'payment', 'healthcare-operations'].includes(purpose)) { // TPO typically covered by Notice of Privacy Practices return { hasConsent: true, restrictions: [] }; } return { hasConsent: false }; } /** * Get all consents for a patient */ getPatientConsents(patientId: string): PatientConsent[] { return this.consents.get(patientId) || []; } // ═══════════════════════════════════════════════════════════════════════════════ // ACCESS CONTROL // ═══════════════════════════════════════════════════════════════════════════════ /** * Add an access policy */ addAccessPolicy(policy: Omit): string { const id = this.generatePolicyId(); const fullPolicy: AccessPolicy = { id, ...policy }; this.accessPolicies.push(fullPolicy); this.accessPolicies.sort((a, b) => b.priority - a.priority); return id; } /** * Check if access should be allowed */ checkAccess(request: { userId: string; role: string; organization?: string; action: 'read' | 'write' | 'delete' | 'share' | 'print' | 'export'; resourceType: string; patientId?: string; dataCategory?: DataCategory; purpose?: ConsentPurpose; ipAddress?: string; time?: Date; emergencyAccess?: boolean; }): AccessDecision { const time = request.time || new Date(); // Emergency access bypass (break-the-glass) if (request.emergencyAccess && this.config.emergencyAccessEnabled) { return { allowed: true, reason: 'Emergency access granted (break-the-glass)', conditions: ['Must document emergency reason', 'Subject to post-hoc review'], auditRequired: true }; } // Check consent if accessing patient data if (request.patientId && request.purpose) { const consentCheck = this.checkConsent( request.patientId, request.purpose, request.dataCategory ); if (!consentCheck.hasConsent) { return { allowed: false, reason: `No consent for ${request.purpose} purpose`, auditRequired: true }; } } // Evaluate policies in priority order for (const policy of this.accessPolicies) { if (!policy.enabled) continue; // Check if policy applies to this subject const subjectMatch = (!policy.subjects.roles || policy.subjects.roles.includes(request.role)) && (!policy.subjects.users || policy.subjects.users.includes(request.userId)) && (!policy.subjects.organizations || policy.subjects.organizations.includes(request.organization || '')); if (!subjectMatch) continue; // Check if policy applies to this resource const resourceMatch = (!policy.resources.types || policy.resources.types.includes(request.resourceType)) && (!policy.resources.patientIds || !request.patientId || policy.resources.patientIds.includes(request.patientId)) && (!policy.resources.dataCategories || !request.dataCategory || policy.resources.dataCategories.includes(request.dataCategory)); if (!resourceMatch) continue; // Check if action is permitted if (!policy.permissions.actions.includes(request.action)) { continue; } // Evaluate conditions const conditionResults = this.evaluateConditions(policy.permissions.conditions || [], request, time); if (conditionResults.allMet) { return { allowed: true, policy: policy.id, reason: `Access granted by policy: ${policy.name}`, conditions: conditionResults.messages, auditRequired: true }; } else if (conditionResults.requiresElevation) { return { allowed: false, policy: policy.id, reason: 'Access requires additional verification', requiresElevation: true, conditions: conditionResults.messages, auditRequired: true }; } } // Default deny return { allowed: false, reason: 'No matching policy found - access denied by default', auditRequired: true }; } private evaluateConditions( conditions: AccessCondition[], request: any, time: Date ): { allMet: boolean; requiresElevation: boolean; messages: string[] } { const messages: string[] = []; let requiresElevation = false; for (const condition of conditions) { switch (condition.type) { case 'time-of-day': const hour = time.getHours(); const startHour = condition.parameters.startHour || 0; const endHour = condition.parameters.endHour || 24; if (hour < startHour || hour >= endHour) { messages.push(`Access outside permitted hours (${startHour}:00 - ${endHour}:00)`); return { allMet: false, requiresElevation: false, messages }; } break; case 'ip-range': // Simplified IP check - would need proper CIDR matching in production const allowedRanges = condition.parameters.ranges as string[]; if (request.ipAddress && !allowedRanges.some(r => request.ipAddress.startsWith(r))) { messages.push('Access from unauthorized network location'); return { allMet: false, requiresElevation: false, messages }; } break; case 'mfa-required': messages.push('Multi-factor authentication required'); requiresElevation = true; break; case 'purpose': const allowedPurposes = condition.parameters.purposes as ConsentPurpose[]; if (!request.purpose || !allowedPurposes.includes(request.purpose)) { messages.push(`Purpose '${request.purpose}' not permitted`); return { allMet: false, requiresElevation: false, messages }; } break; case 'emergency-only': if (!request.emergencyAccess) { messages.push('Access restricted to emergency situations'); return { allMet: false, requiresElevation: false, messages }; } break; } } return { allMet: !requiresElevation, requiresElevation, messages }; } // ═══════════════════════════════════════════════════════════════════════════════ // ENCRYPTION // ═══════════════════════════════════════════════════════════════════════════════ /** * Encrypt sensitive data */ async encryptData(data: string | Buffer, context?: string): Promise { const keyEntry = this.encryptionKeys.get(this.activeKeyId); if (!keyEntry) { throw new Error('No active encryption key'); } const iv = randomBytes(16); const algorithm = this.config.encryption.algorithm; if (algorithm === 'aes-256-gcm') { const cipher = createCipheriv('aes-256-gcm', keyEntry.key, iv); if (context) { cipher.setAAD(Buffer.from(context)); } const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]); const authTag = cipher.getAuthTag(); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), authTag: authTag.toString('base64'), keyId: this.activeKeyId, algorithm, encryptedAt: new Date() }; } else { // AES-256-CBC fallback const cipher = createCipheriv('aes-256-cbc', keyEntry.key, iv); const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; const encrypted = Buffer.concat([cipher.update(dataBuffer), cipher.final()]); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), keyId: this.activeKeyId, algorithm: 'aes-256-cbc', encryptedAt: new Date() }; } } /** * Decrypt data */ async decryptData(encrypted: EncryptedData, context?: string): Promise { const keyEntry = this.encryptionKeys.get(encrypted.keyId); if (!keyEntry) { throw new Error(`Encryption key ${encrypted.keyId} not found`); } const iv = Buffer.from(encrypted.iv, 'base64'); const ciphertext = Buffer.from(encrypted.ciphertext, 'base64'); if (encrypted.algorithm === 'aes-256-gcm') { const decipher = createDecipheriv('aes-256-gcm', keyEntry.key, iv); if (encrypted.authTag) { decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64')); } if (context) { decipher.setAAD(Buffer.from(context)); } return Buffer.concat([decipher.update(ciphertext), decipher.final()]); } else { const decipher = createDecipheriv('aes-256-cbc', keyEntry.key, iv); return Buffer.concat([decipher.update(ciphertext), decipher.final()]); } } /** * Encrypt specific PHI fields in an object */ async encryptPHI>(data: T): Promise }> { const encryptedFields: Record = {}; const result = { ...data } as T & { _encrypted: Record }; for (const field of this.config.encryption.encryptedFields || []) { if (data[field] !== undefined) { encryptedFields[field] = await this.encryptData(JSON.stringify(data[field]), field); (result as any)[field] = '[ENCRYPTED]'; } } result._encrypted = encryptedFields; return result; } /** * Decrypt PHI fields in an object */ async decryptPHI>(data: T & { _encrypted?: Record }): Promise { if (!data._encrypted) return data; const result = { ...data }; for (const [field, encrypted] of Object.entries(data._encrypted)) { const decrypted = await this.decryptData(encrypted, field); (result as any)[field] = JSON.parse(decrypted.toString('utf8')); } delete (result as any)._encrypted; return result; } /** * Rotate encryption keys */ async rotateEncryptionKey(): Promise { // Mark current key as inactive const currentKey = this.encryptionKeys.get(this.activeKeyId); if (currentKey) { currentKey.active = false; } // Generate new key const newKeyId = this.generateKeyId(); this.initializeEncryptionKey(newKeyId); this.activeKeyId = newKeyId; // Log key rotation await this.logAuditEvent({ eventType: 'system-event', action: 'update', outcome: 'success', actor: { userId: 'system', role: 'system', ipAddress: 'localhost' }, resource: { type: 'configuration', description: 'Encryption key rotation' }, details: { previousValues: { keyId: currentKey ? this.activeKeyId : 'none' }, newValues: { keyId: newKeyId } }, security: { authMethod: 'certificate', encryptionUsed: true, integrityVerified: true, accessLevel: 'elevated' } }); return newKeyId; } // ═══════════════════════════════════════════════════════════════════════════════ // DATA MASKING (Minimum Necessary) // ═══════════════════════════════════════════════════════════════════════════════ /** * Mask PHI according to minimum necessary standard */ maskPHI(data: Record, accessLevel: 'full' | 'clinical' | 'billing' | 'research' | 'minimal'): Record { const masked = { ...data }; // Determine masking rules based on access level const maskingRules = this.getMaskingRulesForLevel(accessLevel); // Apply masking if (masked.ssn && maskingRules.ssnPattern !== 'full') { masked.ssn = this.maskSSN(masked.ssn, maskingRules.ssnPattern); } if (masked.dob && maskingRules.dobPattern !== 'full') { masked.dob = this.maskDOB(masked.dob, maskingRules.dobPattern); } if (masked.phone && maskingRules.phonePattern !== 'full') { masked.phone = this.maskPhone(masked.phone, maskingRules.phonePattern); } if (masked.address && maskingRules.addressPattern !== 'full') { masked.address = this.maskAddress(masked.address, maskingRules.addressPattern); } if (masked.mrn && maskingRules.mrnPattern !== 'full') { masked.mrn = this.maskMRN(masked.mrn, maskingRules.mrnPattern); } if (masked.genomics && maskingRules.genomicPattern !== 'full') { masked.genomics = this.maskGenomics(masked.genomics, maskingRules.genomicPattern); } return masked; } private getMaskingRulesForLevel(level: string): DataMaskingConfig { switch (level) { case 'full': return { ssnPattern: 'full', dobPattern: 'full', phonePattern: 'full', addressPattern: 'full', mrnPattern: 'full', genomicPattern: 'full' }; case 'clinical': return { ssnPattern: 'last4', dobPattern: 'full', phonePattern: 'full', addressPattern: 'full', mrnPattern: 'full', genomicPattern: 'full' }; case 'billing': return { ssnPattern: 'last4', dobPattern: 'full', phonePattern: 'last4', addressPattern: 'full', mrnPattern: 'full', genomicPattern: 'hidden' }; case 'research': return { ssnPattern: 'hidden', dobPattern: 'year-only', phonePattern: 'hidden', addressPattern: 'zip-only', mrnPattern: 'hidden', genomicPattern: 'summary' }; case 'minimal': default: return { ssnPattern: 'hidden', dobPattern: 'age-only', phonePattern: 'hidden', addressPattern: 'hidden', mrnPattern: 'hidden', genomicPattern: 'hidden' }; } } private maskSSN(ssn: string, pattern: DataMaskingConfig['ssnPattern']): string { const cleaned = ssn.replace(/\D/g, ''); switch (pattern) { case 'last4': return `***-**-${cleaned.slice(-4)}`; case 'hidden': return '***-**-****'; default: return ssn; } } private maskDOB(dob: string | Date, pattern: DataMaskingConfig['dobPattern']): string { const date = typeof dob === 'string' ? new Date(dob) : dob; switch (pattern) { case 'year-only': return date.getFullYear().toString(); case 'age-only': const age = Math.floor((Date.now() - date.getTime()) / (365.25 * 24 * 60 * 60 * 1000)); return `Age: ${age}`; case 'hidden': return '[REDACTED]'; default: return typeof dob === 'string' ? dob : date.toISOString().split('T')[0]; } } private maskPhone(phone: string, pattern: DataMaskingConfig['phonePattern']): string { const cleaned = phone.replace(/\D/g, ''); switch (pattern) { case 'last4': return `(***) ***-${cleaned.slice(-4)}`; case 'hidden': return '(***) ***-****'; default: return phone; } } private maskAddress(address: any, pattern: DataMaskingConfig['addressPattern']): any { if (typeof address === 'string') { switch (pattern) { case 'city-state': return '[City, State]'; case 'zip-only': const zipMatch = address.match(/\d{5}(-\d{4})?/); return zipMatch ? zipMatch[0] : '[ZIP]'; case 'hidden': return '[REDACTED]'; default: return address; } } // Object address switch (pattern) { case 'city-state': return { city: address.city, state: address.state }; case 'zip-only': return { zip: address.zip || address.postalCode }; case 'hidden': return '[REDACTED]'; default: return address; } } private maskMRN(mrn: string, pattern: DataMaskingConfig['mrnPattern']): string { switch (pattern) { case 'last4': return `****${mrn.slice(-4)}`; case 'hidden': return '[REDACTED]'; default: return mrn; } } private maskGenomics(genomics: any, pattern: DataMaskingConfig['genomicPattern']): any { switch (pattern) { case 'summary': if (Array.isArray(genomics)) { return { count: genomics.length, summary: 'Genomic data available' }; } return { summary: 'Genomic data available' }; case 'hidden': return '[GENOMIC DATA REDACTED]'; default: return genomics; } } // ═══════════════════════════════════════════════════════════════════════════════ // HELPER METHODS // ═══════════════════════════════════════════════════════════════════════════════ private generateAuditId(): string { return `AUD-${Date.now()}-${randomBytes(4).toString('hex')}`; } private generateConsentId(): string { return `CON-${Date.now()}-${randomBytes(4).toString('hex')}`; } private generatePolicyId(): string { return `POL-${Date.now()}-${randomBytes(4).toString('hex')}`; } private generateKeyId(): string { return `KEY-${Date.now()}-${randomBytes(4).toString('hex')}`; } private initializeEncryptionKey(keyId?: string): void { const id = keyId || this.activeKeyId; // In production, this would use a proper key management system (KMS) // For now, derive from a master secret const masterSecret = process.env.HIPAA_MASTER_KEY || 'DEFAULT_DEV_KEY_CHANGE_IN_PRODUCTION'; const derivedKey = scryptSync(masterSecret, id, 32); this.encryptionKeys.set(id, { key: derivedKey, createdAt: new Date(), active: true }); } private calculateEntryHash(entry: Omit): string { const content = JSON.stringify({ ...entry, timestamp: entry.timestamp.toISOString() }); return createHash('sha256').update(content).digest('hex'); } private async detectSuspiciousActivity(entry: AuditLogEntry): Promise { // Check for patterns that might indicate security issues const suspiciousPatterns: { pattern: () => boolean; alert: string }[] = [ { pattern: () => entry.action === 'login-failed', alert: 'Failed login attempt' }, { pattern: () => entry.security.accessLevel === 'emergency', alert: 'Emergency access used' }, { pattern: () => { // Check for after-hours access const hour = entry.timestamp.getHours(); return (hour < 6 || hour > 22) && entry.eventType === 'access'; }, alert: 'After-hours PHI access' }, { pattern: () => { // Check for excessive access const recentAccess = this.auditStore.filter(e => e.actor.userId === entry.actor.userId && e.eventType === 'access' && e.timestamp.getTime() > Date.now() - 60000 // Last minute ); return recentAccess.length > 50; }, alert: 'Excessive PHI access rate detected' } ]; for (const { pattern, alert } of suspiciousPatterns) { if (pattern()) { this.emit('security-alert', { timestamp: new Date(), alert, auditEntryId: entry.id, actor: entry.actor, severity: 'warning' }); } } } /** * Export audit logs for compliance reporting */ exportAuditLogs(format: 'json' | 'csv', params?: { startDate?: Date; endDate?: Date; patientId?: string; }): string { const { entries } = this.queryAuditLogs({ ...params, limit: 100000 }); if (format === 'json') { return JSON.stringify(entries, null, 2); } // CSV format const headers = [ 'ID', 'Timestamp', 'Event Type', 'Action', 'Outcome', 'User ID', 'User Role', 'IP Address', 'Resource Type', 'Patient ID', 'Access Level' ]; const rows = entries.map(e => [ e.id, e.timestamp.toISOString(), e.eventType, e.action, e.outcome, e.actor.userId, e.actor.role, e.actor.ipAddress, e.resource.type, e.resource.patientId || '', e.security.accessLevel ]); return [ headers.join(','), ...rows.map(r => r.map(v => `"${v}"`).join(',')) ].join('\n'); } } export default HIPAAComplianceService;