import * as plugins from '../../plugins.js'; import { CachedDocument, TTL } from '../classes.cached.document.js'; import { CacheDb } from '../classes.cachedb.js'; /** * Helper to get the smartdata database instance */ const getDb = () => CacheDb.getInstance().getDb(); /** * CachedDKIMKey - Stores DKIM key pairs for email signing * * Caches DKIM private/public key pairs per domain and selector. * Default TTL is 90 days (typical key rotation interval). */ @plugins.smartdata.Collection(() => getDb()) export class CachedDKIMKey extends CachedDocument { /** * Composite key: domain:selector */ @plugins.smartdata.unI() @plugins.smartdata.svDb() public domainSelector: string; /** * Domain for this DKIM key */ @plugins.smartdata.svDb() public domain: string; /** * DKIM selector (e.g., 'mta', 'default', '2024') */ @plugins.smartdata.svDb() public selector: string; /** * Private key in PEM format */ @plugins.smartdata.svDb() public privateKey: string; /** * Public key in PEM format */ @plugins.smartdata.svDb() public publicKey: string; /** * Public key for DNS TXT record (base64, no headers) */ @plugins.smartdata.svDb() public publicKeyDns: string; /** * Key size in bits (e.g., 1024, 2048) */ @plugins.smartdata.svDb() public keySize: number = 2048; /** * Key algorithm (e.g., 'rsa-sha256') */ @plugins.smartdata.svDb() public algorithm: string = 'rsa-sha256'; /** * When the key was generated */ @plugins.smartdata.svDb() public generatedAt: Date; /** * When the key was last rotated */ @plugins.smartdata.svDb() public rotatedAt: Date; /** * Previous selector (for key rotation) */ @plugins.smartdata.svDb() public previousSelector: string; /** * Number of emails signed with this key */ @plugins.smartdata.svDb() public signCount: number = 0; /** * Whether this key is currently active */ @plugins.smartdata.svDb() public isActive: boolean = true; constructor() { super(); this.setTTL(TTL.DAYS_90); // Default 90-day TTL this.generatedAt = new Date(); } /** * Create the composite key from domain and selector */ public static createDomainSelector(domain: string, selector: string): string { return `${domain.toLowerCase()}:${selector.toLowerCase()}`; } /** * Create a new DKIM key entry */ public static createNew(domain: string, selector: string): CachedDKIMKey { const key = new CachedDKIMKey(); key.domain = domain.toLowerCase(); key.selector = selector.toLowerCase(); key.domainSelector = CachedDKIMKey.createDomainSelector(domain, selector); return key; } /** * Find by domain and selector */ public static async findByDomainSelector( domain: string, selector: string ): Promise { const domainSelector = CachedDKIMKey.createDomainSelector(domain, selector); return await CachedDKIMKey.getInstance({ domainSelector, }); } /** * Find all keys for a domain */ public static async findByDomain(domain: string): Promise { return await CachedDKIMKey.getInstances({ domain: domain.toLowerCase(), }); } /** * Find the active key for a domain */ public static async findActiveForDomain(domain: string): Promise { const keys = await CachedDKIMKey.getInstances({ domain: domain.toLowerCase(), isActive: true, }); return keys.length > 0 ? keys[0] : null; } /** * Find all active keys */ public static async findAllActive(): Promise { return await CachedDKIMKey.getInstances({ isActive: true, }); } /** * Set the key pair */ public setKeyPair(privateKey: string, publicKey: string, publicKeyDns?: string): void { this.privateKey = privateKey; this.publicKey = publicKey; this.publicKeyDns = publicKeyDns || this.extractPublicKeyDns(publicKey); this.generatedAt = new Date(); } /** * Extract the base64 public key for DNS from PEM format */ private extractPublicKeyDns(publicKeyPem: string): string { // Remove PEM headers and newlines return publicKeyPem .replace(/-----BEGIN PUBLIC KEY-----/g, '') .replace(/-----END PUBLIC KEY-----/g, '') .replace(/\s/g, ''); } /** * Generate the DNS TXT record value */ public getDnsTxtRecord(): string { return `v=DKIM1; k=rsa; p=${this.publicKeyDns}`; } /** * Get the full DNS record name */ public getDnsRecordName(): string { return `${this.selector}._domainkey.${this.domain}`; } /** * Record that this key was used to sign an email */ public recordSign(): void { this.signCount++; this.touch(); } /** * Deactivate this key (e.g., during rotation) */ public deactivate(): void { this.isActive = false; } /** * Activate this key */ public activate(): void { this.isActive = true; } /** * Rotate to a new selector */ public rotate(newSelector: string): void { this.previousSelector = this.selector; this.selector = newSelector.toLowerCase(); this.domainSelector = CachedDKIMKey.createDomainSelector(this.domain, this.selector); this.rotatedAt = new Date(); this.signCount = 0; // Reset TTL on rotation this.setTTL(TTL.DAYS_90); } /** * Check if key needs rotation (based on age or sign count) */ public needsRotation(maxAgeDays: number = 90, maxSignCount: number = 1000000): boolean { const ageMs = Date.now() - this.generatedAt.getTime(); const ageDays = ageMs / (24 * 60 * 60 * 1000); return ageDays > maxAgeDays || this.signCount > maxSignCount; } }