All files / src/profiles profileTokens.ts

80.33% Statements 49/61
57.5% Branches 23/40
100% Functions 4/4
80.33% Lines 49/61

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 1681x 1x   1x 1x                           1x             8x       8x   8x 8x     8x 8x     8x   8x                 8x               1x 7x                           1x   20x 20x 20x         20x 20x               20x 20x               20x         20x 20x   20x 20x 20x 20x   20x   8x   4x           20x 20x       20x 20x       20x                       1x     11x 11x         11x 11x 11x 11x     11x 11x       11x    
import { ECPair } from 'bitcoinjs-lib'
import { decodeToken, SECP256K1Client, TokenSigner, TokenVerifier } from 'jsontokens'
import { TokenInterface } from 'jsontokens/lib/decode'
import { nextYear, makeUUID4 } from '../utils'
import { ecPairToAddress } from '../keys'
 
/**
  * Signs a profile token
  * @param {Object} profile - the JSON of the profile to be signed
  * @param {String} privateKey - the signing private key
  * @param {Object} subject - the entity that the information is about
  * @param {Object} issuer - the entity that is issuing the token
  * @param {String} signingAlgorithm - the signing algorithm to use
  * @param {Date} issuedAt - the time of issuance of the token
  * @param {Date} expiresAt - the time of expiration of the token
  * @returns {Object} - the signed profile token
  * 
  */
export function signProfileToken(profile: any,
                                 privateKey: string,
                                 subject?: any,
                                 issuer?: any,
                                 signingAlgorithm = 'ES256K',
                                 issuedAt = new Date(),
                                 expiresAt = nextYear()): string {
  Iif (signingAlgorithm !== 'ES256K') {
    throw new Error('Signing algorithm not supported')
  }
 
  const publicKey = SECP256K1Client.derivePublicKey(privateKey)
 
  Eif (!subject) {
    subject = { publicKey }
  }
 
  Eif (!issuer) {
    issuer = { publicKey }
  }
 
  const tokenSigner = new TokenSigner(signingAlgorithm, privateKey)
 
  const payload = {
    jti: makeUUID4(),
    iat: issuedAt.toISOString(),
    exp: expiresAt.toISOString(),
    subject,
    issuer,
    claim: profile
  }
 
  return tokenSigner.sign(payload)
}
 
/**
  * Wraps a token for a profile token file
  * @param {String} token - the token to be wrapped
  * @returns {Object} - including `token` and `decodedToken`
  */
export function wrapProfileToken(token: string) {
  return {
    token,
    decodedToken: decodeToken(token)
  }
}
 
/**
  * Verifies a profile token
  * @param {String} token - the token to be verified
  * @param {String} publicKeyOrAddress - the public key or address of the
  *   keypair that is thought to have signed the token
  * @returns {Object} - the verified, decoded profile token
  * @throws {Error} - throws an error if token verification fails
  */
export function verifyProfileToken(token: string, publicKeyOrAddress: string): 
  TokenInterface {
  const decodedToken = decodeToken(token)
  const payload = decodedToken.payload
  Iif (typeof payload === 'string') {
    throw new Error('Unexpected token payload type of string')
  }
  
  // Inspect and verify the subject
  Eif (payload.hasOwnProperty('subject')) {
    Iif (!payload.subject.hasOwnProperty('publicKey')) {
      throw new Error('Token doesn\'t have a subject public key')
    }
  } else {
    throw new Error('Token doesn\'t have a subject')
  }
 
  // Inspect and verify the issuer
  Eif (payload.hasOwnProperty('issuer')) {
    Iif (!payload.issuer.hasOwnProperty('publicKey')) {
      throw new Error('Token doesn\'t have an issuer public key')
    }
  } else {
    throw new Error('Token doesn\'t have an issuer')
  }
 
  // Inspect and verify the claim
  Iif (!payload.hasOwnProperty('claim')) {
    throw new Error('Token doesn\'t have a claim')
  }
 
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
  const issuerPublicKey = (payload.issuer as Record<string, string>).publicKey as string
  const publicKeyBuffer = Buffer.from(issuerPublicKey, 'hex')
 
  const compressedKeyPair =  ECPair.fromPublicKey(publicKeyBuffer, { compressed: true })
  const compressedAddress = ecPairToAddress(compressedKeyPair)
  const uncompressedKeyPair = ECPair.fromPublicKey(publicKeyBuffer, { compressed: false })
  const uncompressedAddress = ecPairToAddress(uncompressedKeyPair)
 
  if (publicKeyOrAddress === issuerPublicKey) {
    // pass
  } else if (publicKeyOrAddress === compressedAddress) {
    // pass
  } else Eif (publicKeyOrAddress === uncompressedAddress) {
    // pass
  } else {
    throw new Error('Token issuer public key does not match the verifying value')
  }
 
  const tokenVerifier = new TokenVerifier(decodedToken.header.alg, issuerPublicKey)
  Iif (!tokenVerifier) {
    throw new Error('Invalid token verifier')
  }
 
  const tokenVerified = tokenVerifier.verify(token)
  Iif (!tokenVerified) {
    throw new Error('Token verification failed')
  }
 
  return decodedToken
}
 
/**
  * Extracts a profile from an encoded token and optionally verifies it,
  * if `publicKeyOrAddress` is provided.
  * @param {String} token - the token to be extracted
  * @param {String} publicKeyOrAddress - the public key or address of the
  *   keypair that is thought to have signed the token
  * @returns {Object} - the profile extracted from the encoded token
  * @throws {Error} - if the token isn't signed by the provided `publicKeyOrAddress`
  */
export function extractProfile(token: string, publicKeyOrAddress: string | null = null): 
  Record<string, any> {
  let decodedToken
  Eif (publicKeyOrAddress) {
    decodedToken = verifyProfileToken(token, publicKeyOrAddress)
  } else {
    decodedToken = decodeToken(token)
  }
 
  let profile = {}
  Eif (decodedToken.hasOwnProperty('payload')) {
    const payload = decodedToken.payload
    Iif (typeof payload === 'string') {
      throw new Error('Unexpected token payload type of string')
    }
    Eif (payload.hasOwnProperty('claim')) {
      profile = payload.claim
    }
  }
 
  return profile
}