diff --git a/src/string_and_binary/path.ts b/src/string_and_binary/path.ts index 1e2d353e..941e6326 100644 --- a/src/string_and_binary/path.ts +++ b/src/string_and_binary/path.ts @@ -82,12 +82,8 @@ export function expandDocumentIDPrefix(id: DocumentID): [string, FilePathWithPre const _hashString = memorizeFuncWithLRUCache(async (key: string) => { const buff = writeString(key); const webcrypto = await getWebCrypto(); - let digest = await webcrypto.subtle.digest("SHA-256", buff); - const len = key.length; - for (let i = 0; i < len; i++) { - // Stretching - digest = await webcrypto.subtle.digest("SHA-256", buff); - } + // Derive the obfuscated document ID from this input. + const digest = await webcrypto.subtle.digest("SHA-256", buff); return uint8ArrayToHexString(new Uint8Array(digest)); }); diff --git a/src/string_and_binary/path.unit.spec.ts b/src/string_and_binary/path.unit.spec.ts index f1ad2812..3cfa5e84 100644 --- a/src/string_and_binary/path.unit.spec.ts +++ b/src/string_and_binary/path.unit.spec.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const minimatchStats = vi.hoisted(() => ({ constructions: 0 })); +const webCryptoStats = vi.hoisted(() => ({ digests: 0 })); vi.mock("minimatch", async (importOriginal) => { const actual = await importOriginal(); @@ -18,6 +19,24 @@ vi.mock("minimatch", async (importOriginal) => { }; }); +vi.mock("@lib/mods.ts", async (importOriginal) => { + const actual = await importOriginal(); + const webcrypto = await actual.getWebCrypto(); + const countedWebCrypto = { + subtle: { + digest: (algorithm: AlgorithmIdentifier, data: BufferSource) => { + webCryptoStats.digests++; + return webcrypto.subtle.digest(algorithm, data); + }, + }, + } as Crypto; + + return { + ...actual, + getWebCrypto: async () => countedWebCrypto, + }; +}); + import type { FilePath } from "@lib/common/types"; import { isAccepted, path2id_base } from "./path"; @@ -37,6 +56,28 @@ describe("path2id_base path case", () => { }); }); +describe("path2id_base path obfuscation", () => { + it("performs one digest for each uncached hash input", async () => { + webCryptoStats.digests = 0; + const passphrase = "digest-count-regression-secret"; + + await path2id_base("First.md" as FilePath, passphrase, false); + expect(webCryptoStats.digests).toBe(2); + + await path2id_base("Second.md" as FilePath, passphrase, false); + expect(webCryptoStats.digests).toBe(3); + }); + + it.each([ + ["資料/概要.md", true, "f:d17ef57666777963bdb6875c83e16b39fee9ca7f7c3593f1b50af691c7bc2fa8"], + ["_private/Calculus.md", false, "f:2a18fa03d734284a8ace4d3c0a7b65558209b8f092ae36be1fdfc3626bc99268"], + ])("keeps the established document ID for %s", async (path, caseInsensitive, expected) => { + await expect( + path2id_base(path as FilePath, "path-obfuscation-regression-secret", caseInsensitive) + ).resolves.toBe(expected); + }); +}); + describe("isAccepted matcher cache", () => { beforeEach(() => { minimatchStats.constructions = 0;