Revert "add prettier"

This reverts commit fbdf5bca06.
This commit is contained in:
Saleel
2024-10-01 13:34:53 +05:30
parent fbdf5bca06
commit b1a8045289
29 changed files with 1174 additions and 864 deletions

View File

@@ -1 +0,0 @@
packages/helpers/src/lib

View File

@@ -1,5 +0,0 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 120
}

View File

@@ -3,7 +3,6 @@
"license": "MIT",
"private": true,
"scripts": {
"lint": "yarn prettier --write packages/**/**.ts",
"test": "jest"
},
"workspaces": [
@@ -13,8 +12,5 @@
"packageManager": "yarn@3.2.3",
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"prettier": "^3.3.3"
}
}

View File

@@ -7,11 +7,14 @@ describe("Base64 Lookup", () => {
let circuit: any;
beforeAll(async () => {
circuit = await wasm(path.join(__dirname, "./test-circuits/base64-test.circom"), {
circuit = await wasm(
path.join(__dirname, "./test-circuits/base64-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should decode valid base64 chars", async function () {

View File

@@ -5,11 +5,14 @@ describe("ByteMask Circuit", () => {
let circuit: any;
beforeAll(async () => {
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/body-masker-test.circom"), {
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/byte-mask-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should mask the body correctly", async () => {

View File

@@ -12,23 +12,35 @@ describe("EmailVerifier : Without body check", () => {
let circuit: any;
beforeAll(async () => {
const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml"), "utf8");
const rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/test.eml"),
"utf8"
);
dkimResult = await verifyDKIMSignature(rawEmail);
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/email-verifier-no-body-test.circom"), {
circuit = await wasm_tester(
path.join(
__dirname,
"./test-circuits/email-verifier-no-body-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should verify email when ignore_body_hash_check is true", async function () {
// The result wont have shaPrecomputeSelector, maxHeadersLength, maxBodyLength, ignoreBodyHashCheck
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
ignoreBodyHashCheck: true,
});
}
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);

View File

@@ -12,28 +12,43 @@ describe("EmailVerifier : With body masking", () => {
let circuit: any;
beforeAll(async () => {
const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml"));
const rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/test.eml")
);
dkimResult = await verifyDKIMSignature(rawEmail);
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/email-verifier-with-body-mask-test.circom"), {
circuit = await wasm_tester(
path.join(
__dirname,
"./test-circuits/email-verifier-with-body-mask-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should verify email with body masking", async function () {
const mask = Array.from({ length: 768 }, (_, i) => (i > 25 && i < 50 ? 1 : 0));
const mask = Array.from({ length: 768 }, (_, i) =>
i > 25 && i < 50 ? 1 : 0
);
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
ignoreBodyHashCheck: false,
enableBodyMasking: true,
bodyMask: mask.map((value) => (value ? 1 : 0)),
});
}
);
const expectedMaskedBody = emailVerifierInputs.emailBody!.map((byte, i) => (mask[i] === 1 ? byte : 0));
const expectedMaskedBody = emailVerifierInputs.emailBody!.map(
(byte, i) => (mask[i] === 1 ? byte : 0)
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);

View File

@@ -12,28 +12,43 @@ describe("EmailVerifier : With header masking", () => {
let circuit: any;
beforeAll(async () => {
const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml"));
const rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/test.eml")
);
dkimResult = await verifyDKIMSignature(rawEmail);
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/email-verifier-with-header-mask-test.circom"), {
circuit = await wasm_tester(
path.join(
__dirname,
"./test-circuits/email-verifier-with-header-mask-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should verify email with header masking", async function () {
const mask = Array.from({ length: 640 }, (_, i) => (i > 25 && i < 50 ? 1 : 0));
const mask = Array.from({ length: 640 }, (_, i) =>
i > 25 && i < 50 ? 1 : 0
);
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
ignoreBodyHashCheck: false,
enableHeaderMasking: true,
headerMask: mask.map((value) => (value ? 1 : 0)),
});
}
);
const expectedMaskedHeader = emailVerifierInputs.emailHeader!.map((byte, i) => (mask[i] === 1 ? byte : 0));
const expectedMaskedHeader = emailVerifierInputs.emailHeader!.map(
(byte, i) => (mask[i] === 1 ? byte : 0)
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);

View File

@@ -12,26 +12,35 @@ describe("EmailVerifier : With soft line breaks", () => {
let circuit: any;
beforeAll(async () => {
const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/lorem_ipsum.eml"), "utf8");
const rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/lorem_ipsum.eml"),
"utf8"
);
dkimResult = await verifyDKIMSignature(rawEmail);
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/email-verifier-with-soft-line-breaks-test.circom"),
path.join(
__dirname,
"./test-circuits/email-verifier-with-soft-line-breaks-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
},
}
);
});
it("should verify email when removeSoftLineBreaks is true", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 1408,
ignoreBodyHashCheck: false,
removeSoftLineBreaks: true,
});
}
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);

View File

@@ -13,35 +13,46 @@ describe("EmailVerifier", () => {
let circuit: any;
beforeAll(async () => {
const rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml"));
const rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/test.eml")
);
dkimResult = await verifyDKIMSignature(rawEmail);
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/email-verifier-test.circom"), {
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/email-verifier-test.circom"),
{
// @dev During development recompile can be set to false if you are only making changes in the tests.
// This will save time by not recompiling the circuit every time.
// Compile: circom "./tests/email-verifier-test.circom" --r1cs --wasm --sym --c --wat --output "./tests/compiled-test-circuits"
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should verify email without any SHA precompute selector", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);
});
it("should verify email with a SHA precompute selector", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
shaPrecomputeSelector: "How are",
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
const witness = await circuit.calculateWitness(emailVerifierInputs);
await circuit.checkConstraints(witness);
@@ -51,10 +62,13 @@ describe("EmailVerifier", () => {
const invalidRSASignature = dkimResult.signature + 1n;
const dkim = { ...dkimResult, signature: invalidRSASignature };
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkim,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
expect.assertions(1);
try {
@@ -71,10 +85,13 @@ describe("EmailVerifier", () => {
const dkim = { ...dkimResult, headers: invalidHeader };
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkim,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
expect.assertions(1);
try {
@@ -86,10 +103,13 @@ describe("EmailVerifier", () => {
});
it("should fail if message padding is tampered", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
emailVerifierInputs.emailHeader[640 - 1] = "1";
expect.assertions(1);
@@ -107,10 +127,13 @@ describe("EmailVerifier", () => {
const dkim = { ...dkimResult, body: invalidBody };
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkim,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
expect.assertions(1);
try {
@@ -122,10 +145,13 @@ describe("EmailVerifier", () => {
});
it("should fail if body padding is tampered", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
emailVerifierInputs.emailBody![768 - 1] = "1";
expect.assertions(1);
@@ -142,10 +168,13 @@ describe("EmailVerifier", () => {
const dkim = { ...dkimResult, bodyHash: invalidBodyHash };
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkim, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkim,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
expect.assertions(1);
try {
@@ -157,11 +186,14 @@ describe("EmailVerifier", () => {
});
it("should produce dkim pubkey hash correctly", async function () {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(dkimResult, {
const emailVerifierInputs = generateEmailVerifierInputsFromDKIMResult(
dkimResult,
{
shaPrecomputeSelector: "How are",
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
// Calculate the Poseidon hash with pubkey chunked to 9*242 like in circuit
const poseidonHash = await poseidonLarge(dkimResult.publicKey, 9, 242);

View File

@@ -1,23 +1,61 @@
import { wasm as wasm_tester } from "circom_tester";
import path from "path";
import { wasm as wasm_tester } from 'circom_tester';
import path from 'path';
describe("RemoveSoftLineBreaks", () => {
describe('RemoveSoftLineBreaks', () => {
jest.setTimeout(20_1000);
let circuit: any;
beforeAll(async () => {
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/remove-soft-line-breaks-test.circom"), {
circuit = await wasm_tester(
path.join(
__dirname,
'./test-circuits/remove-soft-line-breaks-test.circom'
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
});
include: path.join(__dirname, '../../../node_modules'),
output: path.join(__dirname, './compiled-test-circuits'),
}
);
});
it("should correctly remove soft line breaks", async () => {
it('should correctly remove soft line breaks', async () => {
const input = {
encoded: [115, 101, 115, 58, 61, 13, 10, 45, 32, 83, 114, 101, 97, 107, 61, 13, 10, ...Array(15).fill(0)],
decoded: [115, 101, 115, 58, 45, 32, 83, 114, 101, 97, 107, ...Array(21).fill(0)],
encoded: [
115,
101,
115,
58,
61,
13,
10,
45,
32,
83,
114,
101,
97,
107,
61,
13,
10,
...Array(15).fill(0),
],
decoded: [
115,
101,
115,
58,
45,
32,
83,
114,
101,
97,
107,
...Array(21).fill(0),
],
};
const witness = await circuit.calculateWitness(input);
@@ -28,9 +66,28 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should fail when decoded input is incorrect", async () => {
it('should fail when decoded input is incorrect', async () => {
const input = {
encoded: [115, 101, 115, 58, 61, 13, 10, 45, 32, 83, 114, 101, 97, 107, 61, 13, 10, ...Array(15).fill(0)],
encoded: [
115,
101,
115,
58,
61,
13,
10,
45,
32,
83,
114,
101,
97,
107,
61,
13,
10,
...Array(15).fill(0),
],
decoded: [
115,
101,
@@ -55,7 +112,7 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should handle input with no soft line breaks", async () => {
it('should handle input with no soft line breaks', async () => {
const input = {
encoded: [104, 101, 108, 108, 111, ...Array(27).fill(0)],
decoded: [104, 101, 108, 108, 111, ...Array(27).fill(0)],
@@ -69,10 +126,40 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should handle input with multiple consecutive soft line breaks", async () => {
it('should handle input with multiple consecutive soft line breaks', async () => {
const input = {
encoded: [104, 101, 108, 108, 111, 61, 13, 10, 61, 13, 10, 119, 111, 114, 108, 100, ...Array(16).fill(0)],
decoded: [104, 101, 108, 108, 111, 119, 111, 114, 108, 100, ...Array(22).fill(0)],
encoded: [
104,
101,
108,
108,
111,
61,
13,
10,
61,
13,
10,
119,
111,
114,
108,
100,
...Array(16).fill(0),
],
decoded: [
104,
101,
108,
108,
111,
119,
111,
114,
108,
100,
...Array(22).fill(0),
],
};
const witness = await circuit.calculateWitness(input);
@@ -83,7 +170,7 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should handle input with soft line break at the beginning", async () => {
it('should handle input with soft line break at the beginning', async () => {
const input = {
encoded: [61, 13, 10, 104, 101, 108, 108, 111, ...Array(24).fill(0)],
decoded: [104, 101, 108, 108, 111, ...Array(27).fill(0)],
@@ -97,7 +184,7 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should handle input with soft line break at the end", async () => {
it('should handle input with soft line break at the end', async () => {
const input = {
encoded: [104, 101, 108, 108, 111, 61, 13, 10, ...Array(24).fill(0)],
decoded: [104, 101, 108, 108, 111, ...Array(27).fill(0)],
@@ -111,7 +198,7 @@ describe("RemoveSoftLineBreaks", () => {
});
});
it("should handle input with incomplete soft line break sequence", async () => {
it('should handle input with incomplete soft line break sequence', async () => {
const input = {
encoded: [
104,

View File

@@ -11,19 +11,27 @@ describe("RSA", () => {
let rawEmail: Buffer;
beforeAll(async () => {
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/rsa-test.circom"), {
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/rsa-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
});
rawEmail = fs.readFileSync(path.join(__dirname, "./test-emails/test.eml"));
}
);
rawEmail = fs.readFileSync(
path.join(__dirname, "./test-emails/test.eml")
);
});
it("should verify 2048 bit rsa signature correctly", async function () {
const emailVerifierInputs = await generateEmailVerifierInputs(rawEmail, {
const emailVerifierInputs = await generateEmailVerifierInputs(
rawEmail,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
const witness = await circuit.calculateWitness({
signature: emailVerifierInputs.signature,
@@ -56,14 +64,14 @@ describe("RSA", () => {
it("should verify 1024 bit rsa signature correctly", async function () {
const signature = toCircomBigIntBytes(
BigInt(
102386562682221859025549328916727857389789009840935140645361501981959969535413501251999442013082353139290537518086128904993091119534674934202202277050635907008004079788691412782712147797487593510040249832242022835902734939817209358184800954336078838331094308355388211284440290335887813714894626653613586546719n,
),
102386562682221859025549328916727857389789009840935140645361501981959969535413501251999442013082353139290537518086128904993091119534674934202202277050635907008004079788691412782712147797487593510040249832242022835902734939817209358184800954336078838331094308355388211284440290335887813714894626653613586546719n
)
);
const pubkey = toCircomBigIntBytes(
BigInt(
106773687078109007595028366084970322147907086635176067918161636756354740353674098686965493426431314019237945536387044259034050617425729739578628872957481830432099721612688699974185290306098360072264136606623400336518126533605711223527682187548332314997606381158951535480830524587400401856271050333371205030999n,
),
106773687078109007595028366084970322147907086635176067918161636756354740353674098686965493426431314019237945536387044259034050617425729739578628872957481830432099721612688699974185290306098360072264136606623400336518126533605711223527682187548332314997606381158951535480830524587400401856271050333371205030999n
)
);
const witness = await circuit.calculateWitness({
@@ -95,10 +103,13 @@ describe("RSA", () => {
});
it("should fail when verifying with an incorrect signature", async function () {
const emailVerifierInputs = await generateEmailVerifierInputs(rawEmail, {
const emailVerifierInputs = await generateEmailVerifierInputs(
rawEmail,
{
maxHeadersLength: 640,
maxBodyLength: 768,
});
}
);
expect.assertions(1);
try {

View File

@@ -7,16 +7,24 @@ describe("Select Regex Reveal", () => {
let circuit: any;
beforeAll(async () => {
circuit = await wasm(path.join(__dirname, "./test-circuits/select-regex-reveal-test.circom"), {
circuit = await wasm(
path.join(
__dirname,
"./test-circuits/select-regex-reveal-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
});
}
);
});
it("should reveal the substring with maximum revealed length", async function () {
let input = new Array(34).fill(0);
const startIndex = Math.floor(Math.random() * 24);
const revealed = Array.from("zk email").map((char) => char.charCodeAt(0));
const revealed = Array.from("zk email").map((char) =>
char.charCodeAt(0)
);
for (let i = 0; i < revealed.length; i++) {
input[startIndex + i] = revealed[i];
}
@@ -64,7 +72,9 @@ describe("Select Regex Reveal", () => {
it("should fail when startIndex is 0", async function () {
let input = new Array(34).fill(0);
const startIndex = 1 + Math.floor(Math.random() * 24);
const revealed = Array.from("zk email").map((char) => char.charCodeAt(0));
const revealed = Array.from("zk email").map((char) =>
char.charCodeAt(0)
);
for (let i = 0; i < revealed.length; i++) {
input[startIndex + i] = revealed[i];
}
@@ -84,7 +94,9 @@ describe("Select Regex Reveal", () => {
it("should fail when startIndex is not before 0", async function () {
let input = new Array(34).fill(0);
const startIndex = Math.floor(Math.random() * 23);
const revealed = Array.from("zk email").map((char) => char.charCodeAt(0));
const revealed = Array.from("zk email").map((char) =>
char.charCodeAt(0)
);
for (let i = 0; i < revealed.length; i++) {
input[startIndex + i] = revealed[i];
}

View File

@@ -1,7 +1,10 @@
import { wasm as wasm_tester } from "circom_tester";
import path from "path";
import { sha256Pad, shaHash } from "@zk-email/helpers/src/sha-utils";
import { Uint8ArrayToCharArray, uint8ToBits } from "@zk-email/helpers/src/binary-format";
import {
Uint8ArrayToCharArray,
uint8ToBits,
} from "@zk-email/helpers/src/binary-format";
describe("SHA256 for email header", () => {
jest.setTimeout(30 * 60 * 1000); // 30 minutes
@@ -9,17 +12,23 @@ describe("SHA256 for email header", () => {
let circuit: any;
beforeAll(async () => {
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/sha-test.circom"), {
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/sha-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should hash correctly", async function () {
const inputs = ["0", "hello world", ""];
for (const input of inputs) {
const [paddedMsg, messageLen] = sha256Pad(Buffer.from(input, "ascii"), 640);
const [paddedMsg, messageLen] = sha256Pad(
Buffer.from(input, "ascii"),
640
);
const witness = await circuit.calculateWitness({
paddedIn: Uint8ArrayToCharArray(paddedMsg),

View File

@@ -2,16 +2,21 @@ import { wasm as wasm_tester } from "circom_tester";
import path from "path";
import { bigIntToChunkedBytes, Uint8ArrayToCharArray } from "@zk-email/helpers/src/binary-format";
describe("SplitBytesToWords Helper unit test", () => {
jest.setTimeout(0.1 * 60 * 1000);
let circuit: any;
beforeAll(async () => {
circuit = await wasm_tester(path.join(__dirname, "./test-circuits/split-bytes-to-words-test.circom"), {
circuit = await wasm_tester(
path.join(__dirname, "./test-circuits/split-bytes-to-words-test.circom"),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
// output: path.join(__dirname, "./compiled-test-circuits"),
});
}
);
});
it("should split correctly according to bigIntToChunkedBytes function", async function () {
@@ -20,8 +25,9 @@ describe("SplitBytesToWords Helper unit test", () => {
const ts_split_to_words = bigIntToChunkedBytes(bytesBigInt, 121, 17);
const ts_split_to_words_bigint = ts_split_to_words.map((word) => BigInt(word));
const witness = await circuit.calculateWitness({
in: Uint8ArrayToCharArray(bytes),
in: Uint8ArrayToCharArray(bytes)
});
await circuit.assertOut(witness, { out: ts_split_to_words_bigint });
});
});

View File

@@ -1,4 +1,4 @@
import { CIRCOM_BIGINT_N, CIRCOM_BIGINT_K } from "./constants";
import { CIRCOM_BIGINT_N, CIRCOM_BIGINT_K } from './constants';
export function bytesToString(bytes: Uint8Array): string {
return new TextDecoder().decode(bytes);
@@ -38,7 +38,7 @@ export function bufferToUint8Array(buf: Buffer): Uint8Array {
}
export function bufferToHex(buf: Buffer): String {
return buf.toString("hex");
return buf.toString('hex');
}
export function Uint8ArrayToCharArray(a: Uint8Array): string[] {
@@ -48,11 +48,11 @@ export function Uint8ArrayToCharArray(a: Uint8Array): string[] {
export async function Uint8ArrayToString(a: Uint8Array): Promise<string> {
return Array.from(a)
.map((x) => x.toString())
.join(";");
.join(';');
}
export async function Uint8ArrayToHex(a: Uint8Array): Promise<string> {
return Buffer.from(a).toString("hex");
return Buffer.from(a).toString('hex');
}
export function bufferToString(buf: Buffer): String {
@@ -70,7 +70,7 @@ export function bytesToBigInt(bytes: Uint8Array) {
export function bigIntToChunkedBytes(num: BigInt | bigint, bytesPerChunk: number, numChunks: number) {
const res = [];
const bigintNum: bigint = typeof num === "bigint" ? num : num.valueOf();
const bigintNum: bigint = typeof num === 'bigint' ? num : num.valueOf();
const msk = (1n << BigInt(bytesPerChunk)) - 1n;
for (let i = 0; i < numChunks; ++i) {
res.push(((bigintNum >> BigInt(i * bytesPerChunk)) & msk).toString());
@@ -83,7 +83,7 @@ export function toCircomBigIntBytes(num: BigInt | bigint) {
}
// https://stackoverflow.com/a/69585881
const HEX_STRINGS = "0123456789abcdef";
const HEX_STRINGS = '0123456789abcdef';
const MAP_HEX = {
0: 0,
1: 1,
@@ -113,7 +113,7 @@ const MAP_HEX = {
export function toHex(bytes: Uint8Array): string {
return Array.from(bytes || [])
.map((b) => HEX_STRINGS[b >> 4] + HEX_STRINGS[b & 15])
.join("");
.join('');
}
// Mimics Buffer.from(x, 'hex') logic
@@ -121,10 +121,10 @@ export function toHex(bytes: Uint8Array): string {
// https://github.com/nodejs/node/blob/v14.18.1/src/string_bytes.cc#L246-L261
export function fromHex(hexString: string): Uint8Array {
let hexStringTrimmed: string = hexString;
if (hexString[0] === "0" && hexString[1] === "x") {
if (hexString[0] === '0' && hexString[1] === 'x') {
hexStringTrimmed = hexString.slice(2);
}
const bytes = new Uint8Array(Math.floor((hexStringTrimmed || "").length / 2));
const bytes = new Uint8Array(Math.floor((hexStringTrimmed || '').length / 2));
let i;
for (i = 0; i < bytes.length; i++) {
const a = MAP_HEX[hexStringTrimmed[i * 2] as keyof typeof MAP_HEX];
@@ -162,7 +162,7 @@ export function bitsToUint8(bits: string[]): Uint8Array {
}
export function uint8ToBits(uint8: Uint8Array): string {
return uint8.reduce((acc, byte) => acc + byte.toString(2).padStart(8, "0"), "");
return uint8.reduce((acc, byte) => acc + byte.toString(2).padStart(8, '0'), '');
}
export function mergeUInt8Arrays(a1: Uint8Array, a2: Uint8Array): Uint8Array {
@@ -190,18 +190,14 @@ export function packedNBytesToString(packedBytes: bigint[], n: number = 31): str
}
export function packBytesIntoNBytes(messagePaddedRaw: Uint8Array | string, n = 7): Array<bigint> {
const messagePadded: Uint8Array =
typeof messagePaddedRaw === "string" ? stringToBytes(messagePaddedRaw) : messagePaddedRaw;
const messagePadded: Uint8Array = typeof messagePaddedRaw === 'string' ? stringToBytes(messagePaddedRaw) : messagePaddedRaw;
const output: Array<bigint> = [];
for (let i = 0; i < messagePadded.length; i++) {
if (i % n === 0) {
output.push(0n);
}
const j = (i / n) | 0;
console.assert(
j === output.length - 1,
"Not editing the index of the last element -- packing loop invariants bug!",
);
console.assert(j === output.length - 1, 'Not editing the index of the last element -- packing loop invariants bug!');
output[j] += BigInt(messagePadded[i]) << BigInt((i % n) * 8);
}
return output;

View File

@@ -1,16 +1,18 @@
import localforage from "localforage";
import localforage from 'localforage';
// @ts-ignore
import pako from "pako";
import pako from 'pako';
// @ts-ignore
import * as snarkjs from "snarkjs";
import * as snarkjs from 'snarkjs';
const zkeyExtension = ".gz";
const zkeyExtensionRegEx = new RegExp(`\\b${zkeyExtension}$\\b`, "i"); // = /.gz$/i
const zkeySuffix = ["b", "c", "d", "e", "f", "g", "h", "i", "j", "k"];
const zkeyExtension = '.gz';
const zkeyExtensionRegEx = new RegExp(`\\b${zkeyExtension}$\\b`, 'i'); // = /.gz$/i
const zkeySuffix = ['b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'];
// uncompresses single .gz file.
// returns the contents as an ArrayBuffer
export const uncompressGz = async (arrayBuffer: ArrayBuffer): Promise<ArrayBuffer> => {
export const uncompressGz = async (
arrayBuffer: ArrayBuffer,
): Promise<ArrayBuffer> => {
const output = pako.ungzip(arrayBuffer);
const buff = output.buffer;
return buff;
@@ -24,18 +26,24 @@ async function storeArrayBuffer(keyname: string, buffer: ArrayBuffer) {
async function downloadWithRetries(link: string, downloadAttempts: number) {
for (let i = 1; i <= downloadAttempts; i++) {
console.log(`download attempt ${i} for ${link}`);
const response = await fetch(link, { method: "GET" });
const response = await fetch(link, { method: 'GET' });
if (response.status === 200) {
return response;
}
}
throw new Error(`Error downloading ${link} after ${downloadAttempts} retries`);
throw new Error(
`Error downloading ${link} after ${downloadAttempts} retries`,
);
}
// GET the compressed file from the remote server, then store it with localforage
// Note that it must be stored as an uncompressed ArrayBuffer
// and named such that filename===`${name}.zkey${a}` in order for it to be found by snarkjs.
export async function downloadFromFilename(baseUrl: string, filename: string, compressed = false) {
export async function downloadFromFilename(
baseUrl: string,
filename: string,
compressed = false,
) {
const link = baseUrl + filename;
const zkeyResp = await downloadWithRetries(link, 3);
@@ -46,28 +54,38 @@ export async function downloadFromFilename(baseUrl: string, filename: string, co
} else {
// uncompress the data
const zkeyUncompressed = await uncompressGz(zkeyBuff);
const rawFilename = filename.replace(zkeyExtensionRegEx, ""); // replace .gz with ""
const rawFilename = filename.replace(zkeyExtensionRegEx, ''); // replace .gz with ""
// store the uncompressed data
console.log("storing file in localforage", rawFilename);
console.log('storing file in localforage', rawFilename);
await storeArrayBuffer(rawFilename, zkeyUncompressed);
console.log("stored file in localforage", rawFilename);
console.log('stored file in localforage', rawFilename);
// await localforage.setItem(filename, zkeyBuff);
}
console.log(`Storage of ${filename} successful!`);
}
export async function downloadProofFiles(baseUrl: string, circuitName: string, onFileDownloaded: () => void) {
export async function downloadProofFiles(
baseUrl: string,
circuitName: string,
onFileDownloaded: () => void,
) {
const filePromises = [];
for (const c of zkeySuffix) {
const targzFilename = `${circuitName}.zkey${c}${zkeyExtension}`;
// const itemCompressed = await localforage.getItem(targzFilename);
const item = await localforage.getItem(`${circuitName}.zkey${c}`);
if (item) {
console.log(`${circuitName}.zkey${c}${item ? "" : zkeyExtension} already found in localforage!`);
console.log(
`${circuitName}.zkey${c}${
item ? '' : zkeyExtension
} already found in localforage!`,
);
onFileDownloaded();
continue;
}
filePromises.push(downloadFromFilename(baseUrl, targzFilename, true).then(() => onFileDownloaded()));
filePromises.push(
downloadFromFilename(baseUrl, targzFilename, true).then(() => onFileDownloaded()),
);
}
console.log(filePromises);
await Promise.all(filePromises);
@@ -75,7 +93,7 @@ export async function downloadProofFiles(baseUrl: string, circuitName: string, o
export async function generateProof(input: any, baseUrl: string, circuitName: string) {
// TODO: figure out how to generate this s.t. it passes build
console.log("generating proof for input");
console.log('generating proof for input');
console.log(input);
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
@@ -91,15 +109,19 @@ export async function generateProof(input: any, baseUrl: string, circuitName: st
}
export async function verifyProof(proof: any, publicSignals: any, baseUrl: string, circuitName: string) {
console.log("PROOF", proof);
console.log("PUBLIC SIGNALS", publicSignals);
console.log('PROOF', proof);
console.log('PUBLIC SIGNALS', publicSignals);
const response = await downloadWithRetries(`${baseUrl}${circuitName}.vkey.json`, 3);
const vkey = await response.json();
console.log("vkey", vkey);
console.log('vkey', vkey);
const proofVerified = await snarkjs.groth16.verify(vkey, publicSignals, proof);
console.log("proofV", proofVerified);
const proofVerified = await snarkjs.groth16.verify(
vkey,
publicSignals,
proof,
);
console.log('proofV', proofVerified);
return proofVerified;
}
@@ -121,16 +143,24 @@ function bigIntToArray(n: number, k: number, x: bigint) {
// taken from generation code in dizkus-circuits tests
function pubkeyToXYArrays(pk: string) {
const XArr = bigIntToArray(64, 4, BigInt(`0x${pk.slice(4, 4 + 64)}`)).map((el) => el.toString());
const YArr = bigIntToArray(64, 4, BigInt(`0x${pk.slice(68, 68 + 64)}`)).map((el) => el.toString());
const XArr = bigIntToArray(64, 4, BigInt(`0x${pk.slice(4, 4 + 64)}`)).map(
(el) => el.toString(),
);
const YArr = bigIntToArray(64, 4, BigInt(`0x${pk.slice(68, 68 + 64)}`)).map(
(el) => el.toString(),
);
return [XArr, YArr];
}
// taken from generation code in dizkus-circuits tests
function sigToRSArrays(sig: string) {
const rArr = bigIntToArray(64, 4, BigInt(`0x${sig.slice(2, 2 + 64)}`)).map((el) => el.toString());
const sArr = bigIntToArray(64, 4, BigInt(`0x${sig.slice(66, 66 + 64)}`)).map((el) => el.toString());
const rArr = bigIntToArray(64, 4, BigInt(`0x${sig.slice(2, 2 + 64)}`)).map(
(el) => el.toString(),
);
const sArr = bigIntToArray(64, 4, BigInt(`0x${sig.slice(66, 66 + 64)}`)).map(
(el) => el.toString(),
);
return [rArr, sArr];
}

View File

@@ -1,7 +1,7 @@
import { pki } from "node-forge";
import { DkimVerifier } from "../lib/mailauth/dkim-verifier";
import { writeToStream } from "../lib/mailauth/tools";
import sanitizers from "./sanitizers";
import { pki } from 'node-forge';
import { DkimVerifier } from '../lib/mailauth/dkim-verifier';
import { writeToStream } from '../lib/mailauth/tools';
import sanitizers from './sanitizers';
// `./mailauth` is modified version of https://github.com/postalsys/mailauth
// Main modification are including emailHeaders in the DKIM result, making it work in the browser, add types
@@ -30,7 +30,7 @@ export interface DKIMVerificationResult {
*/
export async function verifyDKIMSignature(
email: Buffer | string,
domain: string = "",
domain: string = '',
enableSanitization: boolean = true,
): Promise<DKIMVerificationResult> {
const emailStr = email.toString();
@@ -39,20 +39,20 @@ export async function verifyDKIMSignature(
// If DKIM verification fails, try again after sanitizing email
let appliedSanitization;
if (dkimResult.status.comment === "bad signature" && enableSanitization) {
if (dkimResult.status.comment === 'bad signature' && enableSanitization) {
const results = await Promise.all(
sanitizers.map((sanitize) =>
tryVerifyDKIM(sanitize(emailStr), domain).then((result) => ({
sanitizers.map((sanitize) => tryVerifyDKIM(sanitize(emailStr), domain).then((result) => ({
result,
sanitizer: sanitize.name,
})),
),
}))),
);
const passed = results.find((r) => r.result.status.result === "pass");
const passed = results.find((r) => r.result.status.result === 'pass');
if (passed) {
console.log(`DKIM: Verification passed after applying sanitization "${passed.sanitizer}"`);
console.log(
`DKIM: Verification passed after applying sanitization "${passed.sanitizer}"`,
);
dkimResult = passed.result;
appliedSanitization = passed.sanitizer;
}
@@ -68,14 +68,16 @@ export async function verifyDKIMSignature(
bodyHash,
} = dkimResult;
if (result !== "pass") {
throw new Error(`DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}`);
if (result !== 'pass') {
throw new Error(
`DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}`,
);
}
const pubKeyData = pki.publicKeyFromPem(publicKey.toString());
return {
signature: BigInt(`0x${Buffer.from(signature, "base64").toString("hex")}`),
signature: BigInt(`0x${Buffer.from(signature, 'base64').toString('hex')}`),
headers: status.signedHeaders,
body,
bodyHash,
@@ -89,23 +91,29 @@ export async function verifyDKIMSignature(
};
}
async function tryVerifyDKIM(email: Buffer | string, domain: string = "") {
async function tryVerifyDKIM(email: Buffer | string, domain: string = '') {
const dkimVerifier = new DkimVerifier({});
await writeToStream(dkimVerifier, email as any);
let domainToVerifyDKIM = domain;
if (!domainToVerifyDKIM) {
if (dkimVerifier.headerFrom.length > 1) {
throw new Error("Multiple From header in email and domain for verification not specified");
throw new Error(
'Multiple From header in email and domain for verification not specified',
);
}
domainToVerifyDKIM = dkimVerifier.headerFrom[0].split("@")[1];
domainToVerifyDKIM = dkimVerifier.headerFrom[0].split('@')[1];
}
const dkimResult = dkimVerifier.results.find((d: any) => d.signingDomain === domainToVerifyDKIM);
const dkimResult = dkimVerifier.results.find(
(d: any) => d.signingDomain === domainToVerifyDKIM,
);
if (!dkimResult) {
throw new Error(`DKIM signature not found for domain ${domainToVerifyDKIM}`);
throw new Error(
`DKIM signature not found for domain ${domainToVerifyDKIM}`,
);
}
dkimResult.headers = dkimVerifier.headers;

View File

@@ -1,6 +1,6 @@
function getHeaderValue(email: string, header: string) {
const headerStartIndex = email.indexOf(`${header}: `) + header.length + 2;
const headerEndIndex = email.indexOf("\n", headerStartIndex);
const headerEndIndex = email.indexOf('\n', headerStartIndex);
const headerValue = email.substring(headerStartIndex, headerEndIndex);
return headerValue;
@@ -15,14 +15,17 @@ function setHeaderValue(email: string, header: string, value: string) {
// TODO: Add test for this
function revertGoogleMessageId(email: string): string {
// (Optional check) This only happens when google does ARC
if (!email.includes("ARC-Authentication-Results")) {
if (!email.includes('ARC-Authentication-Results')) {
return email;
}
const googleReplacedMessageId = getHeaderValue(email, "X-Google-Original-Message-ID");
const googleReplacedMessageId = getHeaderValue(
email,
'X-Google-Original-Message-ID',
);
if (googleReplacedMessageId) {
return setHeaderValue(email, "Message-ID", googleReplacedMessageId);
return setHeaderValue(email, 'Message-ID', googleReplacedMessageId);
}
return email;
@@ -31,7 +34,7 @@ function revertGoogleMessageId(email: string): string {
// Remove labels inserted to Subject - `[ListName] Newsletter 2024` to `Newsletter 2024`
function removeLabels(email: string): string {
// Replace Subject: [label] with Subject:
const sanitized = email.replace(/Subject: \[.*\]/, "Subject:");
const sanitized = email.replace(/Subject: \[.*\]/, 'Subject:');
return sanitized;
}
@@ -59,9 +62,14 @@ function insert13Before10(email: string): string {
// Replace `=09` with `\t` in email
// TODO: Add test for this
function sanitizeTabs(email: string): string {
return email.replace("=09", "\t");
return email.replace('=09', '\t');
}
const sanitizers = [revertGoogleMessageId, removeLabels, insert13Before10, sanitizeTabs];
const sanitizers = [
revertGoogleMessageId,
removeLabels,
insert13Before10,
sanitizeTabs,
];
export default sanitizers;

View File

@@ -1,5 +1,5 @@
import { buildPoseidon } from "circomlibjs";
import { bigIntToChunkedBytes } from "./binary-format";
import { buildPoseidon } from 'circomlibjs';
import { bigIntToChunkedBytes } from './binary-format';
export async function poseidonLarge(input: bigint, numChunks: number, bitsPerChunk: number) {
const poseidon = await buildPoseidon();

View File

@@ -1,4 +1,4 @@
export * from "./binary-format";
export * from "./constants";
export * from "./input-generators";
export * from "./sha-utils";
export * from './binary-format';
export * from './constants';
export * from './input-generators';
export * from './sha-utils';

View File

@@ -1,7 +1,7 @@
import { Uint8ArrayToCharArray, toCircomBigIntBytes } from "./binary-format";
import { MAX_BODY_PADDED_BYTES, MAX_HEADER_PADDED_BYTES } from "./constants";
import { DKIMVerificationResult, verifyDKIMSignature } from "./dkim";
import { generatePartialSHA, sha256Pad } from "./sha-utils";
import { Uint8ArrayToCharArray, toCircomBigIntBytes } from './binary-format';
import { MAX_BODY_PADDED_BYTES, MAX_HEADER_PADDED_BYTES } from './constants';
import { DKIMVerificationResult, verifyDKIMSignature } from './dkim';
import { generatePartialSHA, sha256Pad } from './sha-utils';
type CircuitInput = {
emailHeader: string[];
@@ -34,10 +34,10 @@ function removeSoftLineBreaks(body: string[]): string[] {
let i = 0;
while (i < body.length) {
if (
i + 2 < body.length &&
body[i] === "61" && // '=' character
body[i + 1] === "13" && // '\r' character
body[i + 2] === "10"
i + 2 < body.length
&& body[i] === '61' // '=' character
&& body[i + 1] === '13' // '\r' character
&& body[i + 2] === '10'
) {
// '\n' character
// Skip the soft line break sequence
@@ -49,7 +49,7 @@ function removeSoftLineBreaks(body: string[]): string[] {
}
// Pad the result with zeros to make it the same length as the body
while (result.length < body.length) {
result.push("0");
result.push('0');
}
return result;
}
@@ -61,7 +61,10 @@ function removeSoftLineBreaks(body: string[]): string[] {
* @param params Arguments to control the input generation
* @returns Circuit inputs for the EmailVerifier circuit
*/
export async function generateEmailVerifierInputs(rawEmail: Buffer | string, params: InputGenerationArgs = {}) {
export async function generateEmailVerifierInputs(
rawEmail: Buffer | string,
params: InputGenerationArgs = {},
) {
const dkimResult = await verifyDKIMSignature(rawEmail);
return generateEmailVerifierInputsFromDKIMResult(dkimResult, params);
@@ -78,10 +81,15 @@ export function generateEmailVerifierInputsFromDKIMResult(
dkimResult: DKIMVerificationResult,
params: InputGenerationArgs = {},
): CircuitInput {
const { headers, body, bodyHash, publicKey, signature } = dkimResult;
const {
headers, body, bodyHash, publicKey, signature,
} = dkimResult;
// SHA add padding
const [messagePadded, messagePaddedLen] = sha256Pad(headers, params.maxHeadersLength || MAX_HEADER_PADDED_BYTES);
const [messagePadded, messagePaddedLen] = sha256Pad(
headers,
params.maxHeadersLength || MAX_HEADER_PADDED_BYTES,
);
const circuitInputs: CircuitInput = {
emailHeader: Uint8ArrayToCharArray(messagePadded), // Packed into 1 byte signals
@@ -96,7 +104,9 @@ export function generateEmailVerifierInputsFromDKIMResult(
if (!params.ignoreBodyHashCheck) {
if (!body || !bodyHash) {
throw new Error("body and bodyHash are required when ignoreBodyHashCheck is false");
throw new Error(
'body and bodyHash are required when ignoreBodyHashCheck is false',
);
}
const bodyHashIndex = headers.toString().indexOf(bodyHash);
@@ -105,7 +115,10 @@ export function generateEmailVerifierInputsFromDKIMResult(
// 65 comes from the 64 at the end and the 1 bit in the start, then 63 comes from the formula to round it up to the nearest 64.
// see sha256algorithm.com for a more full explanation of padding length
const bodySHALength = Math.floor((body.length + 63 + 65) / 64) * 64;
const [bodyPadded, bodyPaddedLen] = sha256Pad(body, Math.max(maxBodyLength, bodySHALength));
const [bodyPadded, bodyPaddedLen] = sha256Pad(
body,
Math.max(maxBodyLength, bodySHALength),
);
const { precomputedSha, bodyRemaining, bodyRemainingLength } = generatePartialSHA({
body: bodyPadded,
@@ -120,7 +133,9 @@ export function generateEmailVerifierInputsFromDKIMResult(
circuitInputs.emailBody = Uint8ArrayToCharArray(bodyRemaining);
if (params.removeSoftLineBreaks) {
circuitInputs.decodedEmailBodyIn = removeSoftLineBreaks(circuitInputs.emailBody);
circuitInputs.decodedEmailBodyIn = removeSoftLineBreaks(
circuitInputs.emailBody,
);
}
if (params.enableBodyMasking) {

View File

@@ -1,8 +1,16 @@
import * as CryptoJS from "crypto";
import { assert, int64toBytes, int8toBytes, mergeUInt8Arrays } from "./binary-format";
import { Hash } from "./lib/fast-sha256";
import * as CryptoJS from 'crypto';
import {
assert,
int64toBytes,
int8toBytes,
mergeUInt8Arrays,
} from './binary-format';
import { Hash } from './lib/fast-sha256';
export function findIndexInUint8Array(array: Uint8Array, selector: Uint8Array): number {
export function findIndexInUint8Array(
array: Uint8Array,
selector: Uint8Array,
): number {
let i = 0;
let j = 0;
while (i < array.length) {
@@ -62,7 +70,7 @@ export function generatePartialSHA({
}
if (bodyRemaining.length % 64 !== 0) {
throw new Error("Remaining body was not padded correctly with int64s");
throw new Error('Remaining body was not padded correctly with int64s');
}
bodyRemaining = padUint8ArrayWithZeros(bodyRemaining, maxRemainingBodyLength);
@@ -76,7 +84,7 @@ export function generatePartialSHA({
}
export function shaHash(str: Uint8Array) {
return CryptoJS.createHash("sha256").update(str).digest();
return CryptoJS.createHash('sha256').update(str).digest();
}
export function partialSha(msg: Uint8Array, msgLen: number): Uint8Array {
@@ -85,7 +93,10 @@ export function partialSha(msg: Uint8Array, msgLen: number): Uint8Array {
}
// Puts an end selector, a bunch of 0s, then the length, then fill the rest with 0s.
export function sha256Pad(message: Uint8Array, maxShaBytes: number): [Uint8Array, number] {
export function sha256Pad(
message: Uint8Array,
maxShaBytes: number,
): [Uint8Array, number] {
const msgLen = message.length * 8; // bytes to bits
const msgLenBytes = int64toBytes(msgLen);
@@ -96,7 +107,7 @@ export function sha256Pad(message: Uint8Array, maxShaBytes: number): [Uint8Array
}
res = mergeUInt8Arrays(res, msgLenBytes);
assert((res.length * 8) % 512 === 0, "Padding did not complete properly!");
assert((res.length * 8) % 512 === 0, 'Padding did not complete properly!');
const messageLen = res.length;
while (res.length < maxShaBytes) {
res = mergeUInt8Arrays(res, int64toBytes(0));

View File

@@ -1,22 +1,22 @@
import fs from "fs";
import path from "path";
import fs from 'fs';
import path from 'path';
const getUncompressedTestFile = (): ArrayBuffer => {
const buffer = fs.readFileSync(path.join(__dirname, "../test-data/compressed-files/uncompressed-value.txt"));
const buffer = fs.readFileSync(path.join(__dirname, '../test-data/compressed-files/uncompressed-value.txt'));
return buffer;
};
const tempStorage: Record<string, ArrayBuffer> = {
"email.zkeyb": getUncompressedTestFile(),
"email.zkeyc": getUncompressedTestFile(),
"email.zkeyd": getUncompressedTestFile(),
"email.zkeye": getUncompressedTestFile(),
"email.zkeyf": getUncompressedTestFile(),
"email.zkeyg": getUncompressedTestFile(),
"email.zkeyh": getUncompressedTestFile(),
"email.zkeyi": getUncompressedTestFile(),
"email.zkeyj": getUncompressedTestFile(),
"email.zkeyk": getUncompressedTestFile(),
'email.zkeyb': getUncompressedTestFile(),
'email.zkeyc': getUncompressedTestFile(),
'email.zkeyd': getUncompressedTestFile(),
'email.zkeye': getUncompressedTestFile(),
'email.zkeyf': getUncompressedTestFile(),
'email.zkeyg': getUncompressedTestFile(),
'email.zkeyh': getUncompressedTestFile(),
'email.zkeyi': getUncompressedTestFile(),
'email.zkeyj': getUncompressedTestFile(),
'email.zkeyk': getUncompressedTestFile(),
};
const getItem = jest.fn((key) => tempStorage[key]);

View File

@@ -1,13 +1,13 @@
import fs from "fs";
import path from "path";
import { StringDecoder } from "string_decoder";
import _localforage from "localforage";
import { downloadFromFilename, downloadProofFiles, uncompressGz as uncompress } from "../src/chunked-zkey";
import { server } from "./mocks/server";
import { MOCK_BASE_URL } from "./mocks/handlers";
import fs from 'fs';
import path from 'path';
import { StringDecoder } from 'string_decoder';
import _localforage from 'localforage';
import { downloadFromFilename, downloadProofFiles, uncompressGz as uncompress } from '../src/chunked-zkey';
import { server } from './mocks/server';
import { MOCK_BASE_URL } from './mocks/handlers';
// this is mocked in __mocks__/localforage.ts
jest.mock("localforage");
jest.mock('localforage');
const localforage = _localforage as jest.Mocked<typeof _localforage>;
@@ -24,24 +24,24 @@ afterAll(() => server.close());
// localforage should be storing ArrayBuffers.
// We can use this function to simplify checking the mocked value of the ArrayBuffer.
const decodeArrayBufferToString = (buffer: ArrayBuffer): string => {
const decoder = new StringDecoder("utf8");
const decoder = new StringDecoder('utf8');
const str = decoder.write(Buffer.from(buffer));
return str;
};
const getCompressedTestFile = (): ArrayBuffer => {
const buffer = fs.readFileSync(path.join(__dirname, "test-data/compressed-files/compressed.txt.gz"));
const buffer = fs.readFileSync(path.join(__dirname, 'test-data/compressed-files/compressed.txt.gz'));
return buffer;
};
const getUncompressedTestFile = (): ArrayBuffer => {
const buffer = fs.readFileSync(path.join(__dirname, "test-data/compressed-files/uncompressed-value.txt"));
const buffer = fs.readFileSync(path.join(__dirname, 'test-data/compressed-files/uncompressed-value.txt'));
return buffer;
};
describe("Uncompress GZ file", () => {
test("Uncompresss a GZ file", async () => {
const decoder = new StringDecoder("utf8");
describe('Uncompress GZ file', () => {
test('Uncompresss a GZ file', async () => {
const decoder = new StringDecoder('utf8');
const compressedArrayBuffer: ArrayBuffer = getCompressedTestFile();
const expectedArrayBuffer: ArrayBuffer = getUncompressedTestFile();
const expectedString = decoder.write(Buffer.from(expectedArrayBuffer));
@@ -51,13 +51,13 @@ describe("Uncompress GZ file", () => {
});
});
describe("Test zkp fetch and store", () => {
describe('Test zkp fetch and store', () => {
afterEach(() => {
jest.resetAllMocks();
});
test("should fetch a gz file, uncompress it, and store it in indexeddb", async () => {
const filename = "email.zkeyb.gz";
test('should fetch a gz file, uncompress it, and store it in indexeddb', async () => {
const filename = 'email.zkeyb.gz';
// downloadFileFromFilename requests the file from the server, which we mocked with msw.
// The server returns a gz file of a file containing "not compressed 👍",
// which is defined in __fixtures__/compressed-files/compressed.txt.gz
@@ -69,28 +69,28 @@ describe("Test zkp fetch and store", () => {
// expect to be called with...
const str = decodeArrayBufferToString(decompressedBuffer);
expect(filenameRaw).toBe("email.zkeyb");
expect(filenameRaw).toBe('email.zkeyb');
// check that it decompressed the file correctly.
expect(str).toBe("not compressed 👍");
expect(str).toBe('not compressed 👍');
});
test("should should download all the zkeys and save them in local storage for snarkjs to access.", async () => {
test('should should download all the zkeys and save them in local storage for snarkjs to access.', async () => {
// downloadProofFiles calls downloadFromFilename 10 times, one for each zkey, b-k.
const onDownloaded = jest.fn();
await downloadProofFiles(MOCK_BASE_URL, "email", onDownloaded);
await downloadProofFiles(MOCK_BASE_URL, 'email', onDownloaded);
expect(localforage.setItem).toBeCalledTimes(10);
// check the first one
const filenameRawB = localforage.setItem.mock.calls[0][0];
const decompressedBufferB = localforage.setItem.mock.calls[0][1] as ArrayBuffer;
expect(filenameRawB).toBe("email.zkeyb");
expect(decodeArrayBufferToString(decompressedBufferB)).toBe("not compressed 👍");
expect(filenameRawB).toBe('email.zkeyb');
expect(decodeArrayBufferToString(decompressedBufferB)).toBe('not compressed 👍');
// ... c d e f g h i j ... assume these are fine too.
// check the last one
const filenameRawK = localforage.setItem.mock.calls[9][0];
const decompressedBufferK = localforage.setItem.mock.calls[9][1] as ArrayBuffer;
expect(filenameRawK).toBe("email.zkeyk");
expect(decodeArrayBufferToString(decompressedBufferK)).toBe("not compressed 👍");
expect(filenameRawK).toBe('email.zkeyk');
expect(decodeArrayBufferToString(decompressedBufferK)).toBe('not compressed 👍');
expect(onDownloaded).toBeCalledTimes(10);
});
});

View File

@@ -1,33 +1,25 @@
import fs from "fs";
import path from "path";
import { verifyDKIMSignature } from "../src/dkim";
import fs from 'fs';
import path from 'path';
import { verifyDKIMSignature } from '../src/dkim';
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"));
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");
expect(result.signingDomain).toBe('icloud.com');
expect(result.appliedSanitization).toBeFalsy();
});
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"));
it('should fail for invalid selector', async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-invalid-selector.eml'),
);
expect.assertions(1);
@@ -35,27 +27,49 @@ describe("DKIM signature verification", () => {
await verifyDKIMSignature(email);
} catch (e) {
expect(e.message).toBe(
"DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify",
'DKIM signature verification failed for domain icloud.com. Reason: no key',
);
}
});
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"));
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 not found for domain gmail.com");
expect(e.message).toBe(
'DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify',
);
}
});
it("should be able to override domain", async () => {
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"));
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-different-domain.eml'),
);
// Should pass with default domain
await verifyDKIMSignature(email);
@@ -65,22 +79,26 @@ describe("DKIM signature verification", () => {
// different from From domain and the below check pass.
expect.assertions(1);
try {
await verifyDKIMSignature(email, "domain.com");
await verifyDKIMSignature(email, 'domain.com');
} catch (e) {
expect(e.message).toBe("DKIM signature not found for domain domain.com");
expect(e.message).toBe(
'DKIM signature not found for domain domain.com',
);
}
});
});
describe("DKIM with sanitization", () => {
it("should pass after removing label from Subject", async () => {
const email = fs.readFileSync(path.join(__dirname, "test-data/email-good.eml"));
describe('DKIM with sanitization', () => {
it('should pass after removing label from Subject', async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-good.eml'),
);
// Add a label to the subject
const tamperedEmail = email.toString().replace("Subject: ", "Subject: [EmailListABC]");
const tamperedEmail = email.toString().replace('Subject: ', 'Subject: [EmailListABC]');
const result = await verifyDKIMSignature(tamperedEmail);
expect(result.appliedSanitization).toBe("removeLabels");
expect(result.appliedSanitization).toBe('removeLabels');
});
});

View File

@@ -7,7 +7,9 @@ jest.setTimeout(10000);
describe("Input generators", () => {
it("should generate input from raw email", async () => {
const email = fs.readFileSync(path.join(__dirname, "test-data/email-good.eml"));
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);
const inputs = await generateEmailVerifierInputs(email);
@@ -21,7 +23,9 @@ describe("Input generators", () => {
});
it("should generate input without body params when ignoreBodyHash is true", async () => {
const email = fs.readFileSync(path.join(__dirname, "test-data/email-good.eml"));
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);
const inputs = await generateEmailVerifierInputs(email, {
ignoreBodyHashCheck: true,
@@ -37,7 +41,9 @@ describe("Input generators", () => {
});
it("should generate input with SHA precompute selector", async () => {
const email = fs.readFileSync(path.join(__dirname, "test-data/email-good-large.eml"));
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good-large.eml")
);
const inputs = await generateEmailVerifierInputs(email, {
shaPrecomputeSelector: "thousands",
@@ -45,7 +51,9 @@ describe("Input generators", () => {
expect(inputs.emailBody).toBeDefined();
const strBody = bytesToString(Uint8Array.from(inputs.emailBody!.map((b) => Number(b))));
const strBody = bytesToString(
Uint8Array.from(inputs.emailBody!.map((b) => Number(b)))
);
const expected = "h hundreds of thousands of blocks."; // will round till previous 64x th byte
@@ -53,12 +61,14 @@ describe("Input generators", () => {
});
it("should throw if SHA precompute selector is invalid", async () => {
const email = fs.readFileSync(path.join(__dirname, "test-data/email-good.eml"));
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);
await expect(() =>
generateEmailVerifierInputs(email, {
shaPrecomputeSelector: "Bla Bla",
}),
})
).rejects.toThrow('SHA precompute selector "Bla Bla" not found in the body');
});
});

View File

@@ -1,22 +1,22 @@
import { ethers, JsonRpcProvider } from 'ethers';
import { buildPoseidon } from 'circomlibjs';
import dns from 'dns';
import path from 'path';
import forge from 'node-forge';
import { bigIntToChunkedBytes } from '@zk-email/helpers/src/binaryFormat';
const fs = require('fs');
import { abi } from '../abis/DKIMRegistry.json';
import { poseidonLarge } from '@zk-email/helpers/src/hash';
require('dotenv').config();
import { ethers, JsonRpcProvider } from "ethers";
import { buildPoseidon } from "circomlibjs";
import dns from "dns";
import path from "path";
import forge from "node-forge";
import { bigIntToChunkedBytes } from "@zk-email/helpers/src/binaryFormat";
const fs = require("fs");
import { abi } from "../abis/DKIMRegistry.json";
import { poseidonLarge } from "@zk-email/helpers/src/hash";
require("dotenv").config();
async function updateContract(domain: string, pubkeyHashes: string[]) {
if (!pubkeyHashes.length) {
return;
}
if (!process.env.PRIVATE_KEY) throw new Error('Env private key found');
if (!process.env.RPC_URL) throw new Error('Env RPC URL found');
if (!process.env.DKIM_REGISTRY) throw new Error('Env DKIM_REGISTRY found');
if (!process.env.PRIVATE_KEY) throw new Error("Env private key found");
if (!process.env.RPC_URL) throw new Error("Env RPC URL found");
if (!process.env.DKIM_REGISTRY) throw new Error("Env DKIM_REGISTRY found");
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
@@ -30,7 +30,11 @@ async function updateContract(domain: string, pubkeyHashes: string[]) {
console.log(`Updated hashes for domain ${domain}. Tx: ${tx.hash}`);
}
async function getPublicKeyForDomainAndSelector(domain: string, selector: string, print: boolean = true) {
async function getPublicKeyForDomainAndSelector(
domain: string,
selector: string,
print: boolean = true
) {
// Construct the DKIM record name
let dkimRecordName = `${selector}._domainkey.${domain}`;
if (print) console.log(dkimRecordName);
@@ -49,7 +53,7 @@ async function getPublicKeyForDomainAndSelector(domain: string, selector: string
// The DKIM record is a TXT record containing a string
// We need to parse this string to get the public key
let dkimRecord = records[0].join('');
let dkimRecord = records[0].join("");
let match = dkimRecord.match(/p=([^;]+)/);
if (!match) {
console.error(`No public key found in DKIM record for ${domain}`);
@@ -58,16 +62,16 @@ async function getPublicKeyForDomainAndSelector(domain: string, selector: string
// The public key is base64 encoded, we need to decode it
let pubkey = match[1];
let binaryKey = Buffer.from(pubkey, 'base64').toString('base64');
let binaryKey = Buffer.from(pubkey, "base64").toString("base64");
// Get match
let matches = binaryKey.match(/.{1,64}/g);
if (!matches) {
console.error('No matches found');
console.error("No matches found");
return;
}
let formattedKey = matches.join('\n');
if (print) console.log('Key: ', formattedKey);
let formattedKey = matches.join("\n");
if (print) console.log("Key: ", formattedKey);
// Convert to PEM format
let pemKey = `-----BEGIN PUBLIC KEY-----\n${formattedKey}\n-----END PUBLIC KEY-----`;
@@ -77,14 +81,18 @@ async function getPublicKeyForDomainAndSelector(domain: string, selector: string
// Get the modulus n only
let n = publicKey.n;
if (print) console.log('Modulus n:', n.toString(16));
if (print) console.log("Modulus n:", n.toString(16));
return BigInt(publicKey.n.toString());
}
async function checkSelector(domain: string, selector: string) {
try {
const publicKey = await getPublicKeyForDomainAndSelector(domain, selector, false);
const publicKey = await getPublicKeyForDomainAndSelector(
domain,
selector,
false
);
if (publicKey) {
console.log(`Domain: ${domain}, Selector: ${selector} - Match found`);
return {
@@ -97,7 +105,9 @@ async function checkSelector(domain: string, selector: string) {
// console.log(`Domain: ${domain}, Selector: ${selector} - No match found`);
}
} catch (error) {
console.error(`Error processing domain: ${domain}, Selector: ${selector} - ${error}`);
console.error(
`Error processing domain: ${domain}, Selector: ${selector} - ${error}`
);
}
return {
@@ -111,57 +121,57 @@ async function checkSelector(domain: string, selector: string) {
// Filename is a file where each line is a domain
// This searches for default selectors like "google" or "default"
async function getDKIMPublicKeysForDomains(filename: string) {
const domains = fs.readFileSync(filename, 'utf8').split('\n');
const domains = fs.readFileSync(filename, "utf8").split("\n");
const selectors = [
'google',
'default',
'mail',
'smtpapi',
'dkim',
'200608',
'20230601',
'20221208',
'20210112',
'dkim-201406',
'1a1hai',
'v1',
'v2',
'v3',
'k1',
'k2',
'k3',
'hs1',
'hs2',
's1',
's2',
's3',
'sig1',
'sig2',
'sig3',
'selector',
'selector1',
'selector2',
'mindbox',
'bk',
'sm1',
'sm2',
'gmail',
'10dkim1',
'11dkim1',
'12dkim1',
'memdkim',
'm1',
'mx',
'sel1',
'bk',
'scph1220',
'ml',
'pps1',
'scph0819',
'skiff1',
's1024',
'selector1',
'dkim-202308',
"google",
"default",
"mail",
"smtpapi",
"dkim",
"200608",
"20230601",
"20221208",
"20210112",
"dkim-201406",
"1a1hai",
"v1",
"v2",
"v3",
"k1",
"k2",
"k3",
"hs1",
"hs2",
"s1",
"s2",
"s3",
"sig1",
"sig2",
"sig3",
"selector",
"selector1",
"selector2",
"mindbox",
"bk",
"sm1",
"sm2",
"gmail",
"10dkim1",
"11dkim1",
"12dkim1",
"memdkim",
"m1",
"mx",
"sel1",
"bk",
"scph1220",
"ml",
"pps1",
"scph0819",
"skiff1",
"s1024",
"selector1",
"dkim-202308"
];
let results = [];
@@ -186,7 +196,9 @@ async function getDKIMPublicKeysForDomains(filename: string) {
const publicKey = result.publicKey.toString();
if (!matchedSelectors[result.domain].find((d) => d.publicKey === publicKey)) {
if (
!matchedSelectors[result.domain].find((d) => d.publicKey === publicKey)
) {
matchedSelectors[result.domain].push({
selector: result.selector,
publicKey,
@@ -198,17 +210,26 @@ async function getDKIMPublicKeysForDomains(filename: string) {
return matchedSelectors;
}
async function updateDKIMRegistry({ domainListFile, writeToFile }: { domainListFile: string; writeToFile: boolean }) {
async function updateDKIMRegistry({
domainListFile,
writeToFile,
}: {
domainListFile: string;
writeToFile: boolean;
}) {
function _writeToFile(filename: string, data: object) {
if (!writeToFile) return;
if (!fs.existsSync(path.join(__dirname, 'out'))) {
fs.mkdirSync(path.join(__dirname, 'out'));
if (!fs.existsSync(path.join(__dirname, "out"))) {
fs.mkdirSync(path.join(__dirname, "out"));
}
fs.writeFileSync(path.join(__dirname, 'out/' + filename), JSON.stringify(data, null, 2));
fs.writeFileSync(
path.join(__dirname, "out/" + filename),
JSON.stringify(data, null, 2)
);
}
const domainPubKeyMap = await getDKIMPublicKeysForDomains(domainListFile);
_writeToFile('dkim-keys.json', domainPubKeyMap);
_writeToFile("dkim-keys.json", domainPubKeyMap);
// const domainPubKeyMap = JSON.parse(
// fs.readFileSync(path.join(__dirname, "out/dkim-keys.json")).toString()
@@ -228,7 +249,7 @@ async function updateDKIMRegistry({ domainListFile, writeToFile }: { domainListF
chunkedDKIMPubKeyMap[domain].push(pubkeyChunked.map((s) => s.toString()));
}
}
_writeToFile('dkim-keys-chunked.json', chunkedDKIMPubKeyMap);
_writeToFile("dkim-keys-chunked.json", chunkedDKIMPubKeyMap);
// Generate pub key hash using 242 * 9 chunks (Poseidon lib don't take more than 16 inputs)
const domainHashedPubKeyMap: { [key: string]: string[] } = {};
@@ -243,7 +264,7 @@ async function updateDKIMRegistry({ domainListFile, writeToFile }: { domainListF
domainHashedPubKeyMap[domain].push(poseidonHash.toString());
}
}
_writeToFile('dkim-keys-hashed.json', domainHashedPubKeyMap);
_writeToFile("dkim-keys-hashed.json", domainHashedPubKeyMap);
// Update Mailserver contract with found keys
for (let domain of Object.keys(domainHashedPubKeyMap)) {
@@ -252,6 +273,6 @@ async function updateDKIMRegistry({ domainListFile, writeToFile }: { domainListF
}
updateDKIMRegistry({
domainListFile: path.join(__dirname, 'domains.txt'),
domainListFile: path.join(__dirname, "domains.txt"),
writeToFile: true,
});

View File

@@ -7168,15 +7168,6 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^3.3.3":
version: 3.3.3
resolution: "prettier@npm:3.3.3"
bin:
prettier: bin/prettier.cjs
checksum: bc8604354805acfdde6106852d14b045bb20827ad76a5ffc2455b71a8257f94de93f17f14e463fe844808d2ccc87248364a5691488a3304f1031326e62d9276e
languageName: node
linkType: hard
"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0":
version: 29.7.0
resolution: "pretty-format@npm:29.7.0"
@@ -7464,8 +7455,6 @@ __metadata:
"root-workspace-0b6124@workspace:.":
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
prettier: ^3.3.3
languageName: unknown
linkType: soft