mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-09 13:38:03 -05:00
@@ -1 +0,0 @@
|
||||
packages/helpers/src/lib
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user