@forgesworn/shamir-words 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // Shamir's Secret Sharing over GF(256)
2
2
  // Split secrets into threshold-of-n shares using polynomial interpolation
3
3
  // Shares can be encoded as BIP-39 words for human-readable exchange
4
- import { randomBytes } from '@noble/hashes/utils.js';
5
4
  import { sha256 } from '@noble/hashes/sha2.js';
6
5
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
7
6
  /** O(1) word-to-index lookup, built once at module load */
@@ -140,7 +139,8 @@ export function splitSecret(secret, threshold, shares) {
140
139
  // coeffs[0] = secret byte, coeffs[1..threshold-1] = random
141
140
  const coeffs = new Uint8Array(threshold);
142
141
  coeffs[0] = secret[byteIdx];
143
- const rand = randomBytes(threshold - 1);
142
+ const rand = new Uint8Array(threshold - 1);
143
+ crypto.getRandomValues(rand);
144
144
  for (let j = 1; j < threshold; j++) {
145
145
  coeffs[j] = rand[j - 1];
146
146
  }
@@ -184,6 +184,9 @@ export function reconstructSecret(shares, threshold) {
184
184
  if (ids.has(share.id)) {
185
185
  throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
186
186
  }
187
+ if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
188
+ throw new ShamirValidationError(`Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`);
189
+ }
187
190
  ids.add(share.id);
188
191
  }
189
192
  const secretLen = used[0].data.length;
@@ -334,6 +337,12 @@ export function wordsToShare(words) {
334
337
  if (totalExpected > byteList.length) {
335
338
  throw new ShamirValidationError('Word list too short for encoded data length');
336
339
  }
340
+ // Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
341
+ for (let i = totalExpected; i < byteList.length; i++) {
342
+ if (byteList[i] !== 0) {
343
+ throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
344
+ }
345
+ }
337
346
  // Enforce canonical encoding: word count must match expected
338
347
  const expectedWords = Math.ceil(totalExpected * 8 / 11);
339
348
  if (words.length !== expectedWords) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/shamir-words",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Shamir's Secret Sharing over GF(256) with BIP-39 word encoding for human-readable share exchange",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,6 +53,7 @@
53
53
  "devDependencies": {
54
54
  "@semantic-release/changelog": "^6.0.3",
55
55
  "@semantic-release/git": "^10.0.1",
56
+ "@types/node": "^25.5.0",
56
57
  "semantic-release": "^25.0.3",
57
58
  "typescript": "^5.7.0",
58
59
  "vitest": "^3.0.0"
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // Split secrets into threshold-of-n shares using polynomial interpolation
3
3
  // Shares can be encoded as BIP-39 words for human-readable exchange
4
4
 
5
- import { randomBytes } from '@noble/hashes/utils.js';
6
5
  import { sha256 } from '@noble/hashes/sha2.js';
7
6
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
8
7
 
@@ -172,7 +171,8 @@ export function splitSecret(
172
171
  const coeffs = new Uint8Array(threshold);
173
172
  coeffs[0] = secret[byteIdx]!;
174
173
 
175
- const rand = randomBytes(threshold - 1);
174
+ const rand = new Uint8Array(threshold - 1);
175
+ crypto.getRandomValues(rand);
176
176
  for (let j = 1; j < threshold; j++) {
177
177
  coeffs[j] = rand[j - 1]!;
178
178
  }
@@ -225,6 +225,11 @@ export function reconstructSecret(
225
225
  if (ids.has(share.id)) {
226
226
  throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
227
227
  }
228
+ if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
229
+ throw new ShamirValidationError(
230
+ `Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`,
231
+ );
232
+ }
228
233
  ids.add(share.id);
229
234
  }
230
235
 
@@ -398,6 +403,13 @@ export function wordsToShare(words: string[]): ShamirShare {
398
403
  throw new ShamirValidationError('Word list too short for encoded data length');
399
404
  }
400
405
 
406
+ // Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
407
+ for (let i = totalExpected; i < byteList.length; i++) {
408
+ if (byteList[i] !== 0) {
409
+ throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
410
+ }
411
+ }
412
+
401
413
  // Enforce canonical encoding: word count must match expected
402
414
  const expectedWords = Math.ceil(totalExpected * 8 / 11);
403
415
  if (words.length !== expectedWords) {