@forgesworn/shamir-words 1.0.0 → 1.0.1
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 +12 -2
- package/package.json +2 -1
- package/src/index.ts +15 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 {
|
|
4
|
+
import { randomFillSync } from 'node:crypto';
|
|
5
5
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
6
6
|
import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
|
|
7
7
|
/** O(1) word-to-index lookup, built once at module load */
|
|
@@ -140,7 +140,8 @@ export function splitSecret(secret, threshold, shares) {
|
|
|
140
140
|
// coeffs[0] = secret byte, coeffs[1..threshold-1] = random
|
|
141
141
|
const coeffs = new Uint8Array(threshold);
|
|
142
142
|
coeffs[0] = secret[byteIdx];
|
|
143
|
-
const rand =
|
|
143
|
+
const rand = new Uint8Array(threshold - 1);
|
|
144
|
+
randomFillSync(rand);
|
|
144
145
|
for (let j = 1; j < threshold; j++) {
|
|
145
146
|
coeffs[j] = rand[j - 1];
|
|
146
147
|
}
|
|
@@ -184,6 +185,9 @@ export function reconstructSecret(shares, threshold) {
|
|
|
184
185
|
if (ids.has(share.id)) {
|
|
185
186
|
throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
|
|
186
187
|
}
|
|
188
|
+
if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
|
|
189
|
+
throw new ShamirValidationError(`Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`);
|
|
190
|
+
}
|
|
187
191
|
ids.add(share.id);
|
|
188
192
|
}
|
|
189
193
|
const secretLen = used[0].data.length;
|
|
@@ -334,6 +338,12 @@ export function wordsToShare(words) {
|
|
|
334
338
|
if (totalExpected > byteList.length) {
|
|
335
339
|
throw new ShamirValidationError('Word list too short for encoded data length');
|
|
336
340
|
}
|
|
341
|
+
// Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
|
|
342
|
+
for (let i = totalExpected; i < byteList.length; i++) {
|
|
343
|
+
if (byteList[i] !== 0) {
|
|
344
|
+
throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
337
347
|
// Enforce canonical encoding: word count must match expected
|
|
338
348
|
const expectedWords = Math.ceil(totalExpected * 8 / 11);
|
|
339
349
|
if (words.length !== expectedWords) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/shamir-words",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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,7 @@
|
|
|
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 {
|
|
5
|
+
import { randomFillSync } from 'node:crypto';
|
|
6
6
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
7
7
|
import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
|
|
8
8
|
|
|
@@ -172,7 +172,8 @@ export function splitSecret(
|
|
|
172
172
|
const coeffs = new Uint8Array(threshold);
|
|
173
173
|
coeffs[0] = secret[byteIdx]!;
|
|
174
174
|
|
|
175
|
-
const rand =
|
|
175
|
+
const rand = new Uint8Array(threshold - 1);
|
|
176
|
+
randomFillSync(rand);
|
|
176
177
|
for (let j = 1; j < threshold; j++) {
|
|
177
178
|
coeffs[j] = rand[j - 1]!;
|
|
178
179
|
}
|
|
@@ -225,6 +226,11 @@ export function reconstructSecret(
|
|
|
225
226
|
if (ids.has(share.id)) {
|
|
226
227
|
throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
|
|
227
228
|
}
|
|
229
|
+
if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
|
|
230
|
+
throw new ShamirValidationError(
|
|
231
|
+
`Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
228
234
|
ids.add(share.id);
|
|
229
235
|
}
|
|
230
236
|
|
|
@@ -398,6 +404,13 @@ export function wordsToShare(words: string[]): ShamirShare {
|
|
|
398
404
|
throw new ShamirValidationError('Word list too short for encoded data length');
|
|
399
405
|
}
|
|
400
406
|
|
|
407
|
+
// Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
|
|
408
|
+
for (let i = totalExpected; i < byteList.length; i++) {
|
|
409
|
+
if (byteList[i] !== 0) {
|
|
410
|
+
throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
401
414
|
// Enforce canonical encoding: word count must match expected
|
|
402
415
|
const expectedWords = Math.ceil(totalExpected * 8 / 11);
|
|
403
416
|
if (words.length !== expectedWords) {
|