diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 229383567..059d928bc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,6 +36,8 @@ jobs: # - os: macos-latest # node-version: "20" # edgedb-version: "stable" + env: + NODE_OPTIONS: ${{ matrix.node-version == '18' && '--experimental-global-webcrypto' || '' }} steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 1285f7a47..d2d263ec3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ writing some simple queries. ### Requirements - Node.js 18+ + - The driver requires the Browser API `crypto` to be available as a global. + Therefore, for Node versions before 19, you _must_ run `node` with the + special command line flag `--experimental-global-webcrypto`. - For TypeScript users: - TypeScript 4.4+ is required - `yarn add @types/node --dev` diff --git a/compileForDeno.ts b/compileForDeno.ts index dfee239ed..4335f5563 100644 --- a/compileForDeno.ts +++ b/compileForDeno.ts @@ -135,7 +135,7 @@ export async function run({ let resolvedImportPath = resolveImportPath(importPath, sourcePath); - for (const name of ["adapter", "adapter.shared", "adapter.crypto"]) { + for (const name of ["adapter", "adapter.shared"]) { if (resolvedImportPath.endsWith(`/${name}.node.ts`)) { resolvedImportPath = resolvedImportPath.replace( `/${name}.node.ts`, diff --git a/integration-tests/legacy/package.json b/integration-tests/legacy/package.json index eaff740e4..816ca74b8 100644 --- a/integration-tests/legacy/package.json +++ b/integration-tests/legacy/package.json @@ -6,7 +6,7 @@ "typecheck": "echo 'Integration tests, skipping typecheck...'", "generate": "../../packages/generate/dist/cli.js", "build": "echo 'Integration tests, no build output...'", - "test": "NODE_OPTIONS=\"--experimental-vm-modules\" yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", + "test": "yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", "test:ci": "tsx ./testRunner.ts" }, "devDependencies": { diff --git a/integration-tests/lts/package.json b/integration-tests/lts/package.json index bdf6368d8..3478802d7 100644 --- a/integration-tests/lts/package.json +++ b/integration-tests/lts/package.json @@ -7,7 +7,7 @@ "generate": "../../packages/generate/dist/cli.js", "build": "echo 'Integration tests, no build output...'", "test": "yarn test:ts && yarn test:non_ts", - "test:ts": "NODE_OPTIONS=\"--experimental-vm-modules\" pwd && yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit", + "test:ts": "pwd && yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit", "test:non_ts": "yarn test:esm && yarn test:cjs && yarn test:mts && yarn test:deno", "test:esm": "yarn generate queries --target esm --file esm/queries && yarn generate edgeql-js --target esm --output-dir esm/edgeql-js && cd esm && node test.js", "test:cjs": "yarn generate queries --target cjs --file cjs/queries && yarn generate edgeql-js --target cjs --output-dir cjs/edgeql-js && cd cjs && node test.js", diff --git a/integration-tests/nightly/package.json b/integration-tests/nightly/package.json index 3d66b27e3..de6e694b9 100644 --- a/integration-tests/nightly/package.json +++ b/integration-tests/nightly/package.json @@ -6,7 +6,7 @@ "typecheck": "echo 'Integration tests, skipping typecheck...'", "generate": "../../packages/generate/dist/cli.js", "build": "echo 'Integration tests, no build output...'", - "test": "NODE_OPTIONS=\"--experimental-vm-modules\" yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", + "test": "yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", "test:ci": "tsx ./testRunner.ts" }, "devDependencies": { diff --git a/integration-tests/stable/package.json b/integration-tests/stable/package.json index 33c2d95dc..6815e9b66 100644 --- a/integration-tests/stable/package.json +++ b/integration-tests/stable/package.json @@ -6,7 +6,7 @@ "typecheck": "echo 'Integration tests, skipping typecheck...'", "generate": "../../packages/generate/dist/cli.js", "build": "echo 'Integration tests, no build output...'", - "test": "NODE_OPTIONS=\"--experimental-vm-modules\" yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", + "test": "yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit --passWithNoTests", "test:ci": "tsx ./testRunner.ts" }, "devDependencies": { diff --git a/packages/ai/src/core.ts b/packages/ai/src/core.ts index d85612efa..938c6d402 100644 --- a/packages/ai/src/core.ts +++ b/packages/ai/src/core.ts @@ -1,7 +1,6 @@ import type { Client } from "edgedb"; import type { ResolvedConnectConfig } from "edgedb/dist/conUtils.js"; -import { getHTTPSCRAMAuth } from "edgedb/dist/httpScram.js"; -import cryptoUtils from "edgedb/dist/adapter.crypto.node.js"; +import { HTTPSCRAMAuth } from "edgedb/dist/httpScram.js"; import { getAuthenticatedFetch, type AuthenticatedFetch, @@ -13,8 +12,6 @@ export function createAI(client: Client, options: AIOptions) { return new EdgeDBAI(client, options); } -const httpSCRAMAuth = getHTTPSCRAMAuth(cryptoUtils.default); - export class EdgeDBAI { /** @internal */ private readonly authenticatedFetch: Promise; @@ -41,7 +38,7 @@ export class EdgeDBAI { await (client as any).pool._getNormalizedConnectConfig() ).connectionParams; - return getAuthenticatedFetch(connectConfig, httpSCRAMAuth, "ext/ai/"); + return getAuthenticatedFetch(connectConfig, "ext/ai/"); } withConfig(options: Partial) { diff --git a/packages/driver/src/adapter.crypto.deno.ts b/packages/driver/src/adapter.crypto.deno.ts deleted file mode 100644 index f73da558e..000000000 --- a/packages/driver/src/adapter.crypto.deno.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; diff --git a/packages/driver/src/adapter.crypto.node.ts b/packages/driver/src/adapter.crypto.node.ts deleted file mode 100644 index 42e24c05f..000000000 --- a/packages/driver/src/adapter.crypto.node.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { CryptoUtils } from "./utils"; - -let cryptoUtils: CryptoUtils; - -if (typeof crypto === "undefined") { - // tslint:disable-next-line:no-var-requires - 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(); - }, - }; -} else { - // tslint:disable-next-line:no-var-requires - cryptoUtils = require("./browserCrypto").default; -} - -export default cryptoUtils; diff --git a/packages/driver/src/browserClient.ts b/packages/driver/src/browserClient.ts index 348c881b2..d2a391bd7 100644 --- a/packages/driver/src/browserClient.ts +++ b/packages/driver/src/browserClient.ts @@ -1,17 +1,15 @@ import { BaseClientPool, Client, type ConnectOptions } from "./baseClient"; import { getConnectArgumentsParser } from "./conUtils"; -import cryptoUtils from "./browserCrypto"; import { EdgeDBError } from "./errors"; import { FetchConnection } from "./fetchConn"; -import { getHTTPSCRAMAuth } from "./httpScram"; import { Options } from "./options"; const parseConnectArguments = getConnectArgumentsParser(null); -const httpSCRAMAuth = getHTTPSCRAMAuth(cryptoUtils); class FetchClientPool extends BaseClientPool { isStateless = true; - _connectWithTimeout = FetchConnection.createConnectWithTimeout(httpSCRAMAuth); + _connectWithTimeout = + FetchConnection.connectWithTimeout.bind(FetchConnection); } export function createClient(): Client { diff --git a/packages/driver/src/browserCrypto.ts b/packages/driver/src/browserCrypto.ts index 5570947c6..ecb023491 100644 --- a/packages/driver/src/browserCrypto.ts +++ b/packages/driver/src/browserCrypto.ts @@ -1,32 +1,29 @@ -import type { CryptoUtils } from "./utils"; +export async function randomBytes(size: number): Promise { + return crypto.getRandomValues(new Uint8Array(size)); +} -const cryptoUtils: CryptoUtils = { - async randomBytes(size: number): Promise { - return crypto.getRandomValues(new Uint8Array(size)); - }, +export async function H(msg: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", msg)); +} - 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 async function 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 + ) + ); +} diff --git a/packages/driver/src/fetchConn.ts b/packages/driver/src/fetchConn.ts index 3d7b90293..93b8de1b0 100644 --- a/packages/driver/src/fetchConn.ts +++ b/packages/driver/src/fetchConn.ts @@ -27,7 +27,6 @@ import type { ICodec } from "./codecs/ifaces"; import type { CodecsRegistry } from "./codecs/registry"; import type { NormalizedConnectConfig } from "./conUtils"; import { InternalClientError, ProtocolError } from "./errors"; -import type { HttpSCRAMAuth } from "./httpScram"; import { Cardinality, OutputFormat, @@ -205,22 +204,17 @@ export class AdminUIFetchConnection extends BaseFetchConnection { } export class FetchConnection extends BaseFetchConnection { - static createConnectWithTimeout(httpSCRAMAuth: HttpSCRAMAuth) { - return async function connectWithTimeout( - config: NormalizedConnectConfig, - registry: CodecsRegistry - ) { - const fetch = await getAuthenticatedFetch( - config.connectionParams, - httpSCRAMAuth - ); + static async connectWithTimeout( + config: NormalizedConnectConfig, + registry: CodecsRegistry + ) { + const fetch = await getAuthenticatedFetch(config.connectionParams); - const conn = new FetchConnection(fetch, registry); + const conn = new FetchConnection(fetch, registry); - conn.connected = true; - conn.connWaiter.set(); + conn.connected = true; + conn.connWaiter.set(); - return conn; - }; + return conn; } } diff --git a/packages/driver/src/httpScram.ts b/packages/driver/src/httpScram.ts index 05423be30..ce94de728 100644 --- a/packages/driver/src/httpScram.ts +++ b/packages/driver/src/httpScram.ts @@ -5,126 +5,112 @@ import { utf8Decoder, utf8Encoder, } from "./primitives/buffer"; -import { getSCRAM } from "./scram"; -import { CryptoUtils } from "./utils"; +import { + bufferEquals, + generateNonce, + buildClientFinalMessage, + buildClientFirstMessage, + parseServerFirstMessage, + parseServerFinalMessage, +} from "./scram"; const AUTH_ENDPOINT = "/auth/token"; -export type HttpSCRAMAuth = ( +export async function HTTPSCRAMAuth( baseUrl: string, username: string, password: string -) => Promise; - -export function getHTTPSCRAMAuth(cryptoUtils: CryptoUtils): HttpSCRAMAuth { - const { - bufferEquals, - generateNonce, - buildClientFirstMessage, - buildClientFinalMessage, - parseServerFirstMessage, - parseServerFinalMessage, - } = getSCRAM(cryptoUtils); - - return async function HTTPSCRAMAuth( - baseUrl: string, - username: string, - password: string - ): Promise { - const authUrl = baseUrl + AUTH_ENDPOINT; - const clientNonce = await generateNonce(); - const [clientFirst, clientFirstBare] = buildClientFirstMessage( - clientNonce, - username +): Promise { + const authUrl = baseUrl + AUTH_ENDPOINT; + const clientNonce = await generateNonce(); + const [clientFirst, clientFirstBare] = buildClientFirstMessage( + clientNonce, + username + ); + + const serverFirstRes = await fetch(authUrl, { + headers: { + Authorization: `SCRAM-SHA-256 data=${utf8ToB64(clientFirst)}`, + }, + }); + + // The first request must have status 401 Unauthorized and provide a + // WWW-Authenticate header with a SCRAM-SHA-256 challenge. + // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L153-L157 + const authenticateHeader = serverFirstRes.headers.get("WWW-Authenticate"); + if (serverFirstRes.status !== 401 || !authenticateHeader) { + const body = await serverFirstRes.text(); + throw new ProtocolError(`authentication failed: ${body}`); + } + + // WWW-Authenticate can contain multiple comma-separated authentication + // schemes (each with own comma-separated parameter pairs), but we only support + // one SCRAM-SHA-256 challenge, e.g., `SCRAM-SHA-256 sid=..., data=...`. + if (!authenticateHeader.startsWith("SCRAM-SHA-256")) { + throw new ProtocolError( + `unsupported authentication scheme: ${authenticateHeader}` + ); + } + + // The server may respond with a 401 Unauthorized and `WWW-Authenticate: SCRAM-SHA-256` with + // no parameters if authentication fails, e.g., due to an incorrect username. + // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L112-L120 + const authParams = authenticateHeader.split(/ (.+)?/, 2)[1] ?? ""; + if (authParams.length === 0) { + const body = await serverFirstRes.text(); + throw new ProtocolError(`authentication failed: ${body}`); + } + + const { sid, data: serverFirst } = parseScramAttrs(authParams); + if (!sid || !serverFirst) { + throw new ProtocolError( + `authentication challenge missing attributes: expected "sid" and "data", got '${authParams}'` ); + } + + const [serverNonce, salt, iterCount] = parseServerFirstMessage(serverFirst); + const [clientFinal, expectedServerSig] = await buildClientFinalMessage( + password, + salt, + iterCount, + clientFirstBare, + serverFirst, + serverNonce + ); - const serverFirstRes = await fetch(authUrl, { - headers: { - Authorization: `SCRAM-SHA-256 data=${utf8ToB64(clientFirst)}`, - }, - }); - - // The first request must have status 401 Unauthorized and provide a - // WWW-Authenticate header with a SCRAM-SHA-256 challenge. - // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L153-L157 - const authenticateHeader = serverFirstRes.headers.get("WWW-Authenticate"); - if (serverFirstRes.status !== 401 || !authenticateHeader) { - const body = await serverFirstRes.text(); - throw new ProtocolError(`authentication failed: ${body}`); - } - - // WWW-Authenticate can contain multiple comma-separated authentication - // schemes (each with own comma-separated parameter pairs), but we only support - // one SCRAM-SHA-256 challenge, e.g., `SCRAM-SHA-256 sid=..., data=...`. - if (!authenticateHeader.startsWith("SCRAM-SHA-256")) { - throw new ProtocolError( - `unsupported authentication scheme: ${authenticateHeader}` - ); - } - - // The server may respond with a 401 Unauthorized and `WWW-Authenticate: SCRAM-SHA-256` with - // no parameters if authentication fails, e.g., due to an incorrect username. - // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L112-L120 - const authParams = authenticateHeader.split(/ (.+)?/, 2)[1] ?? ""; - if (authParams.length === 0) { - const body = await serverFirstRes.text(); - throw new ProtocolError(`authentication failed: ${body}`); - } - - const { sid, data: serverFirst } = parseScramAttrs(authParams); - if (!sid || !serverFirst) { - throw new ProtocolError( - `authentication challenge missing attributes: expected "sid" and "data", got '${authParams}'` - ); - } - - const [serverNonce, salt, iterCount] = parseServerFirstMessage(serverFirst); - const [clientFinal, expectedServerSig] = await buildClientFinalMessage( - password, - salt, - iterCount, - clientFirstBare, - serverFirst, - serverNonce + const serverFinalRes = await fetch(authUrl, { + headers: { + Authorization: `SCRAM-SHA-256 sid=${sid}, data=${utf8ToB64(clientFinal)}`, + }, + }); + + // The second request is successful if the server responds with a 200 and an + // Authentication-Info header (see https://datatracker.ietf.org/doc/html/rfc7615#section-3). + // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L252-L254 + const authInfoHeader = serverFinalRes.headers.get("Authentication-Info"); + if (!serverFinalRes.ok || !authInfoHeader) { + const body = await serverFinalRes.text(); + throw new ProtocolError(`authentication failed: ${body}`); + } + + const { data: serverFinal, sid: sidFinal } = parseScramAttrs(authInfoHeader); + if (!sidFinal || !serverFinal) { + throw new ProtocolError( + `authentication info missing attributes: expected "sid" and "data", got '${authInfoHeader}'` ); + } + + if (sidFinal !== sid) { + throw new ProtocolError("SCRAM session id does not match"); + } + + const serverSig = parseServerFinalMessage(serverFinal); + if (!bufferEquals(serverSig, expectedServerSig)) { + throw new ProtocolError("server SCRAM proof does not match"); + } - const serverFinalRes = await fetch(authUrl, { - headers: { - Authorization: `SCRAM-SHA-256 sid=${sid}, data=${utf8ToB64( - clientFinal - )}`, - }, - }); - - // The second request is successful if the server responds with a 200 and an - // Authentication-Info header (see https://datatracker.ietf.org/doc/html/rfc7615#section-3). - // See: https://github.com/edgedb/edgedb/blob/09782afd3b759440abbb1b26ee19b6589be04275/edb/server/protocol/auth/scram.py#L252-L254 - const authInfoHeader = serverFinalRes.headers.get("Authentication-Info"); - if (!serverFinalRes.ok || !authInfoHeader) { - const body = await serverFinalRes.text(); - throw new ProtocolError(`authentication failed: ${body}`); - } - - const { data: serverFinal, sid: sidFinal } = - parseScramAttrs(authInfoHeader); - if (!sidFinal || !serverFinal) { - throw new ProtocolError( - `authentication info missing attributes: expected "sid" and "data", got '${authInfoHeader}'` - ); - } - - if (sidFinal !== sid) { - throw new ProtocolError("SCRAM session id does not match"); - } - - const serverSig = parseServerFinalMessage(serverFinal); - if (!bufferEquals(serverSig, expectedServerSig)) { - throw new ProtocolError("server SCRAM proof does not match"); - } - - const authToken = await serverFinalRes.text(); - return authToken; - }; + const authToken = await serverFinalRes.text(); + return authToken; } function utf8ToB64(str: string): string { diff --git a/packages/driver/src/nodeClient.ts b/packages/driver/src/nodeClient.ts index f9c63871d..40cc7dad7 100644 --- a/packages/driver/src/nodeClient.ts +++ b/packages/driver/src/nodeClient.ts @@ -1,10 +1,8 @@ import { BaseClientPool, Client, type ConnectOptions } from "./baseClient"; import { parseConnectArguments } from "./conUtils.server"; -import cryptoUtils from "./adapter.crypto.node"; import { Options } from "./options"; import { RawConnection } from "./rawConn"; import { FetchConnection } from "./fetchConn"; -import { getHTTPSCRAMAuth } from "./httpScram"; class ClientPool extends BaseClientPool { isStateless = false; @@ -21,11 +19,10 @@ export function createClient(options?: string | ConnectOptions | null): Client { ); } -const httpSCRAMAuth = getHTTPSCRAMAuth(cryptoUtils); - class FetchClientPool extends BaseClientPool { isStateless = true; - _connectWithTimeout = FetchConnection.createConnectWithTimeout(httpSCRAMAuth); + _connectWithTimeout = + FetchConnection.connectWithTimeout.bind(FetchConnection); } export function createHttpClient( diff --git a/packages/driver/src/rawConn.ts b/packages/driver/src/rawConn.ts index 7b8b71d01..12de92a8d 100644 --- a/packages/driver/src/rawConn.ts +++ b/packages/driver/src/rawConn.ts @@ -29,9 +29,8 @@ import { WriteMessageBuffer } from "./primitives/buffer"; import Event from "./primitives/event"; import type char from "./primitives/chars"; import * as chars from "./primitives/chars"; -import { getSCRAM } from "./scram"; +import * as scram from "./scram"; import * as errors from "./errors"; -import cryptoUtils from "./adapter.crypto.node"; enum AuthenticationStatuses { AUTH_OK = 0, @@ -40,8 +39,6 @@ enum AuthenticationStatuses { AUTH_SASL_FINAL = 12, } -const scram = getSCRAM(cryptoUtils); - const _tlsOptions = new WeakMap(); function getTlsOptions(config: ResolvedConnectConfig): tls.ConnectionOptions { if (_tlsOptions.has(config)) { diff --git a/packages/driver/src/scram.ts b/packages/driver/src/scram.ts index 7de0f8303..d7d0c165a 100644 --- a/packages/driver/src/scram.ts +++ b/packages/driver/src/scram.ts @@ -18,7 +18,7 @@ import { utf8Encoder, encodeB64, decodeB64 } from "./primitives/buffer"; import { ProtocolError } from "./errors"; -import type { CryptoUtils } from "./utils"; +import { randomBytes, H, HMAC } from "./browserCrypto"; const RAW_NONCE_LENGTH = 18; @@ -32,176 +32,161 @@ export function saslprep(str: string): string { return str.normalize("NFKC"); } -export function getSCRAM({ randomBytes, H, HMAC }: CryptoUtils) { - function bufferEquals(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { +export function bufferEquals(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0, len = a.length; i < len; i++) { + if (a[i] !== b[i]) { return false; } - for (let i = 0, len = a.length; i < len; i++) { - if (a[i] !== b[i]) { - return false; - } - } - return true; } + return true; +} - function generateNonce( - length: number = RAW_NONCE_LENGTH - ): Promise { - return randomBytes(length); +export function generateNonce( + length: number = RAW_NONCE_LENGTH +): Promise { + return randomBytes(length); +} + +export function buildClientFirstMessage( + clientNonce: Uint8Array, + username: string +): [string, string] { + const bare = `n=${saslprep(username)},r=${encodeB64(clientNonce)}`; + return [`n,,${bare}`, bare]; +} + +export function parseServerFirstMessage( + msg: string +): [Uint8Array, Uint8Array, number] { + const attrs = msg.split(","); + + if (attrs.length < 3) { + throw new ProtocolError("malformed SCRAM message"); } - function buildClientFirstMessage( - clientNonce: Uint8Array, - username: string - ): [string, string] { - const bare = `n=${saslprep(username)},r=${encodeB64(clientNonce)}`; - return [`n,,${bare}`, bare]; + const nonceAttr = attrs[0]; + if (!nonceAttr || nonceAttr[0] !== "r") { + throw new ProtocolError("malformed SCRAM message"); + } + const nonceB64 = nonceAttr.split("=", 2)[1]; + if (!nonceB64) { + throw new ProtocolError("malformed SCRAM message"); } + const nonce = decodeB64(nonceB64); - function parseServerFirstMessage( - msg: string - ): [Uint8Array, Uint8Array, number] { - const attrs = msg.split(","); + const saltAttr = attrs[1]; + if (!saltAttr || saltAttr[0] !== "s") { + throw new ProtocolError("malformed SCRAM message"); + } + const saltB64 = saltAttr.split("=", 2)[1]; + if (!saltB64) { + throw new ProtocolError("malformed SCRAM message"); + } + const salt = decodeB64(saltB64); - if (attrs.length < 3) { - throw new ProtocolError("malformed SCRAM message"); - } + const iterAttr = attrs[2]; + if (!iterAttr || iterAttr[0] !== "i") { + throw new ProtocolError("malformed SCRAM message"); + } + const iter = iterAttr.split("=", 2)[1]; + if (!iter || !iter.match(/^[0-9]*$/)) { + throw new ProtocolError("malformed SCRAM message"); + } + const iterCount = parseInt(iter, 10); + if (iterCount <= 0) { + throw new ProtocolError("malformed SCRAM message"); + } - const nonceAttr = attrs[0]; - if (!nonceAttr || nonceAttr[0] !== "r") { - throw new ProtocolError("malformed SCRAM message"); - } - const nonceB64 = nonceAttr.split("=", 2)[1]; - if (!nonceB64) { - throw new ProtocolError("malformed SCRAM message"); - } - const nonce = decodeB64(nonceB64); + return [nonce, salt, iterCount]; +} - const saltAttr = attrs[1]; - if (!saltAttr || saltAttr[0] !== "s") { - throw new ProtocolError("malformed SCRAM message"); - } - const saltB64 = saltAttr.split("=", 2)[1]; - if (!saltB64) { - throw new ProtocolError("malformed SCRAM message"); - } - const salt = decodeB64(saltB64); +export function parseServerFinalMessage(msg: string): Uint8Array { + const attrs = msg.split(","); - const iterAttr = attrs[2]; - if (!iterAttr || iterAttr[0] !== "i") { - throw new ProtocolError("malformed SCRAM message"); - } - const iter = iterAttr.split("=", 2)[1]; - if (!iter || !iter.match(/^[0-9]*$/)) { - throw new ProtocolError("malformed SCRAM message"); - } - const iterCount = parseInt(iter, 10); - if (iterCount <= 0) { - throw new ProtocolError("malformed SCRAM message"); - } + if (attrs.length < 1) { + throw new ProtocolError("malformed SCRAM message"); + } - return [nonce, salt, iterCount]; + const nonceAttr = attrs[0]; + if (!nonceAttr || nonceAttr[0] !== "v") { + throw new ProtocolError("malformed SCRAM message"); } + const signatureB64 = nonceAttr.split("=", 2)[1]; + if (!signatureB64) { + throw new ProtocolError("malformed SCRAM message"); + } + return decodeB64(signatureB64); +} - function parseServerFinalMessage(msg: string): Uint8Array { - const attrs = msg.split(","); +export async function buildClientFinalMessage( + password: string, + salt: Uint8Array, + iterations: number, + clientFirstBare: string, + serverFirst: string, + serverNonce: Uint8Array +): Promise<[string, Uint8Array]> { + const clientFinal = `c=biws,r=${encodeB64(serverNonce)}`; + const authMessage = utf8Encoder.encode( + `${clientFirstBare},${serverFirst},${clientFinal}` + ); + const saltedPassword = await _getSaltedPassword( + utf8Encoder.encode(saslprep(password)), + salt, + iterations + ); + const clientKey = await _getClientKey(saltedPassword); + const storedKey = await H(clientKey); + const clientSignature = await HMAC(storedKey, authMessage); + const clientProof = _XOR(clientKey, clientSignature); + + const serverKey = await _getServerKey(saltedPassword); + const serverProof = await HMAC(serverKey, authMessage); + + return [`${clientFinal},p=${encodeB64(clientProof)}`, serverProof]; +} - if (attrs.length < 1) { - throw new ProtocolError("malformed SCRAM message"); - } +export async function _getSaltedPassword( + password: Uint8Array, + salt: Uint8Array, + iterations: number +): Promise { + // U1 := HMAC(str, salt + INT(1)) - const nonceAttr = attrs[0]; - if (!nonceAttr || nonceAttr[0] !== "v") { - throw new ProtocolError("malformed SCRAM message"); - } - const signatureB64 = nonceAttr.split("=", 2)[1]; - if (!signatureB64) { - throw new ProtocolError("malformed SCRAM message"); - } - return decodeB64(signatureB64); - } - - async function buildClientFinalMessage( - password: string, - salt: Uint8Array, - iterations: number, - clientFirstBare: string, - serverFirst: string, - serverNonce: Uint8Array - ): Promise<[string, Uint8Array]> { - const clientFinal = `c=biws,r=${encodeB64(serverNonce)}`; - const authMessage = utf8Encoder.encode( - `${clientFirstBare},${serverFirst},${clientFinal}` - ); - const saltedPassword = await _getSaltedPassword( - utf8Encoder.encode(saslprep(password)), - salt, - iterations - ); - const clientKey = await _getClientKey(saltedPassword); - const storedKey = await H(clientKey); - const clientSignature = await HMAC(storedKey, authMessage); - const clientProof = _XOR(clientKey, clientSignature); - - const serverKey = await _getServerKey(saltedPassword); - const serverProof = await HMAC(serverKey, authMessage); - - return [`${clientFinal},p=${encodeB64(clientProof)}`, serverProof]; - } - - async function _getSaltedPassword( - password: Uint8Array, - salt: Uint8Array, - iterations: number - ): Promise { - // U1 := HMAC(str, salt + INT(1)) - - const msg = new Uint8Array(salt.length + 4); - msg.set(salt); - msg.set([0, 0, 0, 1], salt.length); - - let Hi = await HMAC(password, msg); - let Ui = Hi; - - for (let _ = 0; _ < iterations - 1; _++) { - Ui = await HMAC(password, Ui); - Hi = _XOR(Hi, Ui); - } + const msg = new Uint8Array(salt.length + 4); + msg.set(salt); + msg.set([0, 0, 0, 1], salt.length); - return Hi; - } + let Hi = await HMAC(password, msg); + let Ui = Hi; - function _getClientKey(saltedPassword: Uint8Array): Promise { - return HMAC(saltedPassword, utf8Encoder.encode("Client Key")); + for (let _ = 0; _ < iterations - 1; _++) { + Ui = await HMAC(password, Ui); + Hi = _XOR(Hi, Ui); } - function _getServerKey(saltedPassword: Uint8Array): Promise { - return HMAC(saltedPassword, utf8Encoder.encode("Server Key")); - } + return Hi; +} - function _XOR(a: Uint8Array, b: Uint8Array): Uint8Array { - const len = a.length; - if (len !== b.length) { - throw new ProtocolError("scram.XOR: buffers are of different lengths"); - } - const res = new Uint8Array(len); - for (let i = 0; i < len; i++) { - res[i] = a[i] ^ b[i]; - } - return res; - } - - return { - bufferEquals, - generateNonce, - buildClientFirstMessage, - parseServerFirstMessage, - parseServerFinalMessage, - buildClientFinalMessage, - _getSaltedPassword, - _getClientKey, - _getServerKey, - _XOR, - }; +export function _getClientKey(saltedPassword: Uint8Array): Promise { + return HMAC(saltedPassword, utf8Encoder.encode("Client Key")); +} + +export function _getServerKey(saltedPassword: Uint8Array): Promise { + return HMAC(saltedPassword, utf8Encoder.encode("Server Key")); +} + +export function _XOR(a: Uint8Array, b: Uint8Array): Uint8Array { + const len = a.length; + if (len !== b.length) { + throw new ProtocolError("scram.XOR: buffers are of different lengths"); + } + const res = new Uint8Array(len); + for (let i = 0; i < len; i++) { + res[i] = a[i] ^ b[i]; + } + return res; } diff --git a/packages/driver/src/utils.ts b/packages/driver/src/utils.ts index 071ef1038..09b79be3c 100644 --- a/packages/driver/src/utils.ts +++ b/packages/driver/src/utils.ts @@ -17,7 +17,7 @@ */ import type { ResolvedConnectConfig } from "./conUtils"; -import type { HttpSCRAMAuth } from "./httpScram"; +import { HTTPSCRAMAuth } from "./httpScram"; import type { ProtocolVersion } from "./ifaces"; const idCounter: { [key: string]: number } = {}; @@ -62,12 +62,6 @@ export function versionGreaterThanOrEqual( return versionGreaterThan(left, right); } -export interface CryptoUtils { - randomBytes: (size: number) => Promise; - H: (msg: Uint8Array) => Promise; - HMAC: (key: Uint8Array, msg: Uint8Array) => Promise; -} - const _tokens = new WeakMap(); export type AuthenticatedFetch = ( @@ -77,7 +71,6 @@ export type AuthenticatedFetch = ( export async function getAuthenticatedFetch( config: ResolvedConnectConfig, - httpSCRAMAuth: HttpSCRAMAuth, basePath?: string ): Promise { let token = config.secretKey ?? _tokens.get(config); @@ -89,7 +82,7 @@ export async function getAuthenticatedFetch( const databaseUrl = `${baseUrl}/db/${database}/${basePath ?? ""}`; if (!token && config.password != null) { - token = await httpSCRAMAuth(baseUrl, config.user, config.password); + token = await HTTPSCRAMAuth(baseUrl, config.user, config.password); _tokens.set(config, token); } diff --git a/packages/driver/test/client.test.ts b/packages/driver/test/client.test.ts index 8401dd219..4a7b5b958 100644 --- a/packages/driver/test/client.test.ts +++ b/packages/driver/test/client.test.ts @@ -49,8 +49,6 @@ import { isDeno, } from "./testbase"; import { PG_VECTOR_MAX_DIM } from "../src/codecs/pgvector"; -import { getHTTPSCRAMAuth } from "../src/httpScram"; -import cryptoUtils from "../src/adapter.crypto.node"; import { getAuthenticatedFetch } from "../src/utils"; function setCustomCodecs(codecs: (keyof CustomCodecSpec)[], client: Client) { @@ -2078,10 +2076,7 @@ if (!isDeno && getAvailableFeatures().has("binary-over-http")) { }); const fetchConn = AdminUIFetchConnection.create( - await getAuthenticatedFetch( - config.connectionParams, - getHTTPSCRAMAuth(cryptoUtils) - ), + await getAuthenticatedFetch(config.connectionParams), codecsRegistry ); @@ -2111,12 +2106,10 @@ if (!isDeno && getAvailableFeatures().has("binary-over-http")) { const config = await parseConnectArguments({ ...getConnectOptions(), tlsSecurity: "insecure", + secretKey: "invalid token", }); const fetchConn = AdminUIFetchConnection.create( - await getAuthenticatedFetch( - config.connectionParams, - async () => "invalid token" - ), + await getAuthenticatedFetch(config.connectionParams), codecsRegistry ); diff --git a/packages/driver/test/createClient.test.ts b/packages/driver/test/createClient.test.ts index 738bd1ac3..ded4d30ab 100644 --- a/packages/driver/test/createClient.test.ts +++ b/packages/driver/test/createClient.test.ts @@ -111,9 +111,7 @@ function timeScriptShutdown(script: string, timeout = 5_000) { test("unref idle connections", async () => { const shutdownTime = await timeScriptShutdown( - `delete global.crypto; - - const {createClient} = require('./dist/index.node'); + `const {createClient} = require('./dist/index.node'); (async () => { const client = createClient(${JSON.stringify(getConnectOptions())}); diff --git a/packages/driver/test/scram.test.ts b/packages/driver/test/scram.test.ts index e64388960..30b7d35ab 100644 --- a/packages/driver/test/scram.test.ts +++ b/packages/driver/test/scram.test.ts @@ -16,10 +16,8 @@ * limitations under the License. */ -import { getSCRAM, saslprep } from "../src/scram"; -import cryptoUtils from "../src/adapter.crypto.node"; - -const scram = getSCRAM(cryptoUtils); +import * as scram from "../src/scram"; +import { H, HMAC } from "../src/browserCrypto"; test("scram: RFC example", async () => { // Test SCRAM-SHA-256 against an example in RFC 7677 @@ -39,24 +37,21 @@ test("scram: RFC example", async () => { const authMessage = `${client_first},${server_first},${client_final}`; const saltedPassword = await scram._getSaltedPassword( - Buffer.from(saslprep(password), "utf-8"), + Buffer.from(scram.saslprep(password), "utf-8"), Buffer.from(salt, "base64"), iterations ); const clientKey = await scram._getClientKey(saltedPassword); const serverKey = await scram._getServerKey(saltedPassword); - const storedKey = await cryptoUtils.H(clientKey); + const storedKey = await H(clientKey); - const clientSignature = await cryptoUtils.HMAC( + const clientSignature = await HMAC( storedKey, Buffer.from(authMessage, "utf8") ); const clientProof = scram._XOR(clientKey, clientSignature); - const serverProof = await cryptoUtils.HMAC( - serverKey, - Buffer.from(authMessage, "utf8") - ); + const serverProof = await HMAC(serverKey, Buffer.from(authMessage, "utf8")); expect(Buffer.from(clientProof).toString("base64")).toBe( "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=" diff --git a/packages/generate/package.json b/packages/generate/package.json index 4b78e218a..14985d6cc 100644 --- a/packages/generate/package.json +++ b/packages/generate/package.json @@ -47,7 +47,7 @@ "build:fast": "npx esbuild --tsconfig=tsconfig.build.json --outdir=dist --platform=node --format=cjs src/**/*.ts src/*.ts && yarn syntax:make", "watch": "nodemon --ignore dist --ignore dbschema/edgeql-js -x ", "generate": "./dist/cli.js", - "test": "NODE_OPTIONS=\"--experimental-vm-modules\" yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit", + "test": "yarn generate edgeql-js && yarn generate queries --file && yarn generate interfaces && jest --detectOpenHandles --forceExit", "test:ci": "tsx test/testRunner.ts" } }