secrets-sharing 1.0.0 → 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,137 @@
1
+ # Secrets Sharing
2
+
3
+ A TypeScript library for Shamir's Secret Sharing using **Buffers** instead of strings.
4
+
5
+ Inspired by [grempe/secrets.js](https://github.com/grempe/secrets.js), but operates on `Buffer` objects for native binary data support.
6
+
7
+ ## Features
8
+
9
+ - Split secrets into shares using Shamir's threshold scheme
10
+ - Reconstruct secrets from shares
11
+ - Generate new shares from existing ones without knowing the secret
12
+ - Works with arbitrary binary data (Buffers)
13
+ - Configurable bit-width for the finite field (3–20 bits)
14
+ - Pluggable RNG
15
+ - Zero dependencies
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install secrets-sharing
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { share, combine } from 'secrets-sharing';
27
+
28
+ const secret = Buffer.from('my super secret data');
29
+
30
+ // Split into 5 shares, requiring any 3 to reconstruct
31
+ const shares = share(secret, 5, 3);
32
+
33
+ // Reconstruct from any 3 shares
34
+ const recovered = combine(shares.slice(0, 3));
35
+ console.log(recovered.toString()); // 'my super secret data'
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### `share(secretBuffer, numShares, threshold, padLength?)`
41
+
42
+ Split a secret Buffer into shares using Shamir's threshold secret sharing.
43
+
44
+ | Parameter | Type | Description |
45
+ |---|---|---|
46
+ | `secretBuffer` | `Buffer` | The secret to split. |
47
+ | `numShares` | `number` | Total shares to produce (2 to `config.max`). |
48
+ | `threshold` | `number` | Minimum shares required to reconstruct (2 to `numShares`). |
49
+ | `padLength` | `number` | Zero-pad the binary secret to a multiple of this many bits before splitting. Defaults to `128`. Must be 0–1024. |
50
+
51
+ **Returns:** `Buffer[]` — an array of share Buffers.
52
+
53
+ ### `combine(shares, at?)`
54
+
55
+ Reconstruct a secret Buffer from an array of share Buffers.
56
+
57
+ | Parameter | Type | Description |
58
+ |---|---|---|
59
+ | `shares` | `Buffer[]` | Array of share Buffers (must meet the threshold count). |
60
+ | `at` | `number` | Evaluates the polynomial at this x value instead of 0. Defaults to `0`. Used internally by `newShare`. |
61
+
62
+ **Returns:** `Buffer` — the reconstructed secret.
63
+
64
+ ### `newShare(id, shares)`
65
+
66
+ Generate a new share with the given numeric `id` from existing shares, without reconstructing the secret.
67
+
68
+ | Parameter | Type | Description |
69
+ |---|---|---|
70
+ | `id` | `number` | Share id, an integer in [1, `config.max`]. |
71
+ | `shares` | `Buffer[]` | At least `threshold` existing share Buffers. |
72
+
73
+ **Returns:** `Buffer` — a new share Buffer for the requested id.
74
+
75
+ ### `init(bits?)`
76
+
77
+ Initialize (or reinitialize) the finite field used for sharing. Called automatically on import with 8 bits.
78
+
79
+ | Parameter | Type | Description |
80
+ |---|---|---|
81
+ | `bits` | `number` | Bit-width for the field (3–20). Defaults to `8`. |
82
+
83
+ ### `setRNG(rng)`
84
+
85
+ Provide a custom random number generator. By default the library uses `Math.random`.
86
+
87
+ | Parameter | Type | Description |
88
+ |---|---|---|
89
+ | `rng` | `(() => number) \| null` | A function returning an integer in [0, `config.max`], or `null` to reset to the default. |
90
+
91
+ ### `getConfig()`
92
+
93
+ Return the current library configuration.
94
+
95
+ **Returns:** `{ bits: number; radix: number; maxShares: number }`
96
+
97
+ ### `extractShareComponents(shareBuffer)`
98
+
99
+ Extract the components of a share Buffer.
100
+
101
+ | Parameter | Type | Description |
102
+ |---|---|---|
103
+ | `shareBuffer` | `Buffer` | A share Buffer produced by `share()` or `newShare()`. |
104
+
105
+ **Returns:** `{ bits: number; id: number; data: string }` — where `data` is the hex payload.
106
+
107
+ ### `shareBufferToString(shareBuffer)`
108
+
109
+ Convert a share Buffer to its ASCII string representation.
110
+
111
+ **Returns:** `string`
112
+
113
+ ### `shareStringToBuffer(shareString)`
114
+
115
+ Convert an ASCII share string back to a Buffer.
116
+
117
+ **Returns:** `Buffer`
118
+
119
+ ### `utf16beFromString(str)`
120
+
121
+ Encode a JS string into a UTF-16BE Buffer with reversed code-unit ordering (matches the `secrets.js` `str2hex` convention).
122
+
123
+ **Returns:** `Buffer`
124
+
125
+ ### `utf16beToString(buf)`
126
+
127
+ Decode a UTF-16BE Buffer (with reversed code-unit ordering) back into a JS string.
128
+
129
+ **Returns:** `string`
130
+
131
+ ## Inspiration
132
+
133
+ Based on the well-known [secrets.js](https://github.com/grempe/secrets.js) library, adapted to work natively with `Buffer` objects and binary data.
134
+
135
+ ## License
136
+
137
+ ISC
@@ -75,4 +75,4 @@ export declare const _internal: {
75
75
  parseShareString: typeof parseShareString;
76
76
  };
77
77
  export {};
78
- //# sourceMappingURL=secrets-sharing.d.ts.map
78
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AA0CD,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CA2BxC;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAEvD;AAsBD,iBAAS,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMpC;AAWD,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAUlE;AAUD,iBAAS,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAa1D;AASD,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAU1D;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,CA8BnF;AAED,iBAAS,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAOvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,SAAM,GACd,MAAM,EAAE,CAsCV;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAI,GAAG,MAAM,CAuDxD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAa7D;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAMzC;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAM3E;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,eAAO,MAAM,SAAS;;;;;CAUrB,CAAC"}
package/package.json CHANGED
@@ -1,14 +1,30 @@
1
1
  {
2
2
  "name": "secrets-sharing",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Typescript implementation of Shamir's Secret Sharing Scheme using buffers.",
5
- "main": "index.js",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
6
16
  "scripts": {
7
17
  "test": "node test.js",
8
- "build": "tsc",
18
+ "build": "rm -rf dist && tsc",
19
+ "prepublishOnly": "npm run build",
9
20
  "publish": "npm run build && npm publish"
10
21
  },
11
- "keywords": ["shamir", "secret sharing", "typescript", "buffers"],
22
+ "keywords": [
23
+ "shamir",
24
+ "secret sharing",
25
+ "typescript",
26
+ "buffers"
27
+ ],
12
28
  "author": "",
13
29
  "license": "ISC",
14
30
  "type": "module",
@@ -1 +0,0 @@
1
- {"version":3,"file":"secrets-sharing.d.ts","sourceRoot":"","sources":["../secrets-sharing.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AA0CD,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CA2BxC;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,IAAI,GAAG,IAAI,CAEvD;AAsBD,iBAAS,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMpC;AAWD,iBAAS,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAUlE;AAUD,iBAAS,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAa1D;AASD,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAU1D;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,CA8BnF;AAED,iBAAS,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAOvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,SAAS,SAAM,GACd,MAAM,EAAE,CAsCV;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,SAAI,GAAG,MAAM,CAuDxD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAa7D;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAMzC;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CAM3E;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,eAAO,MAAM,SAAS;;;;;CAUrB,CAAC"}
@@ -1,467 +0,0 @@
1
- 'use strict';
2
-
3
- interface Defaults {
4
- bits: number;
5
- radix: number;
6
- minBits: number;
7
- maxBits: number;
8
- primitivePolynomials: (number | null)[];
9
- }
10
-
11
- interface Config {
12
- bits: number;
13
- radix: number;
14
- size: number;
15
- max: number;
16
- logs: number[];
17
- exps: number[];
18
- /**
19
- * RNG contract differs from grempe/secrets.js: the original's RNG takes a `bits`
20
- * argument and returns a binary string. This buffer implementation's RNG takes no
21
- * arguments and returns a number in [0, config.max], which is simpler and avoids
22
- * binary-string parsing in hot paths.
23
- */
24
- rng: (() => number) | null;
25
- }
26
-
27
- export interface SecretsConfig {
28
- bits: number;
29
- radix: number;
30
- maxShares: number;
31
- }
32
-
33
- export interface ShareComponents {
34
- bits: number;
35
- id: number;
36
- data: string;
37
- }
38
-
39
- interface ParsedShare {
40
- bits: number;
41
- id: number;
42
- value: string;
43
- }
44
-
45
- const defaults: Defaults = {
46
- bits: 8,
47
- radix: 16,
48
- minBits: 3,
49
- maxBits: 20,
50
- primitivePolynomials: [
51
- null,
52
- null,
53
- 1,
54
- 3,
55
- 3,
56
- 5,
57
- 3,
58
- 3,
59
- 29,
60
- 17,
61
- 9,
62
- 5,
63
- 83,
64
- 27,
65
- 43,
66
- 3,
67
- 45,
68
- 9,
69
- 39,
70
- 39,
71
- 9,
72
- ],
73
- };
74
-
75
- const config: Config = {
76
- bits: defaults.bits,
77
- radix: defaults.radix,
78
- size: 256,
79
- max: 255,
80
- logs: [],
81
- exps: [],
82
- rng: null,
83
- };
84
-
85
- export function init(bits?: number): void {
86
- const useBits = bits ?? defaults.bits;
87
- if (useBits < defaults.minBits || useBits > defaults.maxBits) {
88
- throw new Error('Bits out of range.');
89
- }
90
-
91
- config.bits = useBits;
92
- config.size = 1 << useBits;
93
- config.max = config.size - 1;
94
-
95
- const primitive = defaults.primitivePolynomials[useBits] as number;
96
- const logs = new Array<number>(config.size);
97
- const exps = new Array<number>(config.size);
98
-
99
- let x = 1;
100
- for (let i = 0; i < config.size; i += 1) {
101
- exps[i] = x;
102
- logs[x] = i;
103
- x <<= 1;
104
- if (x >= config.size) {
105
- x ^= primitive;
106
- x &= config.max;
107
- }
108
- }
109
-
110
- config.logs = logs;
111
- config.exps = exps;
112
- }
113
-
114
- export function setRNG(rng: (() => number) | null): void {
115
- config.rng = rng;
116
- }
117
-
118
- function random(): number {
119
- if (typeof config.rng === 'function') {
120
- const value = config.rng();
121
- if (!Number.isInteger(value) || value < 0 || value > config.max) {
122
- throw new Error('Invalid RNG output.');
123
- }
124
- return value;
125
- }
126
-
127
- return Math.floor(Math.random() * (config.max + 1));
128
- }
129
-
130
- function padLeftBinary(str: string, multiple: number): string {
131
- const missing = str.length % multiple;
132
- if (missing === 0) {
133
- return str;
134
- }
135
- return `${'0'.repeat(multiple - missing)}${str}`;
136
- }
137
-
138
- function hex2bin(hex: string): string {
139
- if (hex.length === 0) {
140
- return '';
141
- }
142
- const asBigInt = BigInt(`0x${hex}`);
143
- return padLeftBinary(asBigInt.toString(2), 4);
144
- }
145
-
146
- function bin2hex(bin: string): string {
147
- const padded = padLeftBinary(bin, 4);
148
- let out = '';
149
- for (let i = 0; i < padded.length; i += 4) {
150
- out += parseInt(padded.slice(i, i + 4), 2).toString(16);
151
- }
152
- return out;
153
- }
154
-
155
- function splitBinToParts(bin: string, padLength?: number): number[] {
156
- const padded = padLength ? padLeftBinary(bin, padLength) : bin;
157
- const parts: number[] = [];
158
-
159
- for (let i = padded.length; i > config.bits; i -= config.bits) {
160
- parts.push(parseInt(padded.slice(i - config.bits, i), 2));
161
- }
162
- parts.push(parseInt(padded.slice(0, Math.max(1, padded.length % config.bits || config.bits)), 2));
163
-
164
- return parts;
165
- }
166
-
167
- function partsToHex(parts: number[]): string {
168
- let out = '';
169
- for (const part of [...parts].reverse()) {
170
- out += part.toString(config.radix).padStart(2, '0');
171
- }
172
- return out;
173
- }
174
-
175
- function parseShareString(shareString: string): ParsedShare {
176
- // charAt(0) always returns a string unlike bracket indexing under noUncheckedIndexedAccess.
177
- const bits = parseInt(shareString.charAt(0), 36);
178
- const max = (1 << bits) - 1;
179
- const idLength = max.toString(config.radix).length;
180
- const id = parseInt(shareString.slice(1, 1 + idLength), config.radix);
181
- const value = shareString.slice(1 + idLength);
182
-
183
- if (!Number.isInteger(id) || id < 1 || id > max) {
184
- throw new Error('Invalid share id.');
185
- }
186
-
187
- return { bits, id, value };
188
- }
189
-
190
- function constructPublicShareString(id: number, dataParts: number[]): string {
191
- const maxIdChars = config.max.toString(config.radix).length;
192
- const idHex = id.toString(config.radix).padStart(maxIdChars, '0');
193
- // toUpperCase matches the original secrets.js share format for bits >= 10.
194
- return `${config.bits.toString(36).toUpperCase()}${idHex}${partsToHex(dataParts)}`;
195
- }
196
-
197
- export function horner(x: number, coeffs: number[]): number {
198
- let fx = 0;
199
- for (let i = coeffs.length - 1; i >= 0; i -= 1) {
200
- if (fx !== 0) {
201
- // logs and exps are fully populated by init(); non-null assertions are safe here.
202
- fx = config.exps[(config.logs[x]! + config.logs[fx]!) % config.max]!;
203
- }
204
- fx ^= coeffs[i]!;
205
- }
206
- return fx;
207
- }
208
-
209
- export function lagrange(at: number, x: number[], y: (number | undefined)[]): number {
210
- let sum = 0;
211
-
212
- for (let i = 0; i < x.length; i += 1) {
213
- const yi = y[i];
214
- if (!yi) {
215
- continue;
216
- }
217
-
218
- let product: number = config.logs[yi]!;
219
-
220
- for (let j = 0; j < x.length; j += 1) {
221
- if (i === j) {
222
- continue;
223
- }
224
- if (at === x[j]!) {
225
- product = -1;
226
- break;
227
- }
228
- product =
229
- (product + config.logs[at ^ x[j]!]! - config.logs[x[i]! ^ x[j]!]! + config.max) %
230
- config.max;
231
- }
232
-
233
- if (product !== -1) {
234
- sum ^= config.exps[product]!;
235
- }
236
- }
237
-
238
- return sum;
239
- }
240
-
241
- function bufferToSecretBin(secretBuffer: Buffer): string {
242
- const hex = secretBuffer.toString('hex');
243
- if (hex.length === 0) {
244
- return '10';
245
- }
246
- const asBigInt = BigInt(`0x${hex}`);
247
- return `1${padLeftBinary(asBigInt.toString(2), 16)}`;
248
- }
249
-
250
- /**
251
- * Split a secret Buffer into `numShares` shares using Shamir's threshold secret sharing.
252
- *
253
- * @param secretBuffer - The secret to split, expressed as a Buffer.
254
- * @param numShares - Total number of shares to produce (2 to config.max).
255
- * @param threshold - Minimum shares required to reconstruct (2 to numShares).
256
- * @param padLength - Zero-pad the binary secret to a multiple of this many bits before
257
- * splitting. Defaults to 128 (same as the original secrets.js) to prevent leaking
258
- * information about the size of small secrets. Must be 0–1024.
259
- * @returns An array of `numShares` Buffer shares.
260
- */
261
- export function share(
262
- secretBuffer: Buffer,
263
- numShares: number,
264
- threshold: number,
265
- padLength = 128,
266
- ): Buffer[] {
267
- if (!Buffer.isBuffer(secretBuffer)) {
268
- throw new TypeError('secretBuffer must be a Buffer');
269
- }
270
- if (!Number.isInteger(numShares) || !Number.isInteger(threshold)) {
271
- throw new TypeError('numShares and threshold must be integers');
272
- }
273
- if (numShares < 2 || numShares > config.max) {
274
- throw new Error('numShares out of range.');
275
- }
276
- if (threshold < 2 || threshold > numShares) {
277
- throw new Error('threshold out of range.');
278
- }
279
- if (!Number.isInteger(padLength) || padLength < 0 || padLength > 1024) {
280
- throw new Error('Zero-pad length must be an integer between 0 and 1024 inclusive.');
281
- }
282
-
283
- const parts = splitBinToParts(bufferToSecretBin(secretBuffer), padLength);
284
- const coeffs: number[][] = [];
285
-
286
- for (let i = 0; i < parts.length; i += 1) {
287
- const row: number[] = [parts[i]!];
288
- for (let j = 1; j < threshold; j += 1) {
289
- row[j] = random();
290
- }
291
- coeffs.push(row);
292
- }
293
-
294
- const out: Buffer[] = [];
295
- for (let xVal = 1; xVal <= numShares; xVal += 1) {
296
- const dataParts: number[] = [];
297
- for (let j = 0; j < parts.length; j += 1) {
298
- dataParts.push(horner(xVal, coeffs[j]!));
299
- }
300
- out.push(Buffer.from(constructPublicShareString(xVal, dataParts), 'ascii'));
301
- }
302
-
303
- return out;
304
- }
305
-
306
- /**
307
- * Reconstruct a secret Buffer from an array of share Buffers.
308
- *
309
- * @param shares - Array of share Buffers (must satisfy the threshold count).
310
- * @param at - If non-zero, evaluates the polynomial at this x value instead of 0
311
- * (used internally by `newShare`).
312
- * @returns The reconstructed secret as a Buffer, or the share data Buffer when `at` != 0.
313
- */
314
- export function combine(shares: Buffer[], at = 0): Buffer {
315
- if (!Array.isArray(shares) || shares.length === 0) {
316
- throw new Error('shares must be a non-empty array of Buffers');
317
- }
318
-
319
- const xArr: number[] = [];
320
- const yMatrix: (number | undefined)[][] = [];
321
- let setBits: number | undefined;
322
-
323
- for (const shareBuffer of shares) {
324
- if (!Buffer.isBuffer(shareBuffer)) {
325
- throw new TypeError('All shares must be Buffers');
326
- }
327
-
328
- const parsed = parseShareString(shareBuffer.toString('ascii'));
329
-
330
- if (setBits === undefined) {
331
- setBits = parsed.bits;
332
- if (setBits !== config.bits) {
333
- init(setBits);
334
- }
335
- } else if (parsed.bits !== setBits) {
336
- throw new Error('Mismatched share bit settings');
337
- }
338
-
339
- if (xArr.includes(parsed.id)) {
340
- continue;
341
- }
342
-
343
- const idx = xArr.push(parsed.id) - 1;
344
- const parts = splitBinToParts(hex2bin(parsed.value));
345
-
346
- for (let j = 0; j < parts.length; j += 1) {
347
- if (!yMatrix[j]) {
348
- yMatrix[j] = [];
349
- }
350
- yMatrix[j]![idx] = parts[j]!;
351
- }
352
- }
353
-
354
- let resultBin = '';
355
- for (const yRow of yMatrix) {
356
- resultBin = lagrange(at, xArr, yRow!).toString(2).padStart(config.bits, '0') + resultBin;
357
- }
358
-
359
- if (at === 0) {
360
- const marker = resultBin.indexOf('1');
361
- if (marker === -1) {
362
- throw new Error('Invalid reconstructed secret marker');
363
- }
364
- resultBin = resultBin.slice(marker + 1);
365
- }
366
-
367
- const outHex = bin2hex(resultBin);
368
- return Buffer.from(outHex, 'hex');
369
- }
370
-
371
- /**
372
- * Generate a new share with the given numeric `id` from the existing `shares`.
373
- * Mirrors `secrets.newShare(id, shares)` from the original secrets.js.
374
- *
375
- * @param id - Share id, an integer in [1, config.max].
376
- * @param shares - Threshold number of existing share Buffers.
377
- * @returns A new share Buffer for the requested id.
378
- */
379
- export function newShare(id: number, shares: Buffer[]): Buffer {
380
- if (!Number.isInteger(id) || id < 1 || id > config.max) {
381
- throw new Error(`Share id must be an integer between 1 and ${config.max}, inclusive.`);
382
- }
383
- if (!Array.isArray(shares) || shares.length === 0) {
384
- throw new Error("Invalid 'shares' argument to newShare().");
385
- }
386
-
387
- // combine(shares, id) evaluates the polynomial at `id` and returns the raw bytes
388
- // of the share payload. Reconstruct the full share string from that.
389
- const shareData = combine(shares, id);
390
- const dataParts = splitBinToParts(hex2bin(shareData.toString('hex')));
391
- return Buffer.from(constructPublicShareString(id, dataParts), 'ascii');
392
- }
393
-
394
- /**
395
- * Return the current library configuration.
396
- * Mirrors `secrets.getConfig()` from the original secrets.js.
397
- */
398
- export function getConfig(): SecretsConfig {
399
- return {
400
- bits: config.bits,
401
- radix: config.radix,
402
- maxShares: config.max,
403
- };
404
- }
405
-
406
- /**
407
- * Extract the components of a share Buffer.
408
- * Mirrors `secrets.extractShareComponents(share)` from the original secrets.js.
409
- *
410
- * @param shareBuffer - A share Buffer as produced by `share()` or `newShare()`.
411
- * @returns An object with `bits`, `id`, and `data` (hex string of share payload).
412
- */
413
- export function extractShareComponents(shareBuffer: Buffer): ShareComponents {
414
- if (!Buffer.isBuffer(shareBuffer)) {
415
- throw new TypeError('shareBuffer must be a Buffer');
416
- }
417
- const parsed = parseShareString(shareBuffer.toString('ascii'));
418
- return { bits: parsed.bits, id: parsed.id, data: parsed.value };
419
- }
420
-
421
- export function shareBufferToString(shareBuffer: Buffer): string {
422
- if (!Buffer.isBuffer(shareBuffer)) {
423
- throw new TypeError('shareBuffer must be a Buffer');
424
- }
425
- return shareBuffer.toString('ascii');
426
- }
427
-
428
- export function shareStringToBuffer(shareString: string): Buffer {
429
- if (typeof shareString !== 'string') {
430
- throw new TypeError('shareString must be a string');
431
- }
432
- return Buffer.from(shareString, 'ascii');
433
- }
434
-
435
- export function utf16beFromString(str: string): Buffer {
436
- const out = Buffer.allocUnsafe(str.length * 2);
437
- for (let i = 0; i < str.length; i += 1) {
438
- // Match secrets.js str2hex ordering (prepends each code unit).
439
- out.writeUInt16BE(str.charCodeAt(i), (str.length - 1 - i) * 2);
440
- }
441
- return out;
442
- }
443
-
444
- export function utf16beToString(buf: Buffer): string {
445
- if (buf.length % 2 !== 0) {
446
- throw new Error('Invalid UTF-16BE buffer length');
447
- }
448
- let out = '';
449
- for (let i = 0; i < buf.length; i += 2) {
450
- out = String.fromCharCode(buf.readUInt16BE(i)) + out;
451
- }
452
- return out;
453
- }
454
-
455
- export const _internal = {
456
- splitBinToParts,
457
- hex2bin,
458
- bufferToSecretBin,
459
- parseShareString,
460
- } satisfies {
461
- splitBinToParts: (bin: string, padLength?: number) => number[];
462
- hex2bin: (hex: string) => string;
463
- bufferToSecretBin: (secretBuffer: Buffer) => string;
464
- parseShareString: (shareString: string) => ParsedShare;
465
- };
466
-
467
- init(defaults.bits);
package/test.js DELETED
@@ -1,66 +0,0 @@
1
- 'use strict';
2
-
3
- const assert = require('assert');
4
- const {
5
- setRNG,
6
- share,
7
- combine,
8
- shareBufferToString,
9
- shareStringToBuffer,
10
- utf16beFromString,
11
- utf16beToString,
12
- _internal,
13
- } = require('./secrets-buffer');
14
-
15
- const validMnemonic =
16
- 'upset zoo adult butter oxygen grow liberty suit also tape river crack rich limb art guitar crime aware crowd blouse script boss mask west';
17
- const validSharesSplit = [
18
- '8010d66f07a557608f04370d937a7c3d9b106407f6f78a32b1ed69e99a79a2f2b3f9e934cf6bc8251f4890d0b9947392f3f249cda3d60b158514e7f5b9beb4aeebd68d98b33b403063ef8b12b1fb4f8f135ad2bb2477562aaacaaf977840368c86872aa4ed3255b45626c9c37845bff8d27f96ef657287e94b4a5ea7fb9cea78b7cdee4e469834623e9dffa3ded951c0b1f2f6dc5b3bc0c2df3608cf5b50622262a9d49e87899247919068ec56e33e9862636e538c1c78ad30609549a11b7d251defe68d46ce311e2c0418b9238ff3d9ee307e083de6ba998df20dbbee1295063c646efd36f8d7effec2de057a960dda873ccca9ab4a635d371ca533cb631a8382ec632876fbbff437ace9abca2ca73a240608586ffa377c34f129bc8511e21fa06',
19
- '8021accfdf4aaec10fd86e0af6e5398afe30c15fe71f0c2565cb19c2fc629fd56c9215b9864658ca2440fbc164f8eee5eee489ea9ecc0dab0379c9eb684cb01c1f8d01e0bd275a00c1cedd356a7755cfffc47f379eeea6b49d3494cee8c06738db0e4e69c0c4a0d8a52d8806e75b67507edef40f115506335dc57a9fef381c50b5ba1b5d5741b3b4674a35d7aa73780169b5e6197ed65785a46c0a0f7d40cd24cf127f2cd662fe7f2a80cba974a66af11e36c47703c9395bb6c121929b7730da202e1b0b544db99d902829e3910e3f121470e4b1b0ed6e92d18401f61bf5239c6208c55bb77079ce3775a72aee6c02c4d6e8538291551fcbb4d893a78ed62d270fa910413426b57866b818565fa89865931c0a6116d5b8e9b0224848d373cd2e993',
20
- '80317aa0d8eff9a180dc5907659f45a76260a26817b88167d626769b612b3b37d9bbfe8d4e1d97df3df86d31df6c9a371a16c6b73a3a008e815d2c1ed7a20382f30b8a8808dc1c10a0215067dcfc1cb0ebbeabbcb899f6ce30de3d4997f057a45f89629d2b26f3fcf42b47f59d1edf88aab165a072b7868a10ff263813e4f1080467f71317f981065f47ccb476aa2f41de77165522cd95477dea04f02000a826ab8ba9b256cb6a68bc70a5d5256556697a05ad2489c546b684a1b22b3d5c4b3f3bd1ff8615c38e13bb7c366ab081cb5bfd009d998b5bd22b5ae60a8df7e7b1ea589cac8687f8ac21cf577f7f928a088e565499db381f7bb6859431d442f530f48b657169459d0c4c5644f7bd93943d5fb1ea04c97e8f8d95839367a4515228313e0',
21
- ];
22
-
23
- // Original secrets.js string->hex path is UTF-16 code-unit based.
24
- const mnemonicBuffer = utf16beFromString(validMnemonic);
25
- const expectedShareBuffers = validSharesSplit.map(shareStringToBuffer);
26
-
27
- function deriveThreshold2Coefficients(secretBuf, firstShareString, padLength) {
28
- const secretParts = _internal.splitBinToParts(_internal.bufferToSecretBin(secretBuf), padLength);
29
- const firstPayload = _internal.parseShareString(firstShareString).value;
30
- const firstShareParts = _internal.splitBinToParts(_internal.hex2bin(firstPayload));
31
-
32
- assert.strictEqual(secretParts.length, firstShareParts.length, 'internal share lengths mismatch');
33
-
34
- const coeffs = new Array(secretParts.length);
35
- for (let i = 0; i < secretParts.length; i += 1) {
36
- // For threshold=2 and x=1: y1 = secretPart XOR coeff.
37
- coeffs[i] = secretParts[i] ^ firstShareParts[i];
38
- }
39
- return coeffs;
40
- }
41
-
42
- const coeffs = deriveThreshold2Coefficients(mnemonicBuffer, validSharesSplit[0], 128);
43
- let rngIndex = 0;
44
- setRNG(() => {
45
- const value = coeffs[rngIndex];
46
- rngIndex += 1;
47
- return value;
48
- });
49
-
50
- const splitBuffers = share(mnemonicBuffer, 3, 2, 128);
51
- const splitStrings = splitBuffers.map(shareBufferToString);
52
- assert.deepStrictEqual(splitStrings, validSharesSplit, 'split shares mismatch expected fixtures');
53
-
54
- const recombinedFromFixtures = combine([
55
- expectedShareBuffers[0],
56
- expectedShareBuffers[2],
57
- ]);
58
- const recombinedMnemonic = utf16beToString(recombinedFromFixtures);
59
- assert.strictEqual(recombinedMnemonic, validMnemonic, 'recombined mnemonic mismatch');
60
-
61
- // Verify app-layer transport conversion path: Buffer -> string -> Buffer.
62
- const transportRoundtrip = splitBuffers.map((shareBuf) => shareStringToBuffer(shareBufferToString(shareBuf)));
63
- const recombinedFromRoundtrip = combine([transportRoundtrip[1], transportRoundtrip[2]]);
64
- assert.strictEqual(utf16beToString(recombinedFromRoundtrip), validMnemonic, 'roundtrip recombination mismatch');
65
-
66
- console.log('ok');
File without changes