Skip to content

Biased coefficient generation in Shamir SSS: secret/coefficients forced pairwise-distinct #65

Description

@nitinkansal30

Coefficient Non-Distinctness Bias in jsbtc Shamir Secret Sharing

Target: bitaps-com/jsbtc, src/functions/shamir_secret_sharing.js
Referenced by: the live "1 BTC Challenge" at tbtc.bitaps.com/mnemonic/challenge, which explicitly cites this file as its "exact software implementation."
Class: Non-uniform coefficient generation (biased Shamir Secret Sharing), analogous in category to the previously patched and paid pybtc issue #23.

Summary

__split_secret() generates each polynomial's coefficients with rejection sampling that forbids duplicate values within the same coefficient set:

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));   // <-- rejects w if it equals any coefficient already in q
    q.push(w);
}

q starts as [secretByte], so this constraint applies to the secret byte too. The result: for every polynomial generated by this code, the secret byte and all of its random coefficients are guaranteed pairwise distinct. In a correct Shamir Secret Sharing implementation, coefficients (including the constant term) should be free to take any value in GF(256), independently and with replacement — collisions are expected to occur naturally and carry no information.

Exploit

Given t-1 known shares of a threshold-t scheme (exactly the situation of the published challenge: 2 of 3 needed shares are public), an attacker can:

  1. Hypothesize a candidate value s for a secret byte.
  2. Treat (0, s) as the missing third point alongside the two known points (x₁,y₁), (x₂,y₂).
  3. Lagrange-interpolate the unique degree-(t-1) polynomial through all three points to recover its full coefficient vector [q₀, q₁, q₂].
  4. If any two coefficients in that vector are equal, s is proven impossible — the real generator could never have produced such a polynomial — and the guess is eliminated with certainty (zero false negatives).

This is structurally the same technique that earned a 0.2 BTC bounty in pybtc issue #23 (there, the impossible condition was "no coefficient equals 255"; here it's "no two coefficients — including the secret — are equal").

Proof of Concept

Full GF(256) arithmetic, the real coefficient-generation logic, and the elimination check were reimplemented and tested against a simulated 16-byte (128-bit) secret with a 3-of-5 split:

Elimination rate:        1.17% of all (byte position × guess) pairs
Avg. surviving guesses:  253.0 / 256 per byte
True secret byte:        survives elimination at every position (0/16 false negatives)

(Verification script available on request — verify_finding.py.)

Impact

  • Per byte: entropy drops from 8.000 bits to ≈ log₂(253) ≈ 7.983 bits.
  • Across a 16-byte / 128-bit secret: ≈ 0.27 bits of total entropy loss, out of 128.

This is real, reproducible, and provable — but nowhere near sufficient to recover a 128-bit secret from 2 of 3 shares. The remaining ~127.7 bits of search space is exactly as infeasible as brute-forcing a fresh BIP39 seed.

Recommendation

Remove the while (q.includes(w)) rejection loop entirely. Coefficients (including the secret byte) should be drawn independently and allowed to repeat; duplicate values carry no exploitable structure in a correct SSS scheme and rejecting them introduces a provable, if small, statistical bias.
/

jsbtc-shamir-distinctness-bug.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions