All files / src/profiles profileTokens.ts

74.36% Statements 29/39
75% Branches 3/4
100% Functions 4/4
74.36% Lines 29/39

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 1601x     1x   1x                           1x                     8x     8x       8x     8x                     8x               1x 7x                           1x 20x 20x                                                 20x 20x   20x 20x 20x 20x                       20x         20x         20x                       1x     11x         11x   11x   11x       11x    
import { ECPair } from 'bitcoinjs-lib'
 
// @ts-ignore: Could not find a declaration file for module
import { decodeToken, SECP256K1Client, TokenSigner, TokenVerifier } from 'jsontokens'
 
import { nextYear, makeUUID4, ecPairToAddress } from '../utils'
 
/**
  * 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()) {
  if (signingAlgorithm !== 'ES256K') {
    throw new Error('Signing algorithm not supported')
  }
 
  const publicKey = SECP256K1Client.derivePublicKey(privateKey)
 
  if (!subject) {
    subject = { publicKey }
  }
 
  if (!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) {
  const decodedToken = decodeToken(token)
  const payload = decodedToken.payload
 
  // Inspect and verify the subject
  if (payload.hasOwnProperty('subject')) {
    if (!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
  if (payload.hasOwnProperty('issuer')) {
    if (!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
  if (!payload.hasOwnProperty('claim')) {
    throw new Error('Token doesn\'t have a claim')
  }
 
  const issuerPublicKey = payload.issuer.publicKey
  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 if (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)
  if (!tokenVerifier) {
    throw new Error('Invalid token verifier')
  }
 
  const tokenVerified = tokenVerifier.verify(token)
  if (!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) {
  let decodedToken
  if (publicKeyOrAddress) {
    decodedToken = verifyProfileToken(token, publicKeyOrAddress)
  } else {
    decodedToken = decodeToken(token)
  }
 
  let profile = {}
  if (decodedToken.hasOwnProperty('payload')) {
    const payload = decodedToken.payload
    if (payload.hasOwnProperty('claim')) {
      profile = decodedToken.payload.claim
    }
  }
 
  return profile
}