118 lines
3.6 KiB
JavaScript
118 lines
3.6 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const punycode = require('../punycode');
|
||
|
const mimeFuncs = require('../mime-funcs');
|
||
|
const crypto = require('crypto');
|
||
|
|
||
|
/**
|
||
|
* Returns DKIM signature header line
|
||
|
*
|
||
|
* @param {Object} headers Parsed headers object from MessageParser
|
||
|
* @param {String} bodyHash Base64 encoded hash of the message
|
||
|
* @param {Object} options DKIM options
|
||
|
* @param {String} options.domainName Domain name to be signed for
|
||
|
* @param {String} options.keySelector DKIM key selector to use
|
||
|
* @param {String} options.privateKey DKIM private key to use
|
||
|
* @return {String} Complete header line
|
||
|
*/
|
||
|
|
||
|
module.exports = (headers, hashAlgo, bodyHash, options) => {
|
||
|
options = options || {};
|
||
|
|
||
|
// all listed fields from RFC4871 #5.5
|
||
|
let defaultFieldNames =
|
||
|
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
|
||
|
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
|
||
|
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
||
|
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
||
|
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
||
|
'List-Owner:List-Archive';
|
||
|
|
||
|
let fieldNames = options.headerFieldNames || defaultFieldNames;
|
||
|
|
||
|
let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
||
|
let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
||
|
|
||
|
let signer, signature;
|
||
|
|
||
|
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
|
||
|
|
||
|
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||
|
signer.update(canonicalizedHeaderData.headers);
|
||
|
try {
|
||
|
signature = signer.sign(options.privateKey, 'base64');
|
||
|
} catch (E) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
||
|
};
|
||
|
|
||
|
module.exports.relaxedHeaders = relaxedHeaders;
|
||
|
|
||
|
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
||
|
let dkim = [
|
||
|
'v=1',
|
||
|
'a=rsa-' + hashAlgo,
|
||
|
'c=relaxed/relaxed',
|
||
|
'd=' + punycode.toASCII(domainName),
|
||
|
'q=dns/txt',
|
||
|
's=' + keySelector,
|
||
|
'bh=' + bodyHash,
|
||
|
'h=' + fieldNames
|
||
|
].join('; ');
|
||
|
|
||
|
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
|
||
|
}
|
||
|
|
||
|
function relaxedHeaders(headers, fieldNames, skipFields) {
|
||
|
let includedFields = new Set();
|
||
|
let skip = new Set();
|
||
|
let headerFields = new Map();
|
||
|
|
||
|
(skipFields || '')
|
||
|
.toLowerCase()
|
||
|
.split(':')
|
||
|
.forEach(field => {
|
||
|
skip.add(field.trim());
|
||
|
});
|
||
|
|
||
|
(fieldNames || '')
|
||
|
.toLowerCase()
|
||
|
.split(':')
|
||
|
.filter(field => !skip.has(field.trim()))
|
||
|
.forEach(field => {
|
||
|
includedFields.add(field.trim());
|
||
|
});
|
||
|
|
||
|
for (let i = headers.length - 1; i >= 0; i--) {
|
||
|
let line = headers[i];
|
||
|
// only include the first value from bottom to top
|
||
|
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
|
||
|
headerFields.set(line.key, relaxedHeaderLine(line.line));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let headersList = [];
|
||
|
let fields = [];
|
||
|
includedFields.forEach(field => {
|
||
|
if (headerFields.has(field)) {
|
||
|
fields.push(field);
|
||
|
headersList.push(field + ':' + headerFields.get(field));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return {
|
||
|
headers: headersList.join('\r\n') + '\r\n',
|
||
|
fieldNames: fields.join(':')
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function relaxedHeaderLine(line) {
|
||
|
return line
|
||
|
.substr(line.indexOf(':') + 1)
|
||
|
.replace(/\r?\n/g, '')
|
||
|
.replace(/\s+/g, ' ')
|
||
|
.trim();
|
||
|
}
|