@forgesworn/shamir-words 1.0.4 → 1.1.0

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/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  **Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/@forgesworn/shamir-words)](https://www.npmjs.com/package/@forgesworn/shamir-words)
6
+ [![CI](https://github.com/forgesworn/shamir-words/actions/workflows/ci.yml/badge.svg)](https://github.com/forgesworn/shamir-words/actions/workflows/ci.yml)
7
+
5
8
  **Split secrets into human-readable word shares that can be spoken, written down, or stored separately.**
6
9
 
7
10
  Backing up cryptographic keys is hard. Raw byte shares are error-prone to transcribe and impossible to read over the phone. shamir-words combines [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing) over GF(256) with [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) word encoding, so each share becomes a list of familiar English words — just like a Bitcoin seed phrase.
@@ -118,6 +121,22 @@ The byte stream is split into 11-bit groups, each mapped to a BIP-39 word. The c
118
121
  - Share count: up to 255 (the GF(256) field size minus zero)
119
122
  - Threshold: 2-255 (single-share schemes are just copying, not secret sharing)
120
123
 
124
+ ## Part of the ForgeSworn Toolkit
125
+
126
+ [ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr.
127
+
128
+ | Library | What it does |
129
+ |---------|-------------|
130
+ | [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation (uses shamir-words for recovery) |
131
+ | [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control (Shamir key distribution) |
132
+ | [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 |
133
+ | [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs |
134
+ | [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification |
135
+ | [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens |
136
+ | [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware |
137
+ | [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations |
138
+ | [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage |
139
+
121
140
  ## Licence
122
141
 
123
142
  [MIT](LICENCE)
package/dist/index.d.ts CHANGED
@@ -1,34 +1,6 @@
1
- export declare class ShamirError extends Error {
2
- constructor(message: string);
3
- }
4
- export declare class ShamirValidationError extends ShamirError {
5
- constructor(message: string);
6
- }
7
- export declare class ShamirCryptoError extends ShamirError {
8
- constructor(message: string);
9
- }
10
- export interface ShamirShare {
11
- id: number;
12
- threshold: number;
13
- data: Uint8Array;
14
- }
15
- /**
16
- * Split a secret into shares using Shamir's Secret Sharing over GF(256).
17
- *
18
- * @param secret The secret bytes to split
19
- * @param threshold Minimum shares needed to reconstruct (>= 2)
20
- * @param shares Total number of shares to create (>= threshold, <= 255)
21
- * @returns Array of ShamirShare objects (each includes the threshold for encoding)
22
- */
23
- export declare function splitSecret(secret: Uint8Array, threshold: number, shares: number): ShamirShare[];
24
- /**
25
- * Reconstruct a secret from shares using Lagrange interpolation over GF(256).
26
- *
27
- * @param shares Array of shares (at least `threshold` shares)
28
- * @param threshold The threshold used during splitting
29
- * @returns The reconstructed secret bytes
30
- */
31
- export declare function reconstructSecret(shares: ShamirShare[], threshold: number): Uint8Array;
1
+ export { splitSecret, reconstructSecret, ShamirError, ShamirValidationError, ShamirCryptoError, } from '@forgesworn/shamir-core';
2
+ export type { ShamirShare } from '@forgesworn/shamir-core';
3
+ import type { ShamirShare } from '@forgesworn/shamir-core';
32
4
  /**
33
5
  * Encode a share as BIP-39 words.
34
6
  * Format: [data_length, threshold, share_id, ...data, checksum] → 11-bit groups → BIP-39 words.
package/dist/index.js CHANGED
@@ -1,226 +1,16 @@
1
- // Shamir's Secret Sharing over GF(256)
2
- // Split secrets into threshold-of-n shares using polynomial interpolation
3
- // Shares can be encoded as BIP-39 words for human-readable exchange
1
+ // BIP-39 word encoding for Shamir's Secret Sharing shares
2
+ // Core split/reconstruct logic is provided by @forgesworn/shamir-core
4
3
  import { sha256 } from '@noble/hashes/sha2.js';
5
4
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
5
+ // Re-export core Shamir functionality
6
+ export { splitSecret, reconstructSecret, ShamirError, ShamirValidationError, ShamirCryptoError, } from '@forgesworn/shamir-core';
7
+ import { ShamirValidationError } from '@forgesworn/shamir-core';
6
8
  /** O(1) word-to-index lookup, built once at module load */
7
9
  const BIP39_INDEX = new Map();
8
10
  for (let i = 0; i < BIP39_WORDLIST.length; i++) {
9
11
  BIP39_INDEX.set(BIP39_WORDLIST[i], i);
10
12
  }
11
13
  // ---------------------------------------------------------------------------
12
- // Errors
13
- // ---------------------------------------------------------------------------
14
- export class ShamirError extends Error {
15
- constructor(message) {
16
- super(message);
17
- this.name = 'ShamirError';
18
- }
19
- }
20
- export class ShamirValidationError extends ShamirError {
21
- constructor(message) {
22
- super(message);
23
- this.name = 'ShamirValidationError';
24
- }
25
- }
26
- export class ShamirCryptoError extends ShamirError {
27
- constructor(message) {
28
- super(message);
29
- this.name = 'ShamirCryptoError';
30
- }
31
- }
32
- // ---------------------------------------------------------------------------
33
- // GF(256) Arithmetic — irreducible polynomial 0x11b (same as AES)
34
- // ---------------------------------------------------------------------------
35
- const IRREDUCIBLE = 0x11b;
36
- const GENERATOR = 0x03;
37
- /** Log table: log_g(i) for i in [0..255]. LOG[0] is unused. */
38
- const LOG = new Uint8Array(256);
39
- /** Exp table: g^i for i in [0..255]. EXP[255] wraps to EXP[0]. */
40
- const EXP = new Uint8Array(256);
41
- /** Carryless multiplication used only during table construction */
42
- function gf256MulSlow(a, b) {
43
- let result = 0;
44
- let aa = a;
45
- let bb = b;
46
- while (bb > 0) {
47
- if (bb & 1)
48
- result ^= aa;
49
- aa <<= 1;
50
- if (aa & 0x100)
51
- aa ^= IRREDUCIBLE;
52
- bb >>= 1;
53
- }
54
- return result;
55
- }
56
- // Build log/exp tables at module load time using generator 0x03
57
- {
58
- let val = 1;
59
- for (let i = 0; i < 255; i++) {
60
- EXP[i] = val;
61
- LOG[val] = i;
62
- val = gf256MulSlow(val, GENERATOR);
63
- }
64
- // Wrap: makes modular indexing simpler
65
- EXP[255] = EXP[0];
66
- }
67
- /** Addition in GF(256) is XOR */
68
- function gf256Add(a, b) {
69
- return a ^ b;
70
- }
71
- /** Multiplication in GF(256) using log/exp tables */
72
- function gf256Mul(a, b) {
73
- if (a === 0 || b === 0)
74
- return 0;
75
- return EXP[(LOG[a] + LOG[b]) % 255];
76
- }
77
- /** Multiplicative inverse in GF(256) */
78
- function gf256Inv(a) {
79
- if (a === 0)
80
- throw new ShamirCryptoError('No inverse for zero in GF(256)');
81
- return EXP[(255 - LOG[a]) % 255];
82
- }
83
- // ---------------------------------------------------------------------------
84
- // Shamir's Secret Sharing
85
- // ---------------------------------------------------------------------------
86
- /**
87
- * Evaluate a polynomial at x in GF(256) using Horner's method.
88
- * coeffs[0] is the constant term (the secret byte).
89
- */
90
- function evalPoly(coeffs, x) {
91
- let result = 0;
92
- for (let i = coeffs.length - 1; i >= 0; i--) {
93
- result = gf256Add(gf256Mul(result, x), coeffs[i]);
94
- }
95
- return result;
96
- }
97
- /** Zero a byte array (defence-in-depth for secret material) */
98
- function zeroBytes(arr) {
99
- arr.fill(0);
100
- }
101
- /**
102
- * Split a secret into shares using Shamir's Secret Sharing over GF(256).
103
- *
104
- * @param secret The secret bytes to split
105
- * @param threshold Minimum shares needed to reconstruct (>= 2)
106
- * @param shares Total number of shares to create (>= threshold, <= 255)
107
- * @returns Array of ShamirShare objects (each includes the threshold for encoding)
108
- */
109
- export function splitSecret(secret, threshold, shares) {
110
- if (!(secret instanceof Uint8Array)) {
111
- throw new ShamirValidationError('Secret must be a Uint8Array');
112
- }
113
- if (secret.length === 0) {
114
- throw new ShamirValidationError('Secret must not be empty');
115
- }
116
- if (!Number.isSafeInteger(threshold) || !Number.isSafeInteger(shares)) {
117
- throw new ShamirValidationError('Threshold and shares must be safe integers');
118
- }
119
- if (threshold < 2) {
120
- throw new ShamirValidationError('Threshold must be at least 2');
121
- }
122
- if (shares < threshold) {
123
- throw new ShamirValidationError('Number of shares must be >= threshold');
124
- }
125
- if (shares > 255) {
126
- throw new ShamirValidationError('Number of shares must be <= 255');
127
- }
128
- if (secret.length > 255) {
129
- throw new ShamirValidationError('Secret must be at most 255 bytes for BIP-39 word encoding');
130
- }
131
- const secretLen = secret.length;
132
- const result = [];
133
- // Initialize share data arrays
134
- for (let i = 0; i < shares; i++) {
135
- result.push({ id: i + 1, threshold, data: new Uint8Array(secretLen) });
136
- }
137
- // For each byte of the secret, build a random polynomial and evaluate
138
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
139
- // coeffs[0] = secret byte, coeffs[1..threshold-1] = random
140
- const coeffs = new Uint8Array(threshold);
141
- coeffs[0] = secret[byteIdx];
142
- const rand = new Uint8Array(threshold - 1);
143
- crypto.getRandomValues(rand);
144
- for (let j = 1; j < threshold; j++) {
145
- coeffs[j] = rand[j - 1];
146
- }
147
- // Evaluate at x = 1, 2, ..., shares
148
- for (let i = 0; i < shares; i++) {
149
- result[i].data[byteIdx] = evalPoly(coeffs, i + 1);
150
- }
151
- zeroBytes(coeffs);
152
- zeroBytes(rand);
153
- }
154
- return result;
155
- }
156
- /**
157
- * Reconstruct a secret from shares using Lagrange interpolation over GF(256).
158
- *
159
- * @param shares Array of shares (at least `threshold` shares)
160
- * @param threshold The threshold used during splitting
161
- * @returns The reconstructed secret bytes
162
- */
163
- export function reconstructSecret(shares, threshold) {
164
- if (!Number.isSafeInteger(threshold) || threshold < 2) {
165
- throw new ShamirValidationError('Threshold must be an integer >= 2');
166
- }
167
- if (!Array.isArray(shares) || shares.length < threshold) {
168
- throw new ShamirValidationError(`Need at least ${threshold} shares, got ${Array.isArray(shares) ? shares.length : 0}`);
169
- }
170
- // Use only the first `threshold` shares
171
- const used = shares.slice(0, threshold);
172
- // Validate share structure, IDs, and check for duplicates
173
- const ids = new Set();
174
- for (const share of used) {
175
- if (!share || typeof share !== 'object') {
176
- throw new ShamirValidationError('Each share must be an object with id and data properties');
177
- }
178
- if (!Number.isInteger(share.id) || share.id < 1 || share.id > 255) {
179
- throw new ShamirValidationError('Invalid share ID: must be an integer in [1, 255]');
180
- }
181
- if (!(share.data instanceof Uint8Array)) {
182
- throw new ShamirValidationError('Share data must be a Uint8Array');
183
- }
184
- if (ids.has(share.id)) {
185
- throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
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
- }
190
- ids.add(share.id);
191
- }
192
- const secretLen = used[0].data.length;
193
- if (secretLen === 0) {
194
- throw new ShamirValidationError('Share data must not be empty');
195
- }
196
- for (const share of used) {
197
- if (share.data.length !== secretLen) {
198
- throw new ShamirValidationError('Inconsistent share lengths — shares may be from different secrets');
199
- }
200
- }
201
- const result = new Uint8Array(secretLen);
202
- // Lagrange interpolation at x = 0 for each byte position
203
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
204
- let value = 0;
205
- for (let i = 0; i < threshold; i++) {
206
- const xi = used[i].id;
207
- const yi = used[i].data[byteIdx];
208
- // Lagrange basis l_i(0) = product of x_j / (x_i ^ x_j) for j != i
209
- // In GF(256): subtraction = addition = XOR
210
- let basis = 1;
211
- for (let j = 0; j < threshold; j++) {
212
- if (i === j)
213
- continue;
214
- const xj = used[j].id;
215
- basis = gf256Mul(basis, gf256Mul(xj, gf256Inv(gf256Add(xi, xj))));
216
- }
217
- value = gf256Add(value, gf256Mul(yi, basis));
218
- }
219
- result[byteIdx] = value;
220
- }
221
- return result;
222
- }
223
- // ---------------------------------------------------------------------------
224
14
  // BIP-39 Word Encoding
225
15
  // ---------------------------------------------------------------------------
226
16
  /**
@@ -243,7 +33,7 @@ export function shareToWords(share) {
243
33
  throw new ShamirValidationError('Share data must be a non-empty Uint8Array');
244
34
  }
245
35
  if (share.data.length > 255) {
246
- throw new ShamirValidationError('Share data exceeds maximum length (255 bytes)');
36
+ throw new ShamirValidationError('Share data exceeds maximum length (255 bytes) for BIP-39 word encoding');
247
37
  }
248
38
  // Build payload: [data_length, threshold, share_id, ...data]
249
39
  const payloadLen = 3 + share.data.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/shamir-words",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
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",
@@ -34,7 +34,8 @@
34
34
  "key-splitting"
35
35
  ],
36
36
  "publishConfig": {
37
- "access": "public"
37
+ "access": "public",
38
+ "provenance": true
38
39
  },
39
40
  "sideEffects": false,
40
41
  "license": "MIT",
@@ -47,15 +48,17 @@
47
48
  "url": "https://github.com/forgesworn/shamir-words.git"
48
49
  },
49
50
  "dependencies": {
51
+ "@forgesworn/shamir-core": "^1.0.0",
50
52
  "@noble/hashes": "^2.0.1",
51
53
  "@scure/bip39": "^2.0.1"
52
54
  },
53
55
  "devDependencies": {
54
- "@semantic-release/changelog": "^6.0.3",
55
- "@semantic-release/git": "^10.0.1",
56
56
  "@types/node": "^25.5.0",
57
- "semantic-release": "^25.0.3",
58
57
  "typescript": "^5.7.0",
59
58
  "vitest": "^3.0.0"
59
+ },
60
+ "funding": {
61
+ "type": "lightning",
62
+ "url": "lightning:thedonkey@strike.me"
60
63
  }
61
64
  }
package/src/index.ts CHANGED
@@ -1,275 +1,28 @@
1
- // Shamir's Secret Sharing over GF(256)
2
- // Split secrets into threshold-of-n shares using polynomial interpolation
3
- // Shares can be encoded as BIP-39 words for human-readable exchange
1
+ // BIP-39 word encoding for Shamir's Secret Sharing shares
2
+ // Core split/reconstruct logic is provided by @forgesworn/shamir-core
4
3
 
5
4
  import { sha256 } from '@noble/hashes/sha2.js';
6
5
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
7
6
 
7
+ // Re-export core Shamir functionality
8
+ export {
9
+ splitSecret,
10
+ reconstructSecret,
11
+ ShamirError,
12
+ ShamirValidationError,
13
+ ShamirCryptoError,
14
+ } from '@forgesworn/shamir-core';
15
+ export type { ShamirShare } from '@forgesworn/shamir-core';
16
+
17
+ import { ShamirValidationError } from '@forgesworn/shamir-core';
18
+ import type { ShamirShare } from '@forgesworn/shamir-core';
19
+
8
20
  /** O(1) word-to-index lookup, built once at module load */
9
21
  const BIP39_INDEX = new Map<string, number>();
10
22
  for (let i = 0; i < BIP39_WORDLIST.length; i++) {
11
23
  BIP39_INDEX.set(BIP39_WORDLIST[i]!, i);
12
24
  }
13
25
 
14
- // ---------------------------------------------------------------------------
15
- // Errors
16
- // ---------------------------------------------------------------------------
17
-
18
- export class ShamirError extends Error {
19
- constructor(message: string) {
20
- super(message);
21
- this.name = 'ShamirError';
22
- }
23
- }
24
-
25
- export class ShamirValidationError extends ShamirError {
26
- constructor(message: string) {
27
- super(message);
28
- this.name = 'ShamirValidationError';
29
- }
30
- }
31
-
32
- export class ShamirCryptoError extends ShamirError {
33
- constructor(message: string) {
34
- super(message);
35
- this.name = 'ShamirCryptoError';
36
- }
37
- }
38
-
39
- // ---------------------------------------------------------------------------
40
- // Types
41
- // ---------------------------------------------------------------------------
42
-
43
- export interface ShamirShare {
44
- id: number; // 1-255 (the x coordinate)
45
- threshold: number; // 2-255 (minimum shares needed to reconstruct)
46
- data: Uint8Array; // evaluated polynomial bytes
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // GF(256) Arithmetic — irreducible polynomial 0x11b (same as AES)
51
- // ---------------------------------------------------------------------------
52
-
53
- const IRREDUCIBLE = 0x11b;
54
- const GENERATOR = 0x03;
55
-
56
- /** Log table: log_g(i) for i in [0..255]. LOG[0] is unused. */
57
- const LOG = new Uint8Array(256);
58
- /** Exp table: g^i for i in [0..255]. EXP[255] wraps to EXP[0]. */
59
- const EXP = new Uint8Array(256);
60
-
61
- /** Carryless multiplication used only during table construction */
62
- function gf256MulSlow(a: number, b: number): number {
63
- let result = 0;
64
- let aa = a;
65
- let bb = b;
66
- while (bb > 0) {
67
- if (bb & 1) result ^= aa;
68
- aa <<= 1;
69
- if (aa & 0x100) aa ^= IRREDUCIBLE;
70
- bb >>= 1;
71
- }
72
- return result;
73
- }
74
-
75
- // Build log/exp tables at module load time using generator 0x03
76
- {
77
- let val = 1;
78
- for (let i = 0; i < 255; i++) {
79
- EXP[i] = val;
80
- LOG[val] = i;
81
- val = gf256MulSlow(val, GENERATOR);
82
- }
83
- // Wrap: makes modular indexing simpler
84
- EXP[255] = EXP[0]!;
85
- }
86
-
87
- /** Addition in GF(256) is XOR */
88
- function gf256Add(a: number, b: number): number {
89
- return a ^ b;
90
- }
91
-
92
- /** Multiplication in GF(256) using log/exp tables */
93
- function gf256Mul(a: number, b: number): number {
94
- if (a === 0 || b === 0) return 0;
95
- return EXP[(LOG[a]! + LOG[b]!) % 255]!;
96
- }
97
-
98
- /** Multiplicative inverse in GF(256) */
99
- function gf256Inv(a: number): number {
100
- if (a === 0) throw new ShamirCryptoError('No inverse for zero in GF(256)');
101
- return EXP[(255 - LOG[a]!) % 255]!;
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Shamir's Secret Sharing
106
- // ---------------------------------------------------------------------------
107
-
108
- /**
109
- * Evaluate a polynomial at x in GF(256) using Horner's method.
110
- * coeffs[0] is the constant term (the secret byte).
111
- */
112
- function evalPoly(coeffs: Uint8Array, x: number): number {
113
- let result = 0;
114
- for (let i = coeffs.length - 1; i >= 0; i--) {
115
- result = gf256Add(gf256Mul(result, x), coeffs[i]!);
116
- }
117
- return result;
118
- }
119
-
120
- /** Zero a byte array (defence-in-depth for secret material) */
121
- function zeroBytes(arr: Uint8Array): void {
122
- arr.fill(0);
123
- }
124
-
125
- /**
126
- * Split a secret into shares using Shamir's Secret Sharing over GF(256).
127
- *
128
- * @param secret The secret bytes to split
129
- * @param threshold Minimum shares needed to reconstruct (>= 2)
130
- * @param shares Total number of shares to create (>= threshold, <= 255)
131
- * @returns Array of ShamirShare objects (each includes the threshold for encoding)
132
- */
133
- export function splitSecret(
134
- secret: Uint8Array,
135
- threshold: number,
136
- shares: number,
137
- ): ShamirShare[] {
138
- if (!(secret instanceof Uint8Array)) {
139
- throw new ShamirValidationError('Secret must be a Uint8Array');
140
- }
141
- if (secret.length === 0) {
142
- throw new ShamirValidationError('Secret must not be empty');
143
- }
144
- if (!Number.isSafeInteger(threshold) || !Number.isSafeInteger(shares)) {
145
- throw new ShamirValidationError('Threshold and shares must be safe integers');
146
- }
147
- if (threshold < 2) {
148
- throw new ShamirValidationError('Threshold must be at least 2');
149
- }
150
- if (shares < threshold) {
151
- throw new ShamirValidationError('Number of shares must be >= threshold');
152
- }
153
- if (shares > 255) {
154
- throw new ShamirValidationError('Number of shares must be <= 255');
155
- }
156
- if (secret.length > 255) {
157
- throw new ShamirValidationError('Secret must be at most 255 bytes for BIP-39 word encoding');
158
- }
159
-
160
- const secretLen = secret.length;
161
- const result: ShamirShare[] = [];
162
-
163
- // Initialize share data arrays
164
- for (let i = 0; i < shares; i++) {
165
- result.push({ id: i + 1, threshold, data: new Uint8Array(secretLen) });
166
- }
167
-
168
- // For each byte of the secret, build a random polynomial and evaluate
169
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
170
- // coeffs[0] = secret byte, coeffs[1..threshold-1] = random
171
- const coeffs = new Uint8Array(threshold);
172
- coeffs[0] = secret[byteIdx]!;
173
-
174
- const rand = new Uint8Array(threshold - 1);
175
- crypto.getRandomValues(rand);
176
- for (let j = 1; j < threshold; j++) {
177
- coeffs[j] = rand[j - 1]!;
178
- }
179
-
180
- // Evaluate at x = 1, 2, ..., shares
181
- for (let i = 0; i < shares; i++) {
182
- result[i]!.data[byteIdx] = evalPoly(coeffs, i + 1);
183
- }
184
-
185
- zeroBytes(coeffs);
186
- zeroBytes(rand);
187
- }
188
-
189
- return result;
190
- }
191
-
192
- /**
193
- * Reconstruct a secret from shares using Lagrange interpolation over GF(256).
194
- *
195
- * @param shares Array of shares (at least `threshold` shares)
196
- * @param threshold The threshold used during splitting
197
- * @returns The reconstructed secret bytes
198
- */
199
- export function reconstructSecret(
200
- shares: ShamirShare[],
201
- threshold: number,
202
- ): Uint8Array {
203
- if (!Number.isSafeInteger(threshold) || threshold < 2) {
204
- throw new ShamirValidationError('Threshold must be an integer >= 2');
205
- }
206
- if (!Array.isArray(shares) || shares.length < threshold) {
207
- throw new ShamirValidationError(`Need at least ${threshold} shares, got ${Array.isArray(shares) ? shares.length : 0}`);
208
- }
209
-
210
- // Use only the first `threshold` shares
211
- const used = shares.slice(0, threshold);
212
-
213
- // Validate share structure, IDs, and check for duplicates
214
- const ids = new Set<number>();
215
- for (const share of used) {
216
- if (!share || typeof share !== 'object') {
217
- throw new ShamirValidationError('Each share must be an object with id and data properties');
218
- }
219
- if (!Number.isInteger(share.id) || share.id < 1 || share.id > 255) {
220
- throw new ShamirValidationError('Invalid share ID: must be an integer in [1, 255]');
221
- }
222
- if (!(share.data instanceof Uint8Array)) {
223
- throw new ShamirValidationError('Share data must be a Uint8Array');
224
- }
225
- if (ids.has(share.id)) {
226
- throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
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
- }
233
- ids.add(share.id);
234
- }
235
-
236
- const secretLen = used[0]!.data.length;
237
- if (secretLen === 0) {
238
- throw new ShamirValidationError('Share data must not be empty');
239
- }
240
- for (const share of used) {
241
- if (share.data.length !== secretLen) {
242
- throw new ShamirValidationError('Inconsistent share lengths — shares may be from different secrets');
243
- }
244
- }
245
- const result = new Uint8Array(secretLen);
246
-
247
- // Lagrange interpolation at x = 0 for each byte position
248
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
249
- let value = 0;
250
-
251
- for (let i = 0; i < threshold; i++) {
252
- const xi = used[i]!.id;
253
- const yi = used[i]!.data[byteIdx]!;
254
-
255
- // Lagrange basis l_i(0) = product of x_j / (x_i ^ x_j) for j != i
256
- // In GF(256): subtraction = addition = XOR
257
- let basis = 1;
258
- for (let j = 0; j < threshold; j++) {
259
- if (i === j) continue;
260
- const xj = used[j]!.id;
261
- basis = gf256Mul(basis, gf256Mul(xj, gf256Inv(gf256Add(xi, xj))));
262
- }
263
-
264
- value = gf256Add(value, gf256Mul(yi, basis));
265
- }
266
-
267
- result[byteIdx] = value;
268
- }
269
-
270
- return result;
271
- }
272
-
273
26
  // ---------------------------------------------------------------------------
274
27
  // BIP-39 Word Encoding
275
28
  // ---------------------------------------------------------------------------
@@ -294,7 +47,7 @@ export function shareToWords(share: ShamirShare): string[] {
294
47
  throw new ShamirValidationError('Share data must be a non-empty Uint8Array');
295
48
  }
296
49
  if (share.data.length > 255) {
297
- throw new ShamirValidationError('Share data exceeds maximum length (255 bytes)');
50
+ throw new ShamirValidationError('Share data exceeds maximum length (255 bytes) for BIP-39 word encoding');
298
51
  }
299
52
 
300
53
  // Build payload: [data_length, threshold, share_id, ...data]