fission-suite/webnative

View on GitHub
src/ucan/token.ts

Summary

Maintainability
A
1 hr
Test Coverage
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<Fact>
  issuer: string
  lifetimeInSeconds?: number
  expiration?: number
  potency?: Potency
  proof?: string | Ucan
  resource?: Resource
}): Promise<Ucan> {
  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<boolean> {
  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<string> {
  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}\``)
  }
}