up8-ticket

Securely generate UP8-compatible, @q-encoded master tickets.
git clone git://git.jtobin.io/up8-ticket.git
Log | Files | Refs | README | LICENSE

index.js (5357B)


      1 const chunk = require('lodash.chunk')
      2 const flatMap = require('lodash.flatmap')
      3 const hash = require('hash.js')
      4 const DRBG = require('hmac-drbg')
      5 const more = require('more-entropy')
      6 const ob = require('urbit-ob')
      7 const secrets = require('secrets.js-grempe')
      8 const zipWith = require('lodash.zipwith')
      9 
     10 const GALOIS_BITSIZE = 8
     11 
     12 /*
     13  * Strip a leading zero from a string.
     14  */
     15 const unpad = str => {
     16   /* istanbul ignore next */
     17   if (!(str.slice(0, 1) === '0')) {
     18     /* istanbul ignore next */
     19     throw new Error('nonzero leading digit -- please report this as a bug!')
     20   }
     21 
     22   return str.substring(1)
     23 }
     24 
     25 /*
     26  * The XOR operator, as a function.
     27  */
     28 const xor = (x, y) => x ^ y
     29 
     30 /*
     31  * Generate a master ticket of the desired bitlength.
     32  *
     33  * Uses 'crypto.rng' to generate the required entropy.
     34  *
     35  * A buffer provided as the second argument will be XOR'd with the generated
     36  * bytes.  You can use this to provide your own entropy, generated elsewhere.
     37  *
     38  * @param  {Number}  nbits desired bitlength of ticket
     39  * @param  {Buffer}  addl an optional buffer of additional bytes
     40  * @return  {String}  a @q-encoded master ticket
     41  */
     42 const gen_ticket_simple = (nbits, addl) => {
     43   const nbytes  = nbits / 8
     44   const entropy = crypto.rng(nbytes)
     45   const bytes =
     46       Buffer.isBuffer(addl)
     47     ? Buffer.from(
     48         zipWith(entropy, addl, xor)
     49       )
     50       .slice(0, nbytes)
     51     : entropy
     52 
     53   return ob.hex2patq(bytes.toString('hex'))
     54 }
     55 
     56 /*
     57  * Generate a master ticket of the desired bitlength.
     58  *
     59  * Uses both 'crypto.rng' and 'more-entropy' to generate the required entropy.
     60  * Bytes generated by 'more-entropy' are XOR'd with those provided by
     61  * 'crypto.rng'.
     62  *
     63  * A buffer provided as the second argument will be XOR'd with the generated
     64  * bytes.  You can use this to provide your own entropy, generated elsewhere.
     65  *
     66  * @param  {Number}  nbits desired bitlength of ticket
     67  * @param  {Buffer}  addl an optional buffer of additional bytes
     68  * @return  {Promise<String>}  a @q-encoded master ticket, wrapped in a Promise
     69  */
     70 const gen_ticket_more = (nbits, addl) => {
     71   const nbytes = nbits / 8
     72   const prng = new more.Generator()
     73 
     74   return new Promise((resolve, reject) => {
     75     prng.generate(nbits, result => {
     76       const pairs   = chunk(result, 2)
     77       const entropy = pairs.slice(0, nbytes) // only take required entropy
     78       const more    = flatMap(entropy, arr => arr[0] ^ arr[1])
     79 
     80       const ticket    = gen_ticket_simple(nbits, more)
     81       const bufticket = Buffer.from(ob.patq2hex(ticket), 'hex')
     82 
     83       const bytes   =
     84           Buffer.isBuffer(addl)
     85         ? Buffer.from(
     86             zipWith(bufticket, addl, xor)
     87           )
     88           .slice(0, nbytes)
     89         : bufticket
     90 
     91       resolve(ob.hex2patq(bytes.toString('hex')))
     92       reject("entropy generation failed")
     93     })
     94   })
     95 }
     96 
     97 /*
     98  * Generate a master ticket of the desired bitlength.
     99  *
    100  * Uses both 'crypto.rng' and 'more-entropy' to produce the required entropy
    101  * and nonce for input to a HMAC-DRBG generator, respectively.
    102  *
    103  * A buffer provided as the second argument will be used as the DRBG
    104  * personalisation string.
    105  *
    106  * @param  {Number}  nbits desired bitlength of ticket (minimum 192)
    107  * @param  {Buffer}  addl an optional buffer of additional bytes
    108  * @return  {Promise<String>}  a @q-encoded master ticket, wrapped in a Promise
    109  */
    110 const gen_ticket_drbg = async (nbits, addl) => {
    111   const nbytes = nbits / 8
    112   const entropy = crypto.rng(nbytes)
    113 
    114   const prng  = new more.Generator()
    115   const nonce = await new Promise((resolve, reject) => {
    116     prng.generate(nbits, result => {
    117       resolve(result.toString('hex'))
    118       reject("entropy generation failed")
    119     })
    120   })
    121 
    122   const d = new DRBG({
    123     hash: hash.sha256,
    124     entropy: entropy,
    125     nonce: nonce,
    126     pers: Buffer.isBuffer(addl) ? addl.toString('hex') : null
    127   })
    128 
    129   const bytes = d.generate(nbytes, 'hex')
    130   return ob.hex2patq(bytes)
    131 }
    132 
    133 /*
    134  * Shard a ticket via a k/n Shamir's Secret Sharing scheme.
    135  *
    136  * Provided with a ticket, a desired number of shards 'n', and threshold value
    137  * 'k' < 'n', returns an array of 'n' shards such that the original ticket can
    138  * be recovered by combining at least 'k' of the shards together.  Each shard
    139  * leaks no information about the ticket.
    140  *
    141  * @param  {String}  ticket a @q-encoded string
    142  * @param  {Number}  n the desired number of shards to produce
    143  * @param  {Number}  k the desired threshold value, smaller than 'n'
    144  * @return  {Array<String>}  an array of 'n' @q-encoded shards
    145  */
    146 const shard = (ticket, n, k) => {
    147   if (!ob.isValidPatq(ticket)) {
    148     throw new Error('input is not @q-encoded')
    149   }
    150 
    151   secrets.init(GALOIS_BITSIZE)
    152 
    153   const hex = ob.patq2hex(ticket)
    154   const shards = secrets.share(hex, n, k)
    155   return shards.map(ob.hex2patq)
    156 }
    157 
    158 /*
    159  * Combine shards that have been produced via 'shard'.
    160  *
    161  * Provide an array of shards in any order.  So long as at least 'k' shards
    162  * produced with a threshold value of 'k' are provided, they'll combine to
    163  * produce the intended ticket.
    164  *
    165  * @param  {Array<String>}  shards an array of @q-encoded shards
    166  * @return  {String}  a @q-encoded ticket
    167  */
    168 const combine = shards => {
    169   const hexshards = shards.map(ob.patq2hex).map(unpad)
    170   const hexticket = secrets.combine(hexshards)
    171   return ob.hex2patq(hexticket)
    172 }
    173 
    174 module.exports = {
    175   gen_ticket_simple,
    176   gen_ticket_more,
    177   gen_ticket_drbg,
    178 
    179   shard,
    180   combine
    181 }