secrets-sharing 1.0.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.
@@ -0,0 +1,78 @@
1
+ export interface SecretsConfig {
2
+ bits: number;
3
+ radix: number;
4
+ maxShares: number;
5
+ }
6
+ export interface ShareComponents {
7
+ bits: number;
8
+ id: number;
9
+ data: string;
10
+ }
11
+ interface ParsedShare {
12
+ bits: number;
13
+ id: number;
14
+ value: string;
15
+ }
16
+ export declare function init(bits?: number): void;
17
+ export declare function setRNG(rng: (() => number) | null): void;
18
+ declare function hex2bin(hex: string): string;
19
+ declare function splitBinToParts(bin: string, padLength?: number): number[];
20
+ declare function parseShareString(shareString: string): ParsedShare;
21
+ export declare function horner(x: number, coeffs: number[]): number;
22
+ export declare function lagrange(at: number, x: number[], y: (number | undefined)[]): number;
23
+ declare function bufferToSecretBin(secretBuffer: Buffer): string;
24
+ /**
25
+ * Split a secret Buffer into `numShares` shares using Shamir's threshold secret sharing.
26
+ *
27
+ * @param secretBuffer - The secret to split, expressed as a Buffer.
28
+ * @param numShares - Total number of shares to produce (2 to config.max).
29
+ * @param threshold - Minimum shares required to reconstruct (2 to numShares).
30
+ * @param padLength - Zero-pad the binary secret to a multiple of this many bits before
31
+ * splitting. Defaults to 128 (same as the original secrets.js) to prevent leaking
32
+ * information about the size of small secrets. Must be 0–1024.
33
+ * @returns An array of `numShares` Buffer shares.
34
+ */
35
+ export declare function share(secretBuffer: Buffer, numShares: number, threshold: number, padLength?: number): Buffer[];
36
+ /**
37
+ * Reconstruct a secret Buffer from an array of share Buffers.
38
+ *
39
+ * @param shares - Array of share Buffers (must satisfy the threshold count).
40
+ * @param at - If non-zero, evaluates the polynomial at this x value instead of 0
41
+ * (used internally by `newShare`).
42
+ * @returns The reconstructed secret as a Buffer, or the share data Buffer when `at` != 0.
43
+ */
44
+ export declare function combine(shares: Buffer[], at?: number): Buffer;
45
+ /**
46
+ * Generate a new share with the given numeric `id` from the existing `shares`.
47
+ * Mirrors `secrets.newShare(id, shares)` from the original secrets.js.
48
+ *
49
+ * @param id - Share id, an integer in [1, config.max].
50
+ * @param shares - Threshold number of existing share Buffers.
51
+ * @returns A new share Buffer for the requested id.
52
+ */
53
+ export declare function newShare(id: number, shares: Buffer[]): Buffer;
54
+ /**
55
+ * Return the current library configuration.
56
+ * Mirrors `secrets.getConfig()` from the original secrets.js.
57
+ */
58
+ export declare function getConfig(): SecretsConfig;
59
+ /**
60
+ * Extract the components of a share Buffer.
61
+ * Mirrors `secrets.extractShareComponents(share)` from the original secrets.js.
62
+ *
63
+ * @param shareBuffer - A share Buffer as produced by `share()` or `newShare()`.
64
+ * @returns An object with `bits`, `id`, and `data` (hex string of share payload).
65
+ */
66
+ export declare function extractShareComponents(shareBuffer: Buffer): ShareComponents;
67
+ export declare function shareBufferToString(shareBuffer: Buffer): string;
68
+ export declare function shareStringToBuffer(shareString: string): Buffer;
69
+ export declare function utf16beFromString(str: string): Buffer;
70
+ export declare function utf16beToString(buf: Buffer): string;
71
+ export declare const _internal: {
72
+ splitBinToParts: typeof splitBinToParts;
73
+ hex2bin: typeof hex2bin;
74
+ bufferToSecretBin: typeof bufferToSecretBin;
75
+ parseShareString: typeof parseShareString;
76
+ };
77
+ export {};
78
+ //# sourceMappingURL=secrets-sharing.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,361 @@
1
+ 'use strict';
2
+ const defaults = {
3
+ bits: 8,
4
+ radix: 16,
5
+ minBits: 3,
6
+ maxBits: 20,
7
+ primitivePolynomials: [
8
+ null,
9
+ null,
10
+ 1,
11
+ 3,
12
+ 3,
13
+ 5,
14
+ 3,
15
+ 3,
16
+ 29,
17
+ 17,
18
+ 9,
19
+ 5,
20
+ 83,
21
+ 27,
22
+ 43,
23
+ 3,
24
+ 45,
25
+ 9,
26
+ 39,
27
+ 39,
28
+ 9,
29
+ ],
30
+ };
31
+ const config = {
32
+ bits: defaults.bits,
33
+ radix: defaults.radix,
34
+ size: 256,
35
+ max: 255,
36
+ logs: [],
37
+ exps: [],
38
+ rng: null,
39
+ };
40
+ export function init(bits) {
41
+ const useBits = bits ?? defaults.bits;
42
+ if (useBits < defaults.minBits || useBits > defaults.maxBits) {
43
+ throw new Error('Bits out of range.');
44
+ }
45
+ config.bits = useBits;
46
+ config.size = 1 << useBits;
47
+ config.max = config.size - 1;
48
+ const primitive = defaults.primitivePolynomials[useBits];
49
+ const logs = new Array(config.size);
50
+ const exps = new Array(config.size);
51
+ let x = 1;
52
+ for (let i = 0; i < config.size; i += 1) {
53
+ exps[i] = x;
54
+ logs[x] = i;
55
+ x <<= 1;
56
+ if (x >= config.size) {
57
+ x ^= primitive;
58
+ x &= config.max;
59
+ }
60
+ }
61
+ config.logs = logs;
62
+ config.exps = exps;
63
+ }
64
+ export function setRNG(rng) {
65
+ config.rng = rng;
66
+ }
67
+ function random() {
68
+ if (typeof config.rng === 'function') {
69
+ const value = config.rng();
70
+ if (!Number.isInteger(value) || value < 0 || value > config.max) {
71
+ throw new Error('Invalid RNG output.');
72
+ }
73
+ return value;
74
+ }
75
+ return Math.floor(Math.random() * (config.max + 1));
76
+ }
77
+ function padLeftBinary(str, multiple) {
78
+ const missing = str.length % multiple;
79
+ if (missing === 0) {
80
+ return str;
81
+ }
82
+ return `${'0'.repeat(multiple - missing)}${str}`;
83
+ }
84
+ function hex2bin(hex) {
85
+ if (hex.length === 0) {
86
+ return '';
87
+ }
88
+ const asBigInt = BigInt(`0x${hex}`);
89
+ return padLeftBinary(asBigInt.toString(2), 4);
90
+ }
91
+ function bin2hex(bin) {
92
+ const padded = padLeftBinary(bin, 4);
93
+ let out = '';
94
+ for (let i = 0; i < padded.length; i += 4) {
95
+ out += parseInt(padded.slice(i, i + 4), 2).toString(16);
96
+ }
97
+ return out;
98
+ }
99
+ function splitBinToParts(bin, padLength) {
100
+ const padded = padLength ? padLeftBinary(bin, padLength) : bin;
101
+ const parts = [];
102
+ for (let i = padded.length; i > config.bits; i -= config.bits) {
103
+ parts.push(parseInt(padded.slice(i - config.bits, i), 2));
104
+ }
105
+ parts.push(parseInt(padded.slice(0, Math.max(1, padded.length % config.bits || config.bits)), 2));
106
+ return parts;
107
+ }
108
+ function partsToHex(parts) {
109
+ let out = '';
110
+ for (const part of [...parts].reverse()) {
111
+ out += part.toString(config.radix).padStart(2, '0');
112
+ }
113
+ return out;
114
+ }
115
+ function parseShareString(shareString) {
116
+ // charAt(0) always returns a string unlike bracket indexing under noUncheckedIndexedAccess.
117
+ const bits = parseInt(shareString.charAt(0), 36);
118
+ const max = (1 << bits) - 1;
119
+ const idLength = max.toString(config.radix).length;
120
+ const id = parseInt(shareString.slice(1, 1 + idLength), config.radix);
121
+ const value = shareString.slice(1 + idLength);
122
+ if (!Number.isInteger(id) || id < 1 || id > max) {
123
+ throw new Error('Invalid share id.');
124
+ }
125
+ return { bits, id, value };
126
+ }
127
+ function constructPublicShareString(id, dataParts) {
128
+ const maxIdChars = config.max.toString(config.radix).length;
129
+ const idHex = id.toString(config.radix).padStart(maxIdChars, '0');
130
+ // toUpperCase matches the original secrets.js share format for bits >= 10.
131
+ return `${config.bits.toString(36).toUpperCase()}${idHex}${partsToHex(dataParts)}`;
132
+ }
133
+ export function horner(x, coeffs) {
134
+ let fx = 0;
135
+ for (let i = coeffs.length - 1; i >= 0; i -= 1) {
136
+ if (fx !== 0) {
137
+ // logs and exps are fully populated by init(); non-null assertions are safe here.
138
+ fx = config.exps[(config.logs[x] + config.logs[fx]) % config.max];
139
+ }
140
+ fx ^= coeffs[i];
141
+ }
142
+ return fx;
143
+ }
144
+ export function lagrange(at, x, y) {
145
+ let sum = 0;
146
+ for (let i = 0; i < x.length; i += 1) {
147
+ const yi = y[i];
148
+ if (!yi) {
149
+ continue;
150
+ }
151
+ let product = config.logs[yi];
152
+ for (let j = 0; j < x.length; j += 1) {
153
+ if (i === j) {
154
+ continue;
155
+ }
156
+ if (at === x[j]) {
157
+ product = -1;
158
+ break;
159
+ }
160
+ product =
161
+ (product + config.logs[at ^ x[j]] - config.logs[x[i] ^ x[j]] + config.max) %
162
+ config.max;
163
+ }
164
+ if (product !== -1) {
165
+ sum ^= config.exps[product];
166
+ }
167
+ }
168
+ return sum;
169
+ }
170
+ function bufferToSecretBin(secretBuffer) {
171
+ const hex = secretBuffer.toString('hex');
172
+ if (hex.length === 0) {
173
+ return '10';
174
+ }
175
+ const asBigInt = BigInt(`0x${hex}`);
176
+ return `1${padLeftBinary(asBigInt.toString(2), 16)}`;
177
+ }
178
+ /**
179
+ * Split a secret Buffer into `numShares` shares using Shamir's threshold secret sharing.
180
+ *
181
+ * @param secretBuffer - The secret to split, expressed as a Buffer.
182
+ * @param numShares - Total number of shares to produce (2 to config.max).
183
+ * @param threshold - Minimum shares required to reconstruct (2 to numShares).
184
+ * @param padLength - Zero-pad the binary secret to a multiple of this many bits before
185
+ * splitting. Defaults to 128 (same as the original secrets.js) to prevent leaking
186
+ * information about the size of small secrets. Must be 0–1024.
187
+ * @returns An array of `numShares` Buffer shares.
188
+ */
189
+ export function share(secretBuffer, numShares, threshold, padLength = 128) {
190
+ if (!Buffer.isBuffer(secretBuffer)) {
191
+ throw new TypeError('secretBuffer must be a Buffer');
192
+ }
193
+ if (!Number.isInteger(numShares) || !Number.isInteger(threshold)) {
194
+ throw new TypeError('numShares and threshold must be integers');
195
+ }
196
+ if (numShares < 2 || numShares > config.max) {
197
+ throw new Error('numShares out of range.');
198
+ }
199
+ if (threshold < 2 || threshold > numShares) {
200
+ throw new Error('threshold out of range.');
201
+ }
202
+ if (!Number.isInteger(padLength) || padLength < 0 || padLength > 1024) {
203
+ throw new Error('Zero-pad length must be an integer between 0 and 1024 inclusive.');
204
+ }
205
+ const parts = splitBinToParts(bufferToSecretBin(secretBuffer), padLength);
206
+ const coeffs = [];
207
+ for (let i = 0; i < parts.length; i += 1) {
208
+ const row = [parts[i]];
209
+ for (let j = 1; j < threshold; j += 1) {
210
+ row[j] = random();
211
+ }
212
+ coeffs.push(row);
213
+ }
214
+ const out = [];
215
+ for (let xVal = 1; xVal <= numShares; xVal += 1) {
216
+ const dataParts = [];
217
+ for (let j = 0; j < parts.length; j += 1) {
218
+ dataParts.push(horner(xVal, coeffs[j]));
219
+ }
220
+ out.push(Buffer.from(constructPublicShareString(xVal, dataParts), 'ascii'));
221
+ }
222
+ return out;
223
+ }
224
+ /**
225
+ * Reconstruct a secret Buffer from an array of share Buffers.
226
+ *
227
+ * @param shares - Array of share Buffers (must satisfy the threshold count).
228
+ * @param at - If non-zero, evaluates the polynomial at this x value instead of 0
229
+ * (used internally by `newShare`).
230
+ * @returns The reconstructed secret as a Buffer, or the share data Buffer when `at` != 0.
231
+ */
232
+ export function combine(shares, at = 0) {
233
+ if (!Array.isArray(shares) || shares.length === 0) {
234
+ throw new Error('shares must be a non-empty array of Buffers');
235
+ }
236
+ const xArr = [];
237
+ const yMatrix = [];
238
+ let setBits;
239
+ for (const shareBuffer of shares) {
240
+ if (!Buffer.isBuffer(shareBuffer)) {
241
+ throw new TypeError('All shares must be Buffers');
242
+ }
243
+ const parsed = parseShareString(shareBuffer.toString('ascii'));
244
+ if (setBits === undefined) {
245
+ setBits = parsed.bits;
246
+ if (setBits !== config.bits) {
247
+ init(setBits);
248
+ }
249
+ }
250
+ else if (parsed.bits !== setBits) {
251
+ throw new Error('Mismatched share bit settings');
252
+ }
253
+ if (xArr.includes(parsed.id)) {
254
+ continue;
255
+ }
256
+ const idx = xArr.push(parsed.id) - 1;
257
+ const parts = splitBinToParts(hex2bin(parsed.value));
258
+ for (let j = 0; j < parts.length; j += 1) {
259
+ if (!yMatrix[j]) {
260
+ yMatrix[j] = [];
261
+ }
262
+ yMatrix[j][idx] = parts[j];
263
+ }
264
+ }
265
+ let resultBin = '';
266
+ for (const yRow of yMatrix) {
267
+ resultBin = lagrange(at, xArr, yRow).toString(2).padStart(config.bits, '0') + resultBin;
268
+ }
269
+ if (at === 0) {
270
+ const marker = resultBin.indexOf('1');
271
+ if (marker === -1) {
272
+ throw new Error('Invalid reconstructed secret marker');
273
+ }
274
+ resultBin = resultBin.slice(marker + 1);
275
+ }
276
+ const outHex = bin2hex(resultBin);
277
+ return Buffer.from(outHex, 'hex');
278
+ }
279
+ /**
280
+ * Generate a new share with the given numeric `id` from the existing `shares`.
281
+ * Mirrors `secrets.newShare(id, shares)` from the original secrets.js.
282
+ *
283
+ * @param id - Share id, an integer in [1, config.max].
284
+ * @param shares - Threshold number of existing share Buffers.
285
+ * @returns A new share Buffer for the requested id.
286
+ */
287
+ export function newShare(id, shares) {
288
+ if (!Number.isInteger(id) || id < 1 || id > config.max) {
289
+ throw new Error(`Share id must be an integer between 1 and ${config.max}, inclusive.`);
290
+ }
291
+ if (!Array.isArray(shares) || shares.length === 0) {
292
+ throw new Error("Invalid 'shares' argument to newShare().");
293
+ }
294
+ // combine(shares, id) evaluates the polynomial at `id` and returns the raw bytes
295
+ // of the share payload. Reconstruct the full share string from that.
296
+ const shareData = combine(shares, id);
297
+ const dataParts = splitBinToParts(hex2bin(shareData.toString('hex')));
298
+ return Buffer.from(constructPublicShareString(id, dataParts), 'ascii');
299
+ }
300
+ /**
301
+ * Return the current library configuration.
302
+ * Mirrors `secrets.getConfig()` from the original secrets.js.
303
+ */
304
+ export function getConfig() {
305
+ return {
306
+ bits: config.bits,
307
+ radix: config.radix,
308
+ maxShares: config.max,
309
+ };
310
+ }
311
+ /**
312
+ * Extract the components of a share Buffer.
313
+ * Mirrors `secrets.extractShareComponents(share)` from the original secrets.js.
314
+ *
315
+ * @param shareBuffer - A share Buffer as produced by `share()` or `newShare()`.
316
+ * @returns An object with `bits`, `id`, and `data` (hex string of share payload).
317
+ */
318
+ export function extractShareComponents(shareBuffer) {
319
+ if (!Buffer.isBuffer(shareBuffer)) {
320
+ throw new TypeError('shareBuffer must be a Buffer');
321
+ }
322
+ const parsed = parseShareString(shareBuffer.toString('ascii'));
323
+ return { bits: parsed.bits, id: parsed.id, data: parsed.value };
324
+ }
325
+ export function shareBufferToString(shareBuffer) {
326
+ if (!Buffer.isBuffer(shareBuffer)) {
327
+ throw new TypeError('shareBuffer must be a Buffer');
328
+ }
329
+ return shareBuffer.toString('ascii');
330
+ }
331
+ export function shareStringToBuffer(shareString) {
332
+ if (typeof shareString !== 'string') {
333
+ throw new TypeError('shareString must be a string');
334
+ }
335
+ return Buffer.from(shareString, 'ascii');
336
+ }
337
+ export function utf16beFromString(str) {
338
+ const out = Buffer.allocUnsafe(str.length * 2);
339
+ for (let i = 0; i < str.length; i += 1) {
340
+ // Match secrets.js str2hex ordering (prepends each code unit).
341
+ out.writeUInt16BE(str.charCodeAt(i), (str.length - 1 - i) * 2);
342
+ }
343
+ return out;
344
+ }
345
+ export function utf16beToString(buf) {
346
+ if (buf.length % 2 !== 0) {
347
+ throw new Error('Invalid UTF-16BE buffer length');
348
+ }
349
+ let out = '';
350
+ for (let i = 0; i < buf.length; i += 2) {
351
+ out = String.fromCharCode(buf.readUInt16BE(i)) + out;
352
+ }
353
+ return out;
354
+ }
355
+ export const _internal = {
356
+ splitBinToParts,
357
+ hex2bin,
358
+ bufferToSecretBin,
359
+ parseShareString,
360
+ };
361
+ init(defaults.bits);
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "secrets-sharing",
3
+ "version": "1.0.0",
4
+ "description": "A Typescript implementation of Shamir's Secret Sharing Scheme using buffers.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test.js",
8
+ "build": "tsc",
9
+ "publish": "npm run build && npm publish"
10
+ },
11
+ "keywords": ["shamir", "secret sharing", "typescript", "buffers"],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "type": "module",
15
+ "devDependencies": {
16
+ "@types/node": "^25.3.2",
17
+ "typescript": "^5.9.3"
18
+ }
19
+ }
@@ -0,0 +1,467 @@
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 ADDED
@@ -0,0 +1,66 @@
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');