diff --git a/config/default.toml b/config/default.toml index 2d8e0615..61796d9d 100644 --- a/config/default.toml +++ b/config/default.toml @@ -29,6 +29,13 @@ bugsnagCode="" [dbs] # @include "dbs.toml" +[dkim] + # If enabled then encrypt DKIM keys with the secret password. By default DKIM keys + # are not encrypted and stored as cleartext. Once set up do not change these values, + # otherwise decrypting DKIM keys is going to fail + #cipher="aes192" + #secret="a secret cat" + [totp] # If enabled then encrypt TOTP seed tokens with the secret password. By default TOTP seeds # are not encrypted and stored as cleartext. Once set up do not change these values, diff --git a/lib/api/dkim.js b/lib/api/dkim.js index 28419503..abc83163 100644 --- a/lib/api/dkim.js +++ b/lib/api/dkim.js @@ -1,14 +1,18 @@ 'use strict'; +const config = require('wild-config'); const Joi = require('../joi'); const MongoPaging = require('mongo-cursor-pagination-node6'); const ObjectID = require('mongodb').ObjectID; -const tools = require('../tools'); -const fingerprint = require('key-fingerprint').fingerprint; -const pki = require('node-forge').pki; -const crypto = require('crypto'); +const DkimHandler = require('../dkim-handler'); module.exports = (db, server) => { + const dkimHandler = new DkimHandler({ + cipher: config.dkim.cipher, + secret: config.dkim.secret, + database: db.database + }); + /** * @api {get} /dkim List registered DKIM keys * @apiName GetDkim @@ -121,7 +125,7 @@ module.exports = (db, server) => { } : {}; - db.users.collection('dkim').count(filter, (err, total) => { + db.database.collection('dkim').count(filter, (err, total) => { if (err) { res.json({ error: err.message @@ -142,7 +146,7 @@ module.exports = (db, server) => { opts.previous = pagePrevious; } - MongoPaging.find(db.users.collection('dkim'), opts, (err, result) => { + MongoPaging.find(db.database.collection('dkim'), opts, (err, result) => { if (err) { res.json({ error: err.message @@ -275,89 +279,20 @@ module.exports = (db, server) => { return next(); } - let domain = tools.normalizeDomain(req.params.domain); - let selector = req.params.selector; - let description = req.params.description; - - let fp, publicKeyPem; - try { - fp = fingerprint(result.value.privateKey, 'sha256', true); - let privateKey = pki.privateKeyFromPem(result.value.privateKey); - let publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e); - publicKeyPem = pki.publicKeyToPem(publicKey); - if (!publicKeyPem) { - throw new Error('Was not able to extract public key from private key'); - } - - let ciphered = crypto.publicEncrypt(publicKeyPem, Buffer.from('secretvalue')); - let deciphered = crypto.privateDecrypt(result.value.privateKey, ciphered); - if (deciphered.toString() !== 'secretvalue') { - throw new Error('Was not able to use key for encryption'); - } - } catch (E) { - res.json({ - error: 'Invalid or incompatible private key', - code: 'InputValidationError' - }); - return next(); - } - - let dkimData = { - domain, - selector, - privateKey: result.value.privateKey, - publicKey: publicKeyPem, - fingerprint: fp, - created: new Date(), - latest: true - }; - - if (description) { - dkimData.description = description; - } - - db.users.collection('dkim').findOneAndReplace( - { - domain - }, - dkimData, - { - upsert: true, - returnOriginal: false - }, - (err, r) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - - if (!r.value) { - res.json({ - error: 'Failed to insert DKIM key', - code: 'InternalDatabaseError' - }); - return next(); - } - + dkimHandler.set(result.value, (err, response) => { + if (err) { res.json({ - success: true, - id: r.value._id, - 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, '') - } + error: err.message, + code: err.code }); return next(); } - ); + if (response) { + response.success = true; + } + res.json(response); + return next(); + }); }); /** @@ -438,44 +373,20 @@ module.exports = (db, server) => { let dkim = new ObjectID(result.value.dkim); - db.users.collection('dkim').findOne( - { - _id: dkim - }, - (err, dkimData) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - if (!dkimData) { - res.status(404); - res.json({ - error: 'Invalid or unknown DKIM key' - }); - return next(); - } - + dkimHandler.get(dkim, false, (err, response) => { + if (err) { res.json({ - success: true, - id: dkimData._id, - 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, '') - }, - created: dkimData.created + error: err.message, + code: err.code }); - return next(); } - ); + if (response) { + response.success = true; + } + res.json(response); + return next(); + }); }); /** @@ -535,33 +446,18 @@ module.exports = (db, server) => { let dkim = new ObjectID(result.value.dkim); - // delete address from email address registry - db.users.collection('dkim').deleteOne( - { - _id: dkim - }, - (err, r) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - - if (!r.deletedCount) { - res.status(404); - res.json({ - error: 'Invalid or unknown DKIM key' - }); - return next(); - } - + dkimHandler.del(dkim, (err, response) => { + if (err) { res.json({ - success: !!r.deletedCount + error: err.message, + code: err.code }); return next(); } - ); + res.json({ + success: response + }); + return next(); + }); }); }; diff --git a/lib/dkim-handler.js b/lib/dkim-handler.js new file mode 100644 index 00000000..f4de6275 --- /dev/null +++ b/lib/dkim-handler.js @@ -0,0 +1,193 @@ +'use strict'; + +const ObjectID = require('mongodb').ObjectID; +const fingerprint = require('key-fingerprint').fingerprint; +const pki = require('node-forge').pki; +const crypto = require('crypto'); +const tools = require('./tools'); + +class DkimHandler { + constructor(options) { + options = options || {}; + this.cipher = options.cipher; + this.secret = options.secret; + + this.database = options.database; + } + + set(options, callback) { + const domain = tools.normalizeDomain(options.domain); + const selector = options.selector; + const description = options.description; + + let privateKeyPem = options.privateKey; + + let fp, publicKeyPem; + try { + fp = fingerprint(privateKeyPem, 'sha256', true); + let privateKey = pki.privateKeyFromPem(privateKeyPem); + let publicKey = pki.setRsaPublicKey(privateKey.n, privateKey.e); + publicKeyPem = pki.publicKeyToPem(publicKey); + if (!publicKeyPem) { + throw new Error('Was not able to extract public key from private key'); + } + + 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); + } + + return callback(null, { + id: r.value._id, + 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(domain, includePrivateKey, callback) { + let query = {}; + if (ObjectID.isValid(domain)) { + query._id = domain; + } else { + query.domain = tools.normalizeDomain(domain); + } + + 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 = 'KeyNotFound'; + 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, + 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(domain, callback) { + let query = {}; + if (ObjectID.isValid(domain)) { + query._id = domain; + } else { + query.domain = tools.normalizeDomain(domain); + } + + // delete address from email address registry + this.database.collection('dkim').deleteOne(query, (err, r) => { + if (err) { + err.code = 'InternalDatabaseError'; + return callback(err); + } + + if (!r.deletedCount) { + let err = new Error('Invalid or unknown DKIM key'); + err.code = 'KeyNotFound'; + return callback(err); + } + + return callback(null, !!r.deletedCount); + }); + } +} + +module.exports = DkimHandler; diff --git a/lib/user-handler.js b/lib/user-handler.js index 882d1476..a1535873 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -1074,9 +1074,16 @@ class UserHandler { if (userData.seed) { let secret = userData.seed; if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) { - let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); - secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); - secret += decipher.final('utf8'); + try { + let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); + secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); + secret += decipher.final('utf8'); + } catch (E) { + log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message); + let err = new Error('Can not use decrypted secret'); + err.code = 'InternalConfigError'; + return callback(err); + } } let otpauth_url = speakeasy.otpauthURL({ @@ -1104,9 +1111,16 @@ class UserHandler { let seed = secret.base32; if (config.totp && config.totp.secret) { - let cipher = crypto.createCipher(config.totp.cipher || 'aes192', config.totp.secret); - seed = '$' + cipher.update(seed, 'utf8', 'hex'); - seed += cipher.final('hex'); + try { + let cipher = crypto.createCipher(config.totp.cipher || 'aes192', config.totp.secret); + seed = '$' + cipher.update(seed, 'utf8', 'hex'); + seed += cipher.final('hex'); + } catch (E) { + log.error('DB', 'TOTPFAIL cipher failed id=%s error=%s', user, E.message); + let err = new Error('Database Error, failed to update user'); + err.code = 'InternalDatabaseError'; + return callback(err); + } } return this.users.collection('users').findOneAndUpdate( @@ -1197,9 +1211,16 @@ class UserHandler { let secret = userData.seed; if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) { - let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); - secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); - secret += decipher.final('utf8'); + try { + let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); + secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); + secret += decipher.final('utf8'); + } catch (E) { + log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message); + let err = new Error('Can not use decrypted secret'); + err.code = 'InternalConfigError'; + return callback(err); + } } let verified = speakeasy.totp.verify({ @@ -1416,9 +1437,16 @@ class UserHandler { let secret = userData.seed; if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) { - let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); - secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); - secret += decipher.final('utf8'); + try { + let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret); + secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8'); + secret += decipher.final('utf8'); + } catch (E) { + log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message); + let err = new Error('Can not use decrypted secret'); + err.code = 'InternalConfigError'; + return callback(err); + } } let verified = speakeasy.totp.verify({