@forgesworn/shamir-words 1.0.3 → 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 +21 -0
- package/dist/index.d.ts +3 -31
- package/dist/index.js +6 -216
- package/package.json +8 -5
- package/src/index.ts +16 -263
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# shamir-words
|
|
2
2
|
|
|
3
|
+
**Nostr:** [`npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`](https://njump.me/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@forgesworn/shamir-words)
|
|
6
|
+
[](https://github.com/forgesworn/shamir-words/actions/workflows/ci.yml)
|
|
7
|
+
|
|
3
8
|
**Split secrets into human-readable word shares that can be spoken, written down, or stored separately.**
|
|
4
9
|
|
|
5
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.
|
|
@@ -116,6 +121,22 @@ The byte stream is split into 11-bit groups, each mapped to a BIP-39 word. The c
|
|
|
116
121
|
- Share count: up to 255 (the GF(256) field size minus zero)
|
|
117
122
|
- Threshold: 2-255 (single-share schemes are just copying, not secret sharing)
|
|
118
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
|
+
|
|
119
140
|
## Licence
|
|
120
141
|
|
|
121
142
|
[MIT](LICENCE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
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
|
|
2
|
-
//
|
|
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
|
|
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
|
|
2
|
-
//
|
|
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]
|