vouchsafe
Version:
Vouchsafe Decentralized Identity and Trust Verification module
1,466 lines (1,398 loc) • 79.8 kB
JavaScript
const VOUCHSAFE_SPEC_VERSION = "1.3.0";
// crypto/index.mjs
let cryptoImpl = null;
async function loadImpl() {
if (cryptoImpl) return cryptoImpl;
const isNode =
typeof process !== 'undefined' &&
process.versions?.node &&
typeof window === 'undefined';
cryptoImpl = isNode
? await import('./node-C2JUHTDs.js')
: await import('./browser-BB_wZzmZ.js');
return cryptoImpl;
}
const generateKeyPair = async function(...args) {
const { generateKeyPair } = await loadImpl();
return generateKeyPair(...args);
};
const sha256 = async function(...args) {
const { sha256 } = await loadImpl();
return sha256(...args);
};
const sha512 = async function(...args) {
const { sha512 } = await loadImpl();
return sha512(...args);
};
const getKeyBytes = async function(...args) {
const { getKeyBytes } = await loadImpl();
return getKeyBytes(...args);
};
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
/**
* Encode a byte array into base32 (RFC 4648, lowercase, no padding)
* @param {Uint8Array} bytes
* @returns {string}
*/
function base32Encode(bytes) {
let bits = 0;
let value = 0;
let output = '';
for (let i = 0; i < bytes.length; i++) {
value = (value << 8) | bytes[i];
bits += 8;
while (bits >= 5) {
output += ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) {
output += ALPHABET[(value << (5 - bits)) & 31];
}
return output;
}
function toBase64(input) {
if (typeof input === 'string') {
// Assume it's already base64 (classic or url-safe) — return as-is
return input;
}
if (!(input instanceof Uint8Array)) {
throw new Error('Expected Uint8Array or base64 string');
}
if (typeof window === 'undefined') {
// Node.js
return Buffer.from(input).toString('base64');
} else {
// Browser
return btoa(String.fromCharCode(...input));
}
}
const SUPPORTED_HASHES = {
sha256,
sha512,
};
/**
* Create a new Vouchsafe identity
* @param {string} label - Required, lowercase a-z0-9 and hyphen
* @param {string} hashAlg - Optional: 'sha256' (default) or 'sha512'
* @returns {Promise<{ urn, publicKey, privateKey, publicKeyHash }>}
*/
async function createVouchsafeIdentity(label, hashAlg = 'sha256') {
if (!label || typeof label !== 'string' || !/^[a-zA-Z0-9\-_%\+]{1,32}$/.test(label)) {
throw new Error("Invalid label. Must be lowercase, 1–32 chars, letters/numbers/hyphens only.");
}
const hashFn = SUPPORTED_HASHES[hashAlg];
if (!hashFn) throw new Error(`Unsupported hash algorithm: ${hashAlg}`);
const {
publicKey,
privateKey
} = await generateKeyPair();
const pemPubBytes = new Uint8Array(publicKey);
const rawPubKey = await getKeyBytes('public', publicKey);
const pubBytes = new Uint8Array(rawPubKey);
const hash = new Uint8Array(await hashFn(pubBytes));
const hashB32 = base32Encode(hash).toLowerCase();
const urn = `urn:vouchsafe:${label}.${hashB32}` + (hashAlg !== 'sha256' ? `.${hashAlg}` : '');
return {
urn,
keypair: {
publicKey: toBase64(pemPubBytes),
privateKey: toBase64(new Uint8Array(privateKey)),
},
publicKeyHash: hashB32,
version: VOUCHSAFE_SPEC_VERSION
};
}
/**
* Verify a Vouchsafe URN matches a given public key
* @param {string} urn
* @param {string} publicKeyBase64
* @returns {Promise<boolean>}
*/
async function verifyUrnMatchesKey(urn, publicKeyBase64) {
const match = urn.match(/^urn:vouchsafe:([a-zA-Z0-9\-_%\+]+)\.([a-z2-7]{52})(?:\.(sha256|sha512))?$/);
if (!match) return false;
const [, , expectedHash, hashAlg = 'sha256'] = match;
const hashFn = SUPPORTED_HASHES[hashAlg];
if (!hashFn) return false;
const rawPubKey = await getKeyBytes('public', publicKeyBase64);
const pubBytes = new Uint8Array(rawPubKey);
const hash = new Uint8Array(await hashFn(pubBytes));
const actualB32 = base32Encode(hash).toLowerCase();
return actualB32 === expectedHash;
}
/**
* Create a Vouchsafe identity from an existing DER-based Ed25519 keypair
* @param {string} label - human-readable label (3–32 chars, a-zA-Z0-9-_+%)
* @param {{ publicKey: string, privateKey: string }} keypair - base64-encoded DER keys
* @param {string} hashAlg - 'sha256' (default) or 'sha512'
* @returns {Promise<{ urn, keypair, publicKeyHash, version }>}
*/
async function createVouchsafeIdentityFromKeypair(label, keypair, hashAlg = 'sha256') {
if (!label || typeof label !== 'string' || !/^[a-zA-Z0-9\-_%\+]{3,32}$/.test(label)) {
throw new Error("Invalid label. Must be 1–32 characters (a–z, 0–9, -, _, %, +).");
}
const hashFn = SUPPORTED_HASHES[hashAlg];
if (!hashFn) throw new Error(`Unsupported hash algorithm: ${hashAlg}`);
if (!keypair || typeof keypair !== 'object' || !keypair.publicKey || !keypair.privateKey) {
throw new Error("Keypair must include base64-encoded publicKey and privateKey.");
}
// ✅ Verify public key and extract raw bytes (throws if invalid)
const rawPubKey = await getKeyBytes('public', keypair.publicKey);
// ✅ Optionally verify private key format and Ed25519 algorithm
await getKeyBytes('private', keypair.privateKey); // throws if invalid
// ✅ Hash the raw public key for URN
const pubBytes = new Uint8Array(rawPubKey);
const hash = new Uint8Array(await hashFn(pubBytes));
const hashB32 = base32Encode(hash).toLowerCase();
/* const hash = new Uint8Array(await hashFn(rawPubKey));
const hashB32 = base32Encode(hash).toLowerCase();
*/
const urn = `urn:vouchsafe:${label}.${hashB32}` + (hashAlg !== 'sha256' ? `.${hashAlg}` : '');
return {
urn,
keypair: {
publicKey: keypair.publicKey, // original DER b64
privateKey: keypair.privateKey
},
publicKeyHash: hashB32,
version: VOUCHSAFE_SPEC_VERSION
};
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function concat(...buffers) {
const size = buffers.reduce((acc, { length }) => acc + length, 0);
const buf = new Uint8Array(size);
let i = 0;
for (const buffer of buffers) {
buf.set(buffer, i);
i += buffer.length;
}
return buf;
}
function encodeBase64(input) {
if (Uint8Array.prototype.toBase64) {
return input.toBase64();
}
const CHUNK_SIZE = 0x8000;
const arr = [];
for (let i = 0; i < input.length; i += CHUNK_SIZE) {
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
}
return btoa(arr.join(''));
}
function decodeBase64(encoded) {
if (Uint8Array.fromBase64) {
return Uint8Array.fromBase64(encoded);
}
const binary = atob(encoded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function decode(input) {
if (Uint8Array.fromBase64) {
return Uint8Array.fromBase64(typeof input === 'string' ? input : decoder.decode(input), {
alphabet: 'base64url',
});
}
let encoded = input;
if (encoded instanceof Uint8Array) {
encoded = decoder.decode(encoded);
}
encoded = encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
try {
return decodeBase64(encoded);
}
catch {
throw new TypeError('The input to be decoded is not correctly encoded.');
}
}
function encode(input) {
let unencoded = input;
if (typeof unencoded === 'string') {
unencoded = encoder.encode(unencoded);
}
if (Uint8Array.prototype.toBase64) {
return unencoded.toBase64({ alphabet: 'base64url', omitPadding: true });
}
return encodeBase64(unencoded).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
class JOSEError extends Error {
static code = 'ERR_JOSE_GENERIC';
code = 'ERR_JOSE_GENERIC';
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
Error.captureStackTrace?.(this, this.constructor);
}
}
class JWTClaimValidationFailed extends JOSEError {
static code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';
code = 'ERR_JWT_CLAIM_VALIDATION_FAILED';
claim;
reason;
payload;
constructor(message, payload, claim = 'unspecified', reason = 'unspecified') {
super(message, { cause: { claim, reason, payload } });
this.claim = claim;
this.reason = reason;
this.payload = payload;
}
}
class JWTExpired extends JOSEError {
static code = 'ERR_JWT_EXPIRED';
code = 'ERR_JWT_EXPIRED';
claim;
reason;
payload;
constructor(message, payload, claim = 'unspecified', reason = 'unspecified') {
super(message, { cause: { claim, reason, payload } });
this.claim = claim;
this.reason = reason;
this.payload = payload;
}
}
class JOSEAlgNotAllowed extends JOSEError {
static code = 'ERR_JOSE_ALG_NOT_ALLOWED';
code = 'ERR_JOSE_ALG_NOT_ALLOWED';
}
class JOSENotSupported extends JOSEError {
static code = 'ERR_JOSE_NOT_SUPPORTED';
code = 'ERR_JOSE_NOT_SUPPORTED';
}
class JWSInvalid extends JOSEError {
static code = 'ERR_JWS_INVALID';
code = 'ERR_JWS_INVALID';
}
class JWTInvalid extends JOSEError {
static code = 'ERR_JWT_INVALID';
code = 'ERR_JWT_INVALID';
}
class JWSSignatureVerificationFailed extends JOSEError {
static code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
code = 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED';
constructor(message = 'signature verification failed', options) {
super(message, options);
}
}
function unusable(name, prop = 'algorithm.name') {
return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
}
function isAlgorithm(algorithm, name) {
return algorithm.name === name;
}
function getHashLength(hash) {
return parseInt(hash.name.slice(4), 10);
}
function getNamedCurve$1(alg) {
switch (alg) {
case 'ES256':
return 'P-256';
case 'ES384':
return 'P-384';
case 'ES512':
return 'P-521';
default:
throw new Error('unreachable');
}
}
function checkUsage(key, usage) {
if (usage && !key.usages.includes(usage)) {
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
}
}
function checkSigCryptoKey(key, alg, usage) {
switch (alg) {
case 'HS256':
case 'HS384':
case 'HS512': {
if (!isAlgorithm(key.algorithm, 'HMAC'))
throw unusable('HMAC');
const expected = parseInt(alg.slice(2), 10);
const actual = getHashLength(key.algorithm.hash);
if (actual !== expected)
throw unusable(`SHA-${expected}`, 'algorithm.hash');
break;
}
case 'RS256':
case 'RS384':
case 'RS512': {
if (!isAlgorithm(key.algorithm, 'RSASSA-PKCS1-v1_5'))
throw unusable('RSASSA-PKCS1-v1_5');
const expected = parseInt(alg.slice(2), 10);
const actual = getHashLength(key.algorithm.hash);
if (actual !== expected)
throw unusable(`SHA-${expected}`, 'algorithm.hash');
break;
}
case 'PS256':
case 'PS384':
case 'PS512': {
if (!isAlgorithm(key.algorithm, 'RSA-PSS'))
throw unusable('RSA-PSS');
const expected = parseInt(alg.slice(2), 10);
const actual = getHashLength(key.algorithm.hash);
if (actual !== expected)
throw unusable(`SHA-${expected}`, 'algorithm.hash');
break;
}
case 'Ed25519':
case 'EdDSA': {
if (!isAlgorithm(key.algorithm, 'Ed25519'))
throw unusable('Ed25519');
break;
}
case 'ES256':
case 'ES384':
case 'ES512': {
if (!isAlgorithm(key.algorithm, 'ECDSA'))
throw unusable('ECDSA');
const expected = getNamedCurve$1(alg);
const actual = key.algorithm.namedCurve;
if (actual !== expected)
throw unusable(expected, 'algorithm.namedCurve');
break;
}
default:
throw new TypeError('CryptoKey does not support this operation');
}
checkUsage(key, usage);
}
function message(msg, actual, ...types) {
types = types.filter(Boolean);
if (types.length > 2) {
const last = types.pop();
msg += `one of type ${types.join(', ')}, or ${last}.`;
}
else if (types.length === 2) {
msg += `one of type ${types[0]} or ${types[1]}.`;
}
else {
msg += `of type ${types[0]}.`;
}
if (actual == null) {
msg += ` Received ${actual}`;
}
else if (typeof actual === 'function' && actual.name) {
msg += ` Received function ${actual.name}`;
}
else if (typeof actual === 'object' && actual != null) {
if (actual.constructor?.name) {
msg += ` Received an instance of ${actual.constructor.name}`;
}
}
return msg;
}
var invalidKeyInput = (actual, ...types) => {
return message('Key must be ', actual, ...types);
};
function withAlg(alg, actual, ...types) {
return message(`Key for the ${alg} algorithm must be `, actual, ...types);
}
function isCryptoKey(key) {
return key?.[Symbol.toStringTag] === 'CryptoKey';
}
function isKeyObject(key) {
return key?.[Symbol.toStringTag] === 'KeyObject';
}
var isKeyLike = (key) => {
return isCryptoKey(key) || isKeyObject(key);
};
var isDisjoint = (...headers) => {
const sources = headers.filter(Boolean);
if (sources.length === 0 || sources.length === 1) {
return true;
}
let acc;
for (const header of sources) {
const parameters = Object.keys(header);
if (!acc || acc.size === 0) {
acc = new Set(parameters);
continue;
}
for (const parameter of parameters) {
if (acc.has(parameter)) {
return false;
}
acc.add(parameter);
}
}
return true;
};
function isObjectLike(value) {
return typeof value === 'object' && value !== null;
}
var isObject = (input) => {
if (!isObjectLike(input) || Object.prototype.toString.call(input) !== '[object Object]') {
return false;
}
if (Object.getPrototypeOf(input) === null) {
return true;
}
let proto = input;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(input) === proto;
};
var checkKeyLength = (alg, key) => {
if (alg.startsWith('RS') || alg.startsWith('PS')) {
const { modulusLength } = key.algorithm;
if (typeof modulusLength !== 'number' || modulusLength < 2048) {
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
}
}
};
const findOid = (keyData, oid, from = 0) => {
if (from === 0) {
oid.unshift(oid.length);
oid.unshift(0x06);
}
const i = keyData.indexOf(oid[0], from);
if (i === -1)
return false;
const sub = keyData.subarray(i, i + oid.length);
if (sub.length !== oid.length)
return false;
return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1);
};
const getNamedCurve = (keyData) => {
switch (true) {
case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]):
return 'P-256';
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]):
return 'P-384';
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):
return 'P-521';
default:
return undefined;
}
};
const genericImport = async (replace, keyFormat, pem, alg, options) => {
let algorithm;
let keyUsages;
const keyData = new Uint8Array(atob(pem.replace(replace, ''))
.split('')
.map((c) => c.charCodeAt(0)));
const isPublic = keyFormat === 'spki';
switch (alg) {
case 'PS256':
case 'PS384':
case 'PS512':
algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
case 'RS256':
case 'RS384':
case 'RS512':
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
case 'RSA-OAEP':
case 'RSA-OAEP-256':
case 'RSA-OAEP-384':
case 'RSA-OAEP-512':
algorithm = {
name: 'RSA-OAEP',
hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
};
keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
break;
case 'ES256':
algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
case 'ES384':
algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
case 'ES512':
algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW': {
const namedCurve = getNamedCurve(keyData);
algorithm = namedCurve?.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: 'X25519' };
keyUsages = isPublic ? [] : ['deriveBits'];
break;
}
case 'Ed25519':
case 'EdDSA':
algorithm = { name: 'Ed25519' };
keyUsages = isPublic ? ['verify'] : ['sign'];
break;
default:
throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value');
}
return crypto.subtle.importKey(keyFormat, keyData, algorithm, (isPublic ? true : false), keyUsages);
};
const fromPKCS8 = (pem, alg, options) => {
return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, 'pkcs8', pem, alg);
};
const fromSPKI = (pem, alg, options) => {
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg);
};
function subtleMapping(jwk) {
let algorithm;
let keyUsages;
switch (jwk.kty) {
case 'RSA': {
switch (jwk.alg) {
case 'PS256':
case 'PS384':
case 'PS512':
algorithm = { name: 'RSA-PSS', hash: `SHA-${jwk.alg.slice(-3)}` };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'RS256':
case 'RS384':
case 'RS512':
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${jwk.alg.slice(-3)}` };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'RSA-OAEP':
case 'RSA-OAEP-256':
case 'RSA-OAEP-384':
case 'RSA-OAEP-512':
algorithm = {
name: 'RSA-OAEP',
hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`,
};
keyUsages = jwk.d ? ['decrypt', 'unwrapKey'] : ['encrypt', 'wrapKey'];
break;
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
}
break;
}
case 'EC': {
switch (jwk.alg) {
case 'ES256':
algorithm = { name: 'ECDSA', namedCurve: 'P-256' };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'ES384':
algorithm = { name: 'ECDSA', namedCurve: 'P-384' };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'ES512':
algorithm = { name: 'ECDSA', namedCurve: 'P-521' };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: 'ECDH', namedCurve: jwk.crv };
keyUsages = jwk.d ? ['deriveBits'] : [];
break;
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
}
break;
}
case 'OKP': {
switch (jwk.alg) {
case 'Ed25519':
case 'EdDSA':
algorithm = { name: 'Ed25519' };
keyUsages = jwk.d ? ['sign'] : ['verify'];
break;
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: jwk.crv };
keyUsages = jwk.d ? ['deriveBits'] : [];
break;
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
}
break;
}
default:
throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value');
}
return { algorithm, keyUsages };
}
var importJWK = async (jwk) => {
if (!jwk.alg) {
throw new TypeError('"alg" argument is required when "jwk.alg" is not present');
}
const { algorithm, keyUsages } = subtleMapping(jwk);
const keyData = { ...jwk };
delete keyData.alg;
delete keyData.use;
return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (jwk.d ? false : true), jwk.key_ops ?? keyUsages);
};
async function importSPKI(spki, alg, options) {
if (typeof spki !== 'string' || spki.indexOf('-----BEGIN PUBLIC KEY-----') !== 0) {
throw new TypeError('"spki" must be SPKI formatted string');
}
return fromSPKI(spki, alg);
}
async function importPKCS8(pkcs8, alg, options) {
if (typeof pkcs8 !== 'string' || pkcs8.indexOf('-----BEGIN PRIVATE KEY-----') !== 0) {
throw new TypeError('"pkcs8" must be PKCS#8 formatted string');
}
return fromPKCS8(pkcs8, alg);
}
var validateCrit = (Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) => {
if (joseHeader.crit !== undefined && protectedHeader?.crit === undefined) {
throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
}
if (!protectedHeader || protectedHeader.crit === undefined) {
return new Set();
}
if (!Array.isArray(protectedHeader.crit) ||
protectedHeader.crit.length === 0 ||
protectedHeader.crit.some((input) => typeof input !== 'string' || input.length === 0)) {
throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
}
let recognized;
if (recognizedOption !== undefined) {
recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
}
else {
recognized = recognizedDefault;
}
for (const parameter of protectedHeader.crit) {
if (!recognized.has(parameter)) {
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
}
if (joseHeader[parameter] === undefined) {
throw new Err(`Extension Header Parameter "${parameter}" is missing`);
}
if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
}
}
return new Set(protectedHeader.crit);
};
var validateAlgorithms = (option, algorithms) => {
if (algorithms !== undefined &&
(!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== 'string'))) {
throw new TypeError(`"${option}" option must be an array of strings`);
}
if (!algorithms) {
return undefined;
}
return new Set(algorithms);
};
function isJWK(key) {
return isObject(key) && typeof key.kty === 'string';
}
function isPrivateJWK(key) {
return key.kty !== 'oct' && typeof key.d === 'string';
}
function isPublicJWK(key) {
return key.kty !== 'oct' && typeof key.d === 'undefined';
}
function isSecretJWK(key) {
return key.kty === 'oct' && typeof key.k === 'string';
}
let cache;
const handleJWK = async (key, jwk, alg, freeze = false) => {
cache ||= new WeakMap();
let cached = cache.get(key);
if (cached?.[alg]) {
return cached[alg];
}
const cryptoKey = await importJWK({ ...jwk, alg });
if (freeze)
Object.freeze(key);
if (!cached) {
cache.set(key, { [alg]: cryptoKey });
}
else {
cached[alg] = cryptoKey;
}
return cryptoKey;
};
const handleKeyObject = (keyObject, alg) => {
cache ||= new WeakMap();
let cached = cache.get(keyObject);
if (cached?.[alg]) {
return cached[alg];
}
const isPublic = keyObject.type === 'public';
const extractable = isPublic ? true : false;
let cryptoKey;
if (keyObject.asymmetricKeyType === 'x25519') {
switch (alg) {
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
break;
default:
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ['deriveBits']);
}
if (keyObject.asymmetricKeyType === 'ed25519') {
if (alg !== 'EdDSA' && alg !== 'Ed25519') {
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [
isPublic ? 'verify' : 'sign',
]);
}
if (keyObject.asymmetricKeyType === 'rsa') {
let hash;
switch (alg) {
case 'RSA-OAEP':
hash = 'SHA-1';
break;
case 'RS256':
case 'PS256':
case 'RSA-OAEP-256':
hash = 'SHA-256';
break;
case 'RS384':
case 'PS384':
case 'RSA-OAEP-384':
hash = 'SHA-384';
break;
case 'RS512':
case 'PS512':
case 'RSA-OAEP-512':
hash = 'SHA-512';
break;
default:
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
if (alg.startsWith('RSA-OAEP')) {
return keyObject.toCryptoKey({
name: 'RSA-OAEP',
hash,
}, extractable, isPublic ? ['encrypt'] : ['decrypt']);
}
cryptoKey = keyObject.toCryptoKey({
name: alg.startsWith('PS') ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5',
hash,
}, extractable, [isPublic ? 'verify' : 'sign']);
}
if (keyObject.asymmetricKeyType === 'ec') {
const nist = new Map([
['prime256v1', 'P-256'],
['secp384r1', 'P-384'],
['secp521r1', 'P-521'],
]);
const namedCurve = nist.get(keyObject.asymmetricKeyDetails?.namedCurve);
if (!namedCurve) {
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
if (alg === 'ES256' && namedCurve === 'P-256') {
cryptoKey = keyObject.toCryptoKey({
name: 'ECDSA',
namedCurve,
}, extractable, [isPublic ? 'verify' : 'sign']);
}
if (alg === 'ES384' && namedCurve === 'P-384') {
cryptoKey = keyObject.toCryptoKey({
name: 'ECDSA',
namedCurve,
}, extractable, [isPublic ? 'verify' : 'sign']);
}
if (alg === 'ES512' && namedCurve === 'P-521') {
cryptoKey = keyObject.toCryptoKey({
name: 'ECDSA',
namedCurve,
}, extractable, [isPublic ? 'verify' : 'sign']);
}
if (alg.startsWith('ECDH-ES')) {
cryptoKey = keyObject.toCryptoKey({
name: 'ECDH',
namedCurve,
}, extractable, isPublic ? [] : ['deriveBits']);
}
}
if (!cryptoKey) {
throw new TypeError('given KeyObject instance cannot be used for this algorithm');
}
if (!cached) {
cache.set(keyObject, { [alg]: cryptoKey });
}
else {
cached[alg] = cryptoKey;
}
return cryptoKey;
};
var normalizeKey = async (key, alg) => {
if (key instanceof Uint8Array) {
return key;
}
if (isCryptoKey(key)) {
return key;
}
if (isKeyObject(key)) {
if (key.type === 'secret') {
return key.export();
}
if ('toCryptoKey' in key && typeof key.toCryptoKey === 'function') {
try {
return handleKeyObject(key, alg);
}
catch (err) {
if (err instanceof TypeError) {
throw err;
}
}
}
let jwk = key.export({ format: 'jwk' });
return handleJWK(key, jwk, alg);
}
if (isJWK(key)) {
if (key.k) {
return decode(key.k);
}
return handleJWK(key, key, alg, true);
}
throw new Error('unreachable');
};
const tag = (key) => key?.[Symbol.toStringTag];
const jwkMatchesOp = (alg, key, usage) => {
if (key.use !== undefined) {
let expected;
switch (usage) {
case 'sign':
case 'verify':
expected = 'sig';
break;
case 'encrypt':
case 'decrypt':
expected = 'enc';
break;
}
if (key.use !== expected) {
throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
}
}
if (key.alg !== undefined && key.alg !== alg) {
throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
}
if (Array.isArray(key.key_ops)) {
let expectedKeyOp;
switch (true) {
case usage === 'sign' || usage === 'verify':
case alg === 'dir':
case alg.includes('CBC-HS'):
expectedKeyOp = usage;
break;
case alg.startsWith('PBES2'):
expectedKeyOp = 'deriveBits';
break;
case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
if (!alg.includes('GCM') && alg.endsWith('KW')) {
expectedKeyOp = usage === 'encrypt' ? 'wrapKey' : 'unwrapKey';
}
else {
expectedKeyOp = usage;
}
break;
case usage === 'encrypt' && alg.startsWith('RSA'):
expectedKeyOp = 'wrapKey';
break;
case usage === 'decrypt':
expectedKeyOp = alg.startsWith('RSA') ? 'unwrapKey' : 'deriveBits';
break;
}
if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) {
throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
}
}
return true;
};
const symmetricTypeCheck = (alg, key, usage) => {
if (key instanceof Uint8Array)
return;
if (isJWK(key)) {
if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
}
if (!isKeyLike(key)) {
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key', 'Uint8Array'));
}
if (key.type !== 'secret') {
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
}
};
const asymmetricTypeCheck = (alg, key, usage) => {
if (isJWK(key)) {
switch (usage) {
case 'decrypt':
case 'sign':
if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for this operation be a private JWK`);
case 'encrypt':
case 'verify':
if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage))
return;
throw new TypeError(`JSON Web Key for this operation be a public JWK`);
}
}
if (!isKeyLike(key)) {
throw new TypeError(withAlg(alg, key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
}
if (key.type === 'secret') {
throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
}
if (key.type === 'public') {
switch (usage) {
case 'sign':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
case 'decrypt':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
}
}
if (key.type === 'private') {
switch (usage) {
case 'verify':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
case 'encrypt':
throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
}
}
};
var checkKeyType = (alg, key, usage) => {
const symmetric = alg.startsWith('HS') ||
alg === 'dir' ||
alg.startsWith('PBES2') ||
/^A(?:128|192|256)(?:GCM)?(?:KW)?$/.test(alg) ||
/^A(?:128|192|256)CBC-HS(?:256|384|512)$/.test(alg);
if (symmetric) {
symmetricTypeCheck(alg, key, usage);
}
else {
asymmetricTypeCheck(alg, key, usage);
}
};
var subtleAlgorithm = (alg, algorithm) => {
const hash = `SHA-${alg.slice(-3)}`;
switch (alg) {
case 'HS256':
case 'HS384':
case 'HS512':
return { hash, name: 'HMAC' };
case 'PS256':
case 'PS384':
case 'PS512':
return { hash, name: 'RSA-PSS', saltLength: parseInt(alg.slice(-3), 10) >> 3 };
case 'RS256':
case 'RS384':
case 'RS512':
return { hash, name: 'RSASSA-PKCS1-v1_5' };
case 'ES256':
case 'ES384':
case 'ES512':
return { hash, name: 'ECDSA', namedCurve: algorithm.namedCurve };
case 'Ed25519':
case 'EdDSA':
return { name: 'Ed25519' };
default:
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
}
};
var getSignKey = async (alg, key, usage) => {
if (key instanceof Uint8Array) {
if (!alg.startsWith('HS')) {
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject', 'JSON Web Key'));
}
return crypto.subtle.importKey('raw', key, { hash: `SHA-${alg.slice(-3)}`, name: 'HMAC' }, false, [usage]);
}
checkSigCryptoKey(key, alg, usage);
return key;
};
var verify = async (alg, key, signature, data) => {
const cryptoKey = await getSignKey(alg, key, 'verify');
checkKeyLength(alg, cryptoKey);
const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
try {
return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
}
catch {
return false;
}
};
async function flattenedVerify(jws, key, options) {
if (!isObject(jws)) {
throw new JWSInvalid('Flattened JWS must be an object');
}
if (jws.protected === undefined && jws.header === undefined) {
throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members');
}
if (jws.protected !== undefined && typeof jws.protected !== 'string') {
throw new JWSInvalid('JWS Protected Header incorrect type');
}
if (jws.payload === undefined) {
throw new JWSInvalid('JWS Payload missing');
}
if (typeof jws.signature !== 'string') {
throw new JWSInvalid('JWS Signature missing or incorrect type');
}
if (jws.header !== undefined && !isObject(jws.header)) {
throw new JWSInvalid('JWS Unprotected Header incorrect type');
}
let parsedProt = {};
if (jws.protected) {
try {
const protectedHeader = decode(jws.protected);
parsedProt = JSON.parse(decoder.decode(protectedHeader));
}
catch {
throw new JWSInvalid('JWS Protected Header is invalid');
}
}
if (!isDisjoint(parsedProt, jws.header)) {
throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
}
const joseHeader = {
...parsedProt,
...jws.header,
};
const extensions = validateCrit(JWSInvalid, new Map([['b64', true]]), options?.crit, parsedProt, joseHeader);
let b64 = true;
if (extensions.has('b64')) {
b64 = parsedProt.b64;
if (typeof b64 !== 'boolean') {
throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
}
}
const { alg } = joseHeader;
if (typeof alg !== 'string' || !alg) {
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
}
const algorithms = options && validateAlgorithms('algorithms', options.algorithms);
if (algorithms && !algorithms.has(alg)) {
throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
}
if (b64) {
if (typeof jws.payload !== 'string') {
throw new JWSInvalid('JWS Payload must be a string');
}
}
else if (typeof jws.payload !== 'string' && !(jws.payload instanceof Uint8Array)) {
throw new JWSInvalid('JWS Payload must be a string or an Uint8Array instance');
}
let resolvedKey = false;
if (typeof key === 'function') {
key = await key(parsedProt, jws);
resolvedKey = true;
}
checkKeyType(alg, key, 'verify');
const data = concat(encoder.encode(jws.protected ?? ''), encoder.encode('.'), typeof jws.payload === 'string' ? encoder.encode(jws.payload) : jws.payload);
let signature;
try {
signature = decode(jws.signature);
}
catch {
throw new JWSInvalid('Failed to base64url decode the signature');
}
const k = await normalizeKey(key, alg);
const verified = await verify(alg, k, signature, data);
if (!verified) {
throw new JWSSignatureVerificationFailed();
}
let payload;
if (b64) {
try {
payload = decode(jws.payload);
}
catch {
throw new JWSInvalid('Failed to base64url decode the payload');
}
}
else if (typeof jws.payload === 'string') {
payload = encoder.encode(jws.payload);
}
else {
payload = jws.payload;
}
const result = { payload };
if (jws.protected !== undefined) {
result.protectedHeader = parsedProt;
}
if (jws.header !== undefined) {
result.unprotectedHeader = jws.header;
}
if (resolvedKey) {
return { ...result, key: k };
}
return result;
}
async function compactVerify(jws, key, options) {
if (jws instanceof Uint8Array) {
jws = decoder.decode(jws);
}
if (typeof jws !== 'string') {
throw new JWSInvalid('Compact JWS must be a string or Uint8Array');
}
const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split('.');
if (length !== 3) {
throw new JWSInvalid('Invalid Compact JWS');
}
const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options);
const result = { payload: verified.payload, protectedHeader: verified.protectedHeader };
if (typeof key === 'function') {
return { ...result, key: verified.key };
}
return result;
}
var epoch = (date) => Math.floor(date.getTime() / 1000);
const minute = 60;
const hour = minute * 60;
const day = hour * 24;
const week = day * 7;
const year = day * 365.25;
const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
var secs = (str) => {
const matched = REGEX.exec(str);
if (!matched || (matched[4] && matched[1])) {
throw new TypeError('Invalid time period format');
}
const value = parseFloat(matched[2]);
const unit = matched[3].toLowerCase();
let numericDate;
switch (unit) {
case 'sec':
case 'secs':
case 'second':
case 'seconds':
case 's':
numericDate = Math.round(value);
break;
case 'minute':
case 'minutes':
case 'min':
case 'mins':
case 'm':
numericDate = Math.round(value * minute);
break;
case 'hour':
case 'hours':
case 'hr':
case 'hrs':
case 'h':
numericDate = Math.round(value * hour);
break;
case 'day':
case 'days':
case 'd':
numericDate = Math.round(value * day);
break;
case 'week':
case 'weeks':
case 'w':
numericDate = Math.round(value * week);
break;
default:
numericDate = Math.round(value * year);
break;
}
if (matched[1] === '-' || matched[4] === 'ago') {
return -numericDate;
}
return numericDate;
};
function validateInput(label, input) {
if (!Number.isFinite(input)) {
throw new TypeError(`Invalid ${label} input`);
}
return input;
}
const normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, '');
const checkAudiencePresence = (audPayload, audOption) => {
if (typeof audPayload === 'string') {
return audOption.includes(audPayload);
}
if (Array.isArray(audPayload)) {
return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
}
return false;
};
function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
let payload;
try {
payload = JSON.parse(decoder.decode(encodedPayload));
}
catch {
}
if (!isObject(payload)) {
throw new JWTInvalid('JWT Claims Set must be a top-level JSON object');
}
const { typ } = options;
if (typ &&
(typeof protectedHeader.typ !== 'string' ||
normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, 'typ', 'check_failed');
}
const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
const presenceCheck = [...requiredClaims];
if (maxTokenAge !== undefined)
presenceCheck.push('iat');
if (audience !== undefined)
presenceCheck.push('aud');
if (subject !== undefined)
presenceCheck.push('sub');
if (issuer !== undefined)
presenceCheck.push('iss');
for (const claim of new Set(presenceCheck.reverse())) {
if (!(claim in payload)) {
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, 'missing');
}
}
if (issuer &&
!(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, 'iss', 'check_failed');
}
if (subject && payload.sub !== subject) {
throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, 'sub', 'check_failed');
}
if (audience &&
!checkAudiencePresence(payload.aud, typeof audience === 'string' ? [audience] : audience)) {
throw new JWTClaimValidationFailed('unexpected "aud" claim value', payload, 'aud', 'check_failed');
}
let tolerance;
switch (typeof options.clockTolerance) {
case 'string':
tolerance = secs(options.clockTolerance);
break;
case 'number':
tolerance = options.clockTolerance;
break;
case 'undefined':
tolerance = 0;
break;
default:
throw new TypeError('Invalid clockTolerance option type');
}
const { currentDate } = options;
const now = epoch(currentDate || new Date());
if ((payload.iat !== undefined || maxTokenAge) && typeof payload.iat !== 'number') {
throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, 'iat', 'invalid');
}
if (payload.nbf !== undefined) {
if (typeof payload.nbf !== 'number') {
throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, 'nbf', 'invalid');
}
if (payload.nbf > now + tolerance) {
throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, 'nbf', 'check_failed');
}
}
if (payload.exp !== undefined) {
if (typeof payload.exp !== 'number') {
throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, 'exp', 'invalid');
}
if (payload.exp <= now - tolerance) {
throw new JWTExpired('"exp" claim timestamp check failed', payload, 'exp', 'check_failed');
}
}
if (maxTokenAge) {
const age = now - payload.iat;
const max = typeof maxTokenAge === 'number' ? maxTokenAge : secs(maxTokenAge);
if (age - tolerance > max) {
throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, 'iat', 'check_failed');
}
if (age < 0 - tolerance) {
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, 'iat', 'check_failed');
}
}
return payload;
}
class JWTClaimsBuilder {
#payload;
constructor(payload) {
if (!isObject(payload)) {
throw new TypeError('JWT Claims Set MUST be an object');
}
this.#payload = structuredClone(payload);
}
data() {
return encoder.encode(JSON.stringify(this.#payload));
}
get iss() {
return this.#payload.iss;
}
set iss(value) {
this.#payload.iss = value;
}
get sub() {
return this.#payload.sub;
}
set sub(value) {
this.#payload.sub = value;
}
get aud() {
return this.#payload.aud;
}
set aud(value) {
this.#payload.aud = value;
}
set jti(value) {
this.#payload.jti = value;
}
set nbf(value) {
if (typeof value === 'number') {
this.#payload.nbf = validateInput('setNotBefore', value);
}
else if (value instanceof Date) {
this.#payload.nbf = validateInput('setNotBefore', epoch(value));
}
else {
this.#payload.nbf = epoch(new Date()) + secs(value);
}
}
set exp(value) {
if (typeof value === 'number') {
this.#payload.exp = validateInput('setExpirationTime', value);
}
else if (value instanceof Date) {
this.#payload.exp = validateInput('setExpirationTime', epoch(value));
}
else {
this.#payload.exp = epoch(new Date()) + secs(value);
}
}
set iat(value) {
if (typeof value === 'undefined') {
this.#payload.iat = epoch(new Date());
}
else if (value instanceof Date) {
this.#payload.iat = validateInput('setIssuedAt', epoch(value));
}
else if (typeof value === 'string') {
this.#payload.iat = validateInput('setIssuedAt', epoch(new Date()) + secs(value));
}
else {
this.#payload.iat = validateInput('setIssuedAt', value);
}
}
}
async function jwtVerify(jwt, key, options) {
const verified = await compactVerify(jwt, key, options);
if (verified.protectedHeader.crit?.includes('b64') && verified.protectedHeader.b64 === false) {
throw new JWTInvalid('JWTs MUST NOT use unencoded payload');
}
const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options);
const result = { payload, protectedHeader: verified.protectedHeader };
if (typeof key === 'function') {
return { ...result, key: verified.key };
}
return result;
}
var sign = async (alg, key, data) => {
const cryptoKey = await getSignKey(alg, key, 'sign');
checkKeyLength(alg, cryptoKey);
const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
return new Uint8Array(signature);
};