All files / src/profiles profileTokens.js

79.59% Statements 39/49
60.53% Branches 23/38
100% Functions 4/4
79.59% Lines 39/49

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                                                  8x       8x   8x 8x     8x 8x     8x   8x                 8x                 7x                             20x 20x     20x 20x               20x 20x               20x       20x 20x   20x 20x 20x 20x   20x   8x   4x           20x 20x       20x 20x       20x                           11x 11x         11x 11x 11x 11x 11x       11x    
import { ECPair } from 'bitcoinjs-lib'
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,
                                 privateKey,
                                 subject = null,
                                 issuer = null,
                                 signingAlgorithm = 'ES256K',
                                 issuedAt = new Date(),
                                 expiresAt = nextYear()) {
  Iif (signingAlgorithm !== 'ES256K') {
    throw new Error('Signing algorithm not supported')
  }
 
  const publicKey = SECP256K1Client.derivePublicKey(privateKey)
 
  Eif (subject === null) {
    subject = { publicKey }
  }
 
  Eif (issuer === null) {
    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) {
  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, publicKeyOrAddress) {
  const decodedToken = decodeToken(token)
  const payload = decodedToken.payload
 
  // 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')
  }
 
  const issuerPublicKey = payload.issuer.publicKey
  const publicKeyBuffer = new Buffer(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, publicKeyOrAddress = null) {
  let decodedToken
  Eif (publicKeyOrAddress) {
    decodedToken = verifyProfileToken(token, publicKeyOrAddress)
  } else {
    decodedToken = decodeToken(token)
  }
 
  let profile = {}
  Eif (decodedToken.hasOwnProperty('payload')) {
    const payload = decodedToken.payload
    Eif (payload.hasOwnProperty('claim')) {
      profile = decodedToken.payload.claim
    }
  }
 
  return profile
}