wildduck/lib/dkim-handler.js
NickOvt 3d7d0a6d6a
fix(api-dkim): DKIM now supports ED25519 keys, both in PEM and raw format as input ZMS-125 (#617)
* dkim, add support for ed25519

* add support for raw ED25519 private key

* magic value make variable. Remove unnecessary variables. Refactor

* add new tests for ED25519
2024-02-19 09:46:46 +02:00

297 lines
9.3 KiB
JavaScript

'use strict';
const ObjectId = require('mongodb').ObjectId;
const fingerprint = require('key-fingerprint').fingerprint;
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);
const ASN1_PADDING = 'MC4CAQAwBQYDK2VwBCIEIA==';
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;
let publicKeyDer;
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
// 1) check that privateKeyPem is ED25519 raw key, which length is 44
if (privateKeyPem.length === 44) {
// privateKeyPem is actually a raw ED25519 base64 string with length of 44
// convert raw ED25519 key to PEM formatted private key
privateKeyPem = `-----BEGIN PRIVATE KEY-----
${Buffer.concat([Buffer.from(ASN1_PADDING, 'base64'), Buffer.from(privateKeyPem, 'base64')]).toString('base64')}
-----END PRIVATE KEY-----`;
}
const publicKey = crypto.createPublicKey({ key: privateKeyPem, format: 'pem' });
publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
if (publicKey.asymmetricKeyType === 'ed25519') {
publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).subarray(12).toString('base64');
} else if (publicKey.asymmetricKeyType === 'rsa') {
publicKeyDer = publicKey.export({ format: 'der', type: 'spki' }).toString('base64');
}
if (!publicKeyPem && !publicKeyDer) {
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);
const testData = Buffer.from('secretvalue');
const signature = crypto.sign(null, testData, privateKeyPem);
const verificationResult = crypto.verify(null, testData, publicKeyPem, signature);
if (!verificationResult) {
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,
publicKeyDer,
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.publicKeyDer
}
};
}
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;