@forgesworn/shamir-core 0.0.0-development → 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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @forgesworn/shamir-core
2
+
3
+ GF(256) Shamir's Secret Sharing for TypeScript. Split a secret into threshold-of-n shares and reconstruct from any threshold-sized subset.
4
+
5
+ **Zero runtime dependencies.** Pure TypeScript, Web Crypto only.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @forgesworn/shamir-core
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { splitSecret, reconstructSecret } from '@forgesworn/shamir-core';
17
+
18
+ // Split a 32-byte key into 5 shares, any 3 can reconstruct
19
+ const secret = crypto.getRandomValues(new Uint8Array(32));
20
+ const shares = splitSecret(secret, 3, 5);
21
+
22
+ // Reconstruct from any 3 shares
23
+ const recovered = reconstructSecret([shares[0], shares[2], shares[4]], 3);
24
+ // recovered is identical to secret
25
+ ```
26
+
27
+ ## API
28
+
29
+ ### `splitSecret(secret, threshold, shares)`
30
+
31
+ Split a secret into Shamir shares.
32
+
33
+ | Parameter | Type | Description |
34
+ |-----------|------|-------------|
35
+ | `secret` | `Uint8Array` | The secret bytes to split (any length) |
36
+ | `threshold` | `number` | Minimum shares needed to reconstruct (2--255) |
37
+ | `shares` | `number` | Total shares to create (threshold--255) |
38
+
39
+ Returns `ShamirShare[]`. Each share has `{ id, threshold, data }`.
40
+
41
+ ### `reconstructSecret(shares, threshold)`
42
+
43
+ Reconstruct a secret from shares using Lagrange interpolation.
44
+
45
+ | Parameter | Type | Description |
46
+ |-----------|------|-------------|
47
+ | `shares` | `ShamirShare[]` | At least `threshold` shares |
48
+ | `threshold` | `number` | The threshold used during splitting |
49
+
50
+ Returns `Uint8Array` (the reconstructed secret).
51
+
52
+ Only the first `threshold` shares are used. Extra shares are ignored.
53
+
54
+ ### `ShamirShare`
55
+
56
+ ```typescript
57
+ interface ShamirShare {
58
+ id: number; // 1--255 (GF(256) evaluation point)
59
+ threshold: number; // 2--255 (minimum shares to reconstruct)
60
+ data: Uint8Array; // Share data (same length as original secret)
61
+ }
62
+ ```
63
+
64
+ ### Error Classes
65
+
66
+ - `ShamirError` -- base class
67
+ - `ShamirValidationError` -- invalid parameters
68
+ - `ShamirCryptoError` -- internal crypto errors
69
+
70
+ ## Why This Library
71
+
72
+ - **Zero dependencies.** No transitive supply chain. Only Web Crypto (`crypto.getRandomValues`).
73
+ - **GF(256) log/exp table lookup.** O(1) field multiplication, same polynomial as AES (0x11b).
74
+ - **Memory zeroing.** Polynomial coefficients are zeroed after use (defence-in-depth).
75
+ - **Strict validation.** Duplicate share IDs, threshold mismatches, and malformed inputs are caught with typed errors.
76
+ - **No secret length limit.** Split any size secret. The maths has no ceiling.
77
+ - **TypeScript-first.** Strict mode, `noUncheckedIndexedAccess`, full type declarations.
78
+
79
+ ## Ecosystem
80
+
81
+ | Package | Purpose |
82
+ |---------|---------|
83
+ | [`@forgesworn/shamir-words`](https://github.com/forgesworn/shamir-words) | BIP-39 word encoding for shares (depends on this package) |
84
+ | [`dominion-protocol`](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control (depends on this package) |
85
+
86
+ ## Licence
87
+
88
+ MIT
package/dist/index.js CHANGED
@@ -112,8 +112,8 @@ export function splitSecret(secret, threshold, shares) {
112
112
  if (!Number.isSafeInteger(threshold) || !Number.isSafeInteger(shares)) {
113
113
  throw new ShamirValidationError('Threshold and shares must be safe integers');
114
114
  }
115
- if (threshold < 2) {
116
- throw new ShamirValidationError('Threshold must be at least 2');
115
+ if (threshold < 2 || threshold > 255) {
116
+ throw new ShamirValidationError('Threshold must be in [2, 255]');
117
117
  }
118
118
  if (shares < threshold) {
119
119
  throw new ShamirValidationError('Number of shares must be >= threshold');
@@ -128,17 +128,21 @@ export function splitSecret(secret, threshold, shares) {
128
128
  }
129
129
  for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
130
130
  const coeffs = new Uint8Array(threshold);
131
- coeffs[0] = secret[byteIdx];
132
131
  const rand = new Uint8Array(threshold - 1);
133
- crypto.getRandomValues(rand);
134
- for (let j = 1; j < threshold; j++) {
135
- coeffs[j] = rand[j - 1];
132
+ try {
133
+ coeffs[0] = secret[byteIdx];
134
+ crypto.getRandomValues(rand);
135
+ for (let j = 1; j < threshold; j++) {
136
+ coeffs[j] = rand[j - 1];
137
+ }
138
+ for (let i = 0; i < shares; i++) {
139
+ result[i].data[byteIdx] = evalPoly(coeffs, i + 1);
140
+ }
136
141
  }
137
- for (let i = 0; i < shares; i++) {
138
- result[i].data[byteIdx] = evalPoly(coeffs, i + 1);
142
+ finally {
143
+ zeroBytes(coeffs);
144
+ zeroBytes(rand);
139
145
  }
140
- zeroBytes(coeffs);
141
- zeroBytes(rand);
142
146
  }
143
147
  return result;
144
148
  }
@@ -156,31 +160,32 @@ export function reconstructSecret(shares, threshold) {
156
160
  if (!Array.isArray(shares) || shares.length < threshold) {
157
161
  throw new ShamirValidationError(`Need at least ${threshold} shares, got ${Array.isArray(shares) ? shares.length : 0}`);
158
162
  }
159
- // Use only the first `threshold` shares
160
- const used = shares.slice(0, threshold);
161
- // Validate share structure
163
+ // Snapshot share properties to prevent TOCTOU via Proxy/getter objects
164
+ const raw = shares.slice(0, threshold);
165
+ const used = [];
162
166
  const ids = new Set();
163
- let firstThreshold;
164
- for (const share of used) {
167
+ for (const share of raw) {
165
168
  if (!share || typeof share !== 'object') {
166
169
  throw new ShamirValidationError('Each share must be an object with id and data properties');
167
170
  }
168
- if (!Number.isInteger(share.id) || share.id < 1 || share.id > 255) {
171
+ // Snapshot once to prevent getter-based TOCTOU
172
+ const id = share.id;
173
+ const shareThreshold = share.threshold;
174
+ const data = share.data;
175
+ if (!Number.isInteger(id) || id < 1 || id > 255) {
169
176
  throw new ShamirValidationError('Invalid share ID: must be an integer in [1, 255]');
170
177
  }
171
- if (!(share.data instanceof Uint8Array)) {
178
+ if (!(data instanceof Uint8Array)) {
172
179
  throw new ShamirValidationError('Share data must be a Uint8Array');
173
180
  }
174
- if (ids.has(share.id)) {
181
+ if (ids.has(id)) {
175
182
  throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
176
183
  }
177
- if (firstThreshold === undefined) {
178
- firstThreshold = share.threshold;
179
- }
180
- else if (share.threshold !== firstThreshold) {
181
- throw new ShamirValidationError('Inconsistent threshold metadata across shares — shares may be from different splits');
184
+ if (Number.isInteger(shareThreshold) && shareThreshold !== threshold) {
185
+ throw new ShamirValidationError(`Share threshold (${shareThreshold}) does not match supplied threshold (${threshold})`);
182
186
  }
183
- ids.add(share.id);
187
+ ids.add(id);
188
+ used.push({ id, threshold: shareThreshold, data });
184
189
  }
185
190
  const secretLen = used[0].data.length;
186
191
  if (secretLen === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/shamir-core",
3
- "version": "0.0.0-development",
3
+ "version": "1.0.1",
4
4
  "description": "GF(256) Shamir's Secret Sharing — split and reconstruct secrets with threshold schemes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,8 +12,7 @@
12
12
  }
13
13
  },
14
14
  "files": [
15
- "dist",
16
- "src"
15
+ "dist"
17
16
  ],
18
17
  "scripts": {
19
18
  "build": "tsc",
package/src/index.ts DELETED
@@ -1,264 +0,0 @@
1
- // Shamir's Secret Sharing over GF(256)
2
- // Split secrets into threshold-of-n shares using polynomial interpolation
3
- // Zero runtime dependencies — only Web Crypto for randomness
4
-
5
- // ---------------------------------------------------------------------------
6
- // Errors
7
- // ---------------------------------------------------------------------------
8
-
9
- export class ShamirError extends Error {
10
- constructor(message: string) {
11
- super(message);
12
- this.name = 'ShamirError';
13
- }
14
- }
15
-
16
- export class ShamirValidationError extends ShamirError {
17
- constructor(message: string) {
18
- super(message);
19
- this.name = 'ShamirValidationError';
20
- }
21
- }
22
-
23
- export class ShamirCryptoError extends ShamirError {
24
- constructor(message: string) {
25
- super(message);
26
- this.name = 'ShamirCryptoError';
27
- }
28
- }
29
-
30
- // ---------------------------------------------------------------------------
31
- // Types
32
- // ---------------------------------------------------------------------------
33
-
34
- export interface ShamirShare {
35
- id: number; // 1-255 (the x coordinate in GF(256))
36
- threshold: number; // 2-255 (minimum shares needed to reconstruct)
37
- data: Uint8Array; // evaluated polynomial bytes
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // GF(256) Arithmetic — irreducible polynomial 0x11b (same as AES)
42
- // ---------------------------------------------------------------------------
43
-
44
- const IRREDUCIBLE = 0x11b;
45
- const GENERATOR = 0x03;
46
-
47
- /** Log table: log_g(i) for i in [0..255]. LOG[0] is unused. */
48
- const LOG = new Uint8Array(256);
49
- /** Exp table: g^i for i in [0..255]. EXP[255] wraps to EXP[0]. */
50
- const EXP = new Uint8Array(256);
51
-
52
- /** Carryless multiplication used only during table construction */
53
- function gf256MulSlow(a: number, b: number): number {
54
- let result = 0;
55
- let aa = a;
56
- let bb = b;
57
- while (bb > 0) {
58
- if (bb & 1) result ^= aa;
59
- aa <<= 1;
60
- if (aa & 0x100) aa ^= IRREDUCIBLE;
61
- bb >>= 1;
62
- }
63
- return result;
64
- }
65
-
66
- // Build log/exp tables at module load time using generator 0x03
67
- {
68
- let val = 1;
69
- for (let i = 0; i < 255; i++) {
70
- EXP[i] = val;
71
- LOG[val] = i;
72
- val = gf256MulSlow(val, GENERATOR);
73
- }
74
- // Wrap: makes modular indexing simpler
75
- EXP[255] = EXP[0]!;
76
- }
77
-
78
- /** Addition in GF(256) is XOR */
79
- function gf256Add(a: number, b: number): number {
80
- return a ^ b;
81
- }
82
-
83
- /** Multiplication in GF(256) using log/exp tables */
84
- function gf256Mul(a: number, b: number): number {
85
- if (a === 0 || b === 0) return 0;
86
- return EXP[(LOG[a]! + LOG[b]!) % 255]!;
87
- }
88
-
89
- /** Multiplicative inverse in GF(256) */
90
- function gf256Inv(a: number): number {
91
- if (a === 0) throw new ShamirCryptoError('No inverse for zero in GF(256)');
92
- return EXP[(255 - LOG[a]!) % 255]!;
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Internal helpers
97
- // ---------------------------------------------------------------------------
98
-
99
- /**
100
- * Evaluate a polynomial at x in GF(256) using Horner's method.
101
- * coeffs[0] is the constant term (the secret byte).
102
- */
103
- function evalPoly(coeffs: Uint8Array, x: number): number {
104
- let result = 0;
105
- for (let i = coeffs.length - 1; i >= 0; i--) {
106
- result = gf256Add(gf256Mul(result, x), coeffs[i]!);
107
- }
108
- return result;
109
- }
110
-
111
- /** Zero a byte array (defence-in-depth for secret material) */
112
- function zeroBytes(arr: Uint8Array): void {
113
- arr.fill(0);
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Shamir's Secret Sharing
118
- // ---------------------------------------------------------------------------
119
-
120
- /**
121
- * Split a secret into shares using Shamir's Secret Sharing over GF(256).
122
- *
123
- * @param secret The secret bytes to split (any length)
124
- * @param threshold Minimum shares needed to reconstruct (>= 2)
125
- * @param shares Total number of shares to create (>= threshold, <= 255)
126
- * @returns Array of ShamirShare objects
127
- */
128
- export function splitSecret(
129
- secret: Uint8Array,
130
- threshold: number,
131
- shares: number,
132
- ): ShamirShare[] {
133
- if (!(secret instanceof Uint8Array)) {
134
- throw new ShamirValidationError('Secret must be a Uint8Array');
135
- }
136
- if (secret.length === 0) {
137
- throw new ShamirValidationError('Secret must not be empty');
138
- }
139
- if (!Number.isSafeInteger(threshold) || !Number.isSafeInteger(shares)) {
140
- throw new ShamirValidationError('Threshold and shares must be safe integers');
141
- }
142
- if (threshold < 2) {
143
- throw new ShamirValidationError('Threshold must be at least 2');
144
- }
145
- if (shares < threshold) {
146
- throw new ShamirValidationError('Number of shares must be >= threshold');
147
- }
148
- if (shares > 255) {
149
- throw new ShamirValidationError('Number of shares must be <= 255');
150
- }
151
-
152
- const secretLen = secret.length;
153
- const result: ShamirShare[] = [];
154
-
155
- for (let i = 0; i < shares; i++) {
156
- result.push({ id: i + 1, threshold, data: new Uint8Array(secretLen) });
157
- }
158
-
159
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
160
- const coeffs = new Uint8Array(threshold);
161
- coeffs[0] = secret[byteIdx]!;
162
-
163
- const rand = new Uint8Array(threshold - 1);
164
- crypto.getRandomValues(rand);
165
- for (let j = 1; j < threshold; j++) {
166
- coeffs[j] = rand[j - 1]!;
167
- }
168
-
169
- for (let i = 0; i < shares; i++) {
170
- result[i]!.data[byteIdx] = evalPoly(coeffs, i + 1);
171
- }
172
-
173
- zeroBytes(coeffs);
174
- zeroBytes(rand);
175
- }
176
-
177
- return result;
178
- }
179
-
180
- /**
181
- * Reconstruct a secret from shares using Lagrange interpolation over GF(256).
182
- *
183
- * @param shares Array of shares (at least `threshold` shares)
184
- * @param threshold The threshold used during splitting
185
- * @returns The reconstructed secret bytes
186
- */
187
- export function reconstructSecret(
188
- shares: ShamirShare[],
189
- threshold: number,
190
- ): Uint8Array {
191
- if (!Number.isSafeInteger(threshold) || threshold < 2) {
192
- throw new ShamirValidationError('Threshold must be an integer >= 2');
193
- }
194
- if (!Array.isArray(shares) || shares.length < threshold) {
195
- throw new ShamirValidationError(
196
- `Need at least ${threshold} shares, got ${Array.isArray(shares) ? shares.length : 0}`,
197
- );
198
- }
199
-
200
- // Use only the first `threshold` shares
201
- const used = shares.slice(0, threshold);
202
-
203
- // Validate share structure
204
- const ids = new Set<number>();
205
- let firstThreshold: number | undefined;
206
- for (const share of used) {
207
- if (!share || typeof share !== 'object') {
208
- throw new ShamirValidationError('Each share must be an object with id and data properties');
209
- }
210
- if (!Number.isInteger(share.id) || share.id < 1 || share.id > 255) {
211
- throw new ShamirValidationError('Invalid share ID: must be an integer in [1, 255]');
212
- }
213
- if (!(share.data instanceof Uint8Array)) {
214
- throw new ShamirValidationError('Share data must be a Uint8Array');
215
- }
216
- if (ids.has(share.id)) {
217
- throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
218
- }
219
- if (firstThreshold === undefined) {
220
- firstThreshold = share.threshold;
221
- } else if (share.threshold !== firstThreshold) {
222
- throw new ShamirValidationError(
223
- 'Inconsistent threshold metadata across shares — shares may be from different splits',
224
- );
225
- }
226
- ids.add(share.id);
227
- }
228
-
229
- const secretLen = used[0]!.data.length;
230
- if (secretLen === 0) {
231
- throw new ShamirValidationError('Share data must not be empty');
232
- }
233
- for (const share of used) {
234
- if (share.data.length !== secretLen) {
235
- throw new ShamirValidationError('Inconsistent share lengths — shares may be from different secrets');
236
- }
237
- }
238
-
239
- const result = new Uint8Array(secretLen);
240
-
241
- // Lagrange interpolation at x = 0 for each byte position
242
- for (let byteIdx = 0; byteIdx < secretLen; byteIdx++) {
243
- let value = 0;
244
-
245
- for (let i = 0; i < threshold; i++) {
246
- const xi = used[i]!.id;
247
- const yi = used[i]!.data[byteIdx]!;
248
-
249
- // Lagrange basis l_i(0) = product of x_j / (x_i ^ x_j) for j != i
250
- let basis = 1;
251
- for (let j = 0; j < threshold; j++) {
252
- if (i === j) continue;
253
- const xj = used[j]!.id;
254
- basis = gf256Mul(basis, gf256Mul(xj, gf256Inv(gf256Add(xi, xj))));
255
- }
256
-
257
- value = gf256Add(value, gf256Mul(yi, basis));
258
- }
259
-
260
- result[byteIdx] = value;
261
- }
262
-
263
- return result;
264
- }