commit 4d594c7a3d9ec84f1b5755b892061478f3072151
parent ce25371eda47b7b979a7d65243ea57112ceca04d
Author: Jared Tobin <jared@jtobin.io>
Date: Tue, 22 Sep 2020 18:57:32 -0230
up8-ticket: api overhaul
Diffstat:
M | src/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
}