mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-12-26 18:01:01 +08:00
276 lines
8.5 KiB
JavaScript
276 lines
8.5 KiB
JavaScript
'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;
|