@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 +11 -2
- package/package.json +2 -1
- package/src/index.ts +14 -2
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 =
|
|
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.
|
|
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 =
|
|
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) {
|