From 11a5a9e91e0d709e0e342dfe32f569f90f15acc0 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 13 Jan 2025 11:30:25 +0100 Subject: [PATCH 1/4] wip --- package.json | 10 +++++ src/crypto/{core.ts => js/_core.ts} | 16 +++---- src/crypto/js/index.ts | 2 + src/crypto/{ => js}/murmur.ts | 0 src/crypto/{ => js}/sha256.ts | 2 +- src/crypto/node/index.ts | 14 ++++++ src/hash/hash.ts | 4 +- src/index.ts | 4 +- src/utils/diff.ts | 2 +- src/utils/is-equal.ts | 2 +- test/crypto.test.ts | 70 +++++++++++++++++++++++++++++ test/index.test.ts | 60 ------------------------- tsconfig.json | 19 +++++--- 13 files changed, 124 insertions(+), 81 deletions(-) rename src/crypto/{core.ts => js/_core.ts} (89%) create mode 100644 src/crypto/js/index.ts rename src/crypto/{ => js}/murmur.ts (100%) rename src/crypto/{ => js}/sha256.ts (98%) create mode 100644 src/crypto/node/index.ts create mode 100644 test/crypto.test.ts delete mode 100644 test/index.test.ts diff --git a/package.json b/package.json index 5e0f5a0..2db33f0 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,16 @@ "import": "./dist/index.mjs", "types": "./dist/index.d.ts", "require": "./dist/index.cjs" + }, + "./crypto": { + "node": { + "import": "./dist/crypto/node.mjs", + "require": "./dist/crypto/node.cjs" + }, + "default": { + "import": "./dist/crypto/js.mjs", + "require": "./dist/crypto/js.cjs" + } } }, "main": "./dist/index.cjs", diff --git a/src/crypto/core.ts b/src/crypto/js/_core.ts similarity index 89% rename from src/crypto/core.ts rename to src/crypto/js/_core.ts index ed9b885..a43adcc 100644 --- a/src/crypto/core.ts +++ b/src/crypto/js/_core.ts @@ -23,8 +23,8 @@ export class WordArray { // Copy one byte at a time for (let i = 0; i < wordArray.sigBytes; i++) { const thatByte = - (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; - this.words[(this.sigBytes + i) >>> 2] |= + (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; + this.words[(this.sigBytes + i) >>> 2]! |= thatByte << (24 - ((this.sigBytes + i) % 4) * 8); } } else { @@ -41,7 +41,7 @@ export class WordArray { clamp() { // Clamp - this.words[this.sigBytes >>> 2] &= + this.words[this.sigBytes >>> 2]! &= 0xff_ff_ff_ff << (32 - (this.sigBytes % 4) * 8); this.words.length = Math.ceil(this.sigBytes / 4); } @@ -56,7 +56,7 @@ export const Hex = { // Convert const hexChars: string[] = []; for (let i = 0; i < wordArray.sigBytes; i++) { - const bite = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + const bite = (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; hexChars.push((bite >>> 4).toString(16), (bite & 0x0f).toString(16)); } @@ -70,11 +70,11 @@ export const Base64 = { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const base64Chars: string[] = []; for (let i = 0; i < wordArray.sigBytes; i += 3) { - const byte1 = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + const byte1 = (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; const byte2 = - (wordArray.words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + (wordArray.words[(i + 1) >>> 2]! >>> (24 - ((i + 1) % 4) * 8)) & 0xff; const byte3 = - (wordArray.words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + (wordArray.words[(i + 2) >>> 2]! >>> (24 - ((i + 2) % 4) * 8)) & 0xff; const triplet = (byte1 << 16) | (byte2 << 8) | byte3; for (let j = 0; j < 4 && i * 8 + j * 6 < wordArray.sigBytes * 8; j++) { @@ -93,7 +93,7 @@ export const Latin1 = { // Convert const words: number[] = []; for (let i = 0; i < latin1StrLength; i++) { - words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + words[i >>> 2]! |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); } return new WordArray(words, latin1StrLength); diff --git a/src/crypto/js/index.ts b/src/crypto/js/index.ts new file mode 100644 index 0000000..5ad0ab7 --- /dev/null +++ b/src/crypto/js/index.ts @@ -0,0 +1,2 @@ +export { murmurHash } from "./murmur"; +export { sha256, sha256base64 } from "./sha256"; diff --git a/src/crypto/murmur.ts b/src/crypto/js/murmur.ts similarity index 100% rename from src/crypto/murmur.ts rename to src/crypto/js/murmur.ts diff --git a/src/crypto/sha256.ts b/src/crypto/js/sha256.ts similarity index 98% rename from src/crypto/sha256.ts rename to src/crypto/js/sha256.ts index de9a20f..31a7fb6 100644 --- a/src/crypto/sha256.ts +++ b/src/crypto/js/sha256.ts @@ -1,6 +1,6 @@ // Based on https://github.com/brix/crypto-js 4.1.1 (MIT) -import { WordArray, Hasher, Base64 } from "./core"; +import { WordArray, Hasher, Base64 } from "./_core"; // Initialization and round constants tables const H = [ diff --git a/src/crypto/node/index.ts b/src/crypto/node/index.ts new file mode 100644 index 0000000..e98e253 --- /dev/null +++ b/src/crypto/node/index.ts @@ -0,0 +1,14 @@ +import { createHash } from "node:crypto"; + +export { murmurHash } from "../js/murmur"; + +export function sha256(data: string): string { + return createHash("sha256").update(data).digest("hex").replace(/=+$/, ""); +} + +export function sha256base64(date: string): string { + return createHash("sha256") + .update(date) + .digest("base64") + .replace(/[+/=]/g, ""); +} diff --git a/src/hash/hash.ts b/src/hash/hash.ts index 199fb2d..cb6cc58 100644 --- a/src/hash/hash.ts +++ b/src/hash/hash.ts @@ -1,5 +1,5 @@ -import { objectHash, HashOptions } from "./object-hash"; -import { sha256base64 } from "../crypto/sha256"; +import { objectHash, type HashOptions } from "./object-hash"; +import { sha256base64 } from "ohash/crypto"; /** * Hash any JS value into a string diff --git a/src/index.ts b/src/index.ts index 3768d77..73b60c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,8 @@ export { objectHash } from "./hash/object-hash"; export { hash } from "./hash/hash"; // Crypto -export { murmurHash } from "./crypto/murmur"; -export { sha256, sha256base64 } from "./crypto/sha256"; +export { murmurHash } from "./crypto/js/murmur"; +export { sha256, sha256base64 } from "./crypto/js/sha256"; // Utils export { isEqual } from "./utils/is-equal"; diff --git a/src/utils/diff.ts b/src/utils/diff.ts index 2f4928c..a4990d2 100644 --- a/src/utils/diff.ts +++ b/src/utils/diff.ts @@ -1,4 +1,4 @@ -import { objectHash, HashOptions } from "../hash/object-hash"; +import { objectHash, type HashOptions } from "../hash/object-hash"; /** * Calculates the difference between two objects and returns a list of differences. diff --git a/src/utils/is-equal.ts b/src/utils/is-equal.ts index e8de138..d21cf88 100644 --- a/src/utils/is-equal.ts +++ b/src/utils/is-equal.ts @@ -1,4 +1,4 @@ -import { objectHash, HashOptions } from "../hash/object-hash"; +import { objectHash, type HashOptions } from "../hash/object-hash"; /** * Compare two objects using reference equality and stable deep hashing. * @param {any} object1 First object diff --git a/test/crypto.test.ts b/test/crypto.test.ts new file mode 100644 index 0000000..ce439d0 --- /dev/null +++ b/test/crypto.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; + +import * as cryptoJS from "../src/crypto/js"; +import * as cryptoNode from "../src/crypto/node"; + +const impls = { + js: cryptoJS, + node: cryptoNode, +}; + +describe("crypto", () => { + for (const [name, { sha256, sha256base64 }] of Object.entries(impls)) { + describe(name, () => { + it("sha256", () => { + expect(sha256("Hello World")).toBe( + "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e", + ); + expect(sha256("")).toBe( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ); + }); + + it("sha256base64", () => { + expect(sha256base64("Hello World")).toBe( + "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4", + ); + expect(sha256base64("")).toBe( + "47DEQpj8HBSaTImW5JCeuQeRkm5NMpJWZG3hSuFU", + ); + }); + }); + } +}); + +describe("crypto:mmurmurHash", () => { + const { murmurHash } = cryptoJS; + it("Generates correct hash for 0 bytes without seed", () => { + expect(murmurHash("")).toMatchInlineSnapshot("0"); + }); + it("Generates correct hash for 0 bytes with seed", () => { + expect(murmurHash("", 1)).toMatchInlineSnapshot("1364076727"); // 0x514E28B7 + }); + it("Generates correct hash for 'Hello World'", () => { + expect(murmurHash("Hello World")).toMatchInlineSnapshot("427197390"); + }); + it("Generates the correct hash for varios string lengths", () => { + expect(murmurHash("a")).toMatchInlineSnapshot("1009084850"); + expect(murmurHash("aa")).toMatchInlineSnapshot("923832745"); + expect(murmurHash("aaa")).toMatchInlineSnapshot("3033554871"); + expect(murmurHash("aaaa")).toMatchInlineSnapshot("2129582471"); + expect(murmurHash("aaaaa")).toMatchInlineSnapshot("3922341931"); + expect(murmurHash("aaaaaa")).toMatchInlineSnapshot("1736445713"); + expect(murmurHash("aaaaaaa")).toMatchInlineSnapshot("1497565372"); + expect(murmurHash("aaaaaaaa")).toMatchInlineSnapshot("3662943087"); + expect(murmurHash("aaaaaaaaa")).toMatchInlineSnapshot("2724714153"); + }); + it("Works with Uint8Arrays", () => { + expect( + murmurHash(new Uint8Array([0x21, 0x43, 0x65, 0x87])), + ).toMatchInlineSnapshot("4116402539"); // 0xF55B516B + }); + it("Handles UTF-8 high characters correctly", () => { + expect(murmurHash("ππππππππ", 0x97_47_b2_8c)).toMatchInlineSnapshot( + "3581961153", + ); + }); + it("Gives correct hash with uint32 maximum value as seed", () => { + expect(murmurHash("a", 2_147_483_647)).toMatchInlineSnapshot("3574244913"); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts deleted file mode 100644 index 1bff9bc..0000000 --- a/test/index.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { murmurHash, sha256, sha256base64 } from "../src"; - -describe("crypto", () => { - describe("murmurHash", () => { - it("Generates correct hash for 0 bytes without seed", () => { - expect(murmurHash("")).toMatchInlineSnapshot("0"); - }); - it("Generates correct hash for 0 bytes with seed", () => { - expect(murmurHash("", 1)).toMatchInlineSnapshot("1364076727"); // 0x514E28B7 - }); - it("Generates correct hash for 'Hello World'", () => { - expect(murmurHash("Hello World")).toMatchInlineSnapshot("427197390"); - }); - it("Generates the correct hash for varios string lengths", () => { - expect(murmurHash("a")).toMatchInlineSnapshot("1009084850"); - expect(murmurHash("aa")).toMatchInlineSnapshot("923832745"); - expect(murmurHash("aaa")).toMatchInlineSnapshot("3033554871"); - expect(murmurHash("aaaa")).toMatchInlineSnapshot("2129582471"); - expect(murmurHash("aaaaa")).toMatchInlineSnapshot("3922341931"); - expect(murmurHash("aaaaaa")).toMatchInlineSnapshot("1736445713"); - expect(murmurHash("aaaaaaa")).toMatchInlineSnapshot("1497565372"); - expect(murmurHash("aaaaaaaa")).toMatchInlineSnapshot("3662943087"); - expect(murmurHash("aaaaaaaaa")).toMatchInlineSnapshot("2724714153"); - }); - it("Works with Uint8Arrays", () => { - expect( - murmurHash(new Uint8Array([0x21, 0x43, 0x65, 0x87])), - ).toMatchInlineSnapshot("4116402539"); // 0xF55B516B - }); - it("Handles UTF-8 high characters correctly", () => { - expect(murmurHash("ππππππππ", 0x97_47_b2_8c)).toMatchInlineSnapshot( - "3581961153", - ); - }); - it("Gives correct hash with uint32 maximum value as seed", () => { - expect(murmurHash("a", 2_147_483_647)).toMatchInlineSnapshot( - "3574244913", - ); - }); - }); - - it("sha256", () => { - expect(sha256("Hello World")).toMatchInlineSnapshot( - '"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"', - ); - expect(sha256("")).toMatchInlineSnapshot( - '"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"', - ); - }); - - it("sha256base64", () => { - expect(sha256base64("Hello World")).toMatchInlineSnapshot( - '"pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4"', - ); - expect(sha256base64("")).toMatchInlineSnapshot( - '"47DEQpj8HBSaTImW5JCeuQeRkm5NMpJWZG3hSuFU"', - ); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 9f9b15b..4733c5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,18 @@ { "compilerOptions": { "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Node", + "module": "preserve", + "moduleDetection": "force", "esModuleInterop": true, - "types": ["node"], - "strict": true - }, - "include": ["src"] + "allowSyntheticDefaultImports": true, + "allowJs": true, + "resolveJsonModule": true, + "strict": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true, + "noImplicitOverride": true, + "noEmit": true + } } From 414d648fc5313695e0149c5697b60c309632b354 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 13 Jan 2025 11:46:37 +0100 Subject: [PATCH 2/4] reduce diff --- src/crypto/js/_core.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/crypto/js/_core.ts b/src/crypto/js/_core.ts index a43adcc..ed9b885 100644 --- a/src/crypto/js/_core.ts +++ b/src/crypto/js/_core.ts @@ -23,8 +23,8 @@ export class WordArray { // Copy one byte at a time for (let i = 0; i < wordArray.sigBytes; i++) { const thatByte = - (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; - this.words[(this.sigBytes + i) >>> 2]! |= + (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + this.words[(this.sigBytes + i) >>> 2] |= thatByte << (24 - ((this.sigBytes + i) % 4) * 8); } } else { @@ -41,7 +41,7 @@ export class WordArray { clamp() { // Clamp - this.words[this.sigBytes >>> 2]! &= + this.words[this.sigBytes >>> 2] &= 0xff_ff_ff_ff << (32 - (this.sigBytes % 4) * 8); this.words.length = Math.ceil(this.sigBytes / 4); } @@ -56,7 +56,7 @@ export const Hex = { // Convert const hexChars: string[] = []; for (let i = 0; i < wordArray.sigBytes; i++) { - const bite = (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; + const bite = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; hexChars.push((bite >>> 4).toString(16), (bite & 0x0f).toString(16)); } @@ -70,11 +70,11 @@ export const Base64 = { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const base64Chars: string[] = []; for (let i = 0; i < wordArray.sigBytes; i += 3) { - const byte1 = (wordArray.words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff; + const byte1 = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; const byte2 = - (wordArray.words[(i + 1) >>> 2]! >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + (wordArray.words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; const byte3 = - (wordArray.words[(i + 2) >>> 2]! >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + (wordArray.words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; const triplet = (byte1 << 16) | (byte2 << 8) | byte3; for (let j = 0; j < 4 && i * 8 + j * 6 < wordArray.sigBytes * 8; j++) { @@ -93,7 +93,7 @@ export const Latin1 = { // Convert const words: number[] = []; for (let i = 0; i < latin1StrLength; i++) { - words[i >>> 2]! |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); } return new WordArray(words, latin1StrLength); From 854ef5a3e225bdf3aaba0881ce996b9afe9e5c2e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Feb 2025 00:26:29 +0100 Subject: [PATCH 3/4] fix exports --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5b93cbb..98716b6 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,12 @@ }, "./crypto": { "node": { - "import": "./dist/crypto/node.mjs", - "require": "./dist/crypto/node.cjs" + "import": "./dist/crypto/node/index.mjs", + "require": "./dist/crypto/node/index.cjs" }, "default": { - "import": "./dist/crypto/js.mjs", - "require": "./dist/crypto/js.cjs" + "import": "./dist/crypto/js/index.mjs", + "require": "./dist/crypto/js/index.cjs" } } }, From eb7cbfa700dc37b8adb3577d95d950ec8b567096 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 19 Feb 2025 00:27:39 +0100 Subject: [PATCH 4/4] build before typecheck --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a857713..a86c257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: cache: "pnpm" - run: pnpm install - run: pnpm lint - - run: pnpm test:types - run: pnpm build + - run: pnpm test:types - run: pnpm vitest --coverage - uses: codecov/codecov-action@v5