-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
301 additions
and
3 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,92 @@ | ||
export declare type JsonObject = { | ||
[Key in string]?: JsonValue; | ||
}; | ||
export declare type JsonArray = JsonValue[]; | ||
export declare type JsonPrimitive = string | number | boolean | null; | ||
export declare type JsonValue = JsonPrimitive | JsonObject | JsonArray; | ||
/** | ||
* Supported JWS `alg` Algorithm identifiers. | ||
* | ||
* @example PS256 CryptoKey algorithm | ||
* ```ts | ||
* interface Ps256Algorithm extends RsaHashedKeyAlgorithm { | ||
* name: 'RSA-PSS' | ||
* hash: { name: 'SHA-256' } | ||
* } | ||
* ``` | ||
* | ||
* @example CryptoKey algorithm for the `ES256` JWS Algorithm Identifier | ||
* ```ts | ||
* interface Es256Algorithm extends EcKeyAlgorithm { | ||
* name: 'ECDSA' | ||
* namedCurve: 'P-256' | ||
* } | ||
* ``` | ||
* | ||
* @example CryptoKey algorithm for the `RS256` JWS Algorithm Identifier | ||
* ```ts | ||
* interface Rs256Algorithm extends RsaHashedKeyAlgorithm { | ||
* name: 'RSASSA-PKCS1-v1_5' | ||
* hash: { name: 'SHA-256' } | ||
* } | ||
* ``` | ||
* | ||
* @example CryptoKey algorithm for the `EdDSA` JWS Algorithm Identifier (Experimental) | ||
* | ||
* Runtime support for this algorithm is very limited, it depends on the [Secure Curves in the Web | ||
* Cryptography API](https://wicg.github.io/webcrypto-secure-curves/) proposal which is yet to be | ||
* widely adopted. If the proposal changes this implementation will follow up with a minor release. | ||
* | ||
* ```ts | ||
* interface EdDSAAlgorithm extends KeyAlgorithm { | ||
* name: 'Ed25519' | ||
* } | ||
* ``` | ||
*/ | ||
export declare type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA'; | ||
export interface KeyPair extends CryptoKeyPair { | ||
/** | ||
* Private | ||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey CryptoKey} | ||
* instance to sign the DPoP Proof JWT with. | ||
* | ||
* Its algorithm must be compatible with a supported | ||
* {@link JWSAlgorithm JWS `alg` Algorithm}. | ||
*/ | ||
privateKey: CryptoKey; | ||
/** | ||
* The public key corresponding to {@link DPoPOptions.privateKey} | ||
*/ | ||
publicKey: CryptoKey; | ||
} | ||
/** | ||
* Generates a unique DPoP Proof JWT. | ||
* | ||
* @param keypair | ||
* @param htu The HTTP URI (without query and fragment parts) of the request | ||
* @param htm The HTTP method of the request | ||
* @param nonce Server-provided nonce. | ||
* @param accessToken Associated access token's value. | ||
* @param additional Any additional claims. | ||
*/ | ||
export default function DPoP(keypair: KeyPair, htu: string, htm: string, nonce?: string, accessToken?: string, additional?: Record<string, JsonValue>): Promise<string>; | ||
export interface GenerateKeyPairOptions { | ||
/** | ||
* Indicates whether or not the private key may be exported. | ||
* Default is `false`. | ||
*/ | ||
extractable?: boolean; | ||
/** | ||
* (RSA algorithms only) The length, in bits, of the RSA modulus. | ||
* Default is `2048`. | ||
*/ | ||
modulusLength?: number; | ||
} | ||
/** | ||
* Generates a | ||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair CryptoKeyPair} | ||
* for a given JWS `alg` Algorithm identifier. | ||
* | ||
* @param alg Supported JWS `alg` Algorithm identifier. | ||
*/ | ||
export declare function generateKeyPair(alg: JWSAlgorithm, options?: GenerateKeyPairOptions): Promise<CryptoKeyPair>; |
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,199 @@ | ||
const encoder = new TextEncoder(); | ||
const decoder = new TextDecoder(); | ||
function buf(input) { | ||
if (typeof input === 'string') { | ||
return encoder.encode(input); | ||
} | ||
return decoder.decode(input); | ||
} | ||
function checkRsaKeyAlgorithm(algorithm) { | ||
if (typeof algorithm.modulusLength !== 'number' || algorithm.modulusLength < 2048) { | ||
throw new OperationProcessingError(`${algorithm.name} modulusLength must be at least 2048 bits`); | ||
} | ||
} | ||
function subtleAlgorithm(key) { | ||
switch (key.algorithm.name) { | ||
case 'ECDSA': | ||
return { name: key.algorithm.name, hash: 'SHA-256' }; | ||
case 'RSA-PSS': | ||
checkRsaKeyAlgorithm(key.algorithm); | ||
return { | ||
name: key.algorithm.name, | ||
saltLength: 256 >> 3, | ||
}; | ||
case 'RSASSA-PKCS1-v1_5': | ||
checkRsaKeyAlgorithm(key.algorithm); | ||
return { name: key.algorithm.name }; | ||
case 'Ed25519': | ||
return { name: key.algorithm.name }; | ||
} | ||
throw new UnsupportedOperationError(); | ||
} | ||
async function jwt(header, claimsSet, key) { | ||
if (key.usages.includes('sign') === false) { | ||
throw new TypeError('private CryptoKey instances used for signing assertions must include "sign" in their "usages"'); | ||
} | ||
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(claimsSet)))}`; | ||
const signature = b64u(await crypto.subtle.sign(subtleAlgorithm(key), key, buf(input))); | ||
return `${input}.${signature}`; | ||
} | ||
const CHUNK_SIZE = 0x8000; | ||
function encodeBase64Url(input) { | ||
if (input instanceof ArrayBuffer) { | ||
input = new Uint8Array(input); | ||
} | ||
const arr = []; | ||
for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) { | ||
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); | ||
} | ||
return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); | ||
} | ||
function b64u(input) { | ||
return encodeBase64Url(input); | ||
} | ||
function randomBytes() { | ||
return b64u(crypto.getRandomValues(new Uint8Array(32))); | ||
} | ||
class UnsupportedOperationError extends Error { | ||
constructor(message) { | ||
super(message ?? 'operation not supported'); | ||
this.name = this.constructor.name; | ||
Error.captureStackTrace?.(this, this.constructor); | ||
} | ||
} | ||
class OperationProcessingError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
Error.captureStackTrace?.(this, this.constructor); | ||
} | ||
} | ||
function psAlg(key) { | ||
switch (key.algorithm.hash.name) { | ||
case 'SHA-256': | ||
return 'PS256'; | ||
default: | ||
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'); | ||
} | ||
} | ||
function rsAlg(key) { | ||
switch (key.algorithm.hash.name) { | ||
case 'SHA-256': | ||
return 'RS256'; | ||
default: | ||
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'); | ||
} | ||
} | ||
function esAlg(key) { | ||
switch (key.algorithm.namedCurve) { | ||
case 'P-256': | ||
return 'ES256'; | ||
default: | ||
throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve'); | ||
} | ||
} | ||
function determineJWSAlgorithm(key) { | ||
switch (key.algorithm.name) { | ||
case 'RSA-PSS': | ||
return psAlg(key); | ||
case 'RSASSA-PKCS1-v1_5': | ||
return rsAlg(key); | ||
case 'ECDSA': | ||
return esAlg(key); | ||
case 'Ed25519': | ||
return 'EdDSA'; | ||
default: | ||
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name'); | ||
} | ||
} | ||
function isCryptoKey(key) { | ||
return key instanceof CryptoKey; | ||
} | ||
function isPrivateKey(key) { | ||
return isCryptoKey(key) && key.type === 'private'; | ||
} | ||
function isPublicKey(key) { | ||
return isCryptoKey(key) && key.type === 'public'; | ||
} | ||
function epochTime() { | ||
return Math.floor(Date.now() / 1000); | ||
} | ||
export default async function DPoP(keypair, htu, htm, nonce, accessToken, additional) { | ||
const privateKey = keypair?.privateKey; | ||
const publicKey = keypair?.publicKey; | ||
if (!isPrivateKey(privateKey)) { | ||
throw new TypeError('"keypair.privateKey" must be a private CryptoKey'); | ||
} | ||
if (!isPublicKey(publicKey)) { | ||
throw new TypeError('"keypair.publicKey" must be a public CryptoKey'); | ||
} | ||
if (publicKey.extractable !== true) { | ||
throw new TypeError('"keypair.publicKey.extractable" must be true'); | ||
} | ||
if (typeof htu !== 'string') { | ||
throw new TypeError('"htu" must be a string'); | ||
} | ||
if (typeof htm !== 'string') { | ||
throw new TypeError('"htm" must be a string'); | ||
} | ||
if (nonce !== undefined && typeof nonce !== 'string') { | ||
throw new TypeError('"nonce" must be a string or undefined'); | ||
} | ||
if (accessToken !== undefined && typeof accessToken !== 'string') { | ||
throw new TypeError('"accessToken" must be a string or undefined'); | ||
} | ||
if (additional !== undefined && | ||
(typeof additional !== 'object' || additional === null || Array.isArray(additional))) { | ||
throw new TypeError('"additional" must be an object'); | ||
} | ||
return jwt({ | ||
alg: determineJWSAlgorithm(privateKey), | ||
typ: 'dpop+jwt', | ||
jwk: await publicJwk(publicKey), | ||
}, { | ||
...additional, | ||
iat: epochTime(), | ||
jti: randomBytes(), | ||
htm, | ||
nonce, | ||
htu, | ||
ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined, | ||
}, privateKey); | ||
} | ||
async function publicJwk(key) { | ||
const { kty, e, n, x, y, crv } = await crypto.subtle.exportKey('jwk', key); | ||
return { kty, crv, e, n, x, y }; | ||
} | ||
export async function generateKeyPair(alg, options) { | ||
let algorithm; | ||
if (typeof alg !== 'string' || alg.length === 0) { | ||
throw new TypeError('"alg" must be a non-empty string'); | ||
} | ||
switch (alg) { | ||
case 'PS256': | ||
algorithm = { | ||
name: 'RSA-PSS', | ||
hash: 'SHA-256', | ||
modulusLength: options?.modulusLength ?? 2048, | ||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), | ||
}; | ||
break; | ||
case 'RS256': | ||
algorithm = { | ||
name: 'RSASSA-PKCS1-v1_5', | ||
hash: 'SHA-256', | ||
modulusLength: options?.modulusLength ?? 2048, | ||
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), | ||
}; | ||
break; | ||
case 'ES256': | ||
algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; | ||
break; | ||
case 'EdDSA': | ||
algorithm = { name: 'Ed25519' }; | ||
break; | ||
default: | ||
throw new UnsupportedOperationError(); | ||
} | ||
return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify'])); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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