@forgesworn/shamir-words 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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT Licence
2
+
3
+ Copyright (c) 2025 Forgesworn
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, sublicence, 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 ADDED
@@ -0,0 +1,121 @@
1
+ # shamir-words
2
+
3
+ **Split secrets into human-readable word shares that can be spoken, written down, or stored separately.**
4
+
5
+ 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.
6
+
7
+ ## Why shamir-words?
8
+
9
+ - **Human-readable shares** — each share is a BIP-39 word list, not a hex blob
10
+ - **Threshold recovery** — any _t_ of _n_ shares reconstruct the secret; fewer reveal nothing
11
+ - **Integrity checking** — SHA-256 checksum detects transcription errors before reconstruction
12
+ - **Minimal dependencies** — only `@noble/hashes` and `@scure/bip39` (audited cryptographic libraries)
13
+ - **TypeScript-first** — full type safety with exported interfaces and error classes
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @forgesworn/shamir-words
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import {
25
+ splitSecret,
26
+ reconstructSecret,
27
+ shareToWords,
28
+ wordsToShare,
29
+ } from '@forgesworn/shamir-words';
30
+
31
+ // Your secret (e.g. a 32-byte private key)
32
+ const secret = new Uint8Array([
33
+ 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe,
34
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
35
+ ]);
36
+
37
+ // Split into 5 shares, any 3 can reconstruct
38
+ const shares = splitSecret(secret, 3, 5);
39
+
40
+ // Convert each share to speakable words
41
+ const wordShares = shares.map(shareToWords);
42
+ // e.g. ["abandon", "ability", "able", ...] — one word list per share
43
+
44
+ // Later: decode words back to shares and reconstruct
45
+ const decoded = wordShares.map(wordsToShare);
46
+ const recovered = reconstructSecret(decoded, 3);
47
+ // recovered === secret
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `splitSecret(secret, threshold, shares)`
53
+
54
+ Split a secret into Shamir shares over GF(256).
55
+
56
+ | Parameter | Type | Description |
57
+ |-----------|------|-------------|
58
+ | `secret` | `Uint8Array` | The secret to split (1-255 bytes) |
59
+ | `threshold` | `number` | Minimum shares needed to reconstruct (2-255) |
60
+ | `shares` | `number` | Total shares to create (threshold-255) |
61
+
62
+ Returns `ShamirShare[]`.
63
+
64
+ ### `reconstructSecret(shares, threshold)`
65
+
66
+ Reconstruct a secret from shares using Lagrange interpolation.
67
+
68
+ | Parameter | Type | Description |
69
+ |-----------|------|-------------|
70
+ | `shares` | `ShamirShare[]` | At least `threshold` shares |
71
+ | `threshold` | `number` | The threshold used during splitting |
72
+
73
+ Returns `Uint8Array` — the original secret.
74
+
75
+ ### `shareToWords(share)`
76
+
77
+ Encode a share as BIP-39 words. The word list embeds the share ID, threshold, data, and a SHA-256 checksum byte for integrity.
78
+
79
+ Returns `string[]`.
80
+
81
+ ### `wordsToShare(words)`
82
+
83
+ Decode BIP-39 words back to a share. Verifies the checksum and rejects corrupted or tampered input.
84
+
85
+ Returns `ShamirShare`.
86
+
87
+ ### Types
88
+
89
+ ```typescript
90
+ interface ShamirShare {
91
+ id: number; // 1-255 (the x-coordinate)
92
+ threshold: number; // 2-255 (minimum shares for reconstruction)
93
+ data: Uint8Array; // evaluated polynomial bytes
94
+ }
95
+ ```
96
+
97
+ ### Error Classes
98
+
99
+ - `ShamirError` — base class for all errors
100
+ - `ShamirValidationError` — invalid inputs (wrong types, out-of-range values)
101
+ - `ShamirCryptoError` — cryptographic failures (e.g. GF(256) zero inverse)
102
+
103
+ ## Wire Format
104
+
105
+ Each word-encoded share packs bytes as:
106
+
107
+ ```
108
+ [data_length, threshold, share_id, ...data, checksum]
109
+ ```
110
+
111
+ The byte stream is split into 11-bit groups, each mapped to a BIP-39 word. The checksum is the first byte of SHA-256 over the preceding bytes.
112
+
113
+ ## Limitations
114
+
115
+ - Secret size: 1-255 bytes (covers all standard key sizes up to 255 bytes)
116
+ - Share count: up to 255 (the GF(256) field size minus zero)
117
+ - Threshold: 2-255 (single-share schemes are just copying, not secret sharing)
118
+
119
+ ## Licence
120
+
121
+ [MIT](LICENCE)
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Shamir's Secret Sharing over GF(256)
2
2
  // Split secrets into threshold-of-n shares using polynomial interpolation
3
3
  // Shares can be encoded as BIP-39 words for human-readable exchange
4
- import { randomBytes } from '@noble/hashes/utils.js';
4
+ import { randomFillSync } from 'node:crypto';
5
5
  import { sha256 } from '@noble/hashes/sha2.js';
6
6
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
7
7
  /** O(1) word-to-index lookup, built once at module load */
@@ -140,7 +140,8 @@ export function splitSecret(secret, threshold, shares) {
140
140
  // coeffs[0] = secret byte, coeffs[1..threshold-1] = random
141
141
  const coeffs = new Uint8Array(threshold);
142
142
  coeffs[0] = secret[byteIdx];
143
- const rand = randomBytes(threshold - 1);
143
+ const rand = new Uint8Array(threshold - 1);
144
+ randomFillSync(rand);
144
145
  for (let j = 1; j < threshold; j++) {
145
146
  coeffs[j] = rand[j - 1];
146
147
  }
@@ -184,6 +185,9 @@ export function reconstructSecret(shares, threshold) {
184
185
  if (ids.has(share.id)) {
185
186
  throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
186
187
  }
188
+ if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
189
+ throw new ShamirValidationError(`Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`);
190
+ }
187
191
  ids.add(share.id);
188
192
  }
189
193
  const secretLen = used[0].data.length;
@@ -334,6 +338,12 @@ export function wordsToShare(words) {
334
338
  if (totalExpected > byteList.length) {
335
339
  throw new ShamirValidationError('Word list too short for encoded data length');
336
340
  }
341
+ // Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
342
+ for (let i = totalExpected; i < byteList.length; i++) {
343
+ if (byteList[i] !== 0) {
344
+ throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
345
+ }
346
+ }
337
347
  // Enforce canonical encoding: word count must match expected
338
348
  const expectedWords = Math.ceil(totalExpected * 8 / 11);
339
349
  if (words.length !== expectedWords) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/shamir-words",
3
- "version": "0.0.0-development",
3
+ "version": "1.0.1",
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",
@@ -33,11 +33,15 @@
33
33
  "cryptography",
34
34
  "key-splitting"
35
35
  ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
36
39
  "sideEffects": false,
37
40
  "license": "MIT",
38
41
  "engines": {
39
42
  "node": ">=18"
40
43
  },
44
+ "homepage": "https://github.com/forgesworn/shamir-words",
41
45
  "repository": {
42
46
  "type": "git",
43
47
  "url": "https://github.com/forgesworn/shamir-words.git"
@@ -49,6 +53,7 @@
49
53
  "devDependencies": {
50
54
  "@semantic-release/changelog": "^6.0.3",
51
55
  "@semantic-release/git": "^10.0.1",
56
+ "@types/node": "^25.5.0",
52
57
  "semantic-release": "^25.0.3",
53
58
  "typescript": "^5.7.0",
54
59
  "vitest": "^3.0.0"
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Split secrets into threshold-of-n shares using polynomial interpolation
3
3
  // Shares can be encoded as BIP-39 words for human-readable exchange
4
4
 
5
- import { randomBytes } from '@noble/hashes/utils.js';
5
+ import { randomFillSync } from 'node:crypto';
6
6
  import { sha256 } from '@noble/hashes/sha2.js';
7
7
  import { wordlist as BIP39_WORDLIST } from '@scure/bip39/wordlists/english.js';
8
8
 
@@ -172,7 +172,8 @@ export function splitSecret(
172
172
  const coeffs = new Uint8Array(threshold);
173
173
  coeffs[0] = secret[byteIdx]!;
174
174
 
175
- const rand = randomBytes(threshold - 1);
175
+ const rand = new Uint8Array(threshold - 1);
176
+ randomFillSync(rand);
176
177
  for (let j = 1; j < threshold; j++) {
177
178
  coeffs[j] = rand[j - 1]!;
178
179
  }
@@ -225,6 +226,11 @@ export function reconstructSecret(
225
226
  if (ids.has(share.id)) {
226
227
  throw new ShamirValidationError('Duplicate share IDs detected — each share must have a unique ID');
227
228
  }
229
+ if (Number.isInteger(share.threshold) && share.threshold !== threshold) {
230
+ throw new ShamirValidationError(
231
+ `Share threshold (${share.threshold}) does not match supplied threshold (${threshold})`,
232
+ );
233
+ }
228
234
  ids.add(share.id);
229
235
  }
230
236
 
@@ -398,6 +404,13 @@ export function wordsToShare(words: string[]): ShamirShare {
398
404
  throw new ShamirValidationError('Word list too short for encoded data length');
399
405
  }
400
406
 
407
+ // Verify phantom bytes (decoded from padding bits) are zero — ensures canonical encoding
408
+ for (let i = totalExpected; i < byteList.length; i++) {
409
+ if (byteList[i] !== 0) {
410
+ throw new ShamirValidationError('Non-zero padding bits detected — word list may be corrupted');
411
+ }
412
+ }
413
+
401
414
  // Enforce canonical encoding: word count must match expected
402
415
  const expectedWords = Math.ceil(totalExpected * 8 / 11);
403
416
  if (words.length !== expectedWords) {