| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | const punycode = require('../punycode');
|
|---|
| 4 | const mimeFuncs = require('../mime-funcs');
|
|---|
| 5 | const crypto = require('crypto');
|
|---|
| 6 |
|
|---|
| 7 | /**
|
|---|
| 8 | * Returns DKIM signature header line
|
|---|
| 9 | *
|
|---|
| 10 | * @param {Object} headers Parsed headers object from MessageParser
|
|---|
| 11 | * @param {String} bodyHash Base64 encoded hash of the message
|
|---|
| 12 | * @param {Object} options DKIM options
|
|---|
| 13 | * @param {String} options.domainName Domain name to be signed for
|
|---|
| 14 | * @param {String} options.keySelector DKIM key selector to use
|
|---|
| 15 | * @param {String} options.privateKey DKIM private key to use
|
|---|
| 16 | * @return {String} Complete header line
|
|---|
| 17 | */
|
|---|
| 18 |
|
|---|
| 19 | module.exports = (headers, hashAlgo, bodyHash, options) => {
|
|---|
| 20 | options = options || {};
|
|---|
| 21 |
|
|---|
| 22 | // all listed fields from RFC4871 #5.5
|
|---|
| 23 | let defaultFieldNames =
|
|---|
| 24 | 'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
|
|---|
| 25 | 'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
|
|---|
| 26 | 'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
|---|
| 27 | 'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
|---|
| 28 | 'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
|---|
| 29 | 'List-Owner:List-Archive';
|
|---|
| 30 |
|
|---|
| 31 | let fieldNames = options.headerFieldNames || defaultFieldNames;
|
|---|
| 32 |
|
|---|
| 33 | let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
|---|
| 34 | let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
|---|
| 35 |
|
|---|
| 36 | let signer, signature;
|
|---|
| 37 |
|
|---|
| 38 | canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
|
|---|
| 39 |
|
|---|
| 40 | signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
|---|
| 41 | signer.update(canonicalizedHeaderData.headers);
|
|---|
| 42 | try {
|
|---|
| 43 | signature = signer.sign(options.privateKey, 'base64');
|
|---|
| 44 | } catch (E) {
|
|---|
| 45 | return false;
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
|---|
| 49 | };
|
|---|
| 50 |
|
|---|
| 51 | module.exports.relaxedHeaders = relaxedHeaders;
|
|---|
| 52 |
|
|---|
| 53 | function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
|---|
| 54 | let dkim = [
|
|---|
| 55 | 'v=1',
|
|---|
| 56 | 'a=rsa-' + hashAlgo,
|
|---|
| 57 | 'c=relaxed/relaxed',
|
|---|
| 58 | 'd=' + punycode.toASCII(domainName),
|
|---|
| 59 | 'q=dns/txt',
|
|---|
| 60 | 's=' + keySelector,
|
|---|
| 61 | 'bh=' + bodyHash,
|
|---|
| 62 | 'h=' + fieldNames
|
|---|
| 63 | ].join('; ');
|
|---|
| 64 |
|
|---|
| 65 | return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | function relaxedHeaders(headers, fieldNames, skipFields) {
|
|---|
| 69 | let includedFields = new Set();
|
|---|
| 70 | let skip = new Set();
|
|---|
| 71 | let headerFields = new Map();
|
|---|
| 72 |
|
|---|
| 73 | (skipFields || '')
|
|---|
| 74 | .toLowerCase()
|
|---|
| 75 | .split(':')
|
|---|
| 76 | .forEach(field => {
|
|---|
| 77 | skip.add(field.trim());
|
|---|
| 78 | });
|
|---|
| 79 |
|
|---|
| 80 | (fieldNames || '')
|
|---|
| 81 | .toLowerCase()
|
|---|
| 82 | .split(':')
|
|---|
| 83 | .filter(field => !skip.has(field.trim()))
|
|---|
| 84 | .forEach(field => {
|
|---|
| 85 | includedFields.add(field.trim());
|
|---|
| 86 | });
|
|---|
| 87 |
|
|---|
| 88 | for (let i = headers.length - 1; i >= 0; i--) {
|
|---|
| 89 | let line = headers[i];
|
|---|
| 90 | // only include the first value from bottom to top
|
|---|
| 91 | if (includedFields.has(line.key) && !headerFields.has(line.key)) {
|
|---|
| 92 | headerFields.set(line.key, relaxedHeaderLine(line.line));
|
|---|
| 93 | }
|
|---|
| 94 | }
|
|---|
| 95 |
|
|---|
| 96 | let headersList = [];
|
|---|
| 97 | let fields = [];
|
|---|
| 98 | includedFields.forEach(field => {
|
|---|
| 99 | if (headerFields.has(field)) {
|
|---|
| 100 | fields.push(field);
|
|---|
| 101 | headersList.push(field + ':' + headerFields.get(field));
|
|---|
| 102 | }
|
|---|
| 103 | });
|
|---|
| 104 |
|
|---|
| 105 | return {
|
|---|
| 106 | headers: headersList.join('\r\n') + '\r\n',
|
|---|
| 107 | fieldNames: fields.join(':')
|
|---|
| 108 | };
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | function relaxedHeaderLine(line) {
|
|---|
| 112 | return line
|
|---|
| 113 | .substr(line.indexOf(':') + 1)
|
|---|
| 114 | .replace(/\r?\n/g, '')
|
|---|
| 115 | .replace(/\s+/g, ' ')
|
|---|
| 116 | .trim();
|
|---|
| 117 | }
|
|---|