diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 387642450..a2f47cb36 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zk-email/helpers", - "version": "3.1.3", + "version": "3.2.0", "main": "dist", "scripts": { "build": "tsc", diff --git a/packages/helpers/src/dkim/arc.ts b/packages/helpers/src/dkim/arc.ts new file mode 100644 index 000000000..a4243875e --- /dev/null +++ b/packages/helpers/src/dkim/arc.ts @@ -0,0 +1,46 @@ +export async function revertCommonARCModifications( + email: string +): Promise { + if (!email.includes("ARC-Authentication-Results")) { + return email; + } + + let modified = revertGoogleModifications(email); + + if (modified === email) { + console.log("ARC Revert: No known ARC modifications found"); + } + + return modified; +} + +function revertGoogleModifications(email: string): string { + // Google sets their own Message-ID and put the original one + // in X-Google-Original-Message-ID when forwarding + const googleReplacedMessageId = getHeaderValue( + email, + "X-Google-Original-Message-ID" + ); + + if (googleReplacedMessageId) { + email = setHeaderValue(email, "Message-ID", googleReplacedMessageId); + + console.info( + "ARC Revert: Setting X-Google-Original-Message-ID to Message-ID header..." + ); + } + + return email; +} + +function getHeaderValue(email: string, header: string) { + const headerStartIndex = email.indexOf(`${header}: `) + header.length + 2; + const headerEndIndex = email.indexOf("\n", headerStartIndex); + const headerValue = email.substring(headerStartIndex, headerEndIndex); + + return headerValue; +} + +function setHeaderValue(email: string, header: string, value: string) { + return email.replace(getHeaderValue(email, header), value); +} diff --git a/packages/helpers/src/dkim/index.ts b/packages/helpers/src/dkim/index.ts index d3350589b..fc2a7fcf4 100644 --- a/packages/helpers/src/dkim/index.ts +++ b/packages/helpers/src/dkim/index.ts @@ -1,6 +1,12 @@ import { pki } from "node-forge"; import { DkimVerifier } from "./dkim-verifier"; -import { getSigningHeaderLines, parseDkimHeaders, parseHeaders, writeToStream } from "./tools"; +import { + getSigningHeaderLines, + parseDkimHeaders, + parseHeaders, + writeToStream, +} from "./tools"; +import { revertCommonARCModifications } from "./arc"; export interface DKIMVerificationResult { publicKey: bigint; @@ -22,20 +28,28 @@ export async function verifyDKIMSignature( ): Promise { let dkimResult = await tryVerifyDKIM(email, domain); - if (dkimResult.status.result !== "pass" && tryRevertARCChanges) { - console.info("DKIM verification failed. Trying to verify after reverting forwarder changes..."); - - const modified = await revertForwarderChanges(email.toString()); + // If DKIM verification fails, revert common modifications made by ARC and try again. + if (dkimResult.status.comment === "bad signature" && tryRevertARCChanges) { + const modified = await revertCommonARCModifications(email.toString()); dkimResult = await tryVerifyDKIM(modified, domain); } - if (dkimResult.status.result !== "pass") { + const { + status: { result, comment }, + signingDomain, + publicKey, + signature, + status, + body, + bodyHash, + } = dkimResult; + + if (result !== "pass") { throw new Error( - `DKIM signature verification failed for domain ${dkimResult.signingDomain}` + `DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}` ); } - const { publicKey, signature, status, body, bodyHash } = dkimResult; const pubKeyData = pki.publicKeyFromPem(publicKey.toString()); return { @@ -72,7 +86,9 @@ async function tryVerifyDKIM(email: Buffer | string, domain: string = "") { ); if (!dkimResult) { - throw new Error(`DKIM signature not found for domain ${domainToVerifyDKIM}`); + throw new Error( + `DKIM signature not found for domain ${domainToVerifyDKIM}` + ); } if (dkimVerifier.headers) { @@ -87,39 +103,15 @@ async function tryVerifyDKIM(email: Buffer | string, domain: string = "") { return dkimResult; } -function getHeaderValue(email: string, header: string) { - const headerStartIndex = email.indexOf(`${header}: `) + header.length + 2; - const headerEndIndex = email.indexOf('\n', headerStartIndex); - const headerValue = email.substring(headerStartIndex, headerEndIndex); - - return headerValue; -} - -function setHeaderValue(email: string, header: string, value: string) { - return email.replace(getHeaderValue(email, header), value); -} - -async function revertForwarderChanges(email: string) { - // Google sets their own Message-ID and put the original one in X-Google-Original-Message-ID when forwarding - const googleReplacedMessageId = getHeaderValue(email, "X-Google-Original-Message-ID"); - if (googleReplacedMessageId) { - console.info("Setting X-Google-Original-Message-ID to Message-ID header..."); - email = setHeaderValue(email, "Message-ID", googleReplacedMessageId); - } - - return email; -} - - -export type SignatureType = 'DKIM' | 'ARC' | 'AS'; +export type SignatureType = "DKIM" | "ARC" | "AS"; export type ParsedHeaders = ReturnType; -export type Parsed = ParsedHeaders['parsed'][0]; +export type Parsed = ParsedHeaders["parsed"][0]; -export type ParseDkimHeaders = ReturnType +export type ParseDkimHeaders = ReturnType; -export type SigningHeaderLines = ReturnType +export type SigningHeaderLines = ReturnType; export interface Options { signatureHeaderLine: string; diff --git a/packages/helpers/tests/__mocks__/localforage.ts b/packages/helpers/tests/__mocks__/localforage.ts index b881c1996..8bdba8780 100644 --- a/packages/helpers/tests/__mocks__/localforage.ts +++ b/packages/helpers/tests/__mocks__/localforage.ts @@ -2,7 +2,6 @@ import fs from 'fs'; import path from 'path'; const getUncompressedTestFile = (): ArrayBuffer => { - console.log("__dirname", __dirname) const buffer = fs.readFileSync(path.join(__dirname, `../test-data/compressed-files/uncompressed-value.txt`)); return buffer; } diff --git a/packages/helpers/tests/dkim.test.ts b/packages/helpers/tests/dkim.test.ts new file mode 100644 index 000000000..49a0cbf7c --- /dev/null +++ b/packages/helpers/tests/dkim.test.ts @@ -0,0 +1,88 @@ +import { verifyDKIMSignature } from "../src/dkim"; +import fs from "fs"; +import path from "path"; + +jest.setTimeout(10000); + +describe("DKIM signature verification", () => { + it("should pass for valid email", async () => { + const email = fs.readFileSync( + path.join(__dirname, `test-data/email-good.eml`) + ); + + const result = await verifyDKIMSignature(email); + + expect(result.signingDomain).toBe("icloud.com"); + }); + + it("should fail for invalid selector", async () => { + const email = fs.readFileSync( + path.join(__dirname, `test-data/email-invalid-selector.eml`) + ); + + expect.assertions(1); + + try { + await verifyDKIMSignature(email); + } catch (e) { + expect(e.message).toBe( + "DKIM signature verification failed for domain icloud.com. Reason: no key" + ); + } + }); + + it("should fail for tampered body", async () => { + const email = fs.readFileSync( + path.join(__dirname, `test-data/email-body-tampered.eml`) + ); + + expect.assertions(1); + + try { + await verifyDKIMSignature(email); + } catch (e) { + expect(e.message).toBe( + "DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify" + ); + } + }); + + it("should fail for when DKIM signature is not present for domain", async () => { + // In this email From address is user@gmail.com, but the DKIM signature is only for icloud.com + const email = fs.readFileSync( + path.join(__dirname, `test-data/email-invalid-domain.eml`) + ); + + expect.assertions(1); + + try { + await verifyDKIMSignature(email); + } catch (e) { + expect(e.message).toBe( + "DKIM signature not found for domain gmail.com" + ); + } + }); + + it("should be able to override domain", async () => { + // From address domain is icloud.com + const email = fs.readFileSync( + path.join(__dirname, `test-data/email-different-domain.eml`) + ); + + // Should pass with default domain + await verifyDKIMSignature(email); + + // Should fail because the email wont have a DKIM signature with the overridden domain + // Can be replaced with a better test email where signer is actually + // different from From domain and the below check pass. + expect.assertions(1); + try { + await verifyDKIMSignature(email, "domain.com"); + } catch (e) { + expect(e.message).toBe( + "DKIM signature not found for domain domain.com" + ); + } + }); +}); diff --git a/packages/helpers/tests/test-data/email-body-tampered.eml b/packages/helpers/tests/test-data/email-body-tampered.eml new file mode 100644 index 000000000..8bfe189d8 --- /dev/null +++ b/packages/helpers/tests/test-data/email-body-tampered.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: runnier.leagues.0j@icloud.com +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +bla bla bla \ No newline at end of file diff --git a/packages/helpers/tests/test-data/email-different-domain.eml b/packages/helpers/tests/test-data/email-different-domain.eml new file mode 100644 index 000000000..d71ddad10 --- /dev/null +++ b/packages/helpers/tests/test-data/email-different-domain.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: runnier.leagues.0j@icloud.com +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +How are you? \ No newline at end of file diff --git a/packages/helpers/tests/test-data/email-good.eml b/packages/helpers/tests/test-data/email-good.eml new file mode 100644 index 000000000..d71ddad10 --- /dev/null +++ b/packages/helpers/tests/test-data/email-good.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: runnier.leagues.0j@icloud.com +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +How are you? \ No newline at end of file diff --git a/packages/helpers/tests/test-data/email-invalid-domain.eml b/packages/helpers/tests/test-data/email-invalid-domain.eml new file mode 100644 index 000000000..4ebb6a2a3 --- /dev/null +++ b/packages/helpers/tests/test-data/email-invalid-domain.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: user@gmail.com +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +How are you? \ No newline at end of file diff --git a/packages/helpers/tests/test-data/email-invalid-selector.eml b/packages/helpers/tests/test-data/email-invalid-selector.eml new file mode 100644 index 000000000..96d2bdf65 --- /dev/null +++ b/packages/helpers/tests/test-data/email-invalid-selector.eml @@ -0,0 +1,18 @@ +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=2a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD + M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx + VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR + 2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ + wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5 + Ry43lwp1/3+sA== +from: runnier.leagues.0j@icloud.com +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\)) +Subject: Hello +Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com> +Date: Sat, 26 Aug 2023 12:25:22 +0400 +to: zkewtest@gmail.com + +Hello, + +How are you? \ No newline at end of file