diff --git a/examples/authenticate.js b/examples/authenticate.js index 4e831ea..146c5fe 100644 --- a/examples/authenticate.js +++ b/examples/authenticate.js @@ -8,10 +8,11 @@ const fs = require('fs'); const main = async () => { let message = await fs.promises.readFile(process.argv[2] || __dirname + '/../test/fixtures/message4.eml'); let res = await authenticate(message, { - ip: '217.146.67.33', - helo: 'uvn-67-33.tll01.zonevs.eu', + //ip: '217.146.67.33', + //helo: 'uvn-67-33.tll01.zonevs.eu', mta: 'mx.ethereal.email', - sender: 'andris@ekiri.ee', + //sender: 'andris@ekiri.ee', + trustReceived: true, // optional. add ARC seal if possible seal: { signingDomain: 'tahvel.info', diff --git a/lib/dkim/body/relaxed.js b/lib/dkim/body/relaxed.js index adc9510..e8759d7 100644 --- a/lib/dkim/body/relaxed.js +++ b/lib/dkim/body/relaxed.js @@ -3,6 +3,7 @@ 'use strict'; const crypto = require('crypto'); +const { MimeStructureStartFinder } = require('../mime-structure-start-finder'); const CHAR_CR = 0x0d; const CHAR_LF = 0x0a; @@ -39,9 +40,20 @@ class RelaxedHash { this.maxSizeReached = maxBodyLength === 0; this.emptyLinesQueue = []; + + this.mimeStructureStartFinder = new MimeStructureStartFinder(); + } + + setContentType(contentTypeObj) { + if (/^multipart\//i.test(contentTypeObj.value) && contentTypeObj.params.boundary) { + this.mimeStructureStartFinder.setBoundary(contentTypeObj.params.boundary); + } } _updateBodyHash(chunk) { + // serach through the entire document, not just signed part + this.mimeStructureStartFinder.update(chunk); + this.canonicalizedLength += chunk.length; if (this.maxSizeReached) { @@ -270,6 +282,10 @@ class RelaxedHash { // finalize return this.bodyHash.digest(encoding); } + + getMimeStructureStart() { + return this.mimeStructureStartFinder.getMimeStructureStart(); + } } module.exports = { RelaxedHash }; diff --git a/lib/dkim/body/simple.js b/lib/dkim/body/simple.js index 7357e62..7c5a209 100644 --- a/lib/dkim/body/simple.js +++ b/lib/dkim/body/simple.js @@ -1,6 +1,7 @@ 'use strict'; const crypto = require('crypto'); +const { MimeStructureStartFinder } = require('../mime-structure-start-finder'); /** * Class for calculating body hash of an email message body stream @@ -30,9 +31,20 @@ class SimpleHash { this.maxSizeReached = maxBodyLength === 0; this.lastNewline = false; + + this.mimeStructureStartFinder = new MimeStructureStartFinder(); + } + + setContentType(contentTypeObj) { + if (/^multipart\//i.test(contentTypeObj.value) && contentTypeObj.params.boundary) { + this.mimeStructureStartFinder.setBoundary(contentTypeObj.params.boundary); + } } _updateBodyHash(chunk) { + // serach through the entire document, not just signed part + this.mimeStructureStartFinder.update(chunk); + this.canonicalizedLength += chunk.length; if (this.maxSizeReached) { @@ -115,6 +127,10 @@ class SimpleHash { return this.bodyHash.digest(encoding); } + + getMimeStructureStart() { + return this.mimeStructureStartFinder.getMimeStructureStart(); + } } module.exports = { SimpleHash }; diff --git a/lib/dkim/dkim-verifier.js b/lib/dkim/dkim-verifier.js index 26cca6a..5558148 100644 --- a/lib/dkim/dkim-verifier.js +++ b/lib/dkim/dkim-verifier.js @@ -8,6 +8,7 @@ const { getARChain } = require('../arc'); const addressparser = require('nodemailer/lib/addressparser'); const crypto = require('crypto'); const { v4: uuidv4 } = require('uuid'); +const libmime = require('libmime'); class DkimVerifier extends MessageParser { constructor(options) { @@ -147,6 +148,18 @@ class DkimVerifier extends MessageParser { if (!this.bodyHashes.has(signatureHeader.bodyHashKey)) { this.bodyHashes.set(signatureHeader.bodyHashKey, dkimBody(signatureHeader.bodyCanon, signatureHeader.hashAlgo, signatureHeader.maxBodyLength)); } + + let contentTypeHeader = this.headers.parsed.findLast(header => header.key === 'content-type'); + if (contentTypeHeader) { + let line = contentTypeHeader.line.toString(); + if (line.indexOf(':') >= 0) { + line = line.substring(line.indexOf(':') + 1).trim(); + } + const parsedContentType = libmime.parseHeaderValue(line); + for (let hasher of this.bodyHashes.values()) { + hasher.setContentType(parsedContentType); + } + } } } @@ -165,6 +178,7 @@ class DkimVerifier extends MessageParser { // convert bodyHashes from hash objects to base64 strings for (let [key, bodyHash] of this.bodyHashes.entries()) { this.bodyHashes.get(key).hash = bodyHash.digest('base64'); + this.bodyHashes.get(key).mimeStructureStart = bodyHash.getMimeStructureStart(); } for (let signatureHeader of this.signatureHeaders) { @@ -210,7 +224,9 @@ class DkimVerifier extends MessageParser { : false; } - let bodyHash = this.bodyHashes.get(signatureHeader.bodyHashKey)?.hash; + const bodyHash = this.bodyHashes.get(signatureHeader.bodyHashKey)?.hash; + const mimeStructureStart = this.bodyHashes.get(signatureHeader.bodyHashKey)?.mimeStructureStart; + if (signatureHeader.parsed?.bh?.value !== bodyHash) { status.result = 'neutral'; status.comment = `body hash did not verify`; @@ -344,6 +360,10 @@ class DkimVerifier extends MessageParser { result.canonBodyLengthLimited = false; } + if (typeof mimeStructureStart === 'number') { + result.mimeStructureStart = mimeStructureStart; + } + if (publicKey) { result.publicKey = publicKey.toString(); } diff --git a/lib/dkim/mime-structure-start-finder.js b/lib/dkim/mime-structure-start-finder.js new file mode 100644 index 0000000..18b5b79 --- /dev/null +++ b/lib/dkim/mime-structure-start-finder.js @@ -0,0 +1,85 @@ +'use strict'; + +class MimeStructureStartFinder { + constructor() { + this.byteCache = []; + + this.matchFound = false; + this.noMatch = false; + this.lineStart = -1; + + this.prevChunks = 0; + + this.mimeStructureStart = -1; + } + + setBoundary(boundary) { + this.boundary = (boundary || '').toString().trim(); + + this.boundaryBuf = Array.from(Buffer.from(`--${this.boundary}`)); + this.boundaryBufLen = this.boundaryBuf.length; + } + + update(chunk) { + if (this.matchFound || !this.boundary) { + return; + } + + for (let i = 0, bufLen = chunk.length; i < bufLen; i++) { + let c = chunk[i]; + + // check ending + if (c === 0x0a || c === 0x0d) { + if (!this.noMatch && this.byteCache.length === this.boundaryBufLen) { + // match found + this.matchFound = true; + this.mimeStructureStart = this.lineStart; + break; + } + // reset counter + this.lineStart = -1; + this.noMatch = false; + this.byteCache = []; + continue; + } + + if (this.noMatch) { + // no need to look + continue; + } + + if (this.lineStart < 0) { + this.lineStart = this.prevChunks + i; + } + + if (this.byteCache.length >= this.boundaryBufLen) { + this.noMatch = true; + continue; + } + + const expectingByte = this.boundaryBuf[this.byteCache.length]; + if (expectingByte !== c) { + this.noMatch = true; + continue; + } + this.byteCache[this.byteCache.length] = c; + } + + this.prevChunks += chunk.length; + } + + getMimeStructureStart() { + if (!this.boundary) { + return 0; + } + + if (!this.matchFound && !this.noMatch && this.byteCache.length === this.boundaryBufLen) { + this.matchFound = true; + this.mimeStructureStart = this.lineStart; + } + + return this.mimeStructureStart; + } +} + +module.exports = { MimeStructureStartFinder };