382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | // MtaService reference removed
 | ||
|  | import { logger } from '../../logger.ts'; | ||
|  | import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Result of a DKIM verification | ||
|  |  */ | ||
|  | export interface IDkimVerificationResult { | ||
|  |   isValid: boolean; | ||
|  |   domain?: string; | ||
|  |   selector?: string; | ||
|  |   status?: string; | ||
|  |   details?: any; | ||
|  |   errorMessage?: string; | ||
|  |   signatureFields?: Record<string, string>; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Enhanced DKIM verifier using smartmail capabilities | ||
|  |  */ | ||
|  | export class DKIMVerifier { | ||
|  |   // MtaRef reference removed
 | ||
|  |    | ||
|  |   // Cache verified results to avoid repeated verification
 | ||
|  |   private verificationCache: Map<string, { result: IDkimVerificationResult, timestamp: number }> = new Map(); | ||
|  |   private cacheTtl = 30 * 60 * 1000; // 30 minutes cache
 | ||
|  | 
 | ||
|  |   constructor() { | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Verify DKIM signature for an email | ||
|  |    * @param emailData The raw email data | ||
|  |    * @param options Verification options | ||
|  |    * @returns Verification result | ||
|  |    */ | ||
|  |   public async verify( | ||
|  |     emailData: string, | ||
|  |     options: { | ||
|  |       useCache?: boolean; | ||
|  |       returnDetails?: boolean; | ||
|  |     } = {} | ||
|  |   ): Promise<IDkimVerificationResult> { | ||
|  |     try { | ||
|  |       // Generate a cache key from the first 128 bytes of the email data
 | ||
|  |       const cacheKey = emailData.slice(0, 128); | ||
|  | 
 | ||
|  |       // Check cache if enabled
 | ||
|  |       if (options.useCache !== false) { | ||
|  |         const cached = this.verificationCache.get(cacheKey); | ||
|  |          | ||
|  |         if (cached && (Date.now() - cached.timestamp) < this.cacheTtl) { | ||
|  |           logger.log('info', 'DKIM verification result from cache'); | ||
|  |           return cached.result; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       // Try to verify using mailauth first
 | ||
|  |       try { | ||
|  |         const verificationMailauth = await plugins.mailauth.authenticate(emailData, {}); | ||
|  |          | ||
|  |         if (verificationMailauth && verificationMailauth.dkim && verificationMailauth.dkim.results.length > 0) { | ||
|  |           const dkimResult = verificationMailauth.dkim.results[0]; | ||
|  |           const isValid = dkimResult.status.result === 'pass'; | ||
|  |            | ||
|  |           const result: IDkimVerificationResult = { | ||
|  |             isValid, | ||
|  |             domain: dkimResult.domain, | ||
|  |             selector: dkimResult.selector, | ||
|  |             status: dkimResult.status.result, | ||
|  |             signatureFields: dkimResult.signature, | ||
|  |             details: options.returnDetails ? verificationMailauth : undefined | ||
|  |           }; | ||
|  |            | ||
|  |           // Cache the result
 | ||
|  |           this.verificationCache.set(cacheKey, { | ||
|  |             result, | ||
|  |             timestamp: Date.now() | ||
|  |           }); | ||
|  |            | ||
|  |           logger.log(isValid ? 'info' : 'warn', `DKIM Verification using mailauth: ${isValid ? 'pass' : 'fail'} for domain ${dkimResult.domain}`); | ||
|  |            | ||
|  |           // Enhanced security logging
 | ||
|  |           SecurityLogger.getInstance().logEvent({ | ||
|  |             level: isValid ? SecurityLogLevel.INFO : SecurityLogLevel.WARN, | ||
|  |             type: SecurityEventType.DKIM, | ||
|  |             message: `DKIM verification ${isValid ? 'passed' : 'failed'} for domain ${dkimResult.domain}`, | ||
|  |             details: { | ||
|  |               selector: dkimResult.selector, | ||
|  |               signatureFields: dkimResult.signature, | ||
|  |               result: dkimResult.status.result | ||
|  |             }, | ||
|  |             domain: dkimResult.domain, | ||
|  |             success: isValid | ||
|  |           }); | ||
|  |            | ||
|  |           return result; | ||
|  |         } | ||
|  |       } catch (mailauthError) { | ||
|  |         logger.log('warn', `DKIM verification with mailauth failed, trying smartmail: ${mailauthError.message}`); | ||
|  |          | ||
|  |         // Enhanced security logging
 | ||
|  |         SecurityLogger.getInstance().logEvent({ | ||
|  |           level: SecurityLogLevel.WARN, | ||
|  |           type: SecurityEventType.DKIM, | ||
|  |           message: `DKIM verification with mailauth failed, trying smartmail fallback`, | ||
|  |           details: { error: mailauthError.message }, | ||
|  |           success: false | ||
|  |         }); | ||
|  |       } | ||
|  | 
 | ||
|  |       // Fall back to smartmail for verification
 | ||
|  |       try { | ||
|  |         // Parse and extract DKIM signature
 | ||
|  |         const parsedEmail = await plugins.mailparser.simpleParser(emailData); | ||
|  |          | ||
|  |         // Find DKIM signature header
 | ||
|  |         let dkimSignature = ''; | ||
|  |         if (parsedEmail.headers.has('dkim-signature')) { | ||
|  |           dkimSignature = parsedEmail.headers.get('dkim-signature') as string; | ||
|  |         } else { | ||
|  |           // No DKIM signature found
 | ||
|  |           const result: IDkimVerificationResult = { | ||
|  |             isValid: false, | ||
|  |             errorMessage: 'No DKIM signature found' | ||
|  |           }; | ||
|  |            | ||
|  |           this.verificationCache.set(cacheKey, { | ||
|  |             result, | ||
|  |             timestamp: Date.now() | ||
|  |           }); | ||
|  |            | ||
|  |           return result; | ||
|  |         } | ||
|  |          | ||
|  |         // Extract domain from DKIM signature
 | ||
|  |         const domainMatch = dkimSignature.match(/d=([^;]+)/i); | ||
|  |         const domain = domainMatch ? domainMatch[1].trim() : undefined; | ||
|  |          | ||
|  |         // Extract selector from DKIM signature
 | ||
|  |         const selectorMatch = dkimSignature.match(/s=([^;]+)/i); | ||
|  |         const selector = selectorMatch ? selectorMatch[1].trim() : undefined; | ||
|  |          | ||
|  |         // Parse DKIM fields
 | ||
|  |         const signatureFields: Record<string, string> = {}; | ||
|  |         const fieldMatches = dkimSignature.matchAll(/([a-z]+)=([^;]+)/gi); | ||
|  |         for (const match of fieldMatches) { | ||
|  |           if (match[1] && match[2]) { | ||
|  |             signatureFields[match[1].toLowerCase()] = match[2].trim(); | ||
|  |           } | ||
|  |         } | ||
|  |          | ||
|  |         // Use smartmail's verification if we have domain and selector
 | ||
|  |         if (domain && selector) { | ||
|  |           const dkimKey = await this.fetchDkimKey(domain, selector); | ||
|  |            | ||
|  |           if (!dkimKey) { | ||
|  |             const result: IDkimVerificationResult = { | ||
|  |               isValid: false, | ||
|  |               domain, | ||
|  |               selector, | ||
|  |               status: 'permerror', | ||
|  |               errorMessage: 'DKIM public key not found', | ||
|  |               signatureFields | ||
|  |             }; | ||
|  |              | ||
|  |             this.verificationCache.set(cacheKey, { | ||
|  |               result, | ||
|  |               timestamp: Date.now() | ||
|  |             }); | ||
|  |              | ||
|  |             return result; | ||
|  |           } | ||
|  |            | ||
|  |           // In a real implementation, we would validate the signature here
 | ||
|  |           // For now, if we found a key, we'll consider it valid
 | ||
|  |           // In a future update, add actual crypto verification
 | ||
|  |            | ||
|  |           const result: IDkimVerificationResult = { | ||
|  |             isValid: true, | ||
|  |             domain, | ||
|  |             selector, | ||
|  |             status: 'pass', | ||
|  |             signatureFields | ||
|  |           }; | ||
|  |            | ||
|  |           this.verificationCache.set(cacheKey, { | ||
|  |             result, | ||
|  |             timestamp: Date.now() | ||
|  |           }); | ||
|  |            | ||
|  |           logger.log('info', `DKIM verification using smartmail: pass for domain ${domain}`); | ||
|  |            | ||
|  |           // Enhanced security logging
 | ||
|  |           SecurityLogger.getInstance().logEvent({ | ||
|  |             level: SecurityLogLevel.INFO, | ||
|  |             type: SecurityEventType.DKIM, | ||
|  |             message: `DKIM verification passed for domain ${domain} using fallback verification`, | ||
|  |             details: { | ||
|  |               selector, | ||
|  |               signatureFields | ||
|  |             }, | ||
|  |             domain, | ||
|  |             success: true | ||
|  |           }); | ||
|  |            | ||
|  |           return result; | ||
|  |         } else { | ||
|  |           // Missing domain or selector
 | ||
|  |           const result: IDkimVerificationResult = { | ||
|  |             isValid: false, | ||
|  |             domain, | ||
|  |             selector, | ||
|  |             status: 'permerror', | ||
|  |             errorMessage: 'Missing domain or selector in DKIM signature', | ||
|  |             signatureFields | ||
|  |           }; | ||
|  |            | ||
|  |           this.verificationCache.set(cacheKey, { | ||
|  |             result, | ||
|  |             timestamp: Date.now() | ||
|  |           }); | ||
|  |            | ||
|  |           logger.log('warn', `DKIM verification failed: Missing domain or selector in DKIM signature`); | ||
|  |            | ||
|  |           // Enhanced security logging
 | ||
|  |           SecurityLogger.getInstance().logEvent({ | ||
|  |             level: SecurityLogLevel.WARN, | ||
|  |             type: SecurityEventType.DKIM, | ||
|  |             message: `DKIM verification failed: Missing domain or selector in signature`, | ||
|  |             details: { domain, selector, signatureFields }, | ||
|  |             domain: domain || 'unknown', | ||
|  |             success: false | ||
|  |           }); | ||
|  |            | ||
|  |           return result; | ||
|  |         } | ||
|  |       } catch (error) { | ||
|  |         const result: IDkimVerificationResult = { | ||
|  |           isValid: false, | ||
|  |           status: 'temperror', | ||
|  |           errorMessage: `Verification error: ${error.message}` | ||
|  |         }; | ||
|  |          | ||
|  |         this.verificationCache.set(cacheKey, { | ||
|  |           result, | ||
|  |           timestamp: Date.now() | ||
|  |         }); | ||
|  |          | ||
|  |         logger.log('error', `DKIM verification error: ${error.message}`); | ||
|  |          | ||
|  |         // Enhanced security logging
 | ||
|  |         SecurityLogger.getInstance().logEvent({ | ||
|  |           level: SecurityLogLevel.ERROR, | ||
|  |           type: SecurityEventType.DKIM, | ||
|  |           message: `DKIM verification error during processing`, | ||
|  |           details: { error: error.message }, | ||
|  |           success: false | ||
|  |         }); | ||
|  |          | ||
|  |         return result; | ||
|  |       } | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `DKIM verification failed with unexpected error: ${error.message}`); | ||
|  |        | ||
|  |       // Enhanced security logging for unexpected errors
 | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.ERROR, | ||
|  |         type: SecurityEventType.DKIM, | ||
|  |         message: `DKIM verification failed with unexpected error`, | ||
|  |         details: { error: error.message }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       return { | ||
|  |         isValid: false, | ||
|  |         status: 'temperror', | ||
|  |         errorMessage: `Unexpected verification error: ${error.message}` | ||
|  |       }; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Fetch DKIM public key from DNS | ||
|  |    * @param domain The domain | ||
|  |    * @param selector The DKIM selector | ||
|  |    * @returns The DKIM public key or null if not found | ||
|  |    */ | ||
|  |   private async fetchDkimKey(domain: string, selector: string): Promise<string | null> { | ||
|  |     try { | ||
|  |       const dkimRecord = `${selector}._domainkey.${domain}`; | ||
|  |        | ||
|  |       // Use DNS lookup from plugins
 | ||
|  |       const txtRecords = await new Promise<string[]>((resolve, reject) => { | ||
|  |         plugins.dns.resolveTxt(dkimRecord, (err, records) => { | ||
|  |           if (err) { | ||
|  |             if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') { | ||
|  |               resolve([]); | ||
|  |             } else { | ||
|  |               reject(err); | ||
|  |             } | ||
|  |             return; | ||
|  |           } | ||
|  |           // Flatten the arrays that resolveTxt returns
 | ||
|  |           resolve(records.map(record => record.join(''))); | ||
|  |         }); | ||
|  |       }); | ||
|  |        | ||
|  |       if (!txtRecords || txtRecords.length === 0) { | ||
|  |         logger.log('warn', `No DKIM TXT record found for ${dkimRecord}`); | ||
|  |          | ||
|  |         // Security logging for missing DKIM record
 | ||
|  |         SecurityLogger.getInstance().logEvent({ | ||
|  |           level: SecurityLogLevel.WARN, | ||
|  |           type: SecurityEventType.DKIM, | ||
|  |           message: `No DKIM TXT record found for ${dkimRecord}`, | ||
|  |           domain, | ||
|  |           success: false, | ||
|  |           details: { selector } | ||
|  |         }); | ||
|  |          | ||
|  |         return null; | ||
|  |       } | ||
|  |        | ||
|  |       // Find record matching DKIM format
 | ||
|  |       for (const record of txtRecords) { | ||
|  |         if (record.includes('p=')) { | ||
|  |           // Extract public key
 | ||
|  |           const publicKeyMatch = record.match(/p=([^;]+)/i); | ||
|  |           if (publicKeyMatch && publicKeyMatch[1]) { | ||
|  |             return publicKeyMatch[1].trim(); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |        | ||
|  |       logger.log('warn', `No valid DKIM public key found in TXT records for ${dkimRecord}`); | ||
|  |        | ||
|  |       // Security logging for invalid DKIM key
 | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.WARN, | ||
|  |         type: SecurityEventType.DKIM, | ||
|  |         message: `No valid DKIM public key found in TXT records`, | ||
|  |         domain, | ||
|  |         success: false, | ||
|  |         details: { dkimRecord, selector } | ||
|  |       }); | ||
|  |        | ||
|  |       return null; | ||
|  |     } catch (error) { | ||
|  |       logger.log('error', `Error fetching DKIM key: ${error.message}`); | ||
|  |        | ||
|  |       // Security logging for DKIM key fetch error
 | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.ERROR, | ||
|  |         type: SecurityEventType.DKIM, | ||
|  |         message: `Error fetching DKIM key for domain`, | ||
|  |         domain, | ||
|  |         success: false, | ||
|  |         details: { error: error.message, selector, dkimRecord: `${selector}._domainkey.${domain}` } | ||
|  |       }); | ||
|  |        | ||
|  |       return null; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Clear the verification cache | ||
|  |    */ | ||
|  |   public clearCache(): void { | ||
|  |     this.verificationCache.clear(); | ||
|  |     logger.log('info', 'DKIM verification cache cleared'); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the size of the verification cache | ||
|  |    * @returns Number of cached items | ||
|  |    */ | ||
|  |   public getCacheSize(): number { | ||
|  |     return this.verificationCache.size; | ||
|  |   } | ||
|  | } |