-
Notifications
You must be signed in to change notification settings - Fork 33
Description
Security Vulnerability Report
Shamir Secret Sharing Implementation Issues in Bitaps Mnemonic Challenge
Target: Bitaps Mnemonic Split Tool
Challenge: https://tbtc.bitaps.com/mnemonic/challenge
Researcher: Chris Yates @ChrisYatesUK
Date: 2026-03-10
Logger Version: v5 (deferred post-split scanning; GF256 Buffer/TypedArray fix)
Session: Clean isolated window — no prior session cross-contamination
Summary
This report documents security vulnerabilities found in the Bitaps mnemonic splitting tool used in the Shamir challenge, based on a single clean instrumented session in a fresh browser window. The session produced one __split_secret operation and a full exhaustive reconstruction verification.
Using browser runtime instrumentation, the following were captured directly from the session:
- Raw entropy used to generate the mnemonic
- The complete 128-bit Shamir secret in plaintext, passed directly to
__split_secret - All 16 Shamir polynomial coefficient tuples, each encoding one secret byte in its first element
- All 5 share buffers returned in the single
__split_secretreturn value - All 5 share mnemonics returned in the
splitMnemonicreturn value - All 16
__restore_secretverification calls, covering every C(5,3)=10 three-share subset, every C(5,4)=5 four-share subset, and the full five-share reconstruction — every single one returning the correct secret - The last generated share mnemonic persisted on the global
windowobject underwindow.share
This confirms that the complete Shamir secret is exposed in plaintext at multiple points during execution and is interceptable by any co-resident browser script.
The GF(256) post-split table scan extracted both GF(256) arithmetic tables (GF256_EXP_TABLE and GF256_LOG_TABLE) directly from the window object. Both match the canonical AES-field exponent and logarithm tables for the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B) exactly. All 16 reconstruction calls across every valid share subset returned the correct secret with zero divergence, confirming the implementation is internally consistent and cross-implementation compatible with any Shamir library that uses the standard AES GF(256) field.
Challenge Overview
The Bitaps challenge describes a wallet mnemonic split using Shamir Secret Sharing.
Mnemonic length: 12 words
Secret entropy: 128 bits
Shamir scheme: 3-of-5
The challenge states the mnemonic was split into five shares and any three shares can recover the original secret.
Methodology
A runtime monitoring script (v5 logger) was injected into the browser console in a fresh browser window before any interaction with the tool. This eliminates any possibility of state carry-over from previous sessions.
The v5 logger captured:
- Cryptographic entropy generation via a
crypto.getRandomValueshook - Function call arguments and return values for all hooked functions, using a
__forensic_hookedguard to prevent double-wrapping - Shamir polynomial evaluation via
__shamirFn— both arguments and return values - Share reconstruction via
__shamirInterpolationand__restore_secret - Post-split deferred scans: GF(256) table search, candidate share array search, and BIP39 mnemonic scan of window-scoped properties
- All expensive scanning deferred until after
__split_secretreturned, triggered by theWORKFLOW_COMPLETEevent
The clean session produced exactly one __split_secret call, confirming no cross-contamination.
Session Timeline
23:48:06 INPUT selector_12 = on (12-word mnemonic length selected)
23:48:08 ENTROPY captured (mnemonic entropy generated)
23:48:08 BUTTON Generate new (mnemonic generated)
23:48:22 BUTTON Split (split UI opened)
23:48:30 INPUT share-threshold = 3
23:48:31 INPUT share-total = 5
23:48:33 BUTTON Calculate (split operation triggered)
23:48:33 ENTROPY ×2 captured (x-coordinate selection + polynomial coefficients)
23:48:33 ENTROPY ×1 additional (coefficient buffer exhausted on final secret byte)
23:48:33 __shamirFn ×80 calls (16 polynomials × 5 x-coordinates)
23:48:33 __split_secret called (all 5 share buffers returned)
23:48:33 WORKFLOW_COMPLETE (post-split scans triggered)
23:48:33 __shamirInterpolation ×256 (16 reconstructions × 16 bytes)
23:48:33 __restore_secret ×16 (all C(5,3)+C(5,4)+C(5,5) subsets verified)
23:48:33 splitMnemonic called (all 5 share mnemonics returned)
23:48:33 GF256_SCAN (2 tables extracted)
23:48:33 CANDIDATE_ARRAY_SCAN (0 arrays found)
23:49:06 window.share detected (last share mnemonic on global window)
23:49:06 POST_SPLIT_SCANS complete
Observed Execution Flow
Entropy Generation
Three entropy values were captured during the split phase. The first is the 32-byte CSPRNG output from which the mnemonic entropy is derived. The second is consumed by the x-coordinate selection loop. The third supplies the random polynomial coefficients. A fourth entropy call fired mid-polynomial evaluation when the 32-byte coefficient buffer was exhausted on the final (16th) secret byte — the implementation correctly requested a fresh buffer rather than reusing stale bytes.
Mnemonic entropy:
76c0b6f95191db4d6ec13e0d8fb2cd09d74c61506fda797118d0c165af55bdbf
Split-phase entropy (x-coordinates):
82a7f4fce9dcbd3d5a0e4397b65822e4472282de1201f2a8ad7ee8ef70298b4d
Split-phase entropy (polynomial coefficients):
e6f2c79a47203273c9e2f63f4d65154dd83005136d87d9c28c2b2d87720a1ea1
Split-phase entropy (coefficient overflow — byte 16):
f02dfa317452537ec8517c00ab6734c1fc1a870db4990163fb0aaa63d2d61c13
Mnemonic Generated
The following 12-word BIP39 mnemonic was generated and passed to the split function:
item aisle salad permit buffalo pluck roast chief assault laugh rebuild barrel
Secret Buffer Passed to __split_secret
The mnemonic entropy is passed as a raw 16-byte buffer directly to the Shamir split function:
window.__split_secret(
3, 5,
{"type":"Buffer","data":[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]},
4
)Entropy cross-check: The mnemonic entropy hex begins 76c0b6f9.... Converting:
0x76 = 118 ✓ (secret[0])
0xc0 = 192 ✓ (secret[1])
0xb6 = 182 ✓ (secret[2])
0xf9 = 249 ✓ (secret[3])
The Shamir secret is the raw mnemonic entropy with no transformation applied.
Shamir Polynomial Construction
For a 3-of-5 scheme the tool constructs a degree-2 polynomial per secret byte:
f(x) = a0 + a1·x + a2·x² (arithmetic over GF(256))
Where a0 = f(0) = secret byte, and a1, a2 are random coefficients.
All 16 polynomials captured — format [a0, a1, a2]:
[118,230,242] [192,199,154] [182, 71, 32] [249, 50,115]
[ 81,201,226] [145,246, 63] [219, 77,101] [ 77, 21,216]
[110, 48, 5] [193, 19,109] [ 62,135,217] [ 13,194,140]
[143, 43, 45] [178,135,114] [205, 10, 30] [ 9,161,240]
Extracting a0 from all 16 tuples reconstructs the complete secret directly:
[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]
This matches the buffer passed to __split_secret exactly, byte-for-byte. Observing the polynomial evaluation calls alone is sufficient to recover the full secret without collecting any shares.
Share Generation
Shares are produced by evaluating each polynomial at five x-coordinates. The x-coordinates used in this session were: 2, 4, 7, 9, 12.
The complete share output returned by __split_secret:
{
"2": "Buffer<440bb84a7d9ace2a1a4862946d74a1bd>",
"4": "Buffer<7a928740fab9fe36fe072b30c5f91e22>",
"7": "Buffer<5c04eeb2fd7bc39cbff029e721ca66c7>",
"9": "Buffer",
"12": "Buffer"
}All five share buffers are returned as a single observable JavaScript object from one function call. Any script intercepting this return value obtains the complete share set simultaneously — negating the distributional security properties of the scheme.
Share Mnemonics
All five share mnemonics were returned in the splitMnemonic return value:
Share 1 (x=2): dune fruit barely will provide bench hard arrange nerve hill chronic kid
Share 2 (x=4): kind neither south voice paper bread way tornado cotton cool elegant duty
Share 3 (x=7): foster cherry protect wife rotate soft zone ahead transfer atom often moment
Share 4 (x=9): scrap dizzy will pupil analyst ski hollow decorate harvest wise bamboo twenty
Share 5 (x=12): sunny dentist eagle promote blush baby wheel corn boss office phrase buzz
These were captured from the splitMnemonic RETURN value in a single interceptable call, exposing all five shares simultaneously.
Secret Reconstruction Verification
After the split the tool exhaustively verifies the scheme by reconstructing the secret from every valid share subset. This produced 16 total __restore_secret calls and 256 total __shamirInterpolation calls (16 bytes × 16 reconstructions):
| Subset size | Calls | Formula |
|---|---|---|
| 3 shares | 10 | C(5,3) = 10 |
| 4 shares | 5 | C(5,4) = 5 |
| 5 shares | 1 | C(5,5) = 1 |
| Total | 16 |
Every one of the 16 __restore_secret calls returned the correct secret:
Buffer<76c0b6f95191db4d6ec13e0d8fb2cd09>
This matches the leading 16 bytes of the original captured entropy 76c0b6f95191db4d6ec13e0d8fb2cd09.... Zero divergence was observed across any share subset combination.
Example interpolation call — three-share subset {2,4,7}, byte 0:
__shamirInterpolation([[2,68],[4,122],[7,92]]) → RETURN=118
118 = 0x76 is the first byte of the secret, directly visible in the return value.
Global window.share Exposure
The post-split mnemonic scanner detected the most recently displayed share mnemonic stored on the global window object — observed approximately 33 seconds after the split completed:
window.share = "sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"
This is Share 5 (x=12) from the split, stored at window.share and directly readable by any script with page-context access without requiring function hooking or timing precision. The property persists until overwritten.
Post-Split Scan Results
GF(256) Table Scan
The v5 scanner broadened the type check from Array.isArray() to also match Buffer and TypedArray values, allowing it to detect the jsbtc tables which are stored as Buffer.alloc() results. Both tables were extracted directly from window:
GF256_SCAN (post-split) begin
GF256_TABLE (named) GF256_EXP_TABLE
0103050f113355ff1a2e7296a1f813355fe13848d87395a4f702060a1e2266aa
e5345ce43759eb266abed97090abe63153f5040c143c44cc4fd168b8d36eb2cd
4cd467a9e03b4dd762a6f10818287888839eb9d06bbddc7f8198b3ce49db769a
b5c457f9103050f00b1d2769bbd661a3fe192b7d8792adec2f7193aee92060a0
fb163a4ed26db7c25de73256fa153f41c35ee23d47c940c05bed2c749cbfda75
9fbad564acef2a7e829dbcdf7a8e89809bb6c158e82365afea256fb1c843c554
fc1f2163a5f407091b2d7799b0cb46ca45cf4ade798b8691a8e33e42c651f30e
12365aee297b8d8c8f8a8594a7f20d17394bdd7c8497a2fd1c246cb4c752f6
GF256_TABLE (named) GF256_LOG_TABLE
0000190132021ac64bc71b6833eedf036404e00e348d81ef4c7108c8f8691cc1
7dc21db5f9b9276a4de4a6729ac90978652f8a05210fe12412f082453593da8e
968fdbbd36d0ce94135cd2f14046833866ddfd30bf068b62b325e29822889110
7e6e48c3a3b61e423a6b2854fa853dba2b790a159b9f5eca4ed4ace5f373a757
af58a850f4ead6744faee9d5e7e6ade82cd7757aeb160bf559cb5fb09ca951a0
7f0cf66f17c449ecd8431f2da4767bb7ccbb3e5afb60b1863b52a16caa55299d
97b2879061bedcfcbc95cfcd373f5bd15339843c41a26d47142a9e5d56f2d3ab
441192d923202e89b47cb8267799e3a5674aeddec531fe180d638c80c0f77007
GF256_SCAN (post-split) complete — tables found: 2
Both tables match the canonical AES-field tables for the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B) exactly — byte-for-byte across all 255 (EXP) and 256 (LOG) values. This confirms the implementation uses the standard AES GF(256) field. Shares produced by this tool are cross-implementation compatible with any Shamir library that uses the same field.
Candidate Share Array Scan
CANDIDATE_ARRAY_SCAN begin
CANDIDATE_ARRAY_SCAN complete — found: 0
No candidate numeric arrays were found on the window object after the split.
Identified Vulnerabilities
Vulnerability 1 — Plaintext Secret Exposure in Browser Memory
The raw 128-bit mnemonic entropy is passed as a plaintext buffer directly to __split_secret:
{"type":"Buffer","data":[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]}This value is observable at the point of the function call, in its argument list, and as the a0 constant in every polynomial coefficient tuple. A co-resident script intercepting any one of these three exposure points recovers the complete wallet entropy without collecting any shares.
Impact: Any script with page-context access — browser extensions, injected scripts, developer console instrumentation — can capture the complete wallet secret before or during share generation.
Vulnerability 2 — Shamir Polynomial Coefficients Expose Secret Bytes Directly
All 16 polynomial coefficient tuples are observable via the __shamirFn hook. The first element of every tuple is a0 = f(0) = secret byte. Collecting all 16 a0 values directly reconstructs the full 128-bit secret:
[118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9]
This matches the __split_secret secret buffer exactly. No shares need to be collected — polynomial observation alone recovers the secret.
Vulnerability 3 — Complete Share Set Returned in a Single Observable Call
__split_secret returns all five share buffers as a single JavaScript object. splitMnemonic returns all five share mnemonics as a plaintext array. Both were captured in full from single interceptable calls:
// __split_secret return value — all 5 shares in one object
{
"2": "Buffer<440bb84a7d9ace2a1a4862946d74a1bd>",
"4": "Buffer<7a928740fab9fe36fe072b30c5f91e22>",
"7": "Buffer<5c04eeb2fd7bc39cbff029e721ca66c7>",
"9": "Buffer",
"12": "Buffer"
}
// splitMnemonic return value — all 5 share mnemonics in one array
[
"dune fruit barely will provide bench hard arrange nerve hill chronic kid",
"kind neither south voice paper bread way tornado cotton cool elegant duty",
"foster cherry protect wife rotate soft zone ahead transfer atom often moment",
"scrap dizzy will pupil analyst ski hollow decorate harvest wise bamboo twenty",
"sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"
]Intercepting either return value yields the complete share set in a single operation. The security model of distributing shares across separate parties is defeated before any distribution can occur.
Vulnerability 4 — Seven Internal Cryptographic Functions Exposed on window
The following functions are attached to the global window object and are callable by any co-resident script:
window.__split_secret
window.__restore_secret
window.__shamirFn
window.__shamirInterpolation
window.splitMnemonic
window.split_mnemonic
window.calc_sharesA malicious script can call window.__restore_secret(shares) directly to reconstruct secrets from any collected shares. It can call window.__shamirFn to evaluate arbitrary polynomials, or window.splitMnemonic to generate its own splits. None of these operations require reverse-engineering the implementation — they are openly callable.
Vulnerability 5 — Last Share Mnemonic Persisted at window.share
After split completion the application stores the most recently displayed share mnemonic at the global property window.share:
window.share = "sunny dentist eagle promote blush baby wheel corn boss office phrase buzz"
Confirmed in this session at 23:49:06, approximately 33 seconds after the split completed. This property is directly readable by any script with page-context access, requires no timing sensitivity, and persists indefinitely until overwritten.
GF(256) Arithmetic Assessment
What the data shows
All 16 __restore_secret calls — covering all C(5,3)=10 three-share subsets, all C(5,4)=5 four-share subsets, and the full five-share reconstruction — returned the correct value Buffer<76c0b6f95191db4d6ec13e0d8fb2cd09>. The implementation is internally consistent: any valid share subset correctly reconstructs the secret.
GF(256) field confirmed — AES 0x11B polynomial
The v5 scanner extracted both GF(256) lookup tables from window. Comparing them against the reference tables generated from the irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B, the AES field):
GF256_EXP_TABLE(255 bytes): exact matchGF256_LOG_TABLE(256 bytes): exact match
The first eight bytes of the extracted EXP table are 01 03 05 0f 11 33 55 ff, identical to the known AES-field reference. This is independently corroborated by the captured __shamirFn return values — for example, __shamirFn(4, [118,230,242]) returns 122, which is the correct result for f(4) = 118 ⊕ (230·4) ⊕ (242·4²) computed in GF(256) with the 0x11B polynomial.
Cross-implementation compatibility is confirmed. Shares produced by this tool can be reconstructed by any Shamir implementation that uses the standard AES GF(256) field, including the reference Python secretsharing library, ssss, and any BIP-compatible implementation.
Recommended Mitigations
-
Remove all global function exposure. Internal cryptographic functions must not be attached to
window. Wrap all Shamir logic in a closed module scope with no global exports. -
Do not return all shares in a single call.
__split_secretandsplitMnemonicmust not aggregate and return the complete share set simultaneously. Shares should be produced and handled individually to prevent full-set interception. -
Remove
window.sharepersistence. Share mnemonics must not be stored as named global window properties. Any temporary reference must be cleared immediately after display. -
Minimise secret lifetime in memory. Zero the raw entropy buffer as soon as share generation completes:
secretBuffer.fill(0);
-
Harden and document GF(256) table provenance. Tables should be hardcoded constants sourced from a documented reference (e.g. FIPS 197), verified against known test vectors at module load time, and covered by independent test cases to ensure cross-implementation compatibility.
-
Conduct a formal security audit. All cryptographic code handling wallet entropy must undergo external review before any production use.
Conclusion
This clean-session run confirms and extends all previously identified findings with no prior-session contamination. The Bitaps Shamir implementation is functionally correct — all 16 reconstruction calls across every valid share subset returned the correct secret with zero divergence. The v5 scanner additionally confirmed the GF(256) field as standard AES 0x11B, resolving the cross-implementation compatibility question. However, the browser-based implementation exposes the secret at multiple points during execution and makes all five shares available to any co-resident script before distribution has occurred.
Confirmed findings — all directly evidenced by this session's log:
| Finding | Log evidence |
|---|---|
Mnemonic entropy 76c0b6f9... captured via getRandomValues hook |
23:48:08.714 |
Secret [118,192,182,249,81,145,219,77,110,193,62,13,143,178,205,9] observable in __split_secret args |
23:48:33.352 |
| Entropy and secret bytes identical (no transformation) | Cross-check: 0x76=118, 0xc0=192, 0xb6=182, 0xf9=249 |
All 16 polynomial a0 values equal secret bytes exactly |
16 __shamirFn calls at x=2 |
All 5 share buffers returned in single __split_secret return |
23:48:33.352 |
All 5 share mnemonics returned in single splitMnemonic return |
23:48:33.410 |
| 16/16 reconstructions return correct secret — zero divergence | All __restore_secret returns (16 calls) |
256 __shamirInterpolation calls — all consistent |
Full log |
window.share = Share 5 mnemonic after split |
23:49:06.144 |
7 internal cryptographic functions exposed on window |
Function hooks throughout |
GF(256) EXP and LOG tables extracted from window — confirmed AES 0x11B field |
GF256_SCAN: 2 tables, exact match |
| Cross-implementation compatibility confirmed | Tables match 0x11B reference byte-for-byte |
| Coefficient buffer overflow on final secret byte — new entropy requested correctly | 4th ENTROPY call at 23:48:33.350 |
| Candidate share arrays not window-scoped | CANDIDATE_ARRAY_SCAN: 0 found |
Disclosure Note: This report was produced through passive runtime observation of the public challenge interface in a fresh isolated browser window. No server-side code was accessed or modified.
My BTC ADDRESS : bc1q2nq97ahyrq29pna42ycqyrvlk349xt2urrm2qm
Files :
FINAL-LOG.txt
On request I can provide the exact code used to produce the above console log output