7 Novel Vulnerabilities in shamir.py — Static Analysis Report
Hello @bitaps-com team,
I have conducted a thorough static and runtime analysis of the shamir_secret_sharing.py implementation in the PyBTC library as part of the ZeroNights X bug bounty program.
Bug 1 — assert in _gf256_div silently removed in production (python -O)
Severity: High
Location: _gf256_div(), line 55
The line assert a == _gf256_mul(r, b) is the only verification that GF(256) division produced a correct result. Python removes all assert statements when executed with the -O (optimize) flag, which is standard practice in many production deployments. Any arithmetic error therefore silently propagates incorrect GF elements into every share for the affected byte, with no exception raised.
# Current — silently gone with python -O
assert a == _gf256_mul(r, b)
# Fix
if _gf256_mul(r, b) != a:
raise ArithmeticError("GF256 division verification failed")
Bug 2 — restore_secret({}) crashes with TypeError instead of a clean error
Severity: High
Location: restore_secret(), end of validation block
Passing an empty dict leaves share_length = None. The subsequent for i in range(share_length) raises TypeError: 'NoneType' object cannot be interpreted as an integer — an internal crash rather than a meaningful error message. Confirmed by execution.
# Fix — add at entry
if not shares:
raise ValueError("No shares provided")
Bug 3 — Empty secret b"" splits silently but restore_secret always fails
Severity: Medium
Location: split_secret() line 114 vs restore_secret() line 141
split_secret(2, 3, b"") succeeds without error (the byte loop runs 0 iterations), producing shares of length 0. restore_secret then raises "Invalid shares" because share_length == 0 is explicitly rejected. The API is asymmetric: a user who accidentally splits an empty secret believes they hold a valid backup when recovery is structurally impossible.
# Fix in split_secret
if len(secret) == 0:
raise ValueError("Secret must not be empty")
Bug 4 — Duplicate share indices cause unhandled ZeroDivisionError inside interpolation
Severity: Medium
Location: restore_secret() → _interpolation()
restore_secret never validates that all share indices are distinct. If two shares carry the same index, the Lagrange denominator GF_sub(x_j, x_m) = x_j XOR x_m = 0, which triggers ZeroDivisionError deep inside _gf256_div with no descriptive context. A caller assembling shares from user input can trivially produce this condition.
# Fix
if len(set(shares.keys())) != len(shares):
raise ValueError("Share indices must be unique")
Bug 5 — _gf256_pow is O(b) loop; should be O(1) via precomputed tables
Severity: Medium / DoS
Location: _gf256_pow() line 28, called from _fn()
_gf256_pow(a, b) multiplies in a loop b-1 times. For threshold=200 and a 32-byte secret this causes approximately 636,800 unnecessary multiplications that the already-present exp/log tables make entirely redundant. This is a DoS surface on resource-constrained devices and is inconsistent with every other arithmetic primitive in the file.
# Fix — O(1)
def _gf256_pow(a, b):
if b == 0: return 1
if a == 0: return 0
return EXP_TABLE[(LOG_TABLE[a] * b) % 255]
Bug 6 — restore_secret(x=0) parameter is publicly exposed — silent wrong output
Severity: Low
Location: restore_secret() function signature
x=0 is the Lagrange evaluation target and must always be 0 to recover f(0) = secret. Exposing it as a public keyword argument allows a caller to accidentally pass x=1 and silently receive a share value instead of the secret, with no error raised.
# Fix — remove the parameter
def restore_secret(shares): # x=0 is an internal detail
Bug 7 — import time is a dead import
Severity: Info
Location: Line 2
time is imported but referenced nowhere in the file. In a cryptographic library this is worth flagging: it suggests timing-based code (possibly entropy seeding) was removed incompletely, and it is worth confirming no timing-dependent security property was inadvertently removed with it.
Verification
All bugs above were identified through static code analysis and confirmed via local execution. No live shares, mnemonics, or wallet keys were used in this research.
Bounty
If any of these findings qualify for a bounty reward, please send to:
Bitcoin address: bc1qw40vafh6cl57hxu8696arnsnqcmvv06qt9hhj2
Thank you
7 Novel Vulnerabilities in
shamir.py— Static Analysis ReportHello @bitaps-com team,
I have conducted a thorough static and runtime analysis of the
shamir_secret_sharing.pyimplementation in the PyBTC library as part of the ZeroNights X bug bounty program.Bug 1 —
assertin_gf256_divsilently removed in production (python -O)Severity: High
Location:
_gf256_div(), line 55The line
assert a == _gf256_mul(r, b)is the only verification that GF(256) division produced a correct result. Python removes allassertstatements when executed with the-O(optimize) flag, which is standard practice in many production deployments. Any arithmetic error therefore silently propagates incorrect GF elements into every share for the affected byte, with no exception raised.Bug 2 —
restore_secret({})crashes withTypeErrorinstead of a clean errorSeverity: High
Location:
restore_secret(), end of validation blockPassing an empty dict leaves
share_length = None. The subsequentfor i in range(share_length)raisesTypeError: 'NoneType' object cannot be interpreted as an integer— an internal crash rather than a meaningful error message. Confirmed by execution.Bug 3 — Empty secret
b""splits silently butrestore_secretalways failsSeverity: Medium
Location:
split_secret()line 114 vsrestore_secret()line 141split_secret(2, 3, b"")succeeds without error (the byte loop runs 0 iterations), producing shares of length 0.restore_secretthen raises"Invalid shares"becauseshare_length == 0is explicitly rejected. The API is asymmetric: a user who accidentally splits an empty secret believes they hold a valid backup when recovery is structurally impossible.Bug 4 — Duplicate share indices cause unhandled
ZeroDivisionErrorinside interpolationSeverity: Medium
Location:
restore_secret()→_interpolation()restore_secretnever validates that all share indices are distinct. If two shares carry the same index, the Lagrange denominatorGF_sub(x_j, x_m) = x_j XOR x_m = 0, which triggersZeroDivisionErrordeep inside_gf256_divwith no descriptive context. A caller assembling shares from user input can trivially produce this condition.Bug 5 —
_gf256_powis O(b) loop; should be O(1) via precomputed tablesSeverity: Medium / DoS
Location:
_gf256_pow()line 28, called from_fn()_gf256_pow(a, b)multiplies in a loopb-1times. For threshold=200 and a 32-byte secret this causes approximately 636,800 unnecessary multiplications that the already-present exp/log tables make entirely redundant. This is a DoS surface on resource-constrained devices and is inconsistent with every other arithmetic primitive in the file.Bug 6 —
restore_secret(x=0)parameter is publicly exposed — silent wrong outputSeverity: Low
Location:
restore_secret()function signaturex=0is the Lagrange evaluation target and must always be 0 to recoverf(0) = secret. Exposing it as a public keyword argument allows a caller to accidentally passx=1and silently receive a share value instead of the secret, with no error raised.Bug 7 —
import timeis a dead importSeverity: Info
Location: Line 2
timeis imported but referenced nowhere in the file. In a cryptographic library this is worth flagging: it suggests timing-based code (possibly entropy seeding) was removed incompletely, and it is worth confirming no timing-dependent security property was inadvertently removed with it.Verification
All bugs above were identified through static code analysis and confirmed via local execution. No live shares, mnemonics, or wallet keys were used in this research.
Bounty
If any of these findings qualify for a bounty reward, please send to:
Bitcoin address:
bc1qw40vafh6cl57hxu8696arnsnqcmvv06qt9hhj2Thank you