From 4744e4d8f978df619a1785f57543ffb7cffb4067 Mon Sep 17 00:00:00 2001 From: Jayden Date: Thu, 22 Aug 2024 00:26:18 +0800 Subject: [PATCH] Using both Google and Cloudflare --- packages/helpers/src/lib/mailauth/DoH.ts | 97 ++++++++++++++++++++++ packages/helpers/src/lib/mailauth/tools.ts | 26 +++--- 2 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 packages/helpers/src/lib/mailauth/DoH.ts diff --git a/packages/helpers/src/lib/mailauth/DoH.ts b/packages/helpers/src/lib/mailauth/DoH.ts new file mode 100644 index 00000000..8fe18d99 --- /dev/null +++ b/packages/helpers/src/lib/mailauth/DoH.ts @@ -0,0 +1,97 @@ +// DoH servers list +export enum DoHServer { + // Google Public DNS + Google = "https://dns.google/resolve", + // Cloudflare DNS + Cloudflare = "https://cloudflare-dns.com/dns-query", +} + +/** + * DNS over HTTPS (DoH) resolver + * + * @export + * @class DoH + */ +export class DoH { + + // DNS response codes + static DoHStatusNoError = 0; + // DNS RR types + static DoHTypeTXT = 16; + + /** + * Resolve DKIM public key from DNS + * + * @static + * @param {string} name DKIM record name (e.g. 20230601._domainkey.gmail.com) + * @param {string} DNSServer DNS over HTTPS API URL + * @return {*} {(Promise)} DKIM public key or null if not found + * @memberof DoH + */ + public static async resolveDKIMPublicKey(name: string, DNSServer: string): Promise { + if (!DNSServer.startsWith('https://')) { + DNSServer = 'https://' + DNSServer; + } + if (DNSServer.endsWith('/')) { + DNSServer = DNSServer.slice(0, -1); + } + const resp = await fetch( + DNSServer + "?" + + new URLSearchParams({ + name: name, + // DKIM public key record type is TXT + type: DoH.DoHTypeTXT.toString(), + }), + { + headers: { + "accept": "application/dns-json", + } + } + ); + if (resp.status === 200) { + const out = await resp.json(); + if (typeof out === 'object' && out !== null && 'Status' in out && 'Answer' in out) { + const resp = out as DoHResponse; + if (resp.Status === DoH.DoHStatusNoError && resp.Answer.length > 0) { + for (const ans of resp.Answer) { + if (ans.type === DoH.DoHTypeTXT) { + let DKIMRecord = ans.data; + /* + Remove all double quotes + Some DNS providers wrap TXT records in double quotes, + and others like Cloudflare may include them. According to + TXT (potentially multi-line) and DKIM (Base64 data) standards, + we can directly remove all double quotes from the DKIM public key. + */ + DKIMRecord = DKIMRecord.replace(/"/g, ''); + return DKIMRecord; + } + } + } + } + } + return null; + } +} + +interface DoHResponse { + Status: number; // NOERROR - Standard DNS response code (32 bit integer). + TC: boolean; // Whether the response is truncated + AD: boolean; // Whether all response data was validated with DNSSEC + CD: boolean; // Whether the client asked to disable DNSSEC + Question: Question[]; + Answer: Answer[]; + Comment: string; +} + +interface Question { + name: string; // FQDN with trailing dot + type: number; // A - Standard DNS RR type. 5:CNAME, 16:TXT +} + +interface Answer { + name: string; // Always matches name in the Question section + type: number; // A - Standard DNS RR type. 5:CNAME, 16:TXT + TTL: number; // Record's time-to-live in seconds + data: string; // Record data +} \ No newline at end of file diff --git a/packages/helpers/src/lib/mailauth/tools.ts b/packages/helpers/src/lib/mailauth/tools.ts index 09c50f48..7d978dec 100644 --- a/packages/helpers/src/lib/mailauth/tools.ts +++ b/packages/helpers/src/lib/mailauth/tools.ts @@ -9,6 +9,7 @@ import crypto, { KeyObject } from "crypto"; import parseDkimHeaders from "./parse-dkim-headers"; import { DkimVerifier } from "./dkim-verifier"; import type { Parsed, SignatureType } from "./dkim-verifier"; +import { DoH, DoHServer } from './DoH'; const IS_BROWSER = typeof window !== "undefined"; @@ -247,16 +248,17 @@ export const formatSignatureHeaderLine = ( }; async function resolveDNSHTTP(name: string, type: string) { - const resp = await fetch( - "https://dns.google/resolve?" + - new URLSearchParams({ - name: name, - type: type, - }) - ); - const out = await resp.json(); - // For some DNS, the Answer response here contains more than 1 element in the array. The last element is the one containing the public key - return [out.Answer[out.Answer.length - 1].data]; + if (type !== "TXT") { + throw new Error("DKIM record type is TXT"); + } + const DKIMRecord = await DoH.resolveDKIMPublicKey(name, DoHServer.Google); + if (!DKIMRecord) { + throw new CustomError("No DKIM record found", "ENODATA"); + } + if (DKIMRecord !== await DoH.resolveDKIMPublicKey(name, DoHServer.Cloudflare)) { + console.error("DKIM record mismatch!"); + } + return [DKIMRecord]; } // from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String @@ -301,7 +303,9 @@ export const getPublicKey = async ( resolver: (...args: [name: string, type: string]) => Promise ) => { minBitLength = minBitLength || 1024; - resolver = resolveDNSHTTP; + if (!resolver) { + resolver = resolveDNSHTTP; + } let list = await resolver(name, "TXT"); let rr =