up8-ticket

Securely generate UP8-compatible, @q-encoded master tickets.
Log | Files | Refs | README | LICENSE

commit 4d594c7a3d9ec84f1b5755b892061478f3072151
parent ce25371eda47b7b979a7d65243ea57112ceca04d
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 22 Sep 2020 18:57:32 -0230

up8-ticket: api overhaul

Diffstat:
Msrc/index.js | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
1 file changed, 122 insertions(+), 35 deletions(-)

diff --git a/src/index.js b/src/index.js @@ -1,53 +1,140 @@ -const more = require('more-entropy') -const ob = require('urbit-ob') const chunk = require('lodash.chunk') const flatMap = require('lodash.flatmap') +const more = require('more-entropy') +const ob = require('urbit-ob') +const secrets = require('secrets.js-grempe') const zipWith = require('lodash.zipwith') -// generate a @q of the desired bitlength -const gen = bits => { - const bytes = bits / 8 - const some = crypto.rng(bytes) - const prng = new more.Generator() +const GALOIS_BITSIZE = 8 - return new Promise((resolve, reject) => { - prng.generate(bits, result => { - const chunked = chunk(result, 2) - const desired = chunked.slice(0, bytes) // only take required entropy - const more = flatMap(desired, arr => arr[0] ^ arr[1]) - const entropy = zipWith(some, more, (x, y) => x ^ y) - const buf = Buffer.from(entropy) - const patq = ob.hex2patq(buf.toString('hex')) - resolve(patq) - reject("entropy generation failed") - }) - }) +/* + * Strip a leading zero from a string. + */ +const unpad = str => { + if (!(str.slice(0, 1) === '0')) { + throw new Error('nonzero leading digit -- please report this as a bug!') + } + + return str.substring(1) } -// generate a @q of the desired bitlength; the second argument should be a -// Buffer that will be XOR'd with the generated entropy -const gen_custom = (bits, addl) => { - const bytes = bits / 8 - const some = crypto.rng(bytes) +/* + * The XOR operator, as a function. + */ +const xor = (x, y) => x ^ y + +/* + * Generate a master ticket of the desired bitlength. + * + * Uses 'crypto.rng' to generate the required entropy. + * + * A buffer provided as the second argument will be XOR'd with the generated + * bytes. You can use this to provide your own entropy, generated elsewhere. + * + * @param {Number} nbits desired bitlength of ticket + * @param {Buffer} addl an optional buffer of additional bytes + * @return {String} a @q-encoded master ticket + */ +const gen_ticket_simple = (nbits, addl) => { + const nbytes = nbits / 8 + const entropy = crypto.rng(nbytes) + const bytes = + Buffer.isBuffer(addl) + ? Buffer.from( + zipWith(entropy, addl, xor) + ) + .slice(0, nbytes) + : entropy + + return ob.hex2patq(bytes.toString('hex')) +} +/* + * Generate a master ticket of the desired bitlength. + * + * Uses both 'crypto.rng' and 'more-entropy' to generate the required entropy. + * Bytes generated by 'more-entropy' are XOR'd with those provided by + * 'crypto.rng'. + * + * A buffer provided as the second argument will be XOR'd with the generated + * bytes. You can use this to provide your own entropy, generated elsewhere. + * + * @param {Number} nbits desired bitlength of ticket + * @param {Buffer} addl an optional buffer of additional bytes + * @return {Promise<String>} a @q-encoded master ticket, wrapped in a Promise + */ +const gen_ticket_more = (nbits, addl) => { + const nbytes = nbits / 8 const prng = new more.Generator() return new Promise((resolve, reject) => { - prng.generate(bits, result => { - const chunked = chunk(result, 2) - const desired = chunked.slice(0, bytes) // only take required entropy - const more = flatMap(desired, arr => arr[0] ^ arr[1]) - const moar = zipWith(some, more, (x, y) => x ^ y) - const entropy = zipWith(moar, addl, (x, y) => x ^ y) - const buf = Buffer.from(entropy) - const patq = ob.hex2patq(buf.toString('hex')) - resolve(patq) + prng.generate(nbits, result => { + const pairs = chunk(result, 2) + const entropy = pairs.slice(0, nbytes) // only take required entropy + const more = flatMap(entropy, arr => arr[0] ^ arr[1]) + + const ticket = gen_ticket_simple(nbits, more) + const bufticket = Buffer.from(ob.patq2hex(ticket), 'hex') + + const bytes = + Buffer.isBuffer(addl) + ? Buffer.from( + zipWith(bufticket, addl, xor) + ) + .slice(0, nbytes) + : bufticket + + resolve(ob.hex2patq(bytes.toString('hex'))) reject("entropy generation failed") }) }) } +/* + * Shard a ticket via a k/n Shamir's Secret Sharing scheme. + * + * Provided with a ticket, a desired number of shards 'n', and threshold value + * 'k' < 'n', returns an array of 'n' shards such that the original ticket can + * be recovered by combining at least 'k' of the shards together. Each shard + * leaks no information about the ticket. + * + * @param {String} ticket a @q-encoded string + * @param {Number} n the desired number of shards to produce + * @param {Number} k the desired threshold value, smaller than 'n' + * @return {Array<String>} an array of 'n' @q-encoded shards + */ +shard = (ticket, n, k) => { + if (!ob.isValidPatq(ticket)) { + throw new Error('input is not @q-encoded') + } + + secrets.init(GALOIS_BITSIZE) + + const hex = ob.patq2hex(ticket) + const shards = secrets.share(hex, n, k) + return shards.map(ob.hex2patq) +} + +/* + * Combine shards that have been produced via 'shard'. + * + * Provide an array of shards in any order. So long as at least 'k' shards + * produced with a threshold value of 'k' are provided, they'll combine to + * produce the intended ticket. + * + * @param {Array<String>} shards an array of @q-encoded shards + * @return {String} a @q-encoded ticket + */ +combine = shards => { + const hexshards = shards.map(ob.patq2hex).map(unpad) + const hexticket = secrets.combine(hexshards) + return ob.hex2patq(hexticket) +} + module.exports = { - gen, - gen_custom + gen_ticket_simple, + gen_ticket_more, + + shard, + combine }