'use strict'; const ObjectID = require('mongodb').ObjectID; const fingerprint = require('key-fingerprint').fingerprint; const forge = require('node-forge'); const crypto = require('crypto'); const tools = require('./tools'); const pem = require('pem'); const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events'); class DkimHandler { constructor(options) { options = options || {}; this.cipher = options.cipher; this.secret = options.secret; this.useOpenSSL = !!options.useOpenSSL; if (options.pathOpenSSL) { pem.config({ pathOpenSSL: options.pathOpenSSL }); } this.database = options.database; this.redis = options.redis; this.loggelf = options.loggelf || (() => false); } set(options, callback) { const domain = tools.normalizeDomain(options.domain); const selector = options.selector; const description = options.description; let privateKeyPem = options.privateKey; let publicKeyPem; let getPrivateKey = done => { if (privateKeyPem) { return done(); } // private key not set, generate a new key if (this.useOpenSSL) { return pem.createPrivateKey(2048, {}, (err, result) => { if (err) { err.code = 'KeyGenereateError'; return callback(err); } if (!result || !result.key) { let err = new Error('Failed to generate private key'); err.code = 'KeyGenereateError'; return callback(err); } privateKeyPem = result.key; return done(); }); } // Fallback to Forge forge.rsa.generateKeyPair({ bits: 2048, workers: -1 }, (err, keypair) => { if (err) { err.code = 'KeyGenereateError'; return callback(err); } privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey); publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey); return done(); }); }; let getPublicKey = done => { if (publicKeyPem) { return done(); } // extract public key from private key if (this.useOpenSSL) { return pem.getPublicKey(privateKeyPem, (err, result) => { if (err) { err.code = 'KeyGenereateError'; return callback(err); } if (!result || !result.publicKey) { let err = new Error('Failed to generate public key'); err.code = 'KeyGenereateError'; return callback(err); } publicKeyPem = result.publicKey; return done(); }); } // Fallback to Forge let privateKey = forge.pki.privateKeyFromPem(privateKeyPem); let publicKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e); publicKeyPem = forge.pki.publicKeyToPem(publicKey); if (!publicKeyPem) { let err = new Error('Failed to generate public key'); err.code = 'KeyGenereateError'; return callback(err); } done(); }; getPrivateKey(() => { getPublicKey(() => { let fp; try { fp = fingerprint(privateKeyPem, 'sha256', true); let ciphered = crypto.publicEncrypt(publicKeyPem, Buffer.from('secretvalue')); let deciphered = crypto.privateDecrypt(privateKeyPem, ciphered); if (deciphered.toString() !== 'secretvalue') { throw new Error('Was not able to use key for encryption'); } } catch (E) { let err = new Error('Invalid or incompatible private key. ' + E.message); err.code = 'InputValidationError'; return callback(err); } if (this.secret) { try { let cipher = crypto.createCipher(this.cipher || 'aes192', this.secret); privateKeyPem = '$' + cipher.update(privateKeyPem, 'utf8', 'hex'); privateKeyPem += cipher.final('hex'); } catch (E) { let err = new Error('Failed to encrypt private key. ' + E.message); err.code = 'InternalConfigError'; return callback(err); } } let dkimData = { domain, selector, privateKey: privateKeyPem, publicKey: publicKeyPem, fingerprint: fp, created: new Date(), latest: true }; if (description) { dkimData.description = description; } this.database.collection('dkim').findOneAndReplace( { domain }, dkimData, { upsert: true, returnOriginal: false }, (err, r) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); } if (!r.value) { let err = new Error('Failed to insert DKIM key'); err.code = 'InternalDatabaseError'; return callback(err); } if (this.redis) { if (r.lastErrorObject.upserted) { publish(this.redis, { ev: DKIM_CREATED, dkim: r.value._id, domain, selector, fingerprint: fp }).catch(() => false); } else if (r.lastErrorObject.updatedExisting) { publish(this.redis, { ev: DKIM_UPDATED, dkim: r.value._id, domain, selector, fingerprint: fp }).catch(() => false); } } return callback(null, { id: r.value._id.toString(), domain: dkimData.domain, selector: dkimData.selector, description: dkimData.description, fingerprint: dkimData.fingerprint, publicKey: dkimData.publicKey, dnsTxt: { name: dkimData.selector + '._domainkey.' + dkimData.domain, value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '') } }); } ); }); }); } get(options, includePrivateKey, callback) { let query = {}; options = options || {}; if (options.domain) { query.domain = tools.normalizeDomain(options.domain); } else if (options._id && tools.isId(options._id)) { query._id = new ObjectID(options._id); } else { let err = new Error('Invalid or unknown DKIM key'); err.code = 'DkimNotFound'; return setImmediate(() => callback(err)); } this.database.collection('dkim').findOne(query, (err, dkimData) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); } if (!dkimData) { let err = new Error('Invalid or unknown DKIM key'); err.code = 'DkimNotFound'; return callback(err); } let privateKey; if (includePrivateKey) { privateKey = dkimData.privateKey; if (privateKey.charAt(0) === '$') { if (this.secret) { try { let decipher = crypto.createDecipher(this.cipher || 'aes192', this.secret); privateKey = decipher.update(privateKey.substr(1), 'hex', 'utf-8'); privateKey += decipher.final('utf8'); } catch (E) { let err = new Error('Failed to decrypt private key. ' + E.message); err.code = 'InternalConfigError'; return callback(err); } } else { let err = new Error('Can not use decrypted key'); err.code = 'InternalConfigError'; return callback(err); } } } callback(null, { id: dkimData._id.toString(), domain: dkimData.domain, selector: dkimData.selector, description: dkimData.description, fingerprint: dkimData.fingerprint, publicKey: dkimData.publicKey, privateKey, dnsTxt: { name: dkimData.selector + '._domainkey.' + dkimData.domain, value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '') }, created: dkimData.created }); }); } del(options, callback) { let query = {}; if (options.domain) { query.domain = tools.normalizeDomain(options.domain); } else if (options._id && tools.isId(options._id)) { query._id = new ObjectID(options._id); } else { let err = new Error('Invalid or unknown DKIM key'); err.code = 'DkimNotFound'; return setImmediate(() => callback(err)); } // delete dkim key from database this.database.collection('dkim').findOneAndDelete(query, (err, r) => { if (err) { err.code = 'InternalDatabaseError'; return callback(err); } if (!r.value) { let err = new Error('Invalid or unknown DKIM key'); err.code = 'DkimNotFound'; return callback(err); } publish(this.redis, { ev: DKIM_DELETED, dkim: r.value._id, domain: r.value.domain, selector: r.value.selector, fingerprint: r.value.fingerprint }).catch(() => false); return callback(null, true); }); } } module.exports = DkimHandler;