'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 { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events'); const { encrypt, decrypt } = require('./encrypt'); const { promisify } = require('util'); const generateKeyPair = promisify(crypto.generateKeyPair); class DkimHandler { constructor(options) { options = options || {}; this.cipher = options.cipher; this.secret = options.secret; this.database = options.database; this.redis = options.redis; this.loggelf = options.loggelf || (() => false); } async generateKey(keyBits, keyExponent) { const { privateKey, publicKey } = await generateKeyPair('rsa', { modulusLength: keyBits || 2048, // options publicExponent: keyExponent || 65537, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); return { privateKey, publicKey }; } async set(options) { const domain = tools.normalizeDomain(options.domain); const selector = options.selector; const description = options.description; let privateKeyPem = options.privateKey; let publicKeyPem; if (!privateKeyPem) { let keyPair = await this.generateKey(); if (!keyPair || !keyPair.privateKey || !keyPair.publicKey) { let err = new Error('Failed to generate key pair'); err.responseCode = 500; err.code = 'KeyGenereateError'; throw err; } privateKeyPem = keyPair.privateKey; publicKeyPem = keyPair.publicKey; } if (!publicKeyPem) { // extract public key from private key using 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.responseCode = 500; err.code = 'KeyGenereateError'; throw err; } } 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.responseCode = 400; err.code = 'InputValidationError'; throw err; } // encrypt if needed privateKeyPem = await encrypt(privateKeyPem, this.secret); let dkimData = { domain, selector, privateKey: privateKeyPem, publicKey: publicKeyPem, fingerprint: fp, created: new Date(), latest: true }; if (description) { dkimData.description = description; } let r; try { r = await this.database.collection('dkim').findOneAndReplace( { domain }, dkimData, { upsert: true, returnDocument: 'after' } ); } catch (err) { err.responseCode = 500; err.code = 'InternalDatabaseError'; throw err; } if (!r.value) { let err = new Error('Failed to insert DKIM key'); err.responseCode = 500; err.code = 'InternalDatabaseError'; throw err; } if (this.redis) { if (r.lastErrorObject.upserted) { try { await publish(this.redis, { ev: DKIM_CREATED, dkim: r.value._id, domain, selector, fingerprint: fp }); } catch (err) { // ignore? } } else if (r.lastErrorObject.updatedExisting) { try { await publish(this.redis, { ev: DKIM_UPDATED, dkim: r.value._id, domain, selector, fingerprint: fp }); } catch (err) { // ignore? } } } return { 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, '') } }; } async get(options, includePrivateKey) { 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.responseCode = 404; err.code = 'DkimNotFound'; throw err; } let dkimData; try { dkimData = await this.database.collection('dkim').findOne(query); } catch (err) { err.responseCode = 500; err.code = 'InternalDatabaseError'; throw err; } if (!dkimData) { let err = new Error('Invalid or unknown DKIM key'); err.responseCode = 404; err.code = 'DkimNotFound'; throw err; } let privateKey; if (includePrivateKey) { privateKey = await decrypt(dkimData.privateKey, this.secret, this.cipher); } return { 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 }; } async del(options) { 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.responseCode = 404; err.code = 'DkimNotFound'; throw err; } // delete dkim key from database let r; try { r = await this.database.collection('dkim').findOneAndDelete(query); } catch (err) { err.responseCode = 500; err.code = 'InternalDatabaseError'; throw err; } if (!r.value) { let err = new Error('Invalid or unknown DKIM key'); err.responseCode = 404; err.code = 'DkimNotFound'; throw err; } try { await publish(this.redis, { ev: DKIM_DELETED, dkim: r.value._id, domain: r.value.domain, selector: r.value.selector, fingerprint: r.value.fingerprint }); } catch (err) { // ignore? } return true; } } module.exports = DkimHandler;