diff --git a/compileForDeno.ts b/compileForDeno.ts index dfee239ed..d70fc554d 100644 --- a/compileForDeno.ts +++ b/compileForDeno.ts @@ -71,7 +71,7 @@ export async function run({ file, ts.ScriptTarget.Latest, false, - ts.ScriptKind.TS + ts.ScriptKind.TS, ); const rewrittenFile: string[] = []; @@ -84,7 +84,7 @@ export async function run({ const neededImports = injectImports.reduce( (neededImports, { imports, from }) => { const usedImports = imports.filter((importName) => - parsedSource.identifiers?.has(importName) + parsedSource.identifiers?.has(importName), ); if (usedImports.length) { neededImports.push({ @@ -94,18 +94,18 @@ export async function run({ } return neededImports; }, - [] as { imports: string[]; from: string }[] + [] as { imports: string[]; from: string }[], ); if (neededImports.length) { const importDecls = neededImports.map((neededImport) => { const imports = neededImport.imports.join(", "); - // no need to resolve path if it is import from url - const importPath = neededImport.from.startsWith("https://") + // no need to resolve path if it is import from a supported protocol + const importPath = _pathUsesSupportedProtocol(neededImport.from) ? neededImport.from : resolveImportPath( relative(dirname(sourcePath), neededImport.from), - sourcePath + sourcePath, ); return `import {${imports}} from "${importPath}";`; }); @@ -139,7 +139,7 @@ export async function run({ if (resolvedImportPath.endsWith(`/${name}.node.ts`)) { resolvedImportPath = resolvedImportPath.replace( `/${name}.node.ts`, - `/${name}.deno.ts` + `/${name}.deno.ts`, ); } } @@ -153,7 +153,7 @@ export async function run({ if (/__dirname/g.test(contents)) { contents = contents.replaceAll( /__dirname/g, - "new URL('.', import.meta.url).pathname" + "new URL('.', import.meta.url).pathname", ); } @@ -175,7 +175,7 @@ export async function run({ const path = importPath.replace(rule.match, (match) => typeof rule.replace === "function" ? rule.replace(match, sourcePath) - : rule.replace + : rule.replace, ); if ( !path.endsWith(".ts") && @@ -187,6 +187,11 @@ export async function run({ } } + // Then check if importPath is already a supported protocol + if (_pathUsesSupportedProtocol(importPath)) { + return importPath; + } + // then resolve normally let resolvedPath = join(dirname(sourcePath), importPath); @@ -200,7 +205,7 @@ export async function run({ if (!sourceFilePathMap.has(resolvedPath)) { throw new Error( - `Cannot find imported file '${importPath}' in '${sourcePath}'` + `Cannot find imported file '${importPath}' in '${sourcePath}'`, ); } } @@ -208,10 +213,18 @@ export async function run({ const relImportPath = relative( dirname(sourceFilePathMap.get(sourcePath)!), - sourceFilePathMap.get(resolvedPath)! + sourceFilePathMap.get(resolvedPath)!, ); return relImportPath.startsWith("../") ? relImportPath : "./" + relImportPath; } } + +function _pathUsesSupportedProtocol(path: string) { + return ( + path.startsWith("https:") || + path.startsWith("node:") || + path.startsWith("npm:") + ); +} diff --git a/packages/driver/package.json b/packages/driver/package.json index e5b8d38bf..d9c6310cc 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -45,7 +45,7 @@ "build:cli": "tsc --project tsconfig.cli.json", "build:cjs": "tsc --project tsconfig.json", "build:deno": "deno run --unstable --allow-all ./buildDeno.ts", - "test": "npx jest --detectOpenHandles", + "test": "NODE_OPTIONS='--experimental-global-webcrypto' npx jest --detectOpenHandles", "lint": "tslint 'packages/*/src/**/*.ts'", "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", "gen-errors": "edb gen-errors-json --client | node genErrors.mjs", diff --git a/packages/driver/src/adapter.crypto.deno.ts b/packages/driver/src/adapter.crypto.deno.ts index 816439271..74f65ae73 100644 --- a/packages/driver/src/adapter.crypto.deno.ts +++ b/packages/driver/src/adapter.crypto.deno.ts @@ -1,35 +1 @@ -import { crypto } from "https://deno.land/std@0.177.0/crypto/mod.ts"; - -import type { CryptoUtils } from "./utils.ts"; - -const cryptoUtils: CryptoUtils = { - async randomBytes(size: number): Promise { - const buf = new Uint8Array(size); - return crypto.getRandomValues(buf); - }, - - async H(msg: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest("SHA-256", msg)); - }, - - async HMAC(key: Uint8Array, msg: Uint8Array): Promise { - return new Uint8Array( - await crypto.subtle.sign( - "HMAC", - await crypto.subtle.importKey( - "raw", - key, - { - name: "HMAC", - hash: { name: "SHA-256" }, - }, - false, - ["sign"], - ), - msg, - ), - ); - }, -}; - -export default cryptoUtils; +export { cryptoUtils as default } from "./browserCrypto.ts"; diff --git a/packages/driver/src/adapter.crypto.node.ts b/packages/driver/src/adapter.crypto.node.ts index 559dd84c8..17fa5f909 100644 --- a/packages/driver/src/adapter.crypto.node.ts +++ b/packages/driver/src/adapter.crypto.node.ts @@ -4,36 +4,10 @@ let cryptoUtils: CryptoUtils; if (typeof crypto === "undefined") { // eslint-disable-next-line @typescript-eslint/no-require-imports - const nodeCrypto = require("crypto"); - - cryptoUtils = { - randomBytes(size: number): Promise { - return new Promise((resolve, reject) => { - nodeCrypto.randomBytes(size, (err: Error | null, buf: Buffer) => { - if (err) { - reject(err); - } else { - resolve(buf); - } - }); - }); - }, - - async H(msg: Uint8Array): Promise { - const sign = nodeCrypto.createHash("sha256"); - sign.update(msg); - return sign.digest(); - }, - - async HMAC(key: Uint8Array, msg: Uint8Array): Promise { - const hm = nodeCrypto.createHmac("sha256", key); - hm.update(msg); - return hm.digest(); - }, - }; + cryptoUtils = require("./nodeCrypto").cryptoUtils; } else { // eslint-disable-next-line @typescript-eslint/no-require-imports - cryptoUtils = require("./browserCrypto").default; + cryptoUtils = require("./browserCrypto").cryptoUtils; } export default cryptoUtils; diff --git a/packages/driver/src/browserClient.ts b/packages/driver/src/browserClient.ts index bd0a30611..901fd7014 100644 --- a/packages/driver/src/browserClient.ts +++ b/packages/driver/src/browserClient.ts @@ -1,6 +1,6 @@ import { BaseClientPool, Client, type ConnectOptions } from "./baseClient"; import { getConnectArgumentsParser } from "./conUtils"; -import cryptoUtils from "./browserCrypto"; +import { cryptoUtils } from "./browserCrypto"; import { EdgeDBError } from "./errors"; import { FetchConnection } from "./fetchConn"; import { getHTTPSCRAMAuth } from "./httpScram"; diff --git a/packages/driver/src/browserCrypto.ts b/packages/driver/src/browserCrypto.ts index 24cf38484..279fa689c 100644 --- a/packages/driver/src/browserCrypto.ts +++ b/packages/driver/src/browserCrypto.ts @@ -1,32 +1,38 @@ import type { CryptoUtils } from "./utils"; -const cryptoUtils: CryptoUtils = { - async randomBytes(size: number): Promise { - return crypto.getRandomValues(new Uint8Array(size)); - }, +async function makeKey(key: Uint8Array): Promise { + return await crypto.subtle.importKey( + "raw", + key, + { + name: "HMAC", + hash: { name: "SHA-256" }, + }, + false, + ["sign"], + ); +} - async H(msg: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest("SHA-256", msg)); - }, +function randomBytes(size: number): Uint8Array { + return crypto.getRandomValues(new Uint8Array(size)); +} - async HMAC(key: Uint8Array, msg: Uint8Array): Promise { - return new Uint8Array( - await crypto.subtle.sign( - "HMAC", - await crypto.subtle.importKey( - "raw", - key, - { - name: "HMAC", - hash: { name: "SHA-256" }, - }, - false, - ["sign"], - ), - msg, - ), - ); - }, -}; +async function H(msg: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", msg)); +} + +async function HMAC( + key: Uint8Array | CryptoKey, + msg: Uint8Array, +): Promise { + const cryptoKey = + key instanceof Uint8Array ? ((await makeKey(key)) as CryptoKey) : key; + return new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, msg)); +} -export default cryptoUtils; +export const cryptoUtils: CryptoUtils = { + makeKey, + randomBytes, + H, + HMAC, +}; diff --git a/packages/driver/src/httpScram.ts b/packages/driver/src/httpScram.ts index d0890e417..ef87f4f29 100644 --- a/packages/driver/src/httpScram.ts +++ b/packages/driver/src/httpScram.ts @@ -32,7 +32,7 @@ export function getHTTPSCRAMAuth(cryptoUtils: CryptoUtils): HttpSCRAMAuth { password: string, ): Promise { const authUrl = baseUrl + AUTH_ENDPOINT; - const clientNonce = await generateNonce(); + const clientNonce = generateNonce(); const [clientFirst, clientFirstBare] = buildClientFirstMessage( clientNonce, username, diff --git a/packages/driver/src/nodeCrypto.ts b/packages/driver/src/nodeCrypto.ts new file mode 100644 index 000000000..93e3a145a --- /dev/null +++ b/packages/driver/src/nodeCrypto.ts @@ -0,0 +1,34 @@ +import crypto from "node:crypto"; +import type { CryptoUtils } from "./utils"; + +function makeKey(keyBytes: Uint8Array): Promise { + return Promise.resolve(keyBytes); +} + +function randomBytes(size: number): Buffer { + return crypto.randomBytes(size); +} + +async function H(msg: Uint8Array): Promise { + const sign = crypto.createHash("sha256"); + sign.update(msg); + return sign.digest(); +} + +async function HMAC( + key: Uint8Array | CryptoKey, + msg: Uint8Array, +): Promise { + const cryptoKey: Uint8Array | crypto.KeyObject = + key instanceof Uint8Array ? key : crypto.KeyObject.from(key); + const hm = crypto.createHmac("sha256", cryptoKey); + hm.update(msg); + return hm.digest(); +} + +export const cryptoUtils: CryptoUtils = { + makeKey, + randomBytes, + H, + HMAC, +}; diff --git a/packages/driver/src/rawConn.ts b/packages/driver/src/rawConn.ts index bc90fecab..2f5d95a52 100644 --- a/packages/driver/src/rawConn.ts +++ b/packages/driver/src/rawConn.ts @@ -502,7 +502,7 @@ export class RawConnection extends BaseRawConnection { ); } - const clientNonce = await scram.generateNonce(); + const clientNonce = scram.generateNonce(); const [clientFirst, clientFirstBare] = scram.buildClientFirstMessage( clientNonce, this.config.connectionParams.user, diff --git a/packages/driver/src/scram.ts b/packages/driver/src/scram.ts index d1a191a6e..61eb61985 100644 --- a/packages/driver/src/scram.ts +++ b/packages/driver/src/scram.ts @@ -32,7 +32,7 @@ export function saslprep(str: string): string { return str.normalize("NFKC"); } -export function getSCRAM({ randomBytes, H, HMAC }: CryptoUtils) { +export function getSCRAM({ randomBytes, H, HMAC, makeKey }: CryptoUtils) { function bufferEquals(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) { return false; @@ -45,9 +45,7 @@ export function getSCRAM({ randomBytes, H, HMAC }: CryptoUtils) { return true; } - function generateNonce( - length: number = RAW_NONCE_LENGTH, - ): Promise { + function generateNonce(length: number = RAW_NONCE_LENGTH): Uint8Array { return randomBytes(length); } @@ -161,11 +159,12 @@ export function getSCRAM({ randomBytes, H, HMAC }: CryptoUtils) { msg.set(salt); msg.set([0, 0, 0, 1], salt.length); - let Hi = await HMAC(password, msg); + const keyFromPassword = await makeKey(password); + let Hi = await HMAC(keyFromPassword, msg); let Ui = Hi; for (let _ = 0; _ < iterations - 1; _++) { - Ui = await HMAC(password, Ui); + Ui = await HMAC(keyFromPassword, Ui); Hi = _XOR(Hi, Ui); } diff --git a/packages/driver/src/utils.ts b/packages/driver/src/utils.ts index c0981b8f1..6bac038bb 100644 --- a/packages/driver/src/utils.ts +++ b/packages/driver/src/utils.ts @@ -63,9 +63,10 @@ export function versionGreaterThanOrEqual( } export interface CryptoUtils { - randomBytes: (size: number) => Promise; + makeKey: (key: Uint8Array) => Promise; + randomBytes: (size: number) => Uint8Array; H: (msg: Uint8Array) => Promise; - HMAC: (key: Uint8Array, msg: Uint8Array) => Promise; + HMAC: (key: Uint8Array | CryptoKey, msg: Uint8Array) => Promise; } const _tokens = new WeakMap(); diff --git a/packages/driver/test/scram.test.ts b/packages/driver/test/scram.test.ts index 7bd6187b7..15aa9fea9 100644 --- a/packages/driver/test/scram.test.ts +++ b/packages/driver/test/scram.test.ts @@ -17,11 +17,11 @@ */ import { getSCRAM, saslprep } from "../src/scram"; +import { cryptoUtils as nodeCryptoUtils } from "../src/nodeCrypto"; +import { cryptoUtils as browserCryptoUtils } from "../src/browserCrypto"; import cryptoUtils from "../src/adapter.crypto.node"; -const scram = getSCRAM(cryptoUtils); - -test("scram: RFC example", async () => { +async function generateScramWith(scram: ReturnType) { // Test SCRAM-SHA-256 against an example in RFC 7677 const username = "user"; @@ -58,6 +58,37 @@ test("scram: RFC example", async () => { Buffer.from(authMessage, "utf8"), ); + return { clientProof, serverProof }; +} + +test("scram from adapter: RFC example", async () => { + const scram = getSCRAM(cryptoUtils); + const { clientProof, serverProof } = await generateScramWith(scram); + + expect(Buffer.from(clientProof).toString("base64")).toBe( + "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", + ); + expect(Buffer.from(serverProof).toString("base64")).toBe( + "6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=", + ); +}); + +test("scram from nodeCrypto: RFC example", async () => { + const scram = getSCRAM(nodeCryptoUtils); + const { clientProof, serverProof } = await generateScramWith(scram); + + expect(Buffer.from(clientProof).toString("base64")).toBe( + "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", + ); + expect(Buffer.from(serverProof).toString("base64")).toBe( + "6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=", + ); +}); + +test("scram from browserCrypto: RFC example", async () => { + const scram = getSCRAM(browserCryptoUtils); + const { clientProof, serverProof } = await generateScramWith(scram); + expect(Buffer.from(clientProof).toString("base64")).toBe( "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", ); @@ -65,3 +96,12 @@ test("scram: RFC example", async () => { "6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=", ); }); + +test("scram is equivalent", async () => { + const scram1 = await generateScramWith(getSCRAM(cryptoUtils)); + const scram2 = await generateScramWith(getSCRAM(nodeCryptoUtils)); + const scram3 = await generateScramWith(getSCRAM(browserCryptoUtils)); + + expect(scram1).toEqual(scram2); + expect(scram1).toEqual(scram3); +});