init project
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
import { invalidKeyInput } from './invalid_key_input.js';
|
||||
import { encodeBase64, decodeBase64 } from '../lib/base64.js';
|
||||
import { JOSENotSupported } from '../util/errors.js';
|
||||
import { keyAlgorithm } from './key_algorithm.js';
|
||||
import { isCryptoKey, isKeyObject } from './is_key_like.js';
|
||||
const formatPEM = (b64, descriptor) => {
|
||||
const newlined = (b64.match(/.{1,64}/g) || []).join('\n');
|
||||
return `-----BEGIN ${descriptor}-----\n${newlined}\n-----END ${descriptor}-----`;
|
||||
};
|
||||
const genericExport = async (keyType, keyFormat, key) => {
|
||||
if (isKeyObject(key)) {
|
||||
if (key.type !== keyType) {
|
||||
throw new TypeError(`key is not a ${keyType} key`);
|
||||
}
|
||||
return key.export({ format: 'pem', type: keyFormat });
|
||||
}
|
||||
if (!isCryptoKey(key)) {
|
||||
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'KeyObject'));
|
||||
}
|
||||
if (!key.extractable) {
|
||||
throw new TypeError('CryptoKey is not extractable');
|
||||
}
|
||||
if (key.type !== keyType) {
|
||||
throw new TypeError(`key is not a ${keyType} key`);
|
||||
}
|
||||
return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`);
|
||||
};
|
||||
export const toSPKI = (key) => genericExport('public', 'spki', key);
|
||||
export const toPKCS8 = (key) => genericExport('private', 'pkcs8', key);
|
||||
const bytesEqual = (a, b) => {
|
||||
if (a.byteLength !== b.length)
|
||||
return false;
|
||||
for (let i = 0; i < a.byteLength; i++) {
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const createASN1State = (data) => ({ data, pos: 0 });
|
||||
const readByte = (state) => {
|
||||
const byte = state.data[state.pos++];
|
||||
if (byte === undefined) {
|
||||
throw new Error('Unexpected end of ASN.1 input');
|
||||
}
|
||||
return byte;
|
||||
};
|
||||
const parseLength = (state) => {
|
||||
const first = readByte(state);
|
||||
if (first & 0x80) {
|
||||
const lengthOfLen = first & 0x7f;
|
||||
let length = 0;
|
||||
for (let i = 0; i < lengthOfLen; i++) {
|
||||
length = (length << 8) | readByte(state);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
return first;
|
||||
};
|
||||
const skipElement = (state, count = 1) => {
|
||||
if (count <= 0)
|
||||
return;
|
||||
state.pos++;
|
||||
const length = parseLength(state);
|
||||
state.pos += length;
|
||||
if (count > 1) {
|
||||
skipElement(state, count - 1);
|
||||
}
|
||||
};
|
||||
const expectTag = (state, expectedTag, errorMessage) => {
|
||||
if (readByte(state) !== expectedTag) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
const getSubarray = (state, length) => {
|
||||
if (length < 0 || state.pos + length > state.data.length) {
|
||||
throw new Error('Unexpected end of ASN.1 input');
|
||||
}
|
||||
const result = state.data.subarray(state.pos, state.pos + length);
|
||||
state.pos += length;
|
||||
return result;
|
||||
};
|
||||
const parseAlgorithmOID = (state) => {
|
||||
expectTag(state, 0x06, 'Expected algorithm OID');
|
||||
const oidLen = parseLength(state);
|
||||
return getSubarray(state, oidLen);
|
||||
};
|
||||
function parsePKCS8Header(state) {
|
||||
expectTag(state, 0x30, 'Invalid PKCS#8 structure');
|
||||
parseLength(state);
|
||||
expectTag(state, 0x02, 'Expected version field');
|
||||
const verLen = parseLength(state);
|
||||
state.pos += verLen;
|
||||
expectTag(state, 0x30, 'Expected algorithm identifier');
|
||||
const algIdLen = parseLength(state);
|
||||
const algIdStart = state.pos;
|
||||
return { algIdStart, algIdLength: algIdLen };
|
||||
}
|
||||
function parseSPKIHeader(state) {
|
||||
expectTag(state, 0x30, 'Invalid SPKI structure');
|
||||
parseLength(state);
|
||||
expectTag(state, 0x30, 'Expected algorithm identifier');
|
||||
const algIdLen = parseLength(state);
|
||||
const algIdStart = state.pos;
|
||||
return { algIdStart, algIdLength: algIdLen };
|
||||
}
|
||||
const parseECAlgorithmIdentifier = (state) => {
|
||||
const algOid = parseAlgorithmOID(state);
|
||||
if (bytesEqual(algOid, [0x2b, 0x65, 0x6e])) {
|
||||
return 'X25519';
|
||||
}
|
||||
if (!bytesEqual(algOid, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01])) {
|
||||
throw new Error('Unsupported key algorithm');
|
||||
}
|
||||
expectTag(state, 0x06, 'Expected curve OID');
|
||||
const curveOidLen = parseLength(state);
|
||||
const curveOid = getSubarray(state, curveOidLen);
|
||||
for (const { name, oid } of [
|
||||
{ name: 'P-256', oid: [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07] },
|
||||
{ name: 'P-384', oid: [0x2b, 0x81, 0x04, 0x00, 0x22] },
|
||||
{ name: 'P-521', oid: [0x2b, 0x81, 0x04, 0x00, 0x23] },
|
||||
]) {
|
||||
if (bytesEqual(curveOid, oid)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
throw new Error('Unsupported named curve');
|
||||
};
|
||||
const genericImport = async (keyFormat, keyData, alg, options) => {
|
||||
const entry = keyAlgorithm(alg);
|
||||
if (entry.symmetric) {
|
||||
throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value');
|
||||
}
|
||||
const isPublic = keyFormat === 'spki';
|
||||
let algorithm;
|
||||
if (entry.subtleFor) {
|
||||
try {
|
||||
algorithm = entry.subtleFor({ crv: options.getNamedCurve(keyData) });
|
||||
}
|
||||
catch (cause) {
|
||||
throw new JOSENotSupported('Invalid or unsupported key format');
|
||||
}
|
||||
}
|
||||
else {
|
||||
algorithm = entry.subtle;
|
||||
}
|
||||
return crypto.subtle.importKey(keyFormat, keyData, algorithm, options?.extractable ?? (isPublic ? true : false), isPublic ? entry.usages.public : entry.usages.private);
|
||||
};
|
||||
const processPEMData = (pem, pattern) => {
|
||||
return decodeBase64(pem.replace(pattern, ''));
|
||||
};
|
||||
export const fromPKCS8 = (pem, alg, options) => {
|
||||
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g);
|
||||
let opts = options;
|
||||
if (alg?.startsWith?.('ECDH-ES')) {
|
||||
opts ||= {};
|
||||
opts.getNamedCurve = (keyData) => {
|
||||
const state = createASN1State(keyData);
|
||||
parsePKCS8Header(state);
|
||||
return parseECAlgorithmIdentifier(state);
|
||||
};
|
||||
}
|
||||
return genericImport('pkcs8', keyData, alg, opts);
|
||||
};
|
||||
export const fromSPKI = (pem, alg, options) => {
|
||||
const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g);
|
||||
let opts = options;
|
||||
if (alg?.startsWith?.('ECDH-ES')) {
|
||||
opts ||= {};
|
||||
opts.getNamedCurve = (keyData) => {
|
||||
const state = createASN1State(keyData);
|
||||
parseSPKIHeader(state);
|
||||
return parseECAlgorithmIdentifier(state);
|
||||
};
|
||||
}
|
||||
return genericImport('spki', keyData, alg, opts);
|
||||
};
|
||||
function spkiFromX509(buf) {
|
||||
const state = createASN1State(buf);
|
||||
expectTag(state, 0x30, 'Invalid certificate structure');
|
||||
parseLength(state);
|
||||
expectTag(state, 0x30, 'Invalid tbsCertificate structure');
|
||||
parseLength(state);
|
||||
if (buf[state.pos] === 0xa0) {
|
||||
skipElement(state, 6);
|
||||
}
|
||||
else {
|
||||
skipElement(state, 5);
|
||||
}
|
||||
const spkiStart = state.pos;
|
||||
expectTag(state, 0x30, 'Invalid SPKI structure');
|
||||
const spkiContentLen = parseLength(state);
|
||||
return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart));
|
||||
}
|
||||
function extractX509SPKI(x509) {
|
||||
const derBytes = processPEMData(x509, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g);
|
||||
return spkiFromX509(derBytes);
|
||||
}
|
||||
export const fromX509 = (pem, alg, options) => {
|
||||
let spki;
|
||||
try {
|
||||
spki = extractX509SPKI(pem);
|
||||
}
|
||||
catch (cause) {
|
||||
throw new TypeError('Failed to parse the X.509 certificate', { cause });
|
||||
}
|
||||
return fromSPKI(formatPEM(encodeBase64(spki), 'PUBLIC KEY'), alg, options);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
export 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(''));
|
||||
}
|
||||
export 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;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
export const encoder = new TextEncoder();
|
||||
export const decoder = new TextDecoder();
|
||||
export const strictDecoder = new TextDecoder('utf-8', { fatal: true });
|
||||
const MAX_INT32 = 2 ** 32;
|
||||
export 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 writeUInt32BE(buf, value, offset) {
|
||||
if (value < 0 || value >= MAX_INT32) {
|
||||
throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`);
|
||||
}
|
||||
buf.set([value >>> 24, value >>> 16, value >>> 8, value & 0xff], offset);
|
||||
}
|
||||
export function uint64be(value) {
|
||||
const high = Math.floor(value / MAX_INT32);
|
||||
const low = value % MAX_INT32;
|
||||
const buf = new Uint8Array(8);
|
||||
writeUInt32BE(buf, high, 0);
|
||||
writeUInt32BE(buf, low, 4);
|
||||
return buf;
|
||||
}
|
||||
export function uint32be(value) {
|
||||
const buf = new Uint8Array(4);
|
||||
writeUInt32BE(buf, value);
|
||||
return buf;
|
||||
}
|
||||
export function encode(string) {
|
||||
const bytes = new Uint8Array(string.length);
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
const code = string.charCodeAt(i);
|
||||
if (code > 127) {
|
||||
throw new TypeError('non-ASCII string encountered in encode()');
|
||||
}
|
||||
bytes[i] = code;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { concat, uint64be } from './buffer_utils.js';
|
||||
import { checkCryptoKey } from './crypto_key.js';
|
||||
import { invalidKeyInput } from './invalid_key_input.js';
|
||||
import { JWEDecryptionFailed, JWEInvalid } from '../util/errors.js';
|
||||
import { isCryptoKey } from './is_key_like.js';
|
||||
export const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3));
|
||||
function checkCekLength(cek, expected) {
|
||||
const actual = cek.byteLength << 3;
|
||||
if (actual !== expected) {
|
||||
throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`);
|
||||
}
|
||||
}
|
||||
export const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3));
|
||||
export function checkIvLength(enc, iv) {
|
||||
if (iv.length << 3 !== enc.ivBits) {
|
||||
throw new JWEInvalid('Invalid Initialization Vector length');
|
||||
}
|
||||
}
|
||||
async function cbcKeySetup(enc, cek, usage) {
|
||||
if (!(cek instanceof Uint8Array)) {
|
||||
throw new TypeError(invalidKeyInput(cek, 'Uint8Array'));
|
||||
}
|
||||
const keySize = enc.cekBits >> 1;
|
||||
const encKey = await crypto.subtle.importKey('raw', cek.subarray(keySize >> 3), 'AES-CBC', false, [usage]);
|
||||
const macKey = await crypto.subtle.importKey('raw', cek.subarray(0, keySize >> 3), {
|
||||
hash: `SHA-${keySize << 1}`,
|
||||
name: 'HMAC',
|
||||
}, false, ['sign']);
|
||||
return { encKey, macKey, keySize };
|
||||
}
|
||||
async function cbcHmacTag(macKey, macData, keySize) {
|
||||
return new Uint8Array((await crypto.subtle.sign('HMAC', macKey, macData)).slice(0, keySize >> 3));
|
||||
}
|
||||
async function cbcEncrypt(enc, plaintext, cek, iv, aad) {
|
||||
const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'encrypt');
|
||||
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
||||
iv: iv,
|
||||
name: 'AES-CBC',
|
||||
}, encKey, plaintext));
|
||||
const macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8));
|
||||
const tag = await cbcHmacTag(macKey, macData, keySize);
|
||||
return { ciphertext, tag, iv };
|
||||
}
|
||||
async function timingSafeEqual(a, b) {
|
||||
if (!(a instanceof Uint8Array)) {
|
||||
throw new TypeError('First argument must be a buffer');
|
||||
}
|
||||
if (!(b instanceof Uint8Array)) {
|
||||
throw new TypeError('Second argument must be a buffer');
|
||||
}
|
||||
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
|
||||
const key = (await crypto.subtle.generateKey(algorithm, false, ['sign']));
|
||||
const aHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, a));
|
||||
const bHmac = new Uint8Array(await crypto.subtle.sign(algorithm, key, b));
|
||||
let out = 0;
|
||||
let i = -1;
|
||||
while (++i < 32) {
|
||||
out |= aHmac[i] ^ bHmac[i];
|
||||
}
|
||||
return out === 0;
|
||||
}
|
||||
async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
||||
const { encKey, macKey, keySize } = await cbcKeySetup(enc, cek, 'decrypt');
|
||||
const macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8));
|
||||
const expectedTag = await cbcHmacTag(macKey, macData, keySize);
|
||||
let macCheckPassed;
|
||||
try {
|
||||
macCheckPassed = await timingSafeEqual(tag, expectedTag);
|
||||
}
|
||||
catch {
|
||||
}
|
||||
if (!macCheckPassed) {
|
||||
throw new JWEDecryptionFailed();
|
||||
}
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = new Uint8Array(await crypto.subtle.decrypt({ iv: iv, name: 'AES-CBC' }, encKey, ciphertext));
|
||||
}
|
||||
catch {
|
||||
}
|
||||
if (!plaintext) {
|
||||
throw new JWEDecryptionFailed();
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
async function gcmEncrypt(enc, plaintext, cek, iv, aad) {
|
||||
let encKey;
|
||||
if (cek instanceof Uint8Array) {
|
||||
encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);
|
||||
}
|
||||
else {
|
||||
checkCryptoKey(cek, enc.subtle, 'encrypt');
|
||||
encKey = cek;
|
||||
}
|
||||
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
||||
additionalData: aad,
|
||||
iv: iv,
|
||||
name: 'AES-GCM',
|
||||
tagLength: 128,
|
||||
}, encKey, plaintext));
|
||||
const tag = encrypted.slice(-16);
|
||||
const ciphertext = encrypted.slice(0, -16);
|
||||
return { ciphertext, tag, iv };
|
||||
}
|
||||
async function gcmDecrypt(enc, cek, ciphertext, iv, tag, aad) {
|
||||
let encKey;
|
||||
if (cek instanceof Uint8Array) {
|
||||
encKey = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['decrypt']);
|
||||
}
|
||||
else {
|
||||
checkCryptoKey(cek, enc.subtle, 'decrypt');
|
||||
encKey = cek;
|
||||
}
|
||||
try {
|
||||
return new Uint8Array(await crypto.subtle.decrypt({
|
||||
additionalData: aad,
|
||||
iv: iv,
|
||||
name: 'AES-GCM',
|
||||
tagLength: 128,
|
||||
}, encKey, concat(ciphertext, tag)));
|
||||
}
|
||||
catch {
|
||||
throw new JWEDecryptionFailed();
|
||||
}
|
||||
}
|
||||
export async function encrypt(enc, plaintext, cek, iv, aad) {
|
||||
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
|
||||
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'KeyObject', 'Uint8Array', 'JSON Web Key'));
|
||||
}
|
||||
if (iv) {
|
||||
checkIvLength(enc, iv);
|
||||
}
|
||||
else {
|
||||
iv = generateIv(enc);
|
||||
}
|
||||
if (cek instanceof Uint8Array) {
|
||||
checkCekLength(cek, enc.cekBits);
|
||||
}
|
||||
return enc.cbc
|
||||
? cbcEncrypt(enc, plaintext, cek, iv, aad)
|
||||
: gcmEncrypt(enc, plaintext, cek, iv, aad);
|
||||
}
|
||||
export async function decrypt(enc, cek, ciphertext, iv, tag, aad) {
|
||||
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
|
||||
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'KeyObject', 'Uint8Array', 'JSON Web Key'));
|
||||
}
|
||||
if (!iv) {
|
||||
throw new JWEInvalid('JWE Initialization Vector missing');
|
||||
}
|
||||
if (!tag) {
|
||||
throw new JWEInvalid('JWE Authentication Tag missing');
|
||||
}
|
||||
checkIvLength(enc, iv);
|
||||
if (cek instanceof Uint8Array) {
|
||||
checkCekLength(cek, enc.cekBits);
|
||||
}
|
||||
return enc.cbc
|
||||
? cbcDecrypt(enc, cek, ciphertext, iv, tag, aad)
|
||||
: gcmDecrypt(enc, cek, ciphertext, iv, tag, aad);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
const unusable = (name, prop = 'algorithm.name') => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
|
||||
export function checkUsage(key, usage) {
|
||||
if (usage && !key.usages.includes(usage)) {
|
||||
throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
|
||||
}
|
||||
}
|
||||
export function checkCryptoKey(key, expected, usage) {
|
||||
const algorithm = key.algorithm;
|
||||
if (algorithm.name !== expected.name) {
|
||||
throw unusable(expected.name);
|
||||
}
|
||||
if (expected.hash && algorithm.hash?.name !== expected.hash) {
|
||||
throw unusable(expected.hash, 'algorithm.hash');
|
||||
}
|
||||
if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) {
|
||||
throw unusable(expected.namedCurve, 'algorithm.namedCurve');
|
||||
}
|
||||
if (expected.length !== undefined && algorithm.length !== expected.length) {
|
||||
throw unusable(expected.length, 'algorithm.length');
|
||||
}
|
||||
checkUsage(key, usage);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { JOSENotSupported, JWEInvalid } from '../util/errors.js';
|
||||
import { concat } from './buffer_utils.js';
|
||||
function supported(name) {
|
||||
if (typeof globalThis[name] === 'undefined') {
|
||||
throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`);
|
||||
}
|
||||
}
|
||||
export async function compress(input) {
|
||||
supported('CompressionStream');
|
||||
const cs = new CompressionStream('deflate-raw');
|
||||
const writer = cs.writable.getWriter();
|
||||
writer.write(input).catch(() => { });
|
||||
writer.close().catch(() => { });
|
||||
const chunks = [];
|
||||
const reader = cs.readable.getReader();
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done)
|
||||
break;
|
||||
chunks.push(value);
|
||||
}
|
||||
return concat(...chunks);
|
||||
}
|
||||
export async function decompress(input, maxLength) {
|
||||
supported('DecompressionStream');
|
||||
const ds = new DecompressionStream('deflate-raw');
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(input).catch(() => { });
|
||||
writer.close().catch(() => { });
|
||||
const chunks = [];
|
||||
let length = 0;
|
||||
const reader = ds.readable.getReader();
|
||||
for (;;) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done)
|
||||
break;
|
||||
chunks.push(value);
|
||||
length += value.byteLength;
|
||||
if (maxLength !== Infinity && length > maxLength) {
|
||||
throw new JWEInvalid('Decompressed plaintext exceeded the configured limit');
|
||||
}
|
||||
}
|
||||
return concat(...chunks);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { decode } from '../util/base64url.js';
|
||||
import { encode, strictDecoder } from './buffer_utils.js';
|
||||
import { isObject } from './type_checks.js';
|
||||
export const unprotected = Symbol();
|
||||
export function assertNotSet(value, name) {
|
||||
if (value) {
|
||||
throw new TypeError(`${name} can only be called once`);
|
||||
}
|
||||
}
|
||||
export function decodeBase64url(value, label, ErrorClass) {
|
||||
try {
|
||||
return decode(value);
|
||||
}
|
||||
catch {
|
||||
throw new ErrorClass(`Failed to base64url decode the ${label}`);
|
||||
}
|
||||
}
|
||||
export function encodeBase64url(value, label, ErrorClass) {
|
||||
try {
|
||||
return encode(value);
|
||||
}
|
||||
catch {
|
||||
throw new ErrorClass(`The ${label} is not a valid base64url string`);
|
||||
}
|
||||
}
|
||||
export async function digest(algorithm, data) {
|
||||
const subtleDigest = `SHA-${algorithm.slice(-3)}`;
|
||||
return new Uint8Array(await crypto.subtle.digest(subtleDigest, data));
|
||||
}
|
||||
export function parseJoseHeader(b64, ErrorClass, message) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(strictDecoder.decode(decode(b64)));
|
||||
}
|
||||
catch {
|
||||
throw new ErrorClass(message);
|
||||
}
|
||||
if (!isObject(parsed)) {
|
||||
throw new ErrorClass(message);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
export const invalidKeyInput = (actual, ...types) => message('Key must be ', actual, ...types);
|
||||
export const withAlg = (alg, actual, ...types) => message(`Key for the ${alg} algorithm must be `, actual, ...types);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export function assertCryptoKey(key) {
|
||||
if (!isCryptoKey(key)) {
|
||||
throw new Error('CryptoKey instance expected');
|
||||
}
|
||||
}
|
||||
export const isCryptoKey = (key) => {
|
||||
if (key?.[Symbol.toStringTag] === 'CryptoKey')
|
||||
return true;
|
||||
try {
|
||||
return key instanceof CryptoKey;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const isKeyObject = (key) => key?.[Symbol.toStringTag] === 'KeyObject';
|
||||
export const isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { JOSENotSupported } from '../util/errors.js';
|
||||
import { table } from './key_descriptor.js';
|
||||
const wrap = {
|
||||
public: ['encrypt', 'wrapKey'],
|
||||
private: ['decrypt', 'unwrapKey'],
|
||||
};
|
||||
const derive = { public: [], private: ['deriveBits'] };
|
||||
const none = { public: [], private: [] };
|
||||
function rsaes(bits) {
|
||||
return {
|
||||
kty: ['RSA'],
|
||||
subtle: { name: 'RSA-OAEP', hash: `SHA-${bits}` },
|
||||
usages: wrap,
|
||||
minModulusLength: 2048,
|
||||
keyOps: { encrypt: 'wrapKey', decrypt: 'unwrapKey' },
|
||||
};
|
||||
}
|
||||
function ecdh(kwBits) {
|
||||
return {
|
||||
kty: ['EC', 'OKP'],
|
||||
subtle: { name: 'ECDH' },
|
||||
subtleFor: ({ kty, crv, asymmetricKeyType }) => {
|
||||
if (crv === 'X25519' || asymmetricKeyType === 'x25519') {
|
||||
return { name: 'X25519' };
|
||||
}
|
||||
if (kty === 'OKP') {
|
||||
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
||||
}
|
||||
return { name: 'ECDH', namedCurve: crv };
|
||||
},
|
||||
usages: derive,
|
||||
kwBits,
|
||||
keyOps: { decrypt: 'deriveBits' },
|
||||
};
|
||||
}
|
||||
function aeskw(bits) {
|
||||
return {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'AES-KW', length: bits },
|
||||
usages: none,
|
||||
keyOps: { encrypt: 'wrapKey', decrypt: 'unwrapKey' },
|
||||
};
|
||||
}
|
||||
function aesgcmkw(bits) {
|
||||
return {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'AES-GCM', length: bits },
|
||||
usages: none,
|
||||
gcmkw: `A${bits}GCM`,
|
||||
keyOps: { encrypt: 'encrypt', decrypt: 'decrypt' },
|
||||
};
|
||||
}
|
||||
function pbes2(bits, kwBits) {
|
||||
return {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'PBKDF2' },
|
||||
usages: none,
|
||||
pbes2Hash: `SHA-${bits}`,
|
||||
kwBits,
|
||||
keyOps: { encrypt: 'deriveBits', decrypt: 'deriveBits' },
|
||||
};
|
||||
}
|
||||
const JWE = table({
|
||||
dir: {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'AES-GCM' },
|
||||
usages: none,
|
||||
keyOps: { encrypt: 'encrypt', decrypt: 'decrypt' },
|
||||
},
|
||||
'RSA-OAEP': rsaes(1),
|
||||
'RSA-OAEP-256': rsaes(256),
|
||||
'RSA-OAEP-384': rsaes(384),
|
||||
'RSA-OAEP-512': rsaes(512),
|
||||
'ECDH-ES': ecdh(),
|
||||
'ECDH-ES+A128KW': ecdh(128),
|
||||
'ECDH-ES+A192KW': ecdh(192),
|
||||
'ECDH-ES+A256KW': ecdh(256),
|
||||
A128KW: aeskw(128),
|
||||
A192KW: aeskw(192),
|
||||
A256KW: aeskw(256),
|
||||
A128GCMKW: aesgcmkw(128),
|
||||
A192GCMKW: aesgcmkw(192),
|
||||
A256GCMKW: aesgcmkw(256),
|
||||
'PBES2-HS256+A128KW': pbes2(256, 128),
|
||||
'PBES2-HS384+A192KW': pbes2(384, 192),
|
||||
'PBES2-HS512+A256KW': pbes2(512, 256),
|
||||
});
|
||||
const content = { public: [], private: [] };
|
||||
const contentOps = { encrypt: 'encrypt', decrypt: 'decrypt' };
|
||||
function gcm(bits) {
|
||||
return {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'AES-GCM', length: bits },
|
||||
usages: content,
|
||||
keyOps: contentOps,
|
||||
cekBits: bits,
|
||||
ivBits: 96,
|
||||
cbc: false,
|
||||
};
|
||||
}
|
||||
function cbc(bits) {
|
||||
return {
|
||||
kty: ['oct'],
|
||||
symmetric: true,
|
||||
subtle: { name: 'AES-CBC', length: bits },
|
||||
usages: content,
|
||||
keyOps: contentOps,
|
||||
cekBits: bits,
|
||||
ivBits: 128,
|
||||
cbc: true,
|
||||
};
|
||||
}
|
||||
const ENC = table({
|
||||
A128GCM: gcm(128),
|
||||
A192GCM: gcm(192),
|
||||
A256GCM: gcm(256),
|
||||
'A128CBC-HS256': cbc(256),
|
||||
'A192CBC-HS384': cbc(384),
|
||||
'A256CBC-HS512': cbc(512),
|
||||
});
|
||||
const unsupportedAlgHeader = 'Invalid or unsupported "alg" (JWE Algorithm) header value';
|
||||
export function jweAlgorithm(alg) {
|
||||
const entry = JWE[alg];
|
||||
if (!entry) {
|
||||
throw new JOSENotSupported(unsupportedAlgHeader);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
export function maybeJWEAlgorithm(alg) {
|
||||
return JWE[alg];
|
||||
}
|
||||
export function jweEncryption(enc) {
|
||||
const entry = ENC[enc];
|
||||
if (!entry) {
|
||||
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${enc}`);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { decrypt, generateCek } from './content_encryption.js';
|
||||
import { decodeBase64url, encodeBase64url, parseJoseHeader } from './helpers.js';
|
||||
import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from '../util/errors.js';
|
||||
import { isDisjoint, isObject } from './type_checks.js';
|
||||
import { decryptKeyManagement } from './key_management.js';
|
||||
import { concat, decoder, encode } from './buffer_utils.js';
|
||||
import { validateCrit, validateAlgorithms, JWE_RECOGNIZED } from './options.js';
|
||||
import { prepareKey } from './key.js';
|
||||
import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js';
|
||||
import { decompress } from './deflate.js';
|
||||
export function checkShared(jwe) {
|
||||
if (jwe.iv !== undefined && typeof jwe.iv !== 'string') {
|
||||
throw new JWEInvalid('JWE Initialization Vector incorrect type');
|
||||
}
|
||||
if (typeof jwe.ciphertext !== 'string') {
|
||||
throw new JWEInvalid('JWE Ciphertext missing or incorrect type');
|
||||
}
|
||||
if (jwe.tag !== undefined && typeof jwe.tag !== 'string') {
|
||||
throw new JWEInvalid('JWE Authentication Tag incorrect type');
|
||||
}
|
||||
if (jwe.protected !== undefined && typeof jwe.protected !== 'string') {
|
||||
throw new JWEInvalid('JWE Protected Header incorrect type');
|
||||
}
|
||||
if (jwe.aad !== undefined && typeof jwe.aad !== 'string') {
|
||||
throw new JWEInvalid('JWE AAD incorrect type');
|
||||
}
|
||||
if (jwe.unprotected !== undefined && !isObject(jwe.unprotected)) {
|
||||
throw new JWEInvalid('JWE Shared Unprotected Header incorrect type');
|
||||
}
|
||||
}
|
||||
export function checkRecipient(jwe) {
|
||||
if (jwe.encrypted_key !== undefined && typeof jwe.encrypted_key !== 'string') {
|
||||
throw new JWEInvalid('JWE Encrypted Key incorrect type');
|
||||
}
|
||||
if (jwe.header !== undefined && !isObject(jwe.header)) {
|
||||
throw new JWEInvalid('JWE Per-Recipient Unprotected Header incorrect type');
|
||||
}
|
||||
if (jwe.protected === undefined && jwe.header === undefined && jwe.unprotected === undefined) {
|
||||
throw new JWEInvalid('JOSE Header missing');
|
||||
}
|
||||
}
|
||||
export function shareJWE(jwe) {
|
||||
let parsedProt;
|
||||
if (jwe.protected) {
|
||||
parsedProt = parseJoseHeader(jwe.protected, JWEInvalid, 'JWE Protected Header is invalid');
|
||||
}
|
||||
const protectedHeader = jwe.protected !== undefined ? encode(jwe.protected) : new Uint8Array();
|
||||
return {
|
||||
parsedProt,
|
||||
ciphertext: decodeBase64url(jwe.ciphertext, 'ciphertext', JWEInvalid),
|
||||
iv: jwe.iv !== undefined ? decodeBase64url(jwe.iv, 'iv', JWEInvalid) : undefined,
|
||||
tag: jwe.tag !== undefined ? decodeBase64url(jwe.tag, 'tag', JWEInvalid) : undefined,
|
||||
additionalData: jwe.aad !== undefined
|
||||
? concat(protectedHeader, encode('.'), encodeBase64url(jwe.aad, 'aad', JWEInvalid))
|
||||
: protectedHeader,
|
||||
};
|
||||
}
|
||||
export function decryptResult(jwe, decrypted) {
|
||||
const result = { plaintext: decrypted.plaintext };
|
||||
if (jwe.protected !== undefined) {
|
||||
result.protectedHeader = decrypted.parsedProt;
|
||||
}
|
||||
if (jwe.aad !== undefined) {
|
||||
result.additionalAuthenticatedData = decodeBase64url(jwe.aad, 'aad', JWEInvalid);
|
||||
}
|
||||
if (jwe.unprotected !== undefined) {
|
||||
result.sharedUnprotectedHeader = jwe.unprotected;
|
||||
}
|
||||
if (jwe.header !== undefined) {
|
||||
result.unprotectedHeader = jwe.header;
|
||||
}
|
||||
if (decrypted.resolvedKey) {
|
||||
return { ...result, key: decrypted.key };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function prepareDecrypt(options) {
|
||||
return {
|
||||
keyManagementAlgorithms: options && validateAlgorithms('keyManagementAlgorithms', options.keyManagementAlgorithms),
|
||||
contentEncryptionAlgorithms: options &&
|
||||
validateAlgorithms('contentEncryptionAlgorithms', options.contentEncryptionAlgorithms),
|
||||
options,
|
||||
};
|
||||
}
|
||||
export async function decryptRecipient(jwe, token, shared, key) {
|
||||
const { options } = shared;
|
||||
const { parsedProt } = token;
|
||||
let joseHeader;
|
||||
if (jwe.header !== undefined || jwe.unprotected !== undefined) {
|
||||
if (!isDisjoint(parsedProt, jwe.header, jwe.unprotected)) {
|
||||
throw new JWEInvalid('JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint');
|
||||
}
|
||||
joseHeader = { ...parsedProt, ...jwe.header, ...jwe.unprotected };
|
||||
}
|
||||
else {
|
||||
joseHeader = parsedProt ?? {};
|
||||
}
|
||||
validateCrit(JWEInvalid, JWE_RECOGNIZED, options?.crit, parsedProt, joseHeader);
|
||||
if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') {
|
||||
throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.');
|
||||
}
|
||||
if (joseHeader.zip !== undefined && !parsedProt?.zip) {
|
||||
throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.');
|
||||
}
|
||||
const { alg, enc } = joseHeader;
|
||||
if (typeof alg !== 'string' || !alg) {
|
||||
throw new JWEInvalid('missing JWE Algorithm (alg) in JWE Header');
|
||||
}
|
||||
if (typeof enc !== 'string' || !enc) {
|
||||
throw new JWEInvalid('missing JWE Encryption Algorithm (enc) in JWE Header');
|
||||
}
|
||||
const { keyManagementAlgorithms, contentEncryptionAlgorithms } = shared;
|
||||
if ((keyManagementAlgorithms && !keyManagementAlgorithms.has(alg)) ||
|
||||
(!keyManagementAlgorithms && alg.startsWith('PBES2'))) {
|
||||
throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
|
||||
}
|
||||
if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) {
|
||||
throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed');
|
||||
}
|
||||
const encEntry = jweEncryption(enc);
|
||||
let encryptedKey;
|
||||
if (jwe.encrypted_key !== undefined) {
|
||||
encryptedKey = decodeBase64url(jwe.encrypted_key, 'encrypted_key', JWEInvalid);
|
||||
}
|
||||
let resolvedKey = false;
|
||||
if (typeof key === 'function') {
|
||||
key = await key(parsedProt, jwe);
|
||||
resolvedKey = true;
|
||||
}
|
||||
const algEntry = jweAlgorithm(alg);
|
||||
const k = await prepareKey(alg === 'dir' ? encEntry : algEntry, key, 'decrypt');
|
||||
let cek;
|
||||
try {
|
||||
cek = await decryptKeyManagement(alg, encEntry, k, encryptedKey, joseHeader, options);
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) {
|
||||
throw err;
|
||||
}
|
||||
cek = generateCek(encEntry);
|
||||
}
|
||||
let plaintext = await decrypt(encEntry, cek, token.ciphertext, token.iv, token.tag, token.additionalData);
|
||||
if (joseHeader.zip === 'DEF') {
|
||||
const maxDecompressedLength = options?.maxDecompressedLength ?? 250_000;
|
||||
if (maxDecompressedLength === 0) {
|
||||
throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.');
|
||||
}
|
||||
if (maxDecompressedLength !== Infinity &&
|
||||
(!Number.isSafeInteger(maxDecompressedLength) || maxDecompressedLength < 1)) {
|
||||
throw new TypeError('maxDecompressedLength must be 0, a positive safe integer, or Infinity');
|
||||
}
|
||||
plaintext = await decompress(plaintext, maxDecompressedLength).catch((cause) => {
|
||||
if (cause instanceof JWEInvalid)
|
||||
throw cause;
|
||||
throw new JWEInvalid('Failed to decompress plaintext', { cause });
|
||||
});
|
||||
}
|
||||
return { plaintext, parsedProt, key: k, resolvedKey };
|
||||
}
|
||||
export async function decryptJWE(jwe, shared, key) {
|
||||
return decryptRecipient(jwe, shareJWE(jwe), shared, key);
|
||||
}
|
||||
export async function decryptCompact(jwe, shared, key) {
|
||||
if (jwe instanceof Uint8Array) {
|
||||
jwe = decoder.decode(jwe);
|
||||
}
|
||||
if (typeof jwe !== 'string') {
|
||||
throw new JWEInvalid('Compact JWE must be a string or Uint8Array');
|
||||
}
|
||||
const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length, } = jwe.split('.');
|
||||
if (length !== 5) {
|
||||
throw new JWEInvalid('Invalid Compact JWE');
|
||||
}
|
||||
return decryptJWE({
|
||||
ciphertext,
|
||||
iv: iv || undefined,
|
||||
protected: protectedHeader,
|
||||
tag: tag || undefined,
|
||||
encrypted_key: encryptedKey || undefined,
|
||||
}, shared, key);
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { encode as b64u } from '../util/base64url.js';
|
||||
import { encrypt } from './content_encryption.js';
|
||||
import { encryptKeyManagement } from './key_management.js';
|
||||
import { JOSENotSupported, JWEInvalid } from '../util/errors.js';
|
||||
import { isDisjoint } from './type_checks.js';
|
||||
import { concat, encode } from './buffer_utils.js';
|
||||
import { validateCrit, JWE_RECOGNIZED } from './options.js';
|
||||
import { prepareKey } from './key.js';
|
||||
import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js';
|
||||
import { compress } from './deflate.js';
|
||||
export function checkEncryptHeaders(input) {
|
||||
const { protectedHeader, unprotectedHeader, sharedUnprotectedHeader } = input;
|
||||
if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) {
|
||||
throw new JWEInvalid('JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint');
|
||||
}
|
||||
const joseHeader = {
|
||||
...protectedHeader,
|
||||
...unprotectedHeader,
|
||||
...sharedUnprotectedHeader,
|
||||
};
|
||||
validateCrit(JWEInvalid, JWE_RECOGNIZED, input.crit, protectedHeader, joseHeader);
|
||||
if (joseHeader.zip !== undefined && joseHeader.zip !== 'DEF') {
|
||||
throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.');
|
||||
}
|
||||
if (joseHeader.zip !== undefined && !protectedHeader?.zip) {
|
||||
throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.');
|
||||
}
|
||||
const { alg, enc } = joseHeader;
|
||||
if (typeof alg !== 'string' || !alg) {
|
||||
throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid');
|
||||
}
|
||||
if (typeof enc !== 'string' || !enc) {
|
||||
throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid');
|
||||
}
|
||||
return { joseHeader, alg, enc, encEntry: jweEncryption(enc) };
|
||||
}
|
||||
export async function encryptJWE(input, checked, key) {
|
||||
const { joseHeader, alg, encEntry } = checked;
|
||||
let { protectedHeader, unprotectedHeader } = input;
|
||||
const { sharedUnprotectedHeader } = input;
|
||||
if (input.cek && (alg === 'dir' || alg === 'ECDH-ES')) {
|
||||
throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`);
|
||||
}
|
||||
const algEntry = jweAlgorithm(alg);
|
||||
const k = await prepareKey(alg === 'dir' ? encEntry : algEntry, key, 'encrypt');
|
||||
const { cek, encryptedKey, parameters } = await encryptKeyManagement(alg, encEntry, k, input.cek, input.keyManagementParameters);
|
||||
if (parameters) {
|
||||
if (input.unprotectedParameters) {
|
||||
unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters;
|
||||
}
|
||||
else {
|
||||
protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters;
|
||||
}
|
||||
}
|
||||
let protectedHeaderS;
|
||||
let protectedHeaderB;
|
||||
if (protectedHeader) {
|
||||
protectedHeaderS = b64u(JSON.stringify(protectedHeader));
|
||||
protectedHeaderB = encode(protectedHeaderS);
|
||||
}
|
||||
else {
|
||||
protectedHeaderS = '';
|
||||
protectedHeaderB = new Uint8Array();
|
||||
}
|
||||
let additionalData;
|
||||
let aadMember;
|
||||
if (input.aad?.byteLength) {
|
||||
aadMember = b64u(input.aad);
|
||||
additionalData = concat(protectedHeaderB, encode('.'), encode(aadMember));
|
||||
}
|
||||
else {
|
||||
additionalData = protectedHeaderB;
|
||||
}
|
||||
let plaintext = input.plaintext;
|
||||
if (joseHeader.zip === 'DEF') {
|
||||
plaintext = await compress(plaintext).catch((cause) => {
|
||||
throw new JWEInvalid('Failed to compress plaintext', { cause });
|
||||
});
|
||||
}
|
||||
const { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, input.iv, additionalData);
|
||||
const jwe = {
|
||||
ciphertext: b64u(ciphertext),
|
||||
};
|
||||
if (iv) {
|
||||
jwe.iv = b64u(iv);
|
||||
}
|
||||
if (tag) {
|
||||
jwe.tag = b64u(tag);
|
||||
}
|
||||
if (encryptedKey) {
|
||||
jwe.encrypted_key = b64u(encryptedKey);
|
||||
}
|
||||
if (aadMember) {
|
||||
jwe.aad = aadMember;
|
||||
}
|
||||
if (protectedHeader) {
|
||||
jwe.protected = protectedHeaderS;
|
||||
}
|
||||
if (sharedUnprotectedHeader) {
|
||||
jwe.unprotected = sharedUnprotectedHeader;
|
||||
}
|
||||
if (unprotectedHeader) {
|
||||
jwe.header = unprotectedHeader;
|
||||
}
|
||||
return jwe;
|
||||
}
|
||||
export async function createJWE(input, key) {
|
||||
return encryptJWE(input, checkEncryptHeaders(input), key);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { JOSENotSupported } from '../util/errors.js';
|
||||
const unsupportedAlg = 'Invalid or unsupported JWK "alg" (Algorithm) Parameter value';
|
||||
function subtleParams(entry, jwk) {
|
||||
if (!entry.kty.includes(jwk.kty)) {
|
||||
throw new JOSENotSupported(unsupportedAlg);
|
||||
}
|
||||
return entry.subtleFor?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle;
|
||||
}
|
||||
export async function jwkToKey(entry, jwk) {
|
||||
if (jwk.kty === 'RSA' && 'oth' in jwk && jwk.oth !== undefined) {
|
||||
throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported');
|
||||
}
|
||||
const algorithm = subtleParams(entry, jwk);
|
||||
const isPrivate = !!(jwk.d || jwk.priv);
|
||||
const keyUsages = isPrivate ? entry.usages.private : entry.usages.public;
|
||||
const keyData = { ...jwk };
|
||||
if (keyData.kty !== 'AKP') {
|
||||
delete keyData.alg;
|
||||
}
|
||||
delete keyData.use;
|
||||
return crypto.subtle.importKey('jwk', keyData, algorithm, jwk.ext ?? (isPrivate ? false : true), jwk.key_ops ?? keyUsages);
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { JOSENotSupported } from '../util/errors.js';
|
||||
import { table } from './key_descriptor.js';
|
||||
const sig = { public: ['verify'], private: ['sign'] };
|
||||
function hmac(bits) {
|
||||
const subtle = { name: 'HMAC', hash: `SHA-${bits}` };
|
||||
return { kty: ['oct'], symmetric: true, subtle, operation: subtle, usages: sig };
|
||||
}
|
||||
function rsa(name, bits, saltLength) {
|
||||
const subtle = { name, hash: `SHA-${bits}` };
|
||||
return {
|
||||
kty: ['RSA'],
|
||||
subtle,
|
||||
operation: saltLength ? { ...subtle, saltLength } : subtle,
|
||||
usages: sig,
|
||||
minModulusLength: 2048,
|
||||
};
|
||||
}
|
||||
function ecdsa(crv, bits) {
|
||||
return {
|
||||
kty: ['EC'],
|
||||
crv,
|
||||
subtle: { name: 'ECDSA', namedCurve: crv },
|
||||
operation: { name: 'ECDSA', hash: `SHA-${bits}` },
|
||||
usages: sig,
|
||||
};
|
||||
}
|
||||
function eddsa() {
|
||||
const subtle = { name: 'Ed25519' };
|
||||
return {
|
||||
kty: ['OKP'],
|
||||
crv: 'Ed25519',
|
||||
subtle,
|
||||
operation: subtle,
|
||||
usages: sig,
|
||||
};
|
||||
}
|
||||
function mldsa(name) {
|
||||
const subtle = { name };
|
||||
return {
|
||||
kty: ['AKP'],
|
||||
subtle,
|
||||
operation: subtle,
|
||||
usages: sig,
|
||||
};
|
||||
}
|
||||
const JWS = table({
|
||||
HS256: hmac(256),
|
||||
HS384: hmac(384),
|
||||
HS512: hmac(512),
|
||||
RS256: rsa('RSASSA-PKCS1-v1_5', 256),
|
||||
RS384: rsa('RSASSA-PKCS1-v1_5', 384),
|
||||
RS512: rsa('RSASSA-PKCS1-v1_5', 512),
|
||||
PS256: rsa('RSA-PSS', 256, 32),
|
||||
PS384: rsa('RSA-PSS', 384, 48),
|
||||
PS512: rsa('RSA-PSS', 512, 64),
|
||||
ES256: ecdsa('P-256', 256),
|
||||
ES384: ecdsa('P-384', 384),
|
||||
ES512: ecdsa('P-521', 512),
|
||||
EdDSA: eddsa(),
|
||||
Ed25519: eddsa(),
|
||||
'ML-DSA-44': mldsa('ML-DSA-44'),
|
||||
'ML-DSA-65': mldsa('ML-DSA-65'),
|
||||
'ML-DSA-87': mldsa('ML-DSA-87'),
|
||||
});
|
||||
export function jwsAlgorithm(alg) {
|
||||
const entry = JWS[alg];
|
||||
if (!entry) {
|
||||
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
export function maybeJWSAlgorithm(alg) {
|
||||
return JWS[alg];
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { encode as b64u } from '../util/base64url.js';
|
||||
import { sign } from './signing.js';
|
||||
import { jwsAlgorithm } from './jws_algorithms.js';
|
||||
import { isDisjoint } from './type_checks.js';
|
||||
import { JWSInvalid } from '../util/errors.js';
|
||||
import { concat, encode } from './buffer_utils.js';
|
||||
import { validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from './options.js';
|
||||
import { prepareKey } from './key.js';
|
||||
export function unencodedPayload(protectedHeader) {
|
||||
return (protectedHeader?.b64 === false &&
|
||||
Array.isArray(protectedHeader.crit) &&
|
||||
protectedHeader.crit.includes('b64'));
|
||||
}
|
||||
export async function createSignature(input, key) {
|
||||
const { protectedHeader, unprotectedHeader } = input;
|
||||
if (!isDisjoint(protectedHeader, unprotectedHeader)) {
|
||||
throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
|
||||
}
|
||||
const joseHeader = { ...protectedHeader, ...unprotectedHeader };
|
||||
validateCritDuplicates(JWSInvalid, protectedHeader);
|
||||
const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, input.crit, protectedHeader, joseHeader);
|
||||
let b64 = true;
|
||||
if (extensions.has('b64')) {
|
||||
b64 = protectedHeader.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 entry = jwsAlgorithm(alg);
|
||||
let payloadS;
|
||||
let payloadB;
|
||||
if (b64) {
|
||||
const encoded = (input.encoded ??= {});
|
||||
encoded.b64 ??= b64u(input.payload);
|
||||
encoded.raw ??= encode(encoded.b64);
|
||||
payloadS = encoded.b64;
|
||||
payloadB = encoded.raw;
|
||||
}
|
||||
else {
|
||||
payloadB = input.payload;
|
||||
payloadS = '';
|
||||
}
|
||||
let protectedHeaderString;
|
||||
let protectedHeaderBytes;
|
||||
if (protectedHeader) {
|
||||
protectedHeaderString = b64u(JSON.stringify(protectedHeader));
|
||||
protectedHeaderBytes = encode(protectedHeaderString);
|
||||
}
|
||||
else {
|
||||
protectedHeaderString = '';
|
||||
protectedHeaderBytes = new Uint8Array();
|
||||
}
|
||||
const data = concat(protectedHeaderBytes, encode('.'), payloadB);
|
||||
const k = await prepareKey(entry, key, 'sign');
|
||||
const signature = await sign(entry, k, data);
|
||||
const jws = {
|
||||
signature: b64u(signature),
|
||||
payload: payloadS,
|
||||
};
|
||||
if (protectedHeader) {
|
||||
jws.protected = protectedHeaderString;
|
||||
}
|
||||
return jws;
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { verify } from './signing.js';
|
||||
import { jwsAlgorithm } from './jws_algorithms.js';
|
||||
import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from '../util/errors.js';
|
||||
import { concat, decoder, encoder, encode } from './buffer_utils.js';
|
||||
import { decodeBase64url, encodeBase64url, parseJoseHeader } from './helpers.js';
|
||||
import { isDisjoint } from './type_checks.js';
|
||||
import { validateCrit, validateAlgorithms, JWS_RECOGNIZED } from './options.js';
|
||||
import { prepareKey } from './key.js';
|
||||
export function verifyResult(jws, verified) {
|
||||
const result = { payload: verified.payload };
|
||||
if (jws.protected !== undefined) {
|
||||
result.protectedHeader = verified.parsedProt;
|
||||
}
|
||||
if (jws.header !== undefined) {
|
||||
result.unprotectedHeader = jws.header;
|
||||
}
|
||||
if (verified.resolvedKey) {
|
||||
return { ...result, key: verified.key };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function prepareVerify(options) {
|
||||
return {
|
||||
algorithms: options && validateAlgorithms('algorithms', options.algorithms),
|
||||
crit: options?.crit,
|
||||
};
|
||||
}
|
||||
export async function verifySignature(jws, shared, key) {
|
||||
let parsedProt = {};
|
||||
if (jws.protected) {
|
||||
parsedProt = parseJoseHeader(jws.protected, JWSInvalid, 'JWS Protected Header is invalid');
|
||||
}
|
||||
let joseHeader;
|
||||
if (jws.header !== undefined) {
|
||||
if (!isDisjoint(parsedProt, jws.header)) {
|
||||
throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint');
|
||||
}
|
||||
joseHeader = { ...parsedProt, ...jws.header };
|
||||
}
|
||||
else {
|
||||
joseHeader = parsedProt;
|
||||
}
|
||||
const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, shared.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');
|
||||
}
|
||||
if (shared.algorithms && !shared.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;
|
||||
}
|
||||
const entry = jwsAlgorithm(alg);
|
||||
const data = concat(jws.protected !== undefined ? encode(jws.protected) : new Uint8Array(), encode('.'), typeof jws.payload === 'string'
|
||||
? b64
|
||||
?
|
||||
(shared.b64p ??= encodeBase64url(jws.payload, 'payload', JWSInvalid))
|
||||
: encoder.encode(jws.payload)
|
||||
: jws.payload);
|
||||
const signature = decodeBase64url(jws.signature, 'signature', JWSInvalid);
|
||||
const k = await prepareKey(entry, key, 'verify');
|
||||
const verified = await verify(entry, k, signature, data);
|
||||
if (!verified) {
|
||||
throw new JWSSignatureVerificationFailed();
|
||||
}
|
||||
let payload;
|
||||
if (b64) {
|
||||
payload = decodeBase64url(jws.payload, 'payload', JWSInvalid);
|
||||
}
|
||||
else if (typeof jws.payload === 'string') {
|
||||
payload = encoder.encode(jws.payload);
|
||||
}
|
||||
else {
|
||||
payload = jws.payload;
|
||||
}
|
||||
return { payload, parsedProt, b64, key: k, resolvedKey };
|
||||
}
|
||||
export async function verifyCompact(jws, shared, key) {
|
||||
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');
|
||||
}
|
||||
return verifySignature({ payload, protected: protectedHeader, signature }, shared, key);
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from '../util/errors.js';
|
||||
import { encoder, strictDecoder } from './buffer_utils.js';
|
||||
import { isObject } from './type_checks.js';
|
||||
const 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;
|
||||
export function 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) => {
|
||||
if (value.includes('/')) {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
return `application/${value.toLowerCase()}`;
|
||||
};
|
||||
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;
|
||||
};
|
||||
export function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(strictDecoder.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 !== undefined &&
|
||||
!(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
|
||||
throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, 'iss', 'check_failed');
|
||||
}
|
||||
if (subject !== undefined && payload.sub !== subject) {
|
||||
throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, 'sub', 'check_failed');
|
||||
}
|
||||
if (audience !== undefined &&
|
||||
!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');
|
||||
}
|
||||
validateInput('clockTolerance option', tolerance);
|
||||
const { currentDate } = options;
|
||||
const now = validateInput('currentDate option', epoch(currentDate || new Date()));
|
||||
if ((payload.iat !== undefined || maxTokenAge !== undefined) && 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 !== undefined) {
|
||||
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;
|
||||
}
|
||||
export 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 (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { withAlg as invalidKeyInput } from './invalid_key_input.js';
|
||||
import { isKeyLike, isCryptoKey } from './is_key_like.js';
|
||||
import * as jwk from './type_checks.js';
|
||||
import { decode } from '../util/base64url.js';
|
||||
import { jwkToKey } from './jwk_to_key.js';
|
||||
const tag = (key) => key[Symbol.toStringTag];
|
||||
const jwkMatchesOp = (entry, key, usage) => {
|
||||
const { alg } = entry;
|
||||
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)) {
|
||||
const expectedKeyOp = usage === 'encrypt' || usage === 'decrypt' ? entry.keyOps?.[usage] : usage;
|
||||
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 = (entry, key, usage) => {
|
||||
const { alg } = entry;
|
||||
if (key instanceof Uint8Array)
|
||||
return { kind: BYTES, key };
|
||||
if (jwk.isJWK(key)) {
|
||||
if (jwk.isSecretJWK(key) && jwkMatchesOp(entry, key, usage))
|
||||
return { kind: JWK, key };
|
||||
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(invalidKeyInput(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"`);
|
||||
}
|
||||
return isCryptoKey(key) ? { kind: CRYPTO, key } : { kind: KEYOBJECT, key };
|
||||
};
|
||||
const asymmetricTypeCheck = (entry, key, usage) => {
|
||||
const { alg } = entry;
|
||||
if (jwk.isJWK(key)) {
|
||||
switch (usage) {
|
||||
case 'decrypt':
|
||||
case 'sign':
|
||||
if (jwk.isPrivateJWK(key) && jwkMatchesOp(entry, key, usage))
|
||||
return { kind: JWK, key };
|
||||
throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
|
||||
case 'encrypt':
|
||||
case 'verify':
|
||||
if (jwk.isPublicJWK(key) && jwkMatchesOp(entry, key, usage))
|
||||
return { kind: JWK, key };
|
||||
throw new TypeError(`JSON Web Key for this operation must be a public JWK`);
|
||||
}
|
||||
}
|
||||
if (!isKeyLike(key)) {
|
||||
throw new TypeError(invalidKeyInput(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"`);
|
||||
}
|
||||
}
|
||||
return isCryptoKey(key) ? { kind: CRYPTO, key } : { kind: KEYOBJECT, key };
|
||||
};
|
||||
const BYTES = Symbol();
|
||||
const CRYPTO = Symbol();
|
||||
const KEYOBJECT = Symbol();
|
||||
const JWK = Symbol();
|
||||
export function checkKeyType(entry, key, usage) {
|
||||
return entry.symmetric
|
||||
? symmetricTypeCheck(entry, key, usage)
|
||||
: asymmetricTypeCheck(entry, key, usage);
|
||||
}
|
||||
let cache;
|
||||
const nist = {
|
||||
__proto__: null,
|
||||
prime256v1: 'P-256',
|
||||
secp384r1: 'P-384',
|
||||
secp521r1: 'P-521',
|
||||
};
|
||||
function cached(key, alg) {
|
||||
cache ||= new WeakMap();
|
||||
return cache.get(key)?.[alg];
|
||||
}
|
||||
function store(key, alg, cryptoKey) {
|
||||
const entry = cache.get(key);
|
||||
if (entry) {
|
||||
entry[alg] = cryptoKey;
|
||||
}
|
||||
else {
|
||||
cache.set(key, { [alg]: cryptoKey });
|
||||
}
|
||||
return cryptoKey;
|
||||
}
|
||||
const handleJWK = async (key, jwk, entry) => {
|
||||
const hit = cached(key, entry.alg);
|
||||
if (hit)
|
||||
return hit;
|
||||
const cryptoKey = await jwkToKey(entry, { ...jwk, alg: entry.alg });
|
||||
return store(key, entry.alg, cryptoKey);
|
||||
};
|
||||
const handleKeyObject = (keyObject, entry) => {
|
||||
const hit = cached(keyObject, entry.alg);
|
||||
if (hit)
|
||||
return hit;
|
||||
const isPublic = keyObject.type === 'public';
|
||||
const usages = isPublic ? entry.usages.public : entry.usages.private;
|
||||
const { asymmetricKeyType } = keyObject;
|
||||
const crv = nist[keyObject.asymmetricKeyDetails?.namedCurve];
|
||||
const params = entry.subtleFor?.({ crv, asymmetricKeyType }) ?? entry.subtle;
|
||||
return store(keyObject, entry.alg, keyObject.toCryptoKey(params, isPublic, usages));
|
||||
};
|
||||
export async function prepareKey(entry, key, usage) {
|
||||
const tagged = checkKeyType(entry, key, usage);
|
||||
switch (tagged.kind) {
|
||||
case BYTES:
|
||||
case CRYPTO:
|
||||
return tagged.key;
|
||||
case JWK: {
|
||||
if (tagged.key.k) {
|
||||
return decode(tagged.key.k);
|
||||
}
|
||||
if (!Object.isFrozen(tagged.key)) {
|
||||
const { key_ops } = tagged.key;
|
||||
if (Array.isArray(key_ops))
|
||||
Object.freeze(key_ops);
|
||||
Object.freeze(tagged.key);
|
||||
}
|
||||
return handleJWK(tagged.key, tagged.key, entry);
|
||||
}
|
||||
case KEYOBJECT: {
|
||||
const keyObject = tagged.key;
|
||||
if (keyObject.type === 'secret') {
|
||||
return keyObject.export();
|
||||
}
|
||||
if ('toCryptoKey' in keyObject && typeof keyObject.toCryptoKey === 'function') {
|
||||
return handleKeyObject(keyObject, entry);
|
||||
}
|
||||
return handleJWK(keyObject, keyObject.export({ format: 'jwk' }), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { JOSENotSupported } from '../util/errors.js';
|
||||
import { maybeJWSAlgorithm } from './jws_algorithms.js';
|
||||
import { maybeJWEAlgorithm } from './jwe_algorithms.js';
|
||||
function unsupportedAlgorithm() {
|
||||
return new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
|
||||
}
|
||||
export function keyAlgorithm(alg) {
|
||||
if (typeof alg !== 'string') {
|
||||
throw unsupportedAlgorithm();
|
||||
}
|
||||
const entry = maybeJWSAlgorithm(alg) ?? maybeJWEAlgorithm(alg);
|
||||
if (!entry) {
|
||||
throw unsupportedAlgorithm();
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export function table(entries) {
|
||||
const out = { __proto__: null };
|
||||
for (const alg of Object.keys(entries)) {
|
||||
out[alg] = { ...entries[alg], alg };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
import { encode as b64u } from '../util/base64url.js';
|
||||
import { prepareKey } from './key.js';
|
||||
import { jwkToKey } from './jwk_to_key.js';
|
||||
import { jweAlgorithm, jweEncryption } from './jwe_algorithms.js';
|
||||
import { JOSENotSupported, JWEInvalid } from '../util/errors.js';
|
||||
import { decodeBase64url, digest } from './helpers.js';
|
||||
import { generateCek, encrypt, decrypt } from './content_encryption.js';
|
||||
import { isObject } from './type_checks.js';
|
||||
import { checkCryptoKey, checkUsage } from './crypto_key.js';
|
||||
import { checkModulusLength } from './signing.js';
|
||||
import { concat, encode, uint32be } from './buffer_utils.js';
|
||||
import { assertCryptoKey } from './is_key_like.js';
|
||||
function checkEcdhCryptoKey(key, usage) {
|
||||
switch (key.algorithm.name) {
|
||||
case 'ECDH':
|
||||
case 'X25519':
|
||||
break;
|
||||
default:
|
||||
throw new TypeError('CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519');
|
||||
}
|
||||
checkUsage(key, usage);
|
||||
}
|
||||
function checkKeySize(key, alg) {
|
||||
if (key.algorithm.length !== parseInt(alg.slice(1, 4), 10)) {
|
||||
throw new TypeError(`Invalid key size for alg: ${alg}`);
|
||||
}
|
||||
}
|
||||
function aeskwCryptoKey(key, alg, usage) {
|
||||
if (key instanceof Uint8Array) {
|
||||
return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage]);
|
||||
}
|
||||
checkCryptoKey(key, jweAlgorithm(alg).subtle, usage);
|
||||
return key;
|
||||
}
|
||||
async function aeskwWrap(alg, key, cek) {
|
||||
const cryptoKey = await aeskwCryptoKey(key, alg, 'wrapKey');
|
||||
checkKeySize(cryptoKey, alg);
|
||||
const cryptoKeyCek = await crypto.subtle.importKey('raw', cek, { hash: 'SHA-256', name: 'HMAC' }, true, ['sign']);
|
||||
return new Uint8Array(await crypto.subtle.wrapKey('raw', cryptoKeyCek, cryptoKey, 'AES-KW'));
|
||||
}
|
||||
async function aeskwUnwrap(alg, key, encryptedKey) {
|
||||
const cryptoKey = await aeskwCryptoKey(key, alg, 'unwrapKey');
|
||||
checkKeySize(cryptoKey, alg);
|
||||
const cryptoKeyCek = await crypto.subtle.unwrapKey('raw', encryptedKey, cryptoKey, 'AES-KW', { hash: 'SHA-256', name: 'HMAC' }, true, ['sign']);
|
||||
return new Uint8Array(await crypto.subtle.exportKey('raw', cryptoKeyCek));
|
||||
}
|
||||
async function aesGcmKwWrap(gcm, key, cek, iv) {
|
||||
const wrapped = await encrypt(gcm, cek, key, iv, new Uint8Array());
|
||||
return {
|
||||
encryptedKey: wrapped.ciphertext,
|
||||
iv: b64u(wrapped.iv),
|
||||
tag: b64u(wrapped.tag),
|
||||
};
|
||||
}
|
||||
async function aesGcmKwUnwrap(gcm, key, encryptedKey, iv, tag) {
|
||||
return decrypt(gcm, key, encryptedKey, iv, tag, new Uint8Array());
|
||||
}
|
||||
const subtleAlgorithm = (alg) => {
|
||||
switch (alg) {
|
||||
case 'RSA-OAEP':
|
||||
case 'RSA-OAEP-256':
|
||||
case 'RSA-OAEP-384':
|
||||
case 'RSA-OAEP-512':
|
||||
return 'RSA-OAEP';
|
||||
default:
|
||||
throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
|
||||
}
|
||||
};
|
||||
async function rsaesEncrypt(alg, key, cek) {
|
||||
checkCryptoKey(key, jweAlgorithm(alg).subtle, 'encrypt');
|
||||
checkModulusLength(alg, key);
|
||||
return new Uint8Array(await crypto.subtle.encrypt(subtleAlgorithm(alg), key, cek));
|
||||
}
|
||||
async function rsaesDecrypt(alg, key, encryptedKey) {
|
||||
checkCryptoKey(key, jweAlgorithm(alg).subtle, 'decrypt');
|
||||
checkModulusLength(alg, key);
|
||||
return new Uint8Array(await crypto.subtle.decrypt(subtleAlgorithm(alg), key, encryptedKey));
|
||||
}
|
||||
function pbes2CryptoKey(key, alg) {
|
||||
if (key instanceof Uint8Array) {
|
||||
return crypto.subtle.importKey('raw', key, 'PBKDF2', false, [
|
||||
'deriveBits',
|
||||
]);
|
||||
}
|
||||
checkCryptoKey(key, jweAlgorithm(alg).subtle, 'deriveBits');
|
||||
return key;
|
||||
}
|
||||
const concatSalt = (alg, p2sInput) => concat(encode(alg), Uint8Array.of(0x00), p2sInput);
|
||||
async function deriveKey(p2s, alg, p2c, key) {
|
||||
if (!(p2s instanceof Uint8Array) || p2s.length < 8) {
|
||||
throw new JWEInvalid('PBES2 Salt Input must be 8 or more octets');
|
||||
}
|
||||
if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) {
|
||||
throw new JWEInvalid('PBES2 Count Input must be a positive integer');
|
||||
}
|
||||
const salt = concatSalt(alg, p2s);
|
||||
const keylen = parseInt(alg.slice(13, 16), 10);
|
||||
const subtleAlg = {
|
||||
hash: `SHA-${alg.slice(8, 11)}`,
|
||||
iterations: p2c,
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
};
|
||||
const cryptoKey = await pbes2CryptoKey(key, alg);
|
||||
return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen));
|
||||
}
|
||||
async function pbes2kwWrap(alg, key, cek, p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16))) {
|
||||
const derived = await deriveKey(p2s, alg, p2c, key);
|
||||
const encryptedKey = await aeskwWrap(alg.slice(-6), derived, cek);
|
||||
return { encryptedKey, p2c, p2s: b64u(p2s) };
|
||||
}
|
||||
async function pbes2kwUnwrap(alg, key, encryptedKey, p2c, p2s) {
|
||||
const derived = await deriveKey(p2s, alg, p2c, key);
|
||||
return aeskwUnwrap(alg.slice(-6), derived, encryptedKey);
|
||||
}
|
||||
function lengthAndInput(input) {
|
||||
return concat(uint32be(input.length), input);
|
||||
}
|
||||
async function concatKdf(Z, L, OtherInfo) {
|
||||
const dkLen = L >> 3;
|
||||
const hashLen = 32;
|
||||
const reps = Math.ceil(dkLen / hashLen);
|
||||
const dk = new Uint8Array(reps * hashLen);
|
||||
for (let i = 1; i <= reps; i++) {
|
||||
const hashInput = new Uint8Array(4 + Z.length + OtherInfo.length);
|
||||
hashInput.set(uint32be(i), 0);
|
||||
hashInput.set(Z, 4);
|
||||
hashInput.set(OtherInfo, 4 + Z.length);
|
||||
const hashResult = await digest('sha256', hashInput);
|
||||
dk.set(hashResult, (i - 1) * hashLen);
|
||||
}
|
||||
return dk.slice(0, dkLen);
|
||||
}
|
||||
async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) {
|
||||
checkEcdhCryptoKey(publicKey);
|
||||
checkEcdhCryptoKey(privateKey, 'deriveBits');
|
||||
const algorithmID = lengthAndInput(encode(algorithm));
|
||||
const partyUInfo = lengthAndInput(apu);
|
||||
const partyVInfo = lengthAndInput(apv);
|
||||
const suppPubInfo = uint32be(keyLength);
|
||||
const suppPrivInfo = new Uint8Array();
|
||||
const otherInfo = concat(algorithmID, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo);
|
||||
const Z = new Uint8Array(await crypto.subtle.deriveBits({
|
||||
name: publicKey.algorithm.name,
|
||||
public: publicKey,
|
||||
}, privateKey, getEcdhBitLength(publicKey)));
|
||||
return concatKdf(Z, keyLength, otherInfo);
|
||||
}
|
||||
function getEcdhBitLength(publicKey) {
|
||||
if (publicKey.algorithm.name === 'X25519') {
|
||||
return 256;
|
||||
}
|
||||
return (Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3);
|
||||
}
|
||||
function ecdhesAllowed(key) {
|
||||
switch (key.algorithm.namedCurve) {
|
||||
case 'P-256':
|
||||
case 'P-384':
|
||||
case 'P-521':
|
||||
return true;
|
||||
default:
|
||||
return key.algorithm.name === 'X25519';
|
||||
}
|
||||
}
|
||||
const unsupportedAlgHeader = 'Invalid or unsupported "alg" (JWE Algorithm) header value';
|
||||
function assertEncryptedKey(encryptedKey) {
|
||||
if (encryptedKey === undefined)
|
||||
throw new JWEInvalid('JWE Encrypted Key missing');
|
||||
}
|
||||
export async function decryptKeyManagement(alg, enc, key, encryptedKey, joseHeader, options) {
|
||||
switch (alg) {
|
||||
case 'dir': {
|
||||
if (encryptedKey !== undefined)
|
||||
throw new JWEInvalid('Encountered unexpected JWE Encrypted Key');
|
||||
return key;
|
||||
}
|
||||
case 'ECDH-ES':
|
||||
if (encryptedKey !== undefined)
|
||||
throw new JWEInvalid('Encountered unexpected JWE Encrypted Key');
|
||||
case 'ECDH-ES+A128KW':
|
||||
case 'ECDH-ES+A192KW':
|
||||
case 'ECDH-ES+A256KW': {
|
||||
if (!isObject(joseHeader.epk))
|
||||
throw new JWEInvalid(`JOSE Header "epk" (Ephemeral Public Key) missing or invalid`);
|
||||
assertCryptoKey(key);
|
||||
if (!ecdhesAllowed(key))
|
||||
throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime');
|
||||
const epk = await jwkToKey(jweAlgorithm(alg), joseHeader.epk);
|
||||
let partyUInfo;
|
||||
let partyVInfo;
|
||||
if (joseHeader.apu !== undefined) {
|
||||
if (typeof joseHeader.apu !== 'string')
|
||||
throw new JWEInvalid(`JOSE Header "apu" (Agreement PartyUInfo) invalid`);
|
||||
partyUInfo = decodeBase64url(joseHeader.apu, 'apu', JWEInvalid);
|
||||
}
|
||||
if (joseHeader.apv !== undefined) {
|
||||
if (typeof joseHeader.apv !== 'string')
|
||||
throw new JWEInvalid(`JOSE Header "apv" (Agreement PartyVInfo) invalid`);
|
||||
partyVInfo = decodeBase64url(joseHeader.apv, 'apv', JWEInvalid);
|
||||
}
|
||||
const sharedSecret = await ecdhesDeriveKey(epk, key, alg === 'ECDH-ES' ? enc.alg : alg, alg === 'ECDH-ES' ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo);
|
||||
if (alg === 'ECDH-ES')
|
||||
return sharedSecret;
|
||||
assertEncryptedKey(encryptedKey);
|
||||
return aeskwUnwrap(alg.slice(-6), sharedSecret, encryptedKey);
|
||||
}
|
||||
case 'RSA-OAEP':
|
||||
case 'RSA-OAEP-256':
|
||||
case 'RSA-OAEP-384':
|
||||
case 'RSA-OAEP-512': {
|
||||
assertEncryptedKey(encryptedKey);
|
||||
assertCryptoKey(key);
|
||||
return rsaesDecrypt(alg, key, encryptedKey);
|
||||
}
|
||||
case 'PBES2-HS256+A128KW':
|
||||
case 'PBES2-HS384+A192KW':
|
||||
case 'PBES2-HS512+A256KW': {
|
||||
assertEncryptedKey(encryptedKey);
|
||||
if (typeof joseHeader.p2c !== 'number')
|
||||
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`);
|
||||
const p2cLimit = options?.maxPBES2Count || 10_000;
|
||||
if (joseHeader.p2c > p2cLimit)
|
||||
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`);
|
||||
if (typeof joseHeader.p2s !== 'string')
|
||||
throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`);
|
||||
let p2s;
|
||||
p2s = decodeBase64url(joseHeader.p2s, 'p2s', JWEInvalid);
|
||||
return pbes2kwUnwrap(alg, key, encryptedKey, joseHeader.p2c, p2s);
|
||||
}
|
||||
case 'A128KW':
|
||||
case 'A192KW':
|
||||
case 'A256KW': {
|
||||
assertEncryptedKey(encryptedKey);
|
||||
return aeskwUnwrap(alg, key, encryptedKey);
|
||||
}
|
||||
case 'A128GCMKW':
|
||||
case 'A192GCMKW':
|
||||
case 'A256GCMKW': {
|
||||
assertEncryptedKey(encryptedKey);
|
||||
if (typeof joseHeader.iv !== 'string')
|
||||
throw new JWEInvalid(`JOSE Header "iv" (Initialization Vector) missing or invalid`);
|
||||
if (typeof joseHeader.tag !== 'string')
|
||||
throw new JWEInvalid(`JOSE Header "tag" (Authentication Tag) missing or invalid`);
|
||||
let iv;
|
||||
iv = decodeBase64url(joseHeader.iv, 'iv', JWEInvalid);
|
||||
let tag;
|
||||
tag = decodeBase64url(joseHeader.tag, 'tag', JWEInvalid);
|
||||
return aesGcmKwUnwrap(jweEncryption(jweAlgorithm(alg).gcmkw), key, encryptedKey, iv, tag);
|
||||
}
|
||||
default: {
|
||||
throw new JOSENotSupported(unsupportedAlgHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function encryptKeyManagement(alg, enc, key, providedCek, providedParameters = {}) {
|
||||
let encryptedKey;
|
||||
let parameters;
|
||||
let cek;
|
||||
switch (alg) {
|
||||
case 'dir': {
|
||||
cek = key;
|
||||
break;
|
||||
}
|
||||
case 'ECDH-ES':
|
||||
case 'ECDH-ES+A128KW':
|
||||
case 'ECDH-ES+A192KW':
|
||||
case 'ECDH-ES+A256KW': {
|
||||
assertCryptoKey(key);
|
||||
if (!ecdhesAllowed(key)) {
|
||||
throw new JOSENotSupported('ECDH with the provided key is not allowed or not supported by your javascript runtime');
|
||||
}
|
||||
const { apu, apv } = providedParameters;
|
||||
let ephemeralKey;
|
||||
if (providedParameters.epk) {
|
||||
ephemeralKey = (await prepareKey(jweAlgorithm(alg), providedParameters.epk, 'decrypt'));
|
||||
}
|
||||
else {
|
||||
ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, true, ['deriveBits'])).privateKey;
|
||||
}
|
||||
const subtle = crypto.subtle;
|
||||
let exportableEpk = ephemeralKey;
|
||||
if (!exportableEpk.extractable) {
|
||||
if (typeof subtle.getPublicKey !== 'function') {
|
||||
throw new TypeError('CryptoKey for "epk" must be extractable');
|
||||
}
|
||||
exportableEpk = await subtle.getPublicKey(ephemeralKey, []);
|
||||
}
|
||||
const { x, y, crv, kty } = (await subtle.exportKey('jwk', exportableEpk));
|
||||
const sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, alg === 'ECDH-ES' ? enc.alg : alg, alg === 'ECDH-ES' ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv);
|
||||
parameters = { epk: { x, crv, kty } };
|
||||
if (kty === 'EC')
|
||||
parameters.epk.y = y;
|
||||
if (apu)
|
||||
parameters.apu = b64u(apu);
|
||||
if (apv)
|
||||
parameters.apv = b64u(apv);
|
||||
if (alg === 'ECDH-ES') {
|
||||
cek = sharedSecret;
|
||||
break;
|
||||
}
|
||||
cek = providedCek || generateCek(enc);
|
||||
const kwAlg = alg.slice(-6);
|
||||
encryptedKey = await aeskwWrap(kwAlg, sharedSecret, cek);
|
||||
break;
|
||||
}
|
||||
case 'RSA-OAEP':
|
||||
case 'RSA-OAEP-256':
|
||||
case 'RSA-OAEP-384':
|
||||
case 'RSA-OAEP-512': {
|
||||
cek = providedCek || generateCek(enc);
|
||||
assertCryptoKey(key);
|
||||
encryptedKey = await rsaesEncrypt(alg, key, cek);
|
||||
break;
|
||||
}
|
||||
case 'PBES2-HS256+A128KW':
|
||||
case 'PBES2-HS384+A192KW':
|
||||
case 'PBES2-HS512+A256KW': {
|
||||
cek = providedCek || generateCek(enc);
|
||||
const { p2c, p2s } = providedParameters;
|
||||
({ encryptedKey, ...parameters } = await pbes2kwWrap(alg, key, cek, p2c, p2s));
|
||||
break;
|
||||
}
|
||||
case 'A128KW':
|
||||
case 'A192KW':
|
||||
case 'A256KW': {
|
||||
cek = providedCek || generateCek(enc);
|
||||
encryptedKey = await aeskwWrap(alg, key, cek);
|
||||
break;
|
||||
}
|
||||
case 'A128GCMKW':
|
||||
case 'A192GCMKW':
|
||||
case 'A256GCMKW': {
|
||||
cek = providedCek || generateCek(enc);
|
||||
const { iv } = providedParameters;
|
||||
({ encryptedKey, ...parameters } = await aesGcmKwWrap(jweEncryption(jweAlgorithm(alg).gcmkw), key, cek, iv));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new JOSENotSupported(unsupportedAlgHeader);
|
||||
}
|
||||
}
|
||||
return { cek, encryptedKey, parameters };
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { JOSENotSupported, JWEInvalid, JWSInvalid } from '../util/errors.js';
|
||||
export const JWS_RECOGNIZED = new Map([['b64', true]]);
|
||||
export const JWE_RECOGNIZED = new Map();
|
||||
export function 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);
|
||||
}
|
||||
export function validateCritDuplicates(Err, protectedHeader) {
|
||||
const { crit } = protectedHeader ?? {};
|
||||
if (Array.isArray(crit) && new Set(crit).size !== crit.length) {
|
||||
throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values');
|
||||
}
|
||||
}
|
||||
export function 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);
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { checkCryptoKey } from './crypto_key.js';
|
||||
export function checkModulusLength(alg, key) {
|
||||
const { modulusLength } = key.algorithm;
|
||||
if (typeof modulusLength !== 'number' || modulusLength < 2048) {
|
||||
throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
|
||||
}
|
||||
}
|
||||
function checkSigCryptoKey(entry, key, usage) {
|
||||
checkCryptoKey(key, entry.subtle, usage);
|
||||
if (entry.minModulusLength) {
|
||||
checkModulusLength(entry.alg, key);
|
||||
}
|
||||
}
|
||||
async function getSigKey(entry, key, usage) {
|
||||
if (key instanceof Uint8Array) {
|
||||
return crypto.subtle.importKey('raw', key, entry.subtle, false, [
|
||||
usage,
|
||||
]);
|
||||
}
|
||||
checkSigCryptoKey(entry, key, usage);
|
||||
return key;
|
||||
}
|
||||
export async function sign(entry, key, data) {
|
||||
const cryptoKey = await getSigKey(entry, key, 'sign');
|
||||
const signature = await crypto.subtle.sign(entry.operation, cryptoKey, data);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
export async function verify(entry, key, signature, data) {
|
||||
const cryptoKey = await getSigKey(entry, key, 'verify');
|
||||
try {
|
||||
return await crypto.subtle.verify(entry.operation, cryptoKey, signature, data);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
const isObjectLike = (value) => typeof value === 'object' && value !== null;
|
||||
export function 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;
|
||||
}
|
||||
export function 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;
|
||||
}
|
||||
export const isJWK = (key) => isObject(key) && typeof key.kty === 'string';
|
||||
export const isPrivateJWK = (key) => key.kty !== 'oct' &&
|
||||
((key.kty === 'AKP' && typeof key.priv === 'string') || typeof key.d === 'string');
|
||||
export const isPublicJWK = (key) => key.kty !== 'oct' && key.d === undefined && key.priv === undefined;
|
||||
export const isSecretJWK = (key) => key.kty === 'oct' && typeof key.k === 'string';
|
||||
Reference in New Issue
Block a user