All files / src ucan.ts

0% Statements 0/56
0% Branches 0/40
0% Functions 0/13
0% Lines 0/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
import { CryptoSystem } from 'keystore-idb/types'
 
import * as did from './did'
import * as keystore from './keystore'
import { base64 } from './common'
 
// TYPES
 
export type SessionKey = {
  sessionKey: string
}
 
export type Fact = SessionKey | Record<string, string>
 
export type Resource =
  "*" | Record<string, string>
 
export type UcanHeader = {
  alg: string
  typ: string
  uav: string
}
 
export type UcanPayload = {
  aud: string
  exp: number
  fct: Array<Fact>
  iss: string
  nbf: number
  prf: Ucan | undefined
  ptc: string | undefined | null
  rsc: Resource
}
 
export type Ucan = {
  header: UcanHeader
  payload: UcanPayload
  signature: string | null
}
 
// CONSTANTS
 
 
// TODO: Waiting on API change.
//       Should be `dnslink`
export const WNFS_PREFIX = "floofs"
 
 
 
// FUNCTIONS
 
 
/**
 * 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,
  facts = [],
  issuer,
  lifetimeInSeconds = 30,
  potency = 'APPEND',
  proof,
  resource = '*'
}: {
  addSignature?: boolean
  audience: string
  facts?: Array<Fact>
  issuer: string
  lifetimeInSeconds?: number
  potency?: string | null
  proof?: Ucan
  resource?: Resource
}): Promise<Ucan> {
  const ks = await keystore.get()
  const currentTimeInSeconds = Math.floor(Date.now() / 1000)
 
  // Header
  const header = {
    alg: jwtAlgorithm(ks.cfg.type) || 'UnknownAlgorithm',
    typ: 'JWT',
    uav: '1.0.0' // actually 0.3.1 but server isn't updated yet
  }
 
  // Timestamps
  let exp = currentTimeInSeconds + lifetimeInSeconds
  let nbf = currentTimeInSeconds - 60
 
  if (proof) {
    const prf = proof.payload
 
    exp = Math.min(prf.exp, exp)
    nbf = Math.max(prf.nbf, nbf)
  }
 
  // Payload
  const payload = {
    aud: audience,
    exp: exp,
    fct: facts,
    iss: issuer || await did.ucan(),
    nbf: nbf,
    prf: proof,
    ptc: potency,
    rsc: resource,
  }
 
  const signature = addSignature ? await sign(header, payload) : null
 
  return {
    header,
    payload,
    signature
  }
}
 
/**
 * Given a list of UCANs, generate a dictionary.
 * The key will be in the form of `${resourceKey}:${resourceValue}`
 */
export function compileDictionary(ucans: Array<string>): Record<string, Ucan> {
  return ucans.reduce((acc, ucanString) => {
    const ucan = decode(ucanString)
    const { rsc } = ucan.payload
 
    if (typeof rsc !== "object") {
      return { ...acc, [rsc]: ucan }
    }
 
    const resource = Array.from(Object.entries(rsc))[0]
    const key = resource[0] + ":" + (
      resource[0] === WNFS_PREFIX
        ? resource[1].replace(/\/+$/, "")
        : resource[1]
    )
 
    return { ...acc, [key]: ucan }
  }, {})
}
 
/**
 * 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: {
      ...payload,
      prf: payload.prf ? decode(payload.prf) : null
    },
    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 || sign(ucan.header, ucan.payload))
}
 
/**
 * 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,
    prf: payload.prf ? encode(payload.prf) : undefined// TODO: 0.3.1 only supports a single proof.
  }))
}
 
/**
 * 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 valid.
 *
 * @param ucan The decoded UCAN
 * @param did The DID associated with the signature of the UCAN
 */
 export async function isValid(ucan: Ucan): Promise<boolean> {
  const encodedHeader = encodeHeader(ucan.header)
  const encodedPayload = encodePayload(ucan.payload)
 
  const a = await did.verifySignedData({
    charSize: 8,
    data: `${encodedHeader}.${encodedPayload}`,
    did: ucan.payload.iss,
    signature: ucan.signature || ""
  })
 
  if (!a) return a
 
  if (!ucan.payload.prf) return true
 
  // Verify proofs
  const b = ucan.payload.prf.payload.aud === ucan.payload.iss
 
  if (!b) return b
 
  return await isValid(ucan.payload.prf)
}
 
/**
 * 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, level = 0): string {
  const p = extractPayload(ucan, level)
  if (p.prf) return rootIssuer(p.prf, level + 1)
  return p.iss
}
 
/**
 * Generate UCAN signature.
 */
export async function sign(header: UcanHeader, payload: UcanPayload): Promise<string> {
  const encodedHeader = encodeHeader(header)
  const encodedPayload = encodePayload(payload)
  const ks = await keystore.get()
 
  return base64.makeUrlSafe(
    await ks.sign(`${encodedHeader}.${encodedPayload}`, { charSize: 8 })
  )
}
 
 
// ㊙️
 
 
/**
 * JWT algorithm to be used in a JWT header.
 */
function jwtAlgorithm(cryptoSystem: CryptoSystem): string | null {
  switch (cryptoSystem) {
    case CryptoSystem.RSA: return 'RS256';
    default: return null
  }
}
 
 
/**
 * 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}\``)
  }
}