-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use Web-compatible crypto for PKCE (#783)
- Loading branch information
1 parent
eb7fa07
commit 9309420
Showing
5 changed files
with
107 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* eslint @typescript-eslint/no-var-requires: ["off"] */ | ||
|
||
// TODO: Drop when Node 18 is EOL: 2025-04-30 | ||
if (!globalThis.crypto) { | ||
// tslint:disable-next-line: no-var-requires | ||
globalThis.crypto = require("node:crypto").webcrypto; | ||
} | ||
|
||
const BASE64_URL_CHARS = | ||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; | ||
|
||
export function bytesToBase64Url(bytes: Uint8Array): string { | ||
const len = bytes.length; | ||
let base64url = ""; | ||
|
||
for (let i = 0; i < len; i += 3) { | ||
const b1 = bytes[i] & 0xff; | ||
const b2 = i + 1 < len ? bytes[i + 1] & 0xff : 0; | ||
const b3 = i + 2 < len ? bytes[i + 2] & 0xff : 0; | ||
|
||
const enc1 = b1 >> 2; | ||
const enc2 = ((b1 & 0x03) << 4) | (b2 >> 4); | ||
const enc3 = ((b2 & 0x0f) << 2) | (b3 >> 6); | ||
const enc4 = b3 & 0x3f; | ||
|
||
base64url += BASE64_URL_CHARS.charAt(enc1) + BASE64_URL_CHARS.charAt(enc2); | ||
if (i + 1 < len) { | ||
base64url += BASE64_URL_CHARS.charAt(enc3); | ||
} | ||
if (i + 2 < len) { | ||
base64url += BASE64_URL_CHARS.charAt(enc4); | ||
} | ||
} | ||
|
||
return base64url; | ||
} | ||
|
||
export async function sha256( | ||
source: BufferSource | string | ||
): Promise<Uint8Array> { | ||
const bytes = | ||
typeof source === "string" ? new TextEncoder().encode(source) : source; | ||
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); | ||
} | ||
|
||
export function randomBytes(length: number): Uint8Array { | ||
return crypto.getRandomValues(new Uint8Array(length)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,11 @@ | ||
import crypto from "node:crypto"; | ||
import { bytesToBase64Url, sha256, randomBytes } from "./crypto"; | ||
|
||
export function createVerifierChallengePair(): { | ||
export async function createVerifierChallengePair(): Promise<{ | ||
verifier: string; | ||
challenge: string; | ||
} { | ||
const verifier = crypto.randomBytes(32).toString("base64url"); | ||
const challenge = crypto | ||
.createHash("sha256") | ||
.update(verifier) | ||
.digest("base64url"); | ||
}> { | ||
const verifier = bytesToBase64Url(randomBytes(32)); | ||
const challenge = await sha256(verifier).then(bytesToBase64Url); | ||
|
||
return { verifier, challenge }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import crypto from "node:crypto"; | ||
import { bytesToBase64Url, sha256, randomBytes } from "../src/crypto"; | ||
|
||
describe("crypto", () => { | ||
describe("bytesToBase64Url", () => { | ||
test("Equivalent to Buffer implementation", () => { | ||
for (let i = 0; i < 100; i++) { | ||
const buffer = crypto.randomBytes(32); | ||
expect(buffer.toString("base64url")).toEqual(bytesToBase64Url(buffer)); | ||
} | ||
}); | ||
}); | ||
|
||
describe("sha256", () => { | ||
test("Equivalent to Node crypto SHA256 implementation", async () => { | ||
for (let i = 0; i < 100; i++) { | ||
const buffer = crypto.randomBytes(32); | ||
expect(crypto.createHash("sha256").update(buffer).digest()).toEqual( | ||
Buffer.from(await sha256(buffer)) | ||
); | ||
} | ||
}); | ||
}); | ||
|
||
describe("randomBytes", () => { | ||
test("Generates Uint8Array of correct length", () => { | ||
const length = 32; | ||
const bytes = randomBytes(length); | ||
expect(bytes).toBeInstanceOf(Uint8Array); | ||
expect(bytes.length).toEqual(length); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters