'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(); }