From de99e2072c95b7ca11ee359f09ac1ca4afaf6338 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Jul 2024 16:57:05 +0100 Subject: [PATCH 01/12] Fix hashed ID server lookups with no Olm It used the hash function from Olm (presumably to work cross-platform) but subtle crypto is available on node nowadays so we can just use that. Refactor existing code that did this out to a common function, add tests. --- spec/unit/crypto/digest.spec.ts | 29 +++++++++++++++++++++ spec/unit/matrix-client.spec.ts | 45 ++++++++++++++++++++++++++++++++- src/client.ts | 27 ++++++++++---------- src/crypto/digest.ts | 32 +++++++++++++++++++++++ src/oidc/authorize.ts | 9 ++----- 5 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 spec/unit/crypto/digest.spec.ts create mode 100644 src/crypto/digest.ts diff --git a/spec/unit/crypto/digest.spec.ts b/spec/unit/crypto/digest.spec.ts new file mode 100644 index 00000000000..50805c325b9 --- /dev/null +++ b/spec/unit/crypto/digest.spec.ts @@ -0,0 +1,29 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { sha256Base64UrlUnpadded } from "../../../src/crypto/digest"; + +describe("sha256Base64UrlUnpadded", () => { + it("should hash a string", async () => { + const hash = await sha256Base64UrlUnpadded("test"); + expect(hash).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg"); + }); + + it("should hash a string with emoji", async () => { + const hash = await sha256Base64UrlUnpadded("test 🍱"); + expect(hash).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw"); + }); +}); diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index fa9cd776902..08a62932d02 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -298,7 +298,9 @@ describe("MatrixClient", function () { ...(opts || {}), }); // FIXME: We shouldn't be yanking http like this. - client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => { + client.http = ( + ["authedRequest", "getContentUri", "request", "uploadContent", "idServerRequest"] as const + ).reduce((r, k) => { r[k] = jest.fn(); return r; }, {} as MatrixHttpApi); @@ -3035,4 +3037,45 @@ describe("MatrixClient", function () { expect(httpLookups.length).toEqual(0); }); }); + + describe("identityHashedLookup", () => { + it("should return hashed lookup results", async () => { + const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request"; + + client.http.idServerRequest = jest.fn().mockImplementation((method, path, params) => { + if (method === "GET" && path === "/hash_details") { + return { algorithms: ["sha256"], lookup_pepper: "carrot" }; + } else if (method === "POST" && path === "/lookup") { + return { + mappings: { + "WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU": "@bob:homeserver.dummy", + }, + }; + } + + throw new Error("Test impl doesn't know about this request"); + }); + + const lookupResult = await client.identityHashedLookup([["bob@email.dummy", "email"]], ID_ACCESS_TOKEN); + + expect(client.http.idServerRequest).toHaveBeenCalledWith( + "GET", + "/hash_details", + undefined, + "/_matrix/identity/v2", + ID_ACCESS_TOKEN, + ); + + expect(client.http.idServerRequest).toHaveBeenCalledWith( + "POST", + "/lookup", + { pepper: "carrot", algorithm: "sha256", addresses: ["WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU"] }, + "/_matrix/identity/v2", + ID_ACCESS_TOKEN, + ); + + expect(lookupResult).toHaveLength(1); + expect(lookupResult[0]).toEqual({ address: "bob@email.dummy", mxid: "@bob:homeserver.dummy" }); + }); + }); }); diff --git a/src/client.ts b/src/client.ts index ae80589e97c..79c2c2c807c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -227,6 +227,7 @@ import { KnownMembership, Membership } from "./@types/membership"; import { RoomMessageEventContent, StickerEventContent } from "./@types/events"; import { ImageInfo } from "./@types/media"; import { Capabilities, ServerCapabilities } from "./serverCapabilities"; +import { sha256Base64UrlUnpadded } from "./crypto/digest"; export type Store = IStore; @@ -9302,20 +9303,18 @@ export class MatrixClient extends TypedEventEmitter { - const addr = p[0].toLowerCase(); // lowercase to get consistent hashes - const med = p[1].toLowerCase(); - const hashed = olmutil - .sha256(`${addr} ${med} ${params["pepper"]}`) - .replace(/\+/g, "-") - .replace(/\//g, "_"); // URL-safe base64 - // Map the hash to a known (case-sensitive) address. We use the case - // sensitive version because the caller might be expecting that. - localMapping[hashed] = p[0]; - return hashed; - }); + params["addresses"] = await Promise.all( + addressPairs.map(async (p) => { + const addr = p[0].toLowerCase(); // lowercase to get consistent hashes + const med = p[1].toLowerCase(); + const hashed = await sha256Base64UrlUnpadded(`${addr} ${med} ${params["pepper"]}`); + + // Map the hash to a known (case-sensitive) address. We use the case + // sensitive version because the caller might be expecting that. + localMapping[hashed] = p[0]; + return hashed; + }), + ); params["algorithm"] = "sha256"; } else if (hashes["algorithms"].includes("none")) { params["addresses"] = addressPairs.map((p) => { diff --git a/src/crypto/digest.ts b/src/crypto/digest.ts new file mode 100644 index 00000000000..78c79c23b61 --- /dev/null +++ b/src/crypto/digest.ts @@ -0,0 +1,32 @@ +/* +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { encodeUnpaddedBase64Url } from "../base64"; + +/** + * @param plaintext The string to hash + * @returns Unpadded base64-url string representing the sha256 hash of the input + */ +export async function sha256Base64UrlUnpadded(plaintext: string): Promise { + if (!globalThis.crypto.subtle) { + throw new Error("No WebCrypto available"); + } + const utf8 = new TextEncoder().encode(plaintext); + + const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8); + + return encodeUnpaddedBase64Url(digest); +} diff --git a/src/oidc/authorize.ts b/src/oidc/authorize.ts index 5f437c58b6a..bc9617cad34 100644 --- a/src/oidc/authorize.ts +++ b/src/oidc/authorize.ts @@ -27,6 +27,7 @@ import { validateIdToken, validateStoredUserState, } from "./validate"; +import { sha256Base64UrlUnpadded } from "../crypto/digest"; // reexport for backwards compatibility export type { BearerTokenResponse }; @@ -61,14 +62,8 @@ const generateCodeChallenge = async (codeVerifier: string): Promise => { logger.warn("A secure context is required to generate code challenge. Using plain text code challenge"); return codeVerifier; } - const utf8 = new TextEncoder().encode(codeVerifier); - const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8); - - return btoa(String.fromCharCode(...new Uint8Array(digest))) - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); + return sha256Base64UrlUnpadded(codeVerifier); }; /** From 2d042d55c3c77963b4d3df5f40bb3358899b6d48 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Jul 2024 17:15:46 +0100 Subject: [PATCH 02/12] Test the code when crypto is available --- spec/unit/oidc/authorize.spec.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/unit/oidc/authorize.spec.ts b/spec/unit/oidc/authorize.spec.ts index 3fab8e5eef4..e786b8fb56c 100644 --- a/spec/unit/oidc/authorize.spec.ts +++ b/spec/unit/oidc/authorize.spec.ts @@ -89,11 +89,8 @@ describe("oidc authorization", () => { describe("generateAuthorizationUrl()", () => { it("should generate url with correct parameters", async () => { - // test the no crypto case here - // @ts-ignore mocking - globalThis.crypto.subtle = undefined; - const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl }); + authorizationParams.codeVerifier = "test-code-verifier"; const authUrl = new URL( await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams), ); @@ -105,6 +102,18 @@ describe("oidc authorization", () => { expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope); expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state); expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce); + expect(authUrl.searchParams.get("code_challenge")).toEqual("0FLIKahrX7kqxncwhV5WD82lu_wi5GA8FsRSLubaOpU"); + }); + + it("should log a warning if crypto is not available", async () => { + // test the no crypto case here + // @ts-ignore mocking + globalThis.crypto.subtle = undefined; + + const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl }); + const authUrl = new URL( + await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams), + ); // crypto not available, plain text code_challenge is used expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier); From fbf50d27e11f133e8c15752ce05fa92860edd34e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Jul 2024 17:58:36 +0100 Subject: [PATCH 03/12] Test case of no crypto available --- spec/unit/crypto/digest.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/unit/crypto/digest.spec.ts b/spec/unit/crypto/digest.spec.ts index 50805c325b9..5fae6f8136e 100644 --- a/spec/unit/crypto/digest.spec.ts +++ b/spec/unit/crypto/digest.spec.ts @@ -26,4 +26,14 @@ describe("sha256Base64UrlUnpadded", () => { const hash = await sha256Base64UrlUnpadded("test 🍱"); expect(hash).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw"); }); + + it("throws if webcrypto is not available", async () => { + const oldCrypto = global.crypto; + try { + global.crypto = {} as any; + await expect(sha256Base64UrlUnpadded("test")).rejects.toThrow(); + } finally { + global.crypto = oldCrypto; + } + }); }); From db2066ad8d24b63d2894d2f36b6677bb4f7c26d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 10:07:52 +0100 Subject: [PATCH 04/12] Move digest file to src to get it out of the way of the olm / e2e stuff --- spec/unit/crypto/digest.spec.ts | 2 +- src/client.ts | 2 +- src/{crypto => }/digest.ts | 0 src/oidc/authorize.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{crypto => }/digest.ts (100%) diff --git a/spec/unit/crypto/digest.spec.ts b/spec/unit/crypto/digest.spec.ts index 5fae6f8136e..9adf3d7f55d 100644 --- a/spec/unit/crypto/digest.spec.ts +++ b/spec/unit/crypto/digest.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { sha256Base64UrlUnpadded } from "../../../src/crypto/digest"; +import { sha256Base64UrlUnpadded } from "../../../src/digest"; describe("sha256Base64UrlUnpadded", () => { it("should hash a string", async () => { diff --git a/src/client.ts b/src/client.ts index 79c2c2c807c..975fead59ba 100644 --- a/src/client.ts +++ b/src/client.ts @@ -227,7 +227,7 @@ import { KnownMembership, Membership } from "./@types/membership"; import { RoomMessageEventContent, StickerEventContent } from "./@types/events"; import { ImageInfo } from "./@types/media"; import { Capabilities, ServerCapabilities } from "./serverCapabilities"; -import { sha256Base64UrlUnpadded } from "./crypto/digest"; +import { sha256Base64UrlUnpadded } from "./digest"; export type Store = IStore; diff --git a/src/crypto/digest.ts b/src/digest.ts similarity index 100% rename from src/crypto/digest.ts rename to src/digest.ts diff --git a/src/oidc/authorize.ts b/src/oidc/authorize.ts index bc9617cad34..c4993293561 100644 --- a/src/oidc/authorize.ts +++ b/src/oidc/authorize.ts @@ -27,7 +27,7 @@ import { validateIdToken, validateStoredUserState, } from "./validate"; -import { sha256Base64UrlUnpadded } from "../crypto/digest"; +import { sha256Base64UrlUnpadded } from "../digest"; // reexport for backwards compatibility export type { BearerTokenResponse }; From 9931a1572d72f6945555b246c81c951200a315db Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 10:09:12 +0100 Subject: [PATCH 05/12] Fix import --- src/digest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/digest.ts b/src/digest.ts index 78c79c23b61..8079a270dc7 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUnpaddedBase64Url } from "../base64"; +import { encodeUnpaddedBase64Url } from "./base64"; /** * @param plaintext The string to hash From 7b00707680b13d0e1ea4d114193dbb54a04ac0dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 10:10:46 +0100 Subject: [PATCH 06/12] Fix error string & doc --- src/digest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/digest.ts b/src/digest.ts index 8079a270dc7..e2d8ef5cf44 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -19,10 +19,12 @@ import { encodeUnpaddedBase64Url } from "./base64"; /** * @param plaintext The string to hash * @returns Unpadded base64-url string representing the sha256 hash of the input + * @throws If the WebCrypto API is not available, for example if the code is running + * in a web page with an insecure context (eg. served over plain HTTP). */ export async function sha256Base64UrlUnpadded(plaintext: string): Promise { if (!globalThis.crypto.subtle) { - throw new Error("No WebCrypto available"); + throw new Error("Crypto.subtle is not available: insecure context?"); } const utf8 = new TextEncoder().encode(plaintext); From e955341b004a1a14e739003d27366a256b3ecb9b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 14:53:08 +0100 Subject: [PATCH 07/12] subtle crypto, not webcrypto --- src/digest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/digest.ts b/src/digest.ts index e2d8ef5cf44..f41ec184e8f 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -19,7 +19,7 @@ import { encodeUnpaddedBase64Url } from "./base64"; /** * @param plaintext The string to hash * @returns Unpadded base64-url string representing the sha256 hash of the input - * @throws If the WebCrypto API is not available, for example if the code is running + * @throws If the subtle crypto API is not available, for example if the code is running * in a web page with an insecure context (eg. served over plain HTTP). */ export async function sha256Base64UrlUnpadded(plaintext: string): Promise { From 2e703e14e2c05d91d0a1a36871b5f1c5c9fba9b1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 14:57:26 +0100 Subject: [PATCH 08/12] Extract the base64 part --- src/client.ts | 7 ++++--- src/digest.ts | 8 +++----- src/oidc/authorize.ts | 6 ++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/client.ts b/src/client.ts index 975fead59ba..3c669b71a81 100644 --- a/src/client.ts +++ b/src/client.ts @@ -47,7 +47,7 @@ import { Direction, EventTimeline } from "./models/event-timeline"; import { IActionsObject, PushProcessor } from "./pushprocessor"; import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery"; import * as olmlib from "./crypto/olmlib"; -import { decodeBase64, encodeBase64 } from "./base64"; +import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64"; import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice"; import { IOlmDevice } from "./crypto/algorithms/megolm"; import { TypedReEmitter } from "./ReEmitter"; @@ -227,7 +227,7 @@ import { KnownMembership, Membership } from "./@types/membership"; import { RoomMessageEventContent, StickerEventContent } from "./@types/events"; import { ImageInfo } from "./@types/media"; import { Capabilities, ServerCapabilities } from "./serverCapabilities"; -import { sha256Base64UrlUnpadded } from "./digest"; +import { sha256 } from "./digest"; export type Store = IStore; @@ -9307,7 +9307,8 @@ export class MatrixClient extends TypedEventEmitter { const addr = p[0].toLowerCase(); // lowercase to get consistent hashes const med = p[1].toLowerCase(); - const hashed = await sha256Base64UrlUnpadded(`${addr} ${med} ${params["pepper"]}`); + const hashBuffer = await sha256(`${addr} ${med} ${params["pepper"]}`); + const hashed = encodeUnpaddedBase64Url(hashBuffer); // Map the hash to a known (case-sensitive) address. We use the case // sensitive version because the caller might be expecting that. diff --git a/src/digest.ts b/src/digest.ts index f41ec184e8f..7503be8f8f2 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUnpaddedBase64Url } from "./base64"; - /** * @param plaintext The string to hash - * @returns Unpadded base64-url string representing the sha256 hash of the input + * @returns An ArrayBuffer containing the SHA-256 hash of the input string * @throws If the subtle crypto API is not available, for example if the code is running * in a web page with an insecure context (eg. served over plain HTTP). */ -export async function sha256Base64UrlUnpadded(plaintext: string): Promise { +export async function sha256(plaintext: string): Promise { if (!globalThis.crypto.subtle) { throw new Error("Crypto.subtle is not available: insecure context?"); } @@ -30,5 +28,5 @@ export async function sha256Base64UrlUnpadded(plaintext: string): Promise => { return codeVerifier; } - return sha256Base64UrlUnpadded(codeVerifier); + const hashBuffer = await sha256(codeVerifier); + return encodeUnpaddedBase64Url(hashBuffer); }; /** From f5c1695bbf289f3038b98a48f4038d0fcaf53f4a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 15:00:26 +0100 Subject: [PATCH 09/12] Fix test --- spec/unit/crypto/digest.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/unit/crypto/digest.spec.ts b/spec/unit/crypto/digest.spec.ts index 9adf3d7f55d..447fde0dce2 100644 --- a/spec/unit/crypto/digest.spec.ts +++ b/spec/unit/crypto/digest.spec.ts @@ -14,24 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { sha256Base64UrlUnpadded } from "../../../src/digest"; +import { encodeUnpaddedBase64Url } from "../../../src"; +import { sha256 } from "../../../src/digest"; -describe("sha256Base64UrlUnpadded", () => { +describe("sha256", () => { it("should hash a string", async () => { - const hash = await sha256Base64UrlUnpadded("test"); - expect(hash).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg"); + const hash = await sha256("test"); + expect(encodeUnpaddedBase64Url(hash)).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg"); }); it("should hash a string with emoji", async () => { - const hash = await sha256Base64UrlUnpadded("test 🍱"); - expect(hash).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw"); + const hash = await sha256("test 🍱"); + expect(encodeUnpaddedBase64Url(hash)).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw"); }); it("throws if webcrypto is not available", async () => { const oldCrypto = global.crypto; try { global.crypto = {} as any; - await expect(sha256Base64UrlUnpadded("test")).rejects.toThrow(); + await expect(sha256("test")).rejects.toThrow(); } finally { global.crypto = oldCrypto; } From bd0bb05c589fe9496c668a71a32b3eb3bc98b9ea Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 17:08:59 +0100 Subject: [PATCH 10/12] Move test file too --- spec/unit/{crypto => }/digest.spec.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/unit/{crypto => }/digest.spec.ts (100%) diff --git a/spec/unit/crypto/digest.spec.ts b/spec/unit/digest.spec.ts similarity index 100% rename from spec/unit/crypto/digest.spec.ts rename to spec/unit/digest.spec.ts From 2e7fb0d66d7d90019dd1becbb7b637554c30b65a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 17:12:42 +0100 Subject: [PATCH 11/12] Add more doc --- src/digest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/digest.ts b/src/digest.ts index 7503be8f8f2..85b5be9643b 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -15,6 +15,8 @@ limitations under the License. */ /** + * Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer. + * * @param plaintext The string to hash * @returns An ArrayBuffer containing the SHA-256 hash of the input string * @throws If the subtle crypto API is not available, for example if the code is running From 758461331eb87a1c8bc61b38caae6a6436830a0b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Jul 2024 17:50:26 +0100 Subject: [PATCH 12/12] Fix imports --- spec/unit/digest.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/digest.spec.ts b/spec/unit/digest.spec.ts index 447fde0dce2..e129cf85070 100644 --- a/spec/unit/digest.spec.ts +++ b/spec/unit/digest.spec.ts @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUnpaddedBase64Url } from "../../../src"; -import { sha256 } from "../../../src/digest"; +import { encodeUnpaddedBase64Url } from "../../src"; +import { sha256 } from "../../src/digest"; describe("sha256", () => { it("should hash a string", async () => {