diff --git a/src/functions/shamir_secret_sharing.js b/src/functions/shamir_secret_sharing.js index def4f16..ed896e3 100644 --- a/src/functions/shamir_secret_sharing.js +++ b/src/functions/shamir_secret_sharing.js @@ -54,8 +54,13 @@ module.exports = function (S) { }; S.__shamirFn = (x, q) => { + // Evaluate the polynomial q[0] + q[1]*x + ... + q[n-1]*x^(n-1) with + // Horner's method, using the coefficient position as the exponent. + // (indexOf-based evaluation returned the first occurrence of a value, + // corrupting the result whenever coefficients repeat.) let r = 0; - for (let a of q) r = S.__GF256_add(r, S.__GF256_mul(a, S.__GF256_pow(x, q.indexOf(a)))); + for (let i = q.length - 1; i >= 0; i--) + r = S.__GF256_add(S.__GF256_mul(r, x), q[i]); return r; }; @@ -121,19 +126,20 @@ module.exports = function (S) { e = S.generateEntropy({hex:false}); ePointer = 0; - let w; for (let b = 0; b < secret.length; b++) { let q = [secret[b]]; + // Sample coefficients uniformly at random from GF(256) (i.i.d.). + // No rejection filter: forcing distinct coefficients (and distinct + // from the secret byte) violates Shamir's perfect-secrecy + // guarantee. Position-based evaluation in __shamirFn handles + // repeated values correctly. for (let i = 0; i < threshold - 1; i++) { - do { - if (ePointer >= e.length) { - ePointer = 0; - e = S.generateEntropy({hex:false}); - } - w = e[ePointer++]; - } while (q.includes(w)); - q.push(w); + if (ePointer >= e.length) { + ePointer = 0; + e = S.generateEntropy({hex:false}); + } + q.push(e[ePointer++]); } for (let i of sharesIndexes) diff --git a/test/jsbtc.test.js b/test/jsbtc.test.js index 99a4ce8..83da18c 100644 --- a/test/jsbtc.test.js +++ b/test/jsbtc.test.js @@ -580,6 +580,41 @@ describe(`${(browser) ? 'Browser' : 'Node'} test jsbtc library`, function () { } }).timeout(14000); + + it('Polynomial evaluation is position-based (repeated coefficients)', () => { + // Regression for the indexOf bug: __shamirFn must use the + // coefficient position as exponent, so repeated coefficient + // values are evaluated correctly. + const explicitEval = (x, q) => { + let r = 0; + for (let i = 0; i < q.length; i++) + r = __GF256_add(r, __GF256_mul(q[i], __GF256_pow(x, i))); + return r; + }; + // q with a repeated coefficient value + const q = [7, 9, 9, 3, 9]; + for (let x = 1; x < 256; x += 13) + equal(__shamirFn(x, q), explicitEval(x, q)); + // random repeats + for (let trial = 0; trial < 50; trial++) { + const rq = []; + for (let i = 0; i < 6; i++) rq.push(Math.floor(Math.random() * 256)); + for (let x = 1; x < 256; x += 97) + equal(__shamirFn(x, rq), explicitEval(x, rq)); + } + }); + + it('Uniform coefficient sampling: high thresholds round-trip', () => { + // The old rejection filter forced distinct coefficients (entropy + // leak, breakdown at t >= 30). Coefficients are now sampled + // uniformly i.i.d.; verify round-trips up to threshold 100. + let secret = Buffer.from("high threshold check 0123456789abcdef", 'utf8'); + for (let i = 30; i <= 100; i += 5) { + let shares = __split_secret(i, i, secret); + let r = __restore_secret(shares); + equal(r.toString('utf8'), secret.toString('utf8')); + } + }); }); describe("Private/Public key functions:", function () {