mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-10 17:47:07 +08:00
298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
// TODO: fix keypair generation
|
|
|
|
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 };
|
|
}
|
|
|
|
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
|
|
this.generateKey()
|
|
.then(keyPair => {
|
|
if (!keyPair || !keyPair.privateKey || !keyPair.publicKey) {
|
|
let err = new Error('Failed to generate key pair');
|
|
err.responseCode = 500;
|
|
err.code = 'KeyGenereateError';
|
|
return callback(err);
|
|
}
|
|
privateKeyPem = keyPair.privateKey;
|
|
publicKeyPem = keyPair.publicKey;
|
|
return done();
|
|
})
|
|
.catch(err => {
|
|
err.responseCode = 500;
|
|
err.code = 'KeyGenereateError';
|
|
return callback(err);
|
|
});
|
|
};
|
|
|
|
let getPublicKey = done => {
|
|
if (publicKeyPem) {
|
|
return done();
|
|
}
|
|
|
|
// 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';
|
|
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.responseCode = 400;
|
|
err.code = 'InputValidationError';
|
|
return callback(err);
|
|
}
|
|
|
|
encrypt(privateKeyPem, this.secret)
|
|
.then(privateKeyPem => {
|
|
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,
|
|
returnDocument: 'after'
|
|
},
|
|
(err, r) => {
|
|
if (err) {
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
return callback(err);
|
|
}
|
|
|
|
if (!r.value) {
|
|
let err = new Error('Failed to insert DKIM key');
|
|
err.responseCode = 500;
|
|
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, '')
|
|
}
|
|
});
|
|
}
|
|
);
|
|
})
|
|
.catch(err => callback(err));
|
|
});
|
|
});
|
|
}
|
|
|
|
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.responseCode = 404;
|
|
err.code = 'DkimNotFound';
|
|
return setImmediate(() => callback(err));
|
|
}
|
|
|
|
this.database.collection('dkim').findOne(query, (err, dkimData) => {
|
|
if (err) {
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
return callback(err);
|
|
}
|
|
if (!dkimData) {
|
|
let err = new Error('Invalid or unknown DKIM key');
|
|
err.responseCode = 404;
|
|
err.code = 'DkimNotFound';
|
|
return callback(err);
|
|
}
|
|
|
|
let loadPrivateKey = next => {
|
|
if (!includePrivateKey) {
|
|
return next();
|
|
}
|
|
|
|
decrypt(dkimData.privateKey, this.secret, this.cipher)
|
|
.then(privateKey => next(null, privateKey))
|
|
.catch(err => next(err));
|
|
};
|
|
|
|
loadPrivateKey((err, privateKey) => {
|
|
if (err) {
|
|
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.responseCode = 404;
|
|
err.code = 'DkimNotFound';
|
|
return setImmediate(() => callback(err));
|
|
}
|
|
|
|
// delete dkim key from database
|
|
this.database.collection('dkim').findOneAndDelete(query, (err, r) => {
|
|
if (err) {
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
return callback(err);
|
|
}
|
|
|
|
if (!r.value) {
|
|
let err = new Error('Invalid or unknown DKIM key');
|
|
err.responseCode = 404;
|
|
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;
|