@forgesworn/shamir-core 1.0.2 → 1.0.5
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/LICENSE +21 -0
- package/README.md +106 -9
- package/llms-full.txt +269 -0
- package/llms.txt +101 -0
- package/package.json +10 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheCryptoDonkey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @forgesworn/shamir-core
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sponsors/TheCryptoDonkey)
|
|
4
|
+
|
|
3
5
|
GF(256) Shamir's Secret Sharing for TypeScript. Split a secret into threshold-of-n shares and reconstruct from any threshold-sized subset.
|
|
4
6
|
|
|
5
7
|
**Zero runtime dependencies.** Pure TypeScript, Web Crypto only.
|
|
@@ -10,6 +12,29 @@ GF(256) Shamir's Secret Sharing for TypeScript. Split a secret into threshold-of
|
|
|
10
12
|
npm install @forgesworn/shamir-core
|
|
11
13
|
```
|
|
12
14
|
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
**Node.js 19 or later**, or any runtime with a global Web Crypto `crypto.getRandomValues` (current browsers, Deno, Bun). `splitSecret` calls the global `crypto` directly, and Node.js only exposes it without a flag from version 19. Node.js 19 is end-of-life, so in practice use a maintained release.
|
|
18
|
+
|
|
19
|
+
On Node.js 18, `splitSecret` throws a plain `ReferenceError` (not a Shamir error) because `crypto` is undefined. Either start Node with `--experimental-global-webcrypto`, or define the global before calling it:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { webcrypto } from 'node:crypto';
|
|
23
|
+
globalThis.crypto ??= webcrypto;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
These are workarounds, not a supported configuration. `reconstructSecret` does not use `crypto`.
|
|
27
|
+
|
|
28
|
+
**ESM only.** The package has `"type": "module"` and its `exports` map has only an `import` condition. `require('@forgesworn/shamir-core')` therefore fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` on every Node.js version, including those that can otherwise `require()` ES modules. From CommonJS, use dynamic `import()`:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
async function main() {
|
|
32
|
+
const { splitSecret, reconstructSecret } = await import('@forgesworn/shamir-core');
|
|
33
|
+
const shares = splitSecret(new Uint8Array([1, 2, 3]), 2, 3);
|
|
34
|
+
return reconstructSecret(shares.slice(0, 2), 2);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
13
38
|
## Quick Start
|
|
14
39
|
|
|
15
40
|
```typescript
|
|
@@ -19,13 +44,27 @@ import { splitSecret, reconstructSecret } from '@forgesworn/shamir-core';
|
|
|
19
44
|
const secret = crypto.getRandomValues(new Uint8Array(32));
|
|
20
45
|
const shares = splitSecret(secret, 3, 5);
|
|
21
46
|
|
|
22
|
-
// Reconstruct from any 3 shares
|
|
23
|
-
const
|
|
24
|
-
|
|
47
|
+
// Reconstruct from shares 1, 3 and 5 (any 3 shares will do)
|
|
48
|
+
const subset = shares.filter((share) => [1, 3, 5].includes(share.id));
|
|
49
|
+
const recovered = reconstructSecret(subset, 3);
|
|
50
|
+
// recovered holds the same bytes as secret
|
|
25
51
|
```
|
|
26
52
|
|
|
27
53
|
## API
|
|
28
54
|
|
|
55
|
+
Everything is a named export of the package entry point. There is no default export.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import {
|
|
59
|
+
splitSecret,
|
|
60
|
+
reconstructSecret,
|
|
61
|
+
ShamirError,
|
|
62
|
+
ShamirValidationError,
|
|
63
|
+
ShamirCryptoError,
|
|
64
|
+
type ShamirShare, // type only, erased at compile time
|
|
65
|
+
} from '@forgesworn/shamir-core';
|
|
66
|
+
```
|
|
67
|
+
|
|
29
68
|
### `splitSecret(secret, threshold, shares)`
|
|
30
69
|
|
|
31
70
|
Split a secret into Shamir shares.
|
|
@@ -61,21 +100,62 @@ interface ShamirShare {
|
|
|
61
100
|
}
|
|
62
101
|
```
|
|
63
102
|
|
|
64
|
-
|
|
103
|
+
The interpolation uses only `id` and `data`. `threshold` is metadata for callers and encoders, but `reconstructSecret` cross-checks it against its own `threshold` argument whenever the share carries an integer value (see below). At runtime a share with a missing or non-integer `threshold` skips that check.
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
### Errors
|
|
106
|
+
|
|
107
|
+
All errors extend `Error`, and the two specific classes extend `ShamirError`, so `catch (err) { if (err instanceof ShamirError) ... }` covers every error the library throws on purpose.
|
|
108
|
+
|
|
109
|
+
- `ShamirError`: base class, never thrown directly.
|
|
110
|
+
- `ShamirValidationError`: invalid input. Every error the public functions can currently throw is this class.
|
|
111
|
+
- `ShamirCryptoError`: internal arithmetic failure. Its only throw site is the GF(256) inverse of zero, which the duplicate-ID and ID-range checks make unreachable through the public API as currently implemented. It is exported so that callers can catch it, but do not expect to see it.
|
|
112
|
+
|
|
113
|
+
`splitSecret` throws `ShamirValidationError` when:
|
|
114
|
+
|
|
115
|
+
- `secret` is not a `Uint8Array` (a Node.js `Buffer` is accepted)
|
|
116
|
+
- `secret` is empty
|
|
117
|
+
- `threshold` or `shares` is not a safe integer
|
|
118
|
+
- `threshold` is outside 2--255
|
|
119
|
+
- `shares` is less than `threshold` or greater than 255
|
|
120
|
+
|
|
121
|
+
`reconstructSecret` throws `ShamirValidationError` when:
|
|
122
|
+
|
|
123
|
+
- its `threshold` argument is not a safe integer of at least 2
|
|
124
|
+
- `shares` is not an array, or holds fewer than `threshold` shares (too few shares)
|
|
125
|
+
- any of the first `threshold` entries is not an object, or has an `id` that is not an integer in 1--255 (so id 0 is rejected)
|
|
126
|
+
- any of those entries has `data` that is not a `Uint8Array`
|
|
127
|
+
- two of those entries share an `id` (duplicate IDs)
|
|
128
|
+
- any of those entries has an integer `threshold` that differs from the `threshold` argument
|
|
129
|
+
- the data of the first of those entries is empty
|
|
130
|
+
- those entries have `data` of different lengths
|
|
131
|
+
|
|
132
|
+
Only the first `threshold` entries are validated or used. Later entries are ignored, even if malformed.
|
|
133
|
+
|
|
134
|
+
Some failures do not throw a Shamir error. If `crypto.getRandomValues` is unavailable, `splitSecret` throws whatever the runtime raises (a `ReferenceError` on Node.js 18 without the flag). Wrong data is not detected at all: see Security notes.
|
|
69
135
|
|
|
70
136
|
## Why This Library
|
|
71
137
|
|
|
72
138
|
- **Zero dependencies.** No transitive supply chain. Only Web Crypto (`crypto.getRandomValues`).
|
|
73
139
|
- **GF(256) log/exp table lookup.** O(1) field multiplication, same polynomial as AES (0x11b).
|
|
74
|
-
- **Memory zeroing.**
|
|
75
|
-
- **Strict validation.** Duplicate share IDs,
|
|
140
|
+
- **Memory zeroing.** Internal polynomial coefficients and random bytes are zeroed after use (defence-in-depth). Secrets, shares and results you hold are yours to zero; see Security notes.
|
|
141
|
+
- **Strict validation.** Duplicate share IDs, out-of-range IDs, mismatched lengths, and malformed inputs are caught with typed errors. Threshold mismatches are caught when the share carries a threshold.
|
|
76
142
|
- **No secret length limit.** Split any size secret. The maths has no ceiling.
|
|
77
143
|
- **TypeScript-first.** Strict mode, `noUncheckedIndexedAccess`, full type declarations.
|
|
78
144
|
|
|
145
|
+
## Security notes
|
|
146
|
+
|
|
147
|
+
- **No integrity check.** Shares carry no checksum, MAC or signature. A corrupted or tampered share, shares from different splits, or a `threshold` argument that is too low for shares without `threshold` metadata all return wrong bytes without any error. Verify the reconstructed secret at the application level (for example with a hash, a MAC, or by successfully decrypting something with it).
|
|
148
|
+
- **Not constant-time.** GF(256) multiplication uses log/exp table lookups indexed by secret-dependent bytes and returns early when an operand is zero. Timing and cache side channels are possible, and JavaScript runtimes give no constant-time guarantee in any case. Do not run it where an attacker can measure its timing at fine granularity.
|
|
149
|
+
- **Zeroing is partial.** `splitSecret` zeroes its internal polynomial coefficient and random-byte buffers. It does not touch your `secret` array or the `data` of the shares it returns. `reconstructSecret` zeroes nothing: it reads the share `data` arrays in place and returns a new array that you own. Call `.fill(0)` on the secret, the shares and the reconstructed result when you have finished with them. Even then, the runtime may have copied the bytes elsewhere, and numeric intermediates cannot be zeroed.
|
|
150
|
+
- **Randomness.** Coefficients come only from `crypto.getRandomValues`. There is no fallback generator and no seeding.
|
|
151
|
+
- **What a share reveals.** `data` has the same length as the secret, and `id` and `threshold` are stored in the clear.
|
|
152
|
+
|
|
153
|
+
To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
154
|
+
|
|
155
|
+
## More documentation
|
|
156
|
+
|
|
157
|
+
[`llms.txt`](llms.txt) is a short machine-readable summary. [`llms-full.txt`](llms-full.txt) is the full reference with more examples and implementation details. Both ship in the npm package.
|
|
158
|
+
|
|
79
159
|
## Ecosystem
|
|
80
160
|
|
|
81
161
|
| Package | Purpose |
|
|
@@ -83,6 +163,23 @@ interface ShamirShare {
|
|
|
83
163
|
| [`@forgesworn/shamir-words`](https://github.com/forgesworn/shamir-words) | BIP-39 word encoding for shares (depends on this package) |
|
|
84
164
|
| [`dominion-protocol`](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control (depends on this package) |
|
|
85
165
|
|
|
166
|
+
## Part of the ForgeSworn Toolkit
|
|
167
|
+
|
|
168
|
+
[ForgeSworn](https://forgesworn.dev) builds open-source cryptographic identity, payments, and coordination tools for Nostr.
|
|
169
|
+
|
|
170
|
+
| Library | What it does |
|
|
171
|
+
|---------|-------------|
|
|
172
|
+
| [nsec-tree](https://github.com/forgesworn/nsec-tree) | Deterministic sub-identity derivation |
|
|
173
|
+
| [ring-sig](https://github.com/forgesworn/ring-sig) | SAG/LSAG ring signatures on secp256k1 |
|
|
174
|
+
| [range-proof](https://github.com/forgesworn/range-proof) | Pedersen commitment range proofs |
|
|
175
|
+
| [canary-kit](https://github.com/forgesworn/canary-kit) | Coercion-resistant spoken verification |
|
|
176
|
+
| [spoken-token](https://github.com/forgesworn/spoken-token) | Human-speakable verification tokens |
|
|
177
|
+
| [toll-booth](https://github.com/forgesworn/toll-booth) | L402 payment middleware |
|
|
178
|
+
| [geohash-kit](https://github.com/forgesworn/geohash-kit) | Geohash toolkit with polygon coverage |
|
|
179
|
+
| [nostr-attestations](https://github.com/forgesworn/nostr-attestations) | NIP-VA verifiable attestations |
|
|
180
|
+
| [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control |
|
|
181
|
+
| [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust |
|
|
182
|
+
|
|
86
183
|
## Licence
|
|
87
184
|
|
|
88
185
|
MIT
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# @forgesworn/shamir-core — Full AI Reference
|
|
2
|
+
|
|
3
|
+
> GF(256) Shamir's Secret Sharing for TypeScript. Zero runtime dependencies.
|
|
4
|
+
> Version: see package.json. Repository: https://github.com/forgesworn/shamir-core
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What It Is
|
|
9
|
+
|
|
10
|
+
`@forgesworn/shamir-core` implements Shamir's Secret Sharing (SSS) over GF(256) — the 256-element Galois field. It splits any `Uint8Array` into n shares such that any threshold-sized subset can reconstruct the original, and fewer than threshold shares reveal nothing about the secret.
|
|
11
|
+
|
|
12
|
+
**Use it when:**
|
|
13
|
+
- You need threshold key splitting (e.g. split an encryption key across 5 custodians, require 3)
|
|
14
|
+
- You want a zero-dependency cryptographic primitive with no supply chain risk
|
|
15
|
+
- You need to split secrets of arbitrary byte length
|
|
16
|
+
- You are building a BIP-39 mnemonic share scheme (see `@forgesworn/shamir-words`)
|
|
17
|
+
- You are building epoch-based encrypted access control (see `dominion-protocol`)
|
|
18
|
+
|
|
19
|
+
**Do not use it when:**
|
|
20
|
+
- You need secret sharing over a large prime field (e.g. for integer secrets requiring >256 parties)
|
|
21
|
+
- You need authenticated encryption of shares — this library provides information-theoretic secrecy but no integrity/authentication of share data; layer HMAC or AEAD on top if you need that
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install @forgesworn/shamir-core
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Node.js >= 19, or any browser/runtime with a global `crypto.getRandomValues` (Bun, Deno, all modern browsers). `splitSecret` uses the bare global `crypto`, which Node.js only exposes without a flag from version 19. On Node.js 18 it throws a plain `ReferenceError` (not a Shamir error) unless Node is started with `--experimental-global-webcrypto`, or `globalThis.crypto ??= webcrypto` (from `node:crypto`) runs first. Those are workarounds, not a supported configuration. `reconstructSecret` does not use `crypto`.
|
|
32
|
+
|
|
33
|
+
The package is ESM only: `package.json` has `"type": "module"` and its `exports` map has only an `import` condition. `require('@forgesworn/shamir-core')` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` on every Node.js version, including those that can otherwise `require()` ES modules. From CommonJS, call `await import('@forgesworn/shamir-core')` inside an async function.
|
|
34
|
+
|
|
35
|
+
All exports are named exports of the entry point (no default export):
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import {
|
|
39
|
+
splitSecret,
|
|
40
|
+
reconstructSecret,
|
|
41
|
+
ShamirError,
|
|
42
|
+
ShamirValidationError,
|
|
43
|
+
ShamirCryptoError,
|
|
44
|
+
type ShamirShare, // type only, erased at compile time
|
|
45
|
+
} from '@forgesworn/shamir-core';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Full API Surface
|
|
51
|
+
|
|
52
|
+
### splitSecret
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
function splitSecret(
|
|
56
|
+
secret: Uint8Array, // Secret bytes to split (any length, must be non-empty)
|
|
57
|
+
threshold: number, // Minimum shares needed to reconstruct — integer in [2, 255]
|
|
58
|
+
shares: number, // Total shares to create — integer in [threshold, 255]
|
|
59
|
+
): ShamirShare[]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Splits `secret` into `shares` shares using a random degree-(threshold-1) polynomial over GF(256), evaluated independently for each byte of the secret. The constant term of each polynomial is the secret byte; the remaining threshold-1 coefficients are drawn from `crypto.getRandomValues`.
|
|
63
|
+
|
|
64
|
+
Returns an array of `ShamirShare` objects with `id` values 1 through `shares`. Polynomial coefficients are zeroed from memory after each byte is processed.
|
|
65
|
+
|
|
66
|
+
Throws `ShamirValidationError` when:
|
|
67
|
+
- `secret` is not a `Uint8Array` (a Node.js `Buffer` is accepted)
|
|
68
|
+
- `secret` is empty
|
|
69
|
+
- `threshold` or `shares` is not a safe integer
|
|
70
|
+
- `threshold` is outside [2, 255]
|
|
71
|
+
- `shares` is less than `threshold` or greater than 255
|
|
72
|
+
|
|
73
|
+
If `crypto.getRandomValues` is unavailable it throws whatever the runtime raises, not a Shamir error. It does not modify `secret`, and it does not zero `secret` or the returned shares.
|
|
74
|
+
|
|
75
|
+
### reconstructSecret
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
function reconstructSecret(
|
|
79
|
+
shares: ShamirShare[], // At least threshold shares from the same split
|
|
80
|
+
threshold: number, // The threshold used during splitting — integer >= 2
|
|
81
|
+
): Uint8Array
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Reconstructs the secret using Lagrange interpolation at x=0 over GF(256). Only the first `threshold` entries in `shares` are used; extras are silently ignored.
|
|
85
|
+
|
|
86
|
+
Only the first `threshold` entries are validated or used; later entries are ignored even if malformed. It throws `ShamirValidationError` when:
|
|
87
|
+
- the `threshold` argument is not a safe integer of at least 2
|
|
88
|
+
- `shares` is not an array, or has fewer than `threshold` entries (too few shares)
|
|
89
|
+
- any of the first `threshold` entries is not an object
|
|
90
|
+
- an entry's `id` is not an integer in [1, 255] (so id 0 is rejected)
|
|
91
|
+
- an entry's `data` is not a `Uint8Array`
|
|
92
|
+
- two entries have the same `id` (duplicate IDs)
|
|
93
|
+
- an entry's `threshold` is an integer that differs from the `threshold` argument (a missing or non-integer value skips this check)
|
|
94
|
+
- the first entry's `data` is empty
|
|
95
|
+
- the entries' `data` arrays have different lengths
|
|
96
|
+
|
|
97
|
+
Each share's `id`, `threshold` and `data` are read once and only those snapshotted values are validated and used, which prevents TOCTOU attacks via Proxy objects with side-effecting getters.
|
|
98
|
+
|
|
99
|
+
It does not zero anything: it reads the share `data` arrays in place and returns a new array that the caller owns.
|
|
100
|
+
|
|
101
|
+
### ShamirShare
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface ShamirShare {
|
|
105
|
+
id: number; // 1–255 — the x-coordinate in GF(256) (evaluation point)
|
|
106
|
+
threshold: number; // 2–255 — minimum shares needed; metadata, not used in maths
|
|
107
|
+
data: Uint8Array; // Same length as the original secret; the y-value for each byte
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Error Classes
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
class ShamirError extends Error {} // Base class, never thrown directly
|
|
115
|
+
class ShamirValidationError extends ShamirError {} // Bad inputs, out-of-range params
|
|
116
|
+
class ShamirCryptoError extends ShamirError {} // Internal GF(256) zero-inverse guard
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
All error classes are exported and can be caught individually. Every deliberate throw from `splitSecret` and `reconstructSecret` is currently a `ShamirValidationError`. `ShamirCryptoError` is thrown only by the internal inverse of zero in GF(256); the duplicate-ID and ID-range checks make that unreachable through the public API as currently implemented, so it exists as a guard that callers can catch. Catching `ShamirError` covers both.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## GF(256) Implementation Details
|
|
124
|
+
|
|
125
|
+
The library implements GF(2^8) — the field with 256 elements — using the same irreducible polynomial as AES:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
p(x) = x^8 + x^4 + x^3 + x + 1 → 0x11b
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Generator:** 0x03
|
|
132
|
+
|
|
133
|
+
**Arithmetic:**
|
|
134
|
+
- Addition: XOR (`a ^ b`)
|
|
135
|
+
- Multiplication: log/exp table lookup, O(1). It is not constant-time: it returns early when an operand is zero and the table indices depend on secret bytes
|
|
136
|
+
- Inverse: `EXP[(255 - LOG[a]) % 255]`
|
|
137
|
+
|
|
138
|
+
**Table construction:** Log and exp tables are built once at module load using generator 0x03 with carryless (peasant) multiplication. `EXP[255]` is aliased to `EXP[0]` to simplify modular index arithmetic.
|
|
139
|
+
|
|
140
|
+
**Polynomial evaluation:** Horner's method — evaluates a degree-(threshold-1) polynomial at a given x using (threshold-1) multiplications and (threshold-1) additions.
|
|
141
|
+
|
|
142
|
+
**Lagrange interpolation:** Reconstructs f(0) from threshold (x, y) pairs. The basis polynomial for each share is computed as the product of `x_j / (x_i XOR x_j)` for all j != i, all in GF(256).
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Security Notes
|
|
147
|
+
|
|
148
|
+
**Memory zeroing:** Polynomial coefficients (the `coeffs` and `rand` arrays) are zeroed with `Uint8Array.fill(0)` in a `finally` block after each byte is processed. This is defence-in-depth and is partial. It does not guarantee the JavaScript engine retains no other copies, and numeric intermediates cannot be zeroed. The library does not zero the caller's `secret`, the `data` of the shares `splitSecret` returns, or the array `reconstructSecret` returns; call `.fill(0)` on those when finished.
|
|
149
|
+
|
|
150
|
+
**Not constant-time:** GF(256) multiplication uses log/exp table lookups indexed by secret-dependent bytes and returns early when an operand is zero. Timing and cache side channels are possible, and JavaScript runtimes give no constant-time guarantee in any case.
|
|
151
|
+
|
|
152
|
+
**Share IDs are 1-indexed:** Valid share IDs are 1–255. Zero is not a valid evaluation point in this scheme because f(0) is the secret itself.
|
|
153
|
+
|
|
154
|
+
**No authentication:** Shares have no MAC or signature. A tampered share, shares from different splits, or a too-low `threshold` argument on shares without `threshold` metadata will silently produce a wrong secret, not an error. If you need share integrity, add a HMAC or encrypt shares with an AEAD cipher, and verify the reconstructed secret.
|
|
155
|
+
|
|
156
|
+
**Threshold metadata:** The `threshold` field on `ShamirShare` is stored for bookkeeping by callers (e.g. `@forgesworn/shamir-words` encodes it in the BIP-39 share). It is not mathematically required: the interpolation only needs the `id` and `data`. `reconstructSecret` does compare it with its `threshold` argument, but only when the share's value is an integer. A missing or non-integer value skips the check.
|
|
157
|
+
|
|
158
|
+
**TOCTOU hardening:** `reconstructSecret` reads `id`, `threshold`, and `data` once from each share object and validates and uses only those snapshotted values. This prevents a malicious Proxy from returning different values on subsequent reads.
|
|
159
|
+
|
|
160
|
+
**What a share reveals:** `data` has the same length as the secret, and `id` and `threshold` are stored in the clear.
|
|
161
|
+
|
|
162
|
+
**Randomness:** Relies solely on `crypto.getRandomValues` for coefficient generation. There is no fallback and no seeding — callers cannot influence the randomness.
|
|
163
|
+
|
|
164
|
+
**No secret length limit:** The library imposes no upper bound on `secret.length`. Consumers may impose their own (e.g. `@forgesworn/shamir-words` limits secret length to what can be BIP-39 encoded).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Code Examples
|
|
169
|
+
|
|
170
|
+
### Basic split and reconstruct
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { splitSecret, reconstructSecret } from '@forgesworn/shamir-core';
|
|
174
|
+
|
|
175
|
+
// Split a 32-byte key: 5 shares, any 3 can reconstruct
|
|
176
|
+
const secret = crypto.getRandomValues(new Uint8Array(32));
|
|
177
|
+
const shares = splitSecret(secret, 3, 5);
|
|
178
|
+
|
|
179
|
+
// shares[0] = { id: 1, threshold: 3, data: Uint8Array(32) }
|
|
180
|
+
// shares[4] = { id: 5, threshold: 3, data: Uint8Array(32) }
|
|
181
|
+
|
|
182
|
+
// Reconstruct from shares 2, 4 and 5 (any 3 shares, in any order)
|
|
183
|
+
const subset = shares.filter((share) => [2, 4, 5].includes(share.id));
|
|
184
|
+
const recovered = reconstructSecret(subset, 3);
|
|
185
|
+
// recovered holds the same bytes as secret
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 2-of-2 split (both shares required)
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { splitSecret, reconstructSecret } from '@forgesworn/shamir-core';
|
|
192
|
+
|
|
193
|
+
const secret = new TextEncoder().encode('hunter2');
|
|
194
|
+
const shares = splitSecret(secret, 2, 2);
|
|
195
|
+
|
|
196
|
+
// Later:
|
|
197
|
+
const recovered = reconstructSecret(shares, 2);
|
|
198
|
+
console.log(new TextDecoder().decode(recovered)); // 'hunter2'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Catching errors
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import {
|
|
205
|
+
splitSecret,
|
|
206
|
+
reconstructSecret,
|
|
207
|
+
ShamirError,
|
|
208
|
+
ShamirValidationError,
|
|
209
|
+
} from '@forgesworn/shamir-core';
|
|
210
|
+
|
|
211
|
+
const mySecret = new Uint8Array([1, 2, 3]);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const shares = splitSecret(mySecret, 3, 5);
|
|
215
|
+
const recovered = reconstructSecret(shares.slice(0, 2), 3); // only 2, needs 3
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err instanceof ShamirValidationError) {
|
|
218
|
+
console.error('Bad inputs:', err.message);
|
|
219
|
+
} else if (err instanceof ShamirError) {
|
|
220
|
+
console.error('Library error:', err.message); // ShamirCryptoError is not expected here
|
|
221
|
+
} else {
|
|
222
|
+
throw err; // for example a missing global crypto on Node.js 18
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Checking share identity
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { splitSecret } from '@forgesworn/shamir-core';
|
|
231
|
+
|
|
232
|
+
// Share IDs are 1-indexed. After a 5-share split:
|
|
233
|
+
const secret = new Uint8Array([1, 2, 3]);
|
|
234
|
+
const shares = splitSecret(secret, 3, 5);
|
|
235
|
+
console.log(shares.map(s => s.id)); // [1, 2, 3, 4, 5]
|
|
236
|
+
|
|
237
|
+
// Do NOT use index 0 as an ID — it is not a valid evaluation point.
|
|
238
|
+
// The id field on the share is authoritative.
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Gotchas
|
|
244
|
+
|
|
245
|
+
- **Share IDs start at 1, not 0.** The id field on each share is its GF(256) evaluation point. Zero is not used (f(0) is the secret itself). Always use `share.id`, not the array index.
|
|
246
|
+
- **`reconstructSecret` uses only the first `threshold` shares.** If you pass 5 shares with threshold 3, only the first 3 are used. Order matters for which shares are consumed, but not for correctness — any threshold-sized subset of the original shares produces the same secret.
|
|
247
|
+
- **Threshold mismatch is validated when the share carries a threshold.** If the integer `threshold` field on any of the first `threshold` shares does not match the `threshold` argument, a `ShamirValidationError` is thrown. A share with a missing or non-integer `threshold` skips the check, so a wrong `threshold` argument then silently returns wrong bytes.
|
|
248
|
+
- **Only the first `threshold` shares are validated.** Later entries are ignored even if malformed.
|
|
249
|
+
- **Duplicate share IDs are rejected.** Passing two shares with the same `id` throws `ShamirValidationError`.
|
|
250
|
+
- **Inconsistent or empty data is rejected.** All shares used by `reconstructSecret` must have the same non-zero `data.length`.
|
|
251
|
+
- **No authentication.** A corrupted share silently produces a wrong result. Layer integrity checks at the application level.
|
|
252
|
+
- **Zeroing is the caller's job** for the secret, the shares and the reconstructed result.
|
|
253
|
+
- **ESM only, and Node.js >= 19.** `require()` fails and Node.js 18 has no global `crypto` without a flag.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Ecosystem
|
|
258
|
+
|
|
259
|
+
| Package | Role |
|
|
260
|
+
|---------|------|
|
|
261
|
+
| `@forgesworn/shamir-core` | This library — raw GF(256) split/reconstruct |
|
|
262
|
+
| `@forgesworn/shamir-words` | BIP-39 word encoding for shares (depends on this package) |
|
|
263
|
+
| `dominion-protocol` | Epoch-based encrypted access control (depends on this package) |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Licence
|
|
268
|
+
|
|
269
|
+
MIT — https://github.com/forgesworn/shamir-core/blob/main/LICENSE
|
package/llms.txt
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @forgesworn/shamir-core
|
|
2
|
+
|
|
3
|
+
> GF(256) Shamir's Secret Sharing for TypeScript. Zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
Full reference with more examples and implementation notes: llms-full.txt (shipped in this package, also at https://github.com/forgesworn/shamir-core/blob/main/llms-full.txt).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
Splits a secret (any Uint8Array) into n shares where any threshold-sized subset can reconstruct the original. Uses Shamir's Secret Sharing over GF(256) with Lagrange interpolation.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
npm install @forgesworn/shamir-core
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node.js >= 19, or any runtime with a global crypto.getRandomValues (browsers, Deno, Bun). splitSecret uses the global crypto directly. On Node.js 18 it throws a ReferenceError unless Node is started with --experimental-global-webcrypto (workaround, not supported). reconstructSecret does not use crypto.
|
|
18
|
+
- ESM only. require('@forgesworn/shamir-core') fails with ERR_PACKAGE_PATH_NOT_EXPORTED because the exports map has only an import condition. From CommonJS use `await import('@forgesworn/shamir-core')` inside an async function.
|
|
19
|
+
|
|
20
|
+
## Imports
|
|
21
|
+
|
|
22
|
+
All exports are named exports of the package entry point. There is no default export.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
splitSecret,
|
|
27
|
+
reconstructSecret,
|
|
28
|
+
ShamirError,
|
|
29
|
+
ShamirValidationError,
|
|
30
|
+
ShamirCryptoError,
|
|
31
|
+
type ShamirShare, // type only
|
|
32
|
+
} from '@forgesworn/shamir-core';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### splitSecret(secret: Uint8Array, threshold: number, shares: number): ShamirShare[]
|
|
38
|
+
|
|
39
|
+
Split a secret into shares. threshold must be in [2, 255]. shares must be in [threshold, 255].
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { splitSecret } from '@forgesworn/shamir-core';
|
|
43
|
+
const shares = splitSecret(new Uint8Array([1, 2, 3]), 2, 3);
|
|
44
|
+
// shares[0] = { id: 1, threshold: 2, data: Uint8Array }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### reconstructSecret(shares: ShamirShare[], threshold: number): Uint8Array
|
|
48
|
+
|
|
49
|
+
Reconstruct from at least threshold shares. Uses first threshold shares only; later entries are neither used nor validated.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { splitSecret, reconstructSecret } from '@forgesworn/shamir-core';
|
|
53
|
+
const shares = splitSecret(new Uint8Array([1, 2, 3]), 2, 3);
|
|
54
|
+
const secret = reconstructSecret(shares.filter((s) => s.id !== 2), 2);
|
|
55
|
+
// secret = Uint8Array [1, 2, 3]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### ShamirShare
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface ShamirShare {
|
|
62
|
+
id: number; // 1-255 (GF(256) evaluation point)
|
|
63
|
+
threshold: number; // 2-255 (minimum shares to reconstruct)
|
|
64
|
+
data: Uint8Array; // same length as original secret
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The maths uses only id and data. reconstructSecret checks threshold against its threshold argument only when the share's threshold is an integer; a missing or non-integer value skips the check.
|
|
69
|
+
|
|
70
|
+
### Errors
|
|
71
|
+
|
|
72
|
+
- ShamirError (base, never thrown directly)
|
|
73
|
+
- ShamirValidationError extends ShamirError (bad inputs; the only class the public functions currently throw)
|
|
74
|
+
- ShamirCryptoError extends ShamirError (internal GF(256) zero-inverse guard; unreachable through the public API with current validation)
|
|
75
|
+
|
|
76
|
+
splitSecret throws ShamirValidationError when: secret is not a Uint8Array or is empty; threshold or shares is not a safe integer; threshold is outside 2-255; shares is below threshold or above 255.
|
|
77
|
+
|
|
78
|
+
reconstructSecret throws ShamirValidationError when, among the first threshold entries: the threshold argument is not an integer >= 2; shares is not an array or has fewer than threshold entries; an entry is not an object; an id is not an integer in 1-255 (id 0 is rejected); data is not a Uint8Array; two ids are equal; an integer threshold on a share differs from the argument; the first entry's data is empty; data lengths differ.
|
|
79
|
+
|
|
80
|
+
If crypto.getRandomValues is missing, splitSecret throws the runtime's own error, not a Shamir error.
|
|
81
|
+
|
|
82
|
+
## Key Properties
|
|
83
|
+
|
|
84
|
+
- Zero runtime dependencies (only Web Crypto for randomness)
|
|
85
|
+
- GF(256) with log/exp table lookup (same irreducible polynomial as AES: 0x11b)
|
|
86
|
+
- Internal polynomial coefficients and random bytes zeroed after use; the caller's secret, the returned shares and the reconstructed secret are not zeroed by the library
|
|
87
|
+
- Share IDs are 1-indexed (1-255), not 0-indexed
|
|
88
|
+
- No secret length limit
|
|
89
|
+
- Threshold metadata on shares is validated during reconstruction when present as an integer
|
|
90
|
+
- Node.js >= 19, browsers, Deno, Bun
|
|
91
|
+
|
|
92
|
+
## Security Notes
|
|
93
|
+
|
|
94
|
+
- No integrity check: a wrong, corrupted or mixed-up share returns wrong bytes without an error. Verify the result at application level.
|
|
95
|
+
- Not constant-time: table lookups are indexed by secret-dependent bytes and multiplication returns early on zero operands.
|
|
96
|
+
- No fallback randomness: only crypto.getRandomValues.
|
|
97
|
+
|
|
98
|
+
## Related Packages
|
|
99
|
+
|
|
100
|
+
- @forgesworn/shamir-words: BIP-39 word encoding for shares
|
|
101
|
+
- dominion-protocol: epoch-based encrypted access control
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/shamir-core",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "GF(256) Shamir's Secret Sharing
|
|
3
|
+
"version": "1.0.5",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"llms.txt",
|
|
17
|
+
"llms-full.txt"
|
|
16
18
|
],
|
|
17
19
|
"scripts": {
|
|
18
20
|
"build": "tsc",
|
|
@@ -31,7 +33,8 @@
|
|
|
31
33
|
"galois-field"
|
|
32
34
|
],
|
|
33
35
|
"publishConfig": {
|
|
34
|
-
"access": "public"
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
35
38
|
},
|
|
36
39
|
"sideEffects": false,
|
|
37
40
|
"author": "ForgeSworn (https://github.com/forgesworn)",
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
],
|
|
42
45
|
"license": "MIT",
|
|
43
46
|
"engines": {
|
|
44
|
-
"node": ">=
|
|
47
|
+
"node": ">=19"
|
|
45
48
|
},
|
|
46
49
|
"homepage": "https://github.com/forgesworn/shamir-core",
|
|
47
50
|
"repository": {
|
|
@@ -52,14 +55,11 @@
|
|
|
52
55
|
"url": "https://github.com/forgesworn/shamir-core/issues"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
55
|
-
"@semantic-release/changelog": "^6.0.3",
|
|
56
|
-
"@semantic-release/git": "^10.0.1",
|
|
57
|
-
"semantic-release": "^25.0.3",
|
|
58
58
|
"typescript": "^5.7.0",
|
|
59
|
-
"vitest": "^
|
|
59
|
+
"vitest": "^5.0.0"
|
|
60
60
|
},
|
|
61
61
|
"funding": {
|
|
62
62
|
"type": "lightning",
|
|
63
|
-
"url": "lightning:
|
|
63
|
+
"url": "lightning:profusemeat89@walletofsatoshi.com"
|
|
64
64
|
}
|
|
65
65
|
}
|