UNPKG

@sphereon/ssi-sdk-ext.jwt-service

Version:

364 lines (338 loc) 11.5 kB
import { defaultRandomSource, randomBytes, type RandomSource } from '@stablelib/random' import { base64ToBytes, bytesToBase64url, decodeBase64url } from '@veramo/utils' import * as jose from 'jose' import type { JWEKeyManagementHeaderParameters, JWTDecryptOptions } from 'jose' // // @ts-ignore // import type { KeyLike } from 'jose/dist/types/types' export type KeyLike = { type: string } // @ts-ignore import * as u8a from 'uint8arrays' const { fromString, toString, concat } = u8a import { type JweAlg, JweAlgs, type JweEnc, JweEncs, type JweHeader, type JweJsonGeneral, type JweProtectedHeader, type JweRecipient, type JweRecipientUnprotectedHeader, type JwsPayload, } from '../types/IJwtService' export interface EncryptionResult { ciphertext: Uint8Array tag: Uint8Array iv: Uint8Array protectedHeader?: string recipients?: JweRecipient[] cek?: Uint8Array } export const generateContentEncryptionKey = async ({ alg, randomSource = defaultRandomSource, }: { alg: JweEnc randomSource?: RandomSource }): Promise<Uint8Array> => { let length: number switch (alg) { case 'A128GCM': length = 16 break case 'A192GCM': length = 24 break case 'A128CBC-HS256': case 'A256GCM': length = 32 break case 'A192CBC-HS384': length = 48 break case 'A256CBC-HS512': length = 64 break default: length = 32 } return randomBytes(length, randomSource) } /* export const generateContentEncryptionKeyfdsdf = async ({type = 'Secp256r1', ...rest}: { type?: Extract<TKeyType, 'Secp256r1' | 'RSA'>, kms?: string }, context: IAgentContext<ISphereonKeyManager>): Promise<EphemeralPublicKey> => { const kms = rest.kms ?? await context.agent.keyManagerGetDefaultKeyManagementSystem() const key = await context.agent.keyManagerCreate({kms, type, opts: {ephemeral: true}}) const jwk = toJwkFromKey(key, {use: JwkKeyUse.Encryption, noKidThumbprint: true}) } */ export interface JwtEncrypter { alg: string enc: string encrypt: (payload: JwsPayload, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise<EncryptionResult> encryptCek?: (cek: Uint8Array) => Promise<JweRecipient> } export interface JweEncrypter { alg: string enc: string encrypt: (payload: Uint8Array, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise<EncryptionResult> encryptCek?: (cek: Uint8Array) => Promise<JweRecipient> } export interface JweDecrypter { alg: string enc: string decrypt: (sealed: Uint8Array, iv: Uint8Array, aad?: Uint8Array, recipient?: JweRecipient) => Promise<Uint8Array | null> } function jweAssertValid(jwe: JweJsonGeneral) { if (!(jwe.protected && jwe.iv && jwe.ciphertext && jwe.tag)) { throw Error('JWE is missing properties: protected, iv, ciphertext and/or tag') } if (jwe.recipients) { jwe.recipients.map((recipient: JweRecipient) => { if (!(recipient.header && recipient.encrypted_key)) { throw Error('Malformed JWE recipients; no header and encrypted key present') } }) } } function jweEncode({ ciphertext, tag, iv, protectedHeader, recipients, aad, unprotected, }: EncryptionResult & { aad?: Uint8Array unprotected?: JweHeader }): JweJsonGeneral { if (!recipients || recipients.length === 0) { throw Error(`No recipient found`) } return { ...(unprotected && { unprotected }), protected: <string>protectedHeader, iv: bytesToBase64url(iv), ciphertext: bytesToBase64url(ciphertext), ...(tag && { tag: bytesToBase64url(tag) }), ...(aad && { aad: bytesToBase64url(aad) }), recipients, } satisfies JweJsonGeneral } export class CompactJwtEncrypter implements JweEncrypter { private _alg: JweAlg | undefined private _enc: JweEnc | undefined private _keyManagementParams: JWEKeyManagementHeaderParameters | undefined private recipientKey: Uint8Array | jose.KeyLike //,EphemeralPublicKey | BaseJWK; private expirationTime private issuer: string | undefined private audience: string | string[] | undefined constructor(args: { key: Uint8Array | jose.KeyLike /*EphemeralPublicKey | BaseJWK*/ alg?: JweAlg enc?: JweEnc keyManagementParams?: JWEKeyManagementHeaderParameters expirationTime?: number | string | Date issuer?: string audience?: string | string[] }) { if (args?.alg) { this._alg = args.alg } if (args?.enc) { this._enc = args.enc } this._keyManagementParams = args.keyManagementParams this.recipientKey = args.key this.expirationTime = args.expirationTime this.issuer = args.issuer this.audience = args.audience } get enc(): string { if (!this._enc) { throw Error(`enc not set`) } return this._enc } set enc(value: JweEnc | string) { // @ts-ignore if (!JweEncs.includes(value)) { throw Error(`invalid JWE enc value ${value}`) } this._enc = value as JweEnc } get alg(): string { if (!this._alg) { throw Error(`alg not set`) } return this._alg } set alg(value: JweAlg | string) { // @ts-ignore if (!JweAlgs.includes(value)) { throw Error(`invalid JWE alg value ${value}`) } this._alg = value as JweAlg } async encryptCompactJWT(payload: JwsPayload, jweProtectedHeader: JweProtectedHeader, aad?: Uint8Array | undefined): Promise<string> { const protectedHeader = { ...jweProtectedHeader, alg: jweProtectedHeader.alg ?? this._alg, enc: jweProtectedHeader.enc ?? this._enc, } if (!protectedHeader.alg || !protectedHeader.enc) { return Promise.reject(Error(`no 'alg' or 'enc' value set for the protected JWE header!`)) } this.enc = protectedHeader.enc this.alg = protectedHeader.alg if (payload.exp) { this.expirationTime = payload.exp } if (payload.iss) { this.issuer = payload.iss } if (payload.aud) { this.audience = payload.aud } const encrypt = new jose.EncryptJWT(payload).setProtectedHeader({ ...protectedHeader, alg: this.alg, enc: this.enc, }) if (this._alg!.startsWith('ECDH')) { if (!this._keyManagementParams) { return Promise.reject(Error(`ECDH requires key management params`)) } encrypt.setKeyManagementParameters(this._keyManagementParams!) } if (this.expirationTime !== undefined) { encrypt.setExpirationTime(this.expirationTime) } if (this.issuer) { encrypt.setIssuer(this.issuer) } if (this.audience) { encrypt.setAudience(this.audience) } return await encrypt.encrypt(this.recipientKey) } public static async decryptCompactJWT(jwt: string, key: KeyLike | Uint8Array, options?: JWTDecryptOptions) { return await jose.jwtDecrypt(jwt, key, options) } async encrypt(payload: Uint8Array, jweProtectedHeader: JweProtectedHeader, aad?: Uint8Array | undefined): Promise<EncryptionResult> { const jwt = await this.encryptCompactJWT(JSON.parse(toString(payload)), jweProtectedHeader, aad) const [protectedHeader, encryptedKey, ivB64, payloadB64, tagB64] = jwt.split('.') //[jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join('.'); console.log(`FIXME: TO EncryptionResult`) return { protectedHeader, tag: base64ToBytes(tagB64), ciphertext: base64ToBytes(payloadB64), iv: base64ToBytes(ivB64), recipients: [ { //fixme // header: protectedHeader, ...(encryptedKey && { encrypted_key: encryptedKey }), }, ], } } // encryptCek?: ((cek: Uint8Array) => Promise<JweRecipient>) | undefined; } export async function createJwe( cleartext: Uint8Array, encrypters: JweEncrypter[], protectedHeader: JweProtectedHeader, aad?: Uint8Array, ): Promise<JweJsonGeneral> { if (encrypters.length === 0) { throw Error('JWE needs at least 1 encryptor') } if (encrypters.find((enc) => enc.alg === 'dir' || enc.alg === 'ECDH-ES')) { if (encrypters.length !== 1) { throw Error(`JWE can only do "dir" or "ECDH-ES" encryption with one key. ${encrypters.length} supplied`) } const encryptionResult = await encrypters[0].encrypt(cleartext, protectedHeader, aad) return jweEncode({ ...encryptionResult, aad }) } else { const tmpEnc = encrypters[0].enc if (!encrypters.reduce((acc, encrypter) => acc && encrypter.enc === tmpEnc, true)) { throw new Error('invalid_argument: Incompatible encrypters passed') } let cek: Uint8Array | undefined = undefined let jwe: JweJsonGeneral | undefined = undefined for (const encrypter of encrypters) { if (!cek) { const encryptionResult = await encrypter.encrypt(cleartext, protectedHeader, aad) cek = encryptionResult.cek jwe = jweEncode({ ...encryptionResult, aad }) } else { const recipient = await encrypter.encryptCek?.(cek) if (recipient) { jwe?.recipients?.push(recipient) } } } if (!jwe) { throw Error(`No JWE constructed`) } return jwe } } /** * Merges all headers, so we get a unified header. * * @param protectedHeader * @param unprotectedHeader * @param recipientUnprotectedHeader */ export function jweMergeHeaders({ protectedHeader, unprotectedHeader, recipientUnprotectedHeader, }: { protectedHeader?: JweProtectedHeader unprotectedHeader?: JweHeader recipientUnprotectedHeader?: JweRecipientUnprotectedHeader }): JweHeader { // TODO: Check that all headers/params are disjoint! const header = { ...protectedHeader, ...unprotectedHeader, ...recipientUnprotectedHeader } if (!header.alg || !header.enc) { throw Error(`Either 'alg' or 'enc' are missing from the headers`) } return header as JweHeader } export async function decryptJwe(jwe: JweJsonGeneral, decrypter: JweDecrypter): Promise<Uint8Array> { jweAssertValid(jwe) const protectedHeader: JweProtectedHeader = JSON.parse(decodeBase64url(jwe.protected)) if (protectedHeader?.enc !== decrypter.enc) { return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`)) } else if (!jwe.tag) { return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`)) } const sealed = toWebCryptoCiphertext(jwe.ciphertext, jwe.tag) const aad = fromString(jwe.aad ? `${jwe.protected}.${jwe.aad}` : jwe.protected) let cleartext = null if (protectedHeader.alg === 'dir' && decrypter.alg === 'dir') { cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad) } else if (!jwe.recipients || jwe.recipients.length === 0) { throw Error('missing recipients for JWE') } else { for (let i = 0; !cleartext && i < jwe.recipients.length; i++) { const recipient: JweRecipient = jwe.recipients[i] recipient.header = { ...recipient.header, ...protectedHeader } as JweRecipientUnprotectedHeader if (recipient.header.alg === decrypter.alg) { cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad, recipient) } } } if (cleartext === null) throw new Error('failure: Failed to decrypt') return cleartext } export function toWebCryptoCiphertext(ciphertext: string, tag: string): Uint8Array { return concat([base64ToBytes(ciphertext), base64ToBytes(tag)]) }