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 }