dkim.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import crypto from 'node:crypto';
  2. export function createDkimKeyPair() {
  3. const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  4. modulusLength: 2048,
  5. publicKeyEncoding: {
  6. type: 'spki',
  7. format: 'pem'
  8. },
  9. privateKeyEncoding: {
  10. type: 'pkcs8',
  11. format: 'pem'
  12. }
  13. });
  14. return {
  15. publicKey: pemToDkimPublic(publicKey),
  16. privateKey
  17. };
  18. }
  19. export function pemToDkimPublic(pem) {
  20. return pem
  21. .replace(/-----BEGIN PUBLIC KEY-----/g, '')
  22. .replace(/-----END PUBLIC KEY-----/g, '')
  23. .replace(/\s+/g, '');
  24. }
  25. export function buildDkimRecord(publicKey) {
  26. return `v=DKIM1; k=rsa; p=${publicKey}`;
  27. }
  28. export function signDkim(rawMessage, options) {
  29. const headers = parseHeaders(rawMessage);
  30. const body = rawMessage.slice(rawMessage.indexOf('\r\n\r\n') + 4);
  31. const signedHeaderNames = [
  32. 'from',
  33. 'to',
  34. 'subject',
  35. 'date',
  36. 'message-id',
  37. 'mime-version',
  38. 'content-type'
  39. ];
  40. const bodyHash = crypto
  41. .createHash('sha256')
  42. .update(canonicalizeBody(body))
  43. .digest('base64');
  44. const signatureFields = [
  45. 'v=1',
  46. 'a=rsa-sha256',
  47. 'c=relaxed/relaxed',
  48. `d=${options.domain}`,
  49. `s=${options.selector}`,
  50. `h=${signedHeaderNames.join(':')}`,
  51. `bh=${bodyHash}`,
  52. 'b='
  53. ];
  54. const dkimValueWithoutSignature = signatureFields.join('; ');
  55. const signingInput = [
  56. ...signedHeaderNames.map((name) => canonicalizeHeader(findHeader(headers, name))),
  57. canonicalizeHeader({ name: 'DKIM-Signature', value: dkimValueWithoutSignature })
  58. ].join('');
  59. const signature = crypto
  60. .createSign('RSA-SHA256')
  61. .update(signingInput)
  62. .sign(options.privateKey, 'base64');
  63. const folded = foldHeader('DKIM-Signature', `${dkimValueWithoutSignature}${signature}`);
  64. return `${folded}\r\n${rawMessage}`;
  65. }
  66. function parseHeaders(rawMessage) {
  67. const head = rawMessage.slice(0, rawMessage.indexOf('\r\n\r\n'));
  68. const lines = head.split('\r\n');
  69. const headers = [];
  70. for (const line of lines) {
  71. if (/^[\t ]/.test(line) && headers.length) {
  72. headers[headers.length - 1].value += ` ${line.trim()}`;
  73. continue;
  74. }
  75. const index = line.indexOf(':');
  76. if (index === -1) continue;
  77. headers.push({
  78. name: line.slice(0, index),
  79. value: line.slice(index + 1)
  80. });
  81. }
  82. return headers;
  83. }
  84. function findHeader(headers, name) {
  85. const found = [...headers].reverse().find((header) => header.name.toLowerCase() === name);
  86. if (!found) return { name, value: '' };
  87. return found;
  88. }
  89. function canonicalizeHeader(header) {
  90. const name = header.name.toLowerCase();
  91. const value = header.value.replace(/\s+/g, ' ').trim();
  92. return `${name}:${value}\r\n`;
  93. }
  94. function canonicalizeBody(body) {
  95. const lines = String(body || '')
  96. .replace(/\r?\n/g, '\r\n')
  97. .split('\r\n')
  98. .map((line) => line.replace(/[ \t]+$/g, '').replace(/[ \t]+/g, ' '));
  99. while (lines.length && lines[lines.length - 1] === '') lines.pop();
  100. return `${lines.join('\r\n')}\r\n`;
  101. }
  102. export function foldHeader(name, value) {
  103. const prefix = `${name}: `;
  104. const limit = 76;
  105. const words = value.split(' ');
  106. const lines = [];
  107. let current = prefix;
  108. for (const word of words) {
  109. if ((current + word).length > limit && current.trim() !== `${name}:`) {
  110. lines.push(current.trimEnd());
  111. current = ` ${word} `;
  112. } else {
  113. current += `${word} `;
  114. }
  115. }
  116. lines.push(current.trimEnd());
  117. return lines.join('\r\n');
  118. }