import * as Uint8arrays from "uint8arrays" import * as Crypto from "../components/crypto/implementation.js" import { Potency, Fact, Resource, Ucan, UcanHeader, UcanPayload } from "./types.js" import { base64 } from "../common/index.js" import { didToPublicKey } from "../did/transformers.js" /** * Create a UCAN, User Controlled Authorization Networks, JWT. * This JWT can be used for authorization. * * ### Header * * `alg`, Algorithm, the type of signature. * `typ`, Type, the type of this data structure, JWT. * `uav`, UCAN version. * * ### Payload * * `aud`, Audience, the ID of who it's intended for. * `exp`, Expiry, unix timestamp of when the jwt is no longer valid. * `iss`, Issuer, the ID of who sent this. * `nbf`, Not Before, unix timestamp of when the jwt becomes valid. * `prf`, Proof, an optional nested token with equal or greater privileges. * `ptc`, Potency, which rights come with the token. * `rsc`, Resource, the involved resource. * */ export async function build({ addSignature = true, audience, dependencies, facts = [], issuer, lifetimeInSeconds = 120, expiration, potency = "APPEND", proof, resource }: { addSignature?: boolean audience: string dependencies: { crypto: Crypto.Implementation } facts?: Array issuer: string lifetimeInSeconds?: number expiration?: number potency?: Potency proof?: string | Ucan resource?: Resource }): Promise { const currentTimeInSeconds = Math.floor(Date.now() / 1000) const decodedProof = proof ? (typeof proof === "string" ? decode(proof) : proof) : null // Header const header = { alg: await dependencies.crypto.keystore.getUcanAlgorithm(), typ: "JWT", uav: "1.0.0" // actually 0.3.1 but server isn't updated yet } // Timestamps let exp = expiration || (currentTimeInSeconds + lifetimeInSeconds) let nbf = currentTimeInSeconds - 120 if (decodedProof) { const prf = decodedProof.payload exp = Math.min(prf.exp, exp) nbf = Math.max(prf.nbf, nbf) } // Payload const payload = { aud: audience, exp: exp, fct: facts, iss: issuer, nbf: nbf, prf: proof ? (typeof proof === "string" ? proof : encode(proof)) : null, ptc: potency, rsc: resource ? resource : (decodedProof ? decodedProof.payload.rsc : "*"), } const signature = addSignature ? await sign(dependencies.crypto, header, payload) : null return { header, payload, signature } } /** * Try to decode a UCAN. * Will throw if it fails. * * @param ucan The encoded UCAN to decode */ export function decode(ucan: string): Ucan { const split = ucan.split(".") const header = JSON.parse(base64.urlDecode(split[0])) const payload = JSON.parse(base64.urlDecode(split[1])) return { header, payload, signature: split[2] || null } } /** * Encode a UCAN. * * @param ucan The UCAN to encode */ export function encode(ucan: Ucan): string { const encodedHeader = encodeHeader(ucan.header) const encodedPayload = encodePayload(ucan.payload) return encodedHeader + "." + encodedPayload + "." + ucan.signature } /** * Encode the header of a UCAN. * * @param header The UcanHeader to encode */ export function encodeHeader(header: UcanHeader): string { return base64.urlEncode(JSON.stringify(header)) } /** * Encode the payload of a UCAN. * * @param payload The UcanPayload to encode */ export function encodePayload(payload: UcanPayload): string { return base64.urlEncode(JSON.stringify({ ...payload })) } /** * Check if a UCAN is expired. * * @param ucan The UCAN to validate */ export function isExpired(ucan: Ucan): boolean { return ucan.payload.exp <= Math.floor(Date.now() / 1000) } /** * Check if a UCAN is self-signed. */ export function isSelfSigned(ucan: Ucan): boolean { return ucan.payload.iss === ucan.payload.aud } /** * Check if a UCAN is valid. * * @param ucan The decoded UCAN * @param did The DID associated with the signature of the UCAN */ export async function isValid(crypto: Crypto.Implementation, ucan: Ucan): Promise { try { const encodedHeader = encodeHeader(ucan.header) const encodedPayload = encodePayload(ucan.payload) const { publicKey, type } = didToPublicKey(crypto, ucan.payload.iss) const algo = crypto.did.keyTypes[type] const a = await algo.verify({ publicKey, message: Uint8arrays.fromString(`${encodedHeader}.${encodedPayload}`, "utf8"), signature: Uint8arrays.fromString(ucan.signature || "", "base64url") }) if (!a) return a if (!ucan.payload.prf) return true // Verify proofs const prf = decode(ucan.payload.prf) const b = prf.payload.aud === ucan.payload.iss if (!b) return b return await isValid(crypto, prf) } catch { return false } } /** * Given a UCAN, lookup the root issuer. * * Throws when given an improperly formatted UCAN. * This could be a nested UCAN (ie. proof). * * @param ucan A UCAN. * @returns The root issuer. */ export function rootIssuer(ucan: string | Ucan, level = 0): string { const p = typeof ucan === "string" ? extractPayload(ucan, level) : ucan.payload if (p.prf) return rootIssuer(p.prf, level + 1) return p.iss } /** * Generate UCAN signature. */ export async function sign( crypto: Crypto.Implementation, header: UcanHeader, payload: UcanPayload ): Promise { const encodedHeader = encodeHeader(header) const encodedPayload = encodePayload(payload) return Uint8arrays.toString( await crypto.keystore.sign( Uint8arrays.fromString(`${encodedHeader}.${encodedPayload}`, "utf8") ), "base64url" ) } // ㊙️ /** * Extract the payload of a UCAN. * * Throws when given an improperly formatted UCAN. */ function extractPayload(ucan: string, level: number): { iss: string; prf: string | null } { try { return JSON.parse(base64.urlDecode(ucan.split(".")[1])) } catch (_) { throw new Error(`Invalid UCAN (${level} level${level === 1 ? "" : "s"} deep): \`${ucan}\``) } }