/** * SMTP TLS Handler * Responsible for handling TLS-related SMTP functionality */ import * as plugins from '../../../plugins.js'; import type { ITlsHandler, ISessionManager } from './interfaces.js'; import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js'; import { SmtpLogger } from './utils/logging.js'; import { getSocketDetails, getTlsDetails } from './utils/helpers.js'; import { SmtpState } from '../interfaces.js'; /** * Handles TLS functionality for SMTP server */ export class TlsHandler implements ITlsHandler { /** * Session manager instance */ private sessionManager: ISessionManager; /** * TLS options */ private options: { key: string; cert: string; ca?: string; rejectUnauthorized?: boolean; }; /** * Creates a new TLS handler * @param sessionManager - Session manager instance * @param options - TLS options */ constructor( sessionManager: ISessionManager, options: { key: string; cert: string; ca?: string; rejectUnauthorized?: boolean; } ) { this.sessionManager = sessionManager; this.options = options; } /** * Handle STARTTLS command * @param socket - Client socket */ public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void { // Get the session for this socket const session = this.sessionManager.getSession(socket); if (!session) { this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`); return; } // Check if already using TLS if (session.useTLS) { this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`); return; } // Check if we have the necessary TLS certificates if (!this.isTlsEnabled()) { this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`); return; } // Send ready for TLS response this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`); // Upgrade the connection to TLS try { this.startTLS(socket); } catch (error) { SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, { sessionId: session.id, remoteAddress: session.remoteAddress, error: error instanceof Error ? error : new Error(String(error)) }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS negotiation failed', { error: error instanceof Error ? error.message : String(error) }, session.remoteAddress ); } } /** * Upgrade a connection to TLS * @param socket - Client socket */ public startTLS(socket: plugins.net.Socket): void { // Get the session for this socket const session = this.sessionManager.getSession(socket); // Use certificate strings directly without Buffer conversion // For ASN.1 encoding issues, keep the raw format which Node.js can parse natively const key = this.options.key.trim(); const cert = this.options.cert.trim(); const ca = this.options.ca ? this.options.ca.trim() : undefined; // Log certificate buffer lengths for debugging SmtpLogger.debug('Upgrading connection with certificates', { keyBufferLength: key.length, certBufferLength: cert.length, caBufferLength: ca ? ca.length : 0 }); // For testing/production compatibility, allow older TLS versions const context: plugins.tls.TlsOptions = { key: key, cert: cert, ca: ca, isServer: true, // Allow older TLS versions for better compatibility with clients minVersion: 'TLSv1', maxVersion: 'TLSv1.3', // Enforce server cipher preference for better security honorCipherOrder: true, // For testing, allow unauthorized (self-signed certs) rejectUnauthorized: false, // Use a more permissive cipher list for testing compatibility ciphers: 'ALL:!aNULL', // Allow legacy renegotiation for SMTP allowRenegotiation: true, // Handling handshake timeout handshakeTimeout: 10000, // 10 seconds }; try { // Direct options approach without separate secureContext creation // Use the simplest possible TLS setup to avoid ASN.1 errors // Create secure socket directly with minimal options const secureSocket = new plugins.tls.TLSSocket(socket, { isServer: true, key: key, cert: cert, ca: ca, minVersion: 'TLSv1', maxVersion: 'TLSv1.3', ciphers: 'ALL', honorCipherOrder: true, requestCert: false, rejectUnauthorized: false }); // Add a specific check for secure event to make sure the handshake completes let secureEventFired = false; // Add specific timeout for 'secure' event const secureEventTimeout = setTimeout(() => { if (!secureEventFired) { SmtpLogger.error('TLS handshake timed out waiting for secure event', { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort }); // Destroy the socket if secure event did not fire socket.destroy(); } }, 5000); // 5 second timeout // Log the upgrade attempt if (session) { SmtpLogger.info(`Attempting to upgrade connection to TLS for session ${session.id}`, { sessionId: session.id, remoteAddress: session.remoteAddress }); } // Securely handle TLS errors secureSocket.on('error', (err) => { clearTimeout(secureEventTimeout); SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, error: err, stack: err.stack }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'TLS error during STARTTLS', { error: err.message, stack: err.stack }, socket.remoteAddress ); socket.destroy(); }); // Log TLS connection details on secure event secureSocket.on('secure', () => { clearTimeout(secureEventTimeout); secureEventFired = true; const tlsDetails = getTlsDetails(secureSocket); SmtpLogger.info('TLS connection successfully established via STARTTLS', { remoteAddress: secureSocket.remoteAddress, remotePort: secureSocket.remotePort, protocol: tlsDetails?.protocol || 'unknown', cipher: tlsDetails?.cipher || 'unknown', authorized: tlsDetails?.authorized || false }); // Log security event with TLS details SmtpLogger.logSecurityEvent( SecurityLogLevel.INFO, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS successful', { protocol: tlsDetails?.protocol, cipher: tlsDetails?.cipher, authorized: tlsDetails?.authorized }, secureSocket.remoteAddress, undefined, true ); // Update session if we have one if (session) { // Update session properties session.useTLS = true; session.secure = true; // Reset session state (per RFC 3207) // After STARTTLS, client must issue a new EHLO if (this.sessionManager.updateSessionState) { this.sessionManager.updateSessionState(session, SmtpState.GREETING); } } else { const socketDetails = getSocketDetails(socket); SmtpLogger.info(`Upgraded connection to TLS without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`); } }); } catch (error) { SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, { remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, error: error instanceof Error ? error : new Error(String(error)), stack: error instanceof Error ? error.stack : 'No stack trace available' }); // Log security event SmtpLogger.logSecurityEvent( SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Failed to upgrade connection to TLS', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : 'No stack trace available' }, socket.remoteAddress, undefined, false ); socket.destroy(); } } /** * Create a secure server * @returns TLS server instance or undefined if TLS is not enabled */ public createSecureServer(): plugins.tls.Server | undefined { if (!this.isTlsEnabled()) { return undefined; } try { // Use certificate strings directly without Buffer conversion // For ASN.1 encoding issues, keep the raw format which Node.js can parse natively const key = this.options.key.trim(); const cert = this.options.cert.trim(); const ca = this.options.ca ? this.options.ca.trim() : undefined; // Log certificate buffer lengths for debugging SmtpLogger.debug('Creating secure server with certificates', { keyBufferLength: key.length, certBufferLength: cert.length, caBufferLength: ca ? ca.length : 0 }); // Simplify options to minimal necessary for test compatibility const context: plugins.tls.TlsOptions = { key: key, cert: cert, ca: ca, // Allow all TLS versions for maximum compatibility minVersion: 'TLSv1', maxVersion: 'TLSv1.3', // Accept all ciphers for testing ciphers: 'ALL', // For testing, always allow self-signed certs rejectUnauthorized: false, // Shorter handshake timeout for testing handshakeTimeout: 5000 }; // Create a simple, standalone server that explicitly doesn't try to // verify or validate client certificates for testing return new plugins.tls.Server(context); } catch (error) { SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, { error: error instanceof Error ? error : new Error(String(error)), stack: error instanceof Error ? error.stack : 'No stack trace available' }); return undefined; } } /** * Check if TLS is enabled * @returns Whether TLS is enabled */ public isTlsEnabled(): boolean { return !!(this.options.key && this.options.cert); } /** * Send a response to the client * @param socket - Client socket * @param response - Response message */ private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void { try { socket.write(`${response}\r\n`); SmtpLogger.logResponse(response, socket); } catch (error) { SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, { response, remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, error: error instanceof Error ? error : new Error(String(error)) }); socket.destroy(); } } }