431 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			431 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | import * as paths from '../../paths.ts'; | ||
|  | 
 | ||
|  | import { Email } from '../core/classes.email.ts'; | ||
|  | // MtaService reference removed
 | ||
|  | 
 | ||
|  | const readFile = plugins.util.promisify(plugins.fs.readFile); | ||
|  | const writeFile = plugins.util.promisify(plugins.fs.writeFile); | ||
|  | const generateKeyPair = plugins.util.promisify(plugins.crypto.generateKeyPair); | ||
|  | 
 | ||
|  | export interface IKeyPaths { | ||
|  |   privateKeyPath: string; | ||
|  |   publicKeyPath: string; | ||
|  | } | ||
|  | 
 | ||
|  | export interface IDkimKeyMetadata { | ||
|  |   domain: string; | ||
|  |   selector: string; | ||
|  |   createdAt: number; | ||
|  |   rotatedAt?: number; | ||
|  |   previousSelector?: string; | ||
|  |   keySize: number; | ||
|  | } | ||
|  | 
 | ||
|  | export class DKIMCreator { | ||
|  |   private keysDir: string; | ||
|  |   private storageManager?: any; // StorageManager instance
 | ||
|  | 
 | ||
|  |   constructor(keysDir = paths.keysDir, storageManager?: any) { | ||
|  |     this.keysDir = keysDir; | ||
|  |     this.storageManager = storageManager; | ||
|  |   } | ||
|  | 
 | ||
|  |   public async getKeyPathsForDomain(domainArg: string): Promise<IKeyPaths> { | ||
|  |     return { | ||
|  |       privateKeyPath: plugins.path.join(this.keysDir, `${domainArg}-private.pem`), | ||
|  |       publicKeyPath: plugins.path.join(this.keysDir, `${domainArg}-public.pem`), | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Check if a DKIM key is present and creates one and stores it to disk otherwise
 | ||
|  |   public async handleDKIMKeysForDomain(domainArg: string): Promise<void> { | ||
|  |     try { | ||
|  |       await this.readDKIMKeys(domainArg); | ||
|  |     } catch (error) { | ||
|  |       console.log(`No DKIM keys found for ${domainArg}. Generating...`); | ||
|  |       await this.createAndStoreDKIMKeys(domainArg); | ||
|  |       const dnsValue = await this.getDNSRecordForDomain(domainArg); | ||
|  |       plugins.smartfile.fs.ensureDirSync(paths.dnsRecordsDir); | ||
|  |       plugins.smartfile.memory.toFsSync(JSON.stringify(dnsValue, null, 2), plugins.path.join(paths.dnsRecordsDir, `${domainArg}.dkimrecord.tson`)); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public async handleDKIMKeysForEmail(email: Email): Promise<void> { | ||
|  |     const domain = email.from.split('@')[1]; | ||
|  |     await this.handleDKIMKeysForDomain(domain); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Read DKIM keys - always use storage manager, migrate from filesystem if needed
 | ||
|  |   public async readDKIMKeys(domainArg: string): Promise<{ privateKey: string; publicKey: string }> { | ||
|  |     // Try to read from storage manager first
 | ||
|  |     if (this.storageManager) { | ||
|  |       try { | ||
|  |         const [privateKey, publicKey] = await Promise.all([ | ||
|  |           this.storageManager.get(`/email/dkim/${domainArg}/private.key`), | ||
|  |           this.storageManager.get(`/email/dkim/${domainArg}/public.key`) | ||
|  |         ]); | ||
|  |          | ||
|  |         if (privateKey && publicKey) { | ||
|  |           return { privateKey, publicKey }; | ||
|  |         } | ||
|  |       } catch (error) { | ||
|  |         // Fall through to migration check
 | ||
|  |       } | ||
|  |        | ||
|  |       // Check if keys exist in filesystem and migrate them to storage manager
 | ||
|  |       const keyPaths = await this.getKeyPathsForDomain(domainArg); | ||
|  |       try { | ||
|  |         const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([ | ||
|  |           readFile(keyPaths.privateKeyPath), | ||
|  |           readFile(keyPaths.publicKeyPath), | ||
|  |         ]); | ||
|  | 
 | ||
|  |         // Convert the buffers to strings
 | ||
|  |         const privateKey = privateKeyBuffer.toString(); | ||
|  |         const publicKey = publicKeyBuffer.toString(); | ||
|  |          | ||
|  |         // Migrate to storage manager
 | ||
|  |         console.log(`Migrating DKIM keys for ${domainArg} from filesystem to StorageManager`); | ||
|  |         await Promise.all([ | ||
|  |           this.storageManager.set(`/email/dkim/${domainArg}/private.key`, privateKey), | ||
|  |           this.storageManager.set(`/email/dkim/${domainArg}/public.key`, publicKey) | ||
|  |         ]); | ||
|  |          | ||
|  |         return { privateKey, publicKey }; | ||
|  |       } catch (error) { | ||
|  |         if (error.code === 'ENOENT') { | ||
|  |           // Keys don't exist anywhere
 | ||
|  |           throw new Error(`DKIM keys not found for domain ${domainArg}`); | ||
|  |         } | ||
|  |         throw error; | ||
|  |       } | ||
|  |     } else { | ||
|  |       // No storage manager, use filesystem directly
 | ||
|  |       const keyPaths = await this.getKeyPathsForDomain(domainArg); | ||
|  |       const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([ | ||
|  |         readFile(keyPaths.privateKeyPath), | ||
|  |         readFile(keyPaths.publicKeyPath), | ||
|  |       ]); | ||
|  | 
 | ||
|  |       const privateKey = privateKeyBuffer.toString(); | ||
|  |       const publicKey = publicKeyBuffer.toString(); | ||
|  |        | ||
|  |       return { privateKey, publicKey }; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Create a DKIM key pair - changed to public for API access
 | ||
|  |   public async createDKIMKeys(): Promise<{ privateKey: string; publicKey: string }> { | ||
|  |     const { privateKey, publicKey } = await generateKeyPair('rsa', { | ||
|  |       modulusLength: 2048, | ||
|  |       publicKeyEncoding: { type: 'spki', format: 'pem' }, | ||
|  |       privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, | ||
|  |     }); | ||
|  | 
 | ||
|  |     return { privateKey, publicKey }; | ||
|  |   } | ||
|  | 
 | ||
|  |   // Store a DKIM key pair - uses storage manager if available, else disk
 | ||
|  |   public async storeDKIMKeys( | ||
|  |     privateKey: string, | ||
|  |     publicKey: string, | ||
|  |     privateKeyPath: string, | ||
|  |     publicKeyPath: string | ||
|  |   ): Promise<void> { | ||
|  |     // Store in storage manager if available
 | ||
|  |     if (this.storageManager) { | ||
|  |       // Extract domain from path (e.g., /path/to/keys/example.com-private.pem -> example.com)
 | ||
|  |       const match = privateKeyPath.match(/\/([^\/]+)-private\.pem$/); | ||
|  |       if (match) { | ||
|  |         const domain = match[1]; | ||
|  |         await Promise.all([ | ||
|  |           this.storageManager.set(`/email/dkim/${domain}/private.key`, privateKey), | ||
|  |           this.storageManager.set(`/email/dkim/${domain}/public.key`, publicKey) | ||
|  |         ]); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Also store to filesystem for backward compatibility
 | ||
|  |     await Promise.all([writeFile(privateKeyPath, privateKey), writeFile(publicKeyPath, publicKey)]); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Create a DKIM key pair and store it to disk - changed to public for API access
 | ||
|  |   public async createAndStoreDKIMKeys(domain: string): Promise<void> { | ||
|  |     const { privateKey, publicKey } = await this.createDKIMKeys(); | ||
|  |     const keyPaths = await this.getKeyPathsForDomain(domain); | ||
|  |     await this.storeDKIMKeys( | ||
|  |       privateKey, | ||
|  |       publicKey, | ||
|  |       keyPaths.privateKeyPath, | ||
|  |       keyPaths.publicKeyPath | ||
|  |     ); | ||
|  |     console.log(`DKIM keys for ${domain} created and stored.`); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Changed to public for API access
 | ||
|  |   public async getDNSRecordForDomain(domainArg: string): Promise<plugins.tsclass.network.IDnsRecord> { | ||
|  |     await this.handleDKIMKeysForDomain(domainArg); | ||
|  |     const keys = await this.readDKIMKeys(domainArg); | ||
|  | 
 | ||
|  |     // Remove the PEM header and footer and newlines
 | ||
|  |     const pemHeader = '-----BEGIN PUBLIC KEY-----'; | ||
|  |     const pemFooter = '-----END PUBLIC KEY-----'; | ||
|  |     const keyContents = keys.publicKey | ||
|  |       .replace(pemHeader, '') | ||
|  |       .replace(pemFooter, '') | ||
|  |       .replace(/\n/g, ''); | ||
|  | 
 | ||
|  |     // Now generate the DKIM DNS TXT record
 | ||
|  |     const dnsRecordValue = `v=DKIM1; h=sha256; k=rsa; p=${keyContents}`; | ||
|  | 
 | ||
|  |     return { | ||
|  |       name: `mta._domainkey.${domainArg}`, | ||
|  |       type: 'TXT', | ||
|  |       dnsSecEnabled: null, | ||
|  |       value: dnsRecordValue, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Get DKIM key metadata for a domain | ||
|  |    */ | ||
|  |   private async getKeyMetadata(domain: string, selector: string = 'default'): Promise<IDkimKeyMetadata | null> { | ||
|  |     if (!this.storageManager) { | ||
|  |       return null; | ||
|  |     } | ||
|  |      | ||
|  |     const metadataKey = `/email/dkim/${domain}/${selector}/metadata`; | ||
|  |     const metadataStr = await this.storageManager.get(metadataKey); | ||
|  |      | ||
|  |     if (!metadataStr) { | ||
|  |       return null; | ||
|  |     } | ||
|  |      | ||
|  |     return JSON.parse(metadataStr) as IDkimKeyMetadata; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Save DKIM key metadata | ||
|  |    */ | ||
|  |   private async saveKeyMetadata(metadata: IDkimKeyMetadata): Promise<void> { | ||
|  |     if (!this.storageManager) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     const metadataKey = `/email/dkim/${metadata.domain}/${metadata.selector}/metadata`; | ||
|  |     await this.storageManager.set(metadataKey, JSON.stringify(metadata)); | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Check if DKIM keys need rotation | ||
|  |    */ | ||
|  |   public async needsRotation(domain: string, selector: string = 'default', rotationIntervalDays: number = 90): Promise<boolean> { | ||
|  |     const metadata = await this.getKeyMetadata(domain, selector); | ||
|  |      | ||
|  |     if (!metadata) { | ||
|  |       // No metadata means old keys, should rotate
 | ||
|  |       return true; | ||
|  |     } | ||
|  |      | ||
|  |     const now = Date.now(); | ||
|  |     const keyAgeMs = now - metadata.createdAt; | ||
|  |     const keyAgeDays = keyAgeMs / (1000 * 60 * 60 * 24); | ||
|  |      | ||
|  |     return keyAgeDays >= rotationIntervalDays; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Rotate DKIM keys for a domain | ||
|  |    */ | ||
|  |   public async rotateDkimKeys(domain: string, currentSelector: string = 'default', keySize: number = 2048): Promise<string> { | ||
|  |     console.log(`Rotating DKIM keys for ${domain}...`); | ||
|  |      | ||
|  |     // Generate new selector based on date
 | ||
|  |     const now = new Date(); | ||
|  |     const newSelector = `key${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`; | ||
|  |      | ||
|  |     // Create new keys with custom key size
 | ||
|  |     const { privateKey, publicKey } = await generateKeyPair('rsa', { | ||
|  |       modulusLength: keySize, | ||
|  |       publicKeyEncoding: { type: 'spki', format: 'pem' }, | ||
|  |       privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, | ||
|  |     }); | ||
|  |      | ||
|  |     // Store new keys with new selector
 | ||
|  |     const newKeyPaths = await this.getKeyPathsForSelector(domain, newSelector); | ||
|  |      | ||
|  |     // Store in storage manager if available
 | ||
|  |     if (this.storageManager) { | ||
|  |       await Promise.all([ | ||
|  |         this.storageManager.set(`/email/dkim/${domain}/${newSelector}/private.key`, privateKey), | ||
|  |         this.storageManager.set(`/email/dkim/${domain}/${newSelector}/public.key`, publicKey) | ||
|  |       ]); | ||
|  |     } | ||
|  |      | ||
|  |     // Also store to filesystem
 | ||
|  |     await this.storeDKIMKeys( | ||
|  |       privateKey, | ||
|  |       publicKey, | ||
|  |       newKeyPaths.privateKeyPath, | ||
|  |       newKeyPaths.publicKeyPath | ||
|  |     ); | ||
|  |      | ||
|  |     // Save metadata for new keys
 | ||
|  |     const metadata: IDkimKeyMetadata = { | ||
|  |       domain, | ||
|  |       selector: newSelector, | ||
|  |       createdAt: Date.now(), | ||
|  |       previousSelector: currentSelector, | ||
|  |       keySize | ||
|  |     }; | ||
|  |     await this.saveKeyMetadata(metadata); | ||
|  |      | ||
|  |     // Update metadata for old keys
 | ||
|  |     const oldMetadata = await this.getKeyMetadata(domain, currentSelector); | ||
|  |     if (oldMetadata) { | ||
|  |       oldMetadata.rotatedAt = Date.now(); | ||
|  |       await this.saveKeyMetadata(oldMetadata); | ||
|  |     } | ||
|  |      | ||
|  |     console.log(`DKIM keys rotated for ${domain}. New selector: ${newSelector}`); | ||
|  |     return newSelector; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Get key paths for a specific selector | ||
|  |    */ | ||
|  |   public async getKeyPathsForSelector(domain: string, selector: string): Promise<IKeyPaths> { | ||
|  |     return { | ||
|  |       privateKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-private.pem`), | ||
|  |       publicKeyPath: plugins.path.join(this.keysDir, `${domain}-${selector}-public.pem`), | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Read DKIM keys for a specific selector | ||
|  |    */ | ||
|  |   public async readDKIMKeysForSelector(domain: string, selector: string): Promise<{ privateKey: string; publicKey: string }> { | ||
|  |     // Try to read from storage manager first
 | ||
|  |     if (this.storageManager) { | ||
|  |       try { | ||
|  |         const [privateKey, publicKey] = await Promise.all([ | ||
|  |           this.storageManager.get(`/email/dkim/${domain}/${selector}/private.key`), | ||
|  |           this.storageManager.get(`/email/dkim/${domain}/${selector}/public.key`) | ||
|  |         ]); | ||
|  |          | ||
|  |         if (privateKey && publicKey) { | ||
|  |           return { privateKey, publicKey }; | ||
|  |         } | ||
|  |       } catch (error) { | ||
|  |         // Fall through to migration check
 | ||
|  |       } | ||
|  |        | ||
|  |       // Check if keys exist in filesystem and migrate them to storage manager
 | ||
|  |       const keyPaths = await this.getKeyPathsForSelector(domain, selector); | ||
|  |       try { | ||
|  |         const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([ | ||
|  |           readFile(keyPaths.privateKeyPath), | ||
|  |           readFile(keyPaths.publicKeyPath), | ||
|  |         ]); | ||
|  | 
 | ||
|  |         const privateKey = privateKeyBuffer.toString(); | ||
|  |         const publicKey = publicKeyBuffer.toString(); | ||
|  |          | ||
|  |         // Migrate to storage manager
 | ||
|  |         console.log(`Migrating DKIM keys for ${domain}/${selector} from filesystem to StorageManager`); | ||
|  |         await Promise.all([ | ||
|  |           this.storageManager.set(`/email/dkim/${domain}/${selector}/private.key`, privateKey), | ||
|  |           this.storageManager.set(`/email/dkim/${domain}/${selector}/public.key`, publicKey) | ||
|  |         ]); | ||
|  |          | ||
|  |         return { privateKey, publicKey }; | ||
|  |       } catch (error) { | ||
|  |         if (error.code === 'ENOENT') { | ||
|  |           throw new Error(`DKIM keys not found for domain ${domain} with selector ${selector}`); | ||
|  |         } | ||
|  |         throw error; | ||
|  |       } | ||
|  |     } else { | ||
|  |       // No storage manager, use filesystem directly
 | ||
|  |       const keyPaths = await this.getKeyPathsForSelector(domain, selector); | ||
|  |       const [privateKeyBuffer, publicKeyBuffer] = await Promise.all([ | ||
|  |         readFile(keyPaths.privateKeyPath), | ||
|  |         readFile(keyPaths.publicKeyPath), | ||
|  |       ]); | ||
|  | 
 | ||
|  |       const privateKey = privateKeyBuffer.toString(); | ||
|  |       const publicKey = publicKeyBuffer.toString(); | ||
|  | 
 | ||
|  |       return { privateKey, publicKey }; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Get DNS record for a specific selector | ||
|  |    */ | ||
|  |   public async getDNSRecordForSelector(domain: string, selector: string): Promise<plugins.tsclass.network.IDnsRecord> { | ||
|  |     const keys = await this.readDKIMKeysForSelector(domain, selector); | ||
|  | 
 | ||
|  |     // Remove the PEM header and footer and newlines
 | ||
|  |     const pemHeader = '-----BEGIN PUBLIC KEY-----'; | ||
|  |     const pemFooter = '-----END PUBLIC KEY-----'; | ||
|  |     const keyContents = keys.publicKey | ||
|  |       .replace(pemHeader, '') | ||
|  |       .replace(pemFooter, '') | ||
|  |       .replace(/\n/g, ''); | ||
|  | 
 | ||
|  |     // Generate the DKIM DNS TXT record
 | ||
|  |     const dnsRecordValue = `v=DKIM1; h=sha256; k=rsa; p=${keyContents}`; | ||
|  | 
 | ||
|  |     return { | ||
|  |       name: `${selector}._domainkey.${domain}`, | ||
|  |       type: 'TXT', | ||
|  |       dnsSecEnabled: null, | ||
|  |       value: dnsRecordValue, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Clean up old DKIM keys after grace period | ||
|  |    */ | ||
|  |   public async cleanupOldKeys(domain: string, gracePeriodDays: number = 30): Promise<void> { | ||
|  |     if (!this.storageManager) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     // List all selectors for the domain
 | ||
|  |     const metadataKeys = await this.storageManager.list(`/email/dkim/${domain}/`); | ||
|  |      | ||
|  |     for (const key of metadataKeys) { | ||
|  |       if (key.endsWith('/metadata')) { | ||
|  |         const metadataStr = await this.storageManager.get(key); | ||
|  |         if (metadataStr) { | ||
|  |           const metadata = JSON.parse(metadataStr) as IDkimKeyMetadata; | ||
|  |            | ||
|  |           // Check if key is rotated and past grace period
 | ||
|  |           if (metadata.rotatedAt) { | ||
|  |             const gracePeriodMs = gracePeriodDays * 24 * 60 * 60 * 1000; | ||
|  |             const now = Date.now(); | ||
|  |              | ||
|  |             if (now - metadata.rotatedAt > gracePeriodMs) { | ||
|  |               console.log(`Cleaning up old DKIM keys for ${domain} selector ${metadata.selector}`); | ||
|  |                | ||
|  |               // Delete key files
 | ||
|  |               const keyPaths = await this.getKeyPathsForSelector(domain, metadata.selector); | ||
|  |               try { | ||
|  |                 await plugins.fs.promises.unlink(keyPaths.privateKeyPath); | ||
|  |                 await plugins.fs.promises.unlink(keyPaths.publicKeyPath); | ||
|  |               } catch (error) { | ||
|  |                 console.warn(`Failed to delete old key files: ${error.message}`); | ||
|  |               } | ||
|  |                | ||
|  |               // Delete metadata
 | ||
|  |               await this.storageManager.delete(key); | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } |