@noy-db/on-shamir 0.1.0-pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/index.cjs +322 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +280 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vLannaAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @noy-db/on-shamir
|
|
2
|
+
|
|
3
|
+
**k-of-n Shamir Secret Sharing** of the vault KEK for multi-party unlock. Any **K** of **N** enrolled shares recombines the KEK; fewer than K leaks zero bits.
|
|
4
|
+
|
|
5
|
+
The defining feature is **composability** — each share can itself be protected by any other `@noy-db/on-*` method. Share 1 behind a WebAuthn passkey, share 2 behind an OIDC login, share 3 printed on paper in a corporate safe. Fractional trust across different authentication modes.
|
|
6
|
+
|
|
7
|
+
Part of the `@noy-db/on-*` authentication family.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @noy-db/on-shamir
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use cases
|
|
16
|
+
|
|
17
|
+
- *"Any 2 of 3 admins must authorise unlocking the audit vault."*
|
|
18
|
+
- *"CFO + COO consent required to unlock the CEO vault during vacation."*
|
|
19
|
+
- *"3-of-5 board escrow — the vault survives any 2 resignations."*
|
|
20
|
+
- *"Executive key is split across a passkey + OIDC + paper backup — any 2 of 3 unlock."*
|
|
21
|
+
|
|
22
|
+
## Threat model
|
|
23
|
+
|
|
24
|
+
**Protects against:**
|
|
25
|
+
- Up to K-1 colluding share holders (mathematically — fewer than K shares reveals zero bits of the KEK)
|
|
26
|
+
- Loss of up to N-K shares (remaining K shares still reconstruct)
|
|
27
|
+
|
|
28
|
+
**Does NOT protect against:**
|
|
29
|
+
- K colluding share holders (by design — that's the threshold contract)
|
|
30
|
+
- Device compromise of the combining machine during reconstruction (the KEK is briefly in memory; if that machine is malicious, no library-level hedge helps)
|
|
31
|
+
- Side-channel attacks on the Lagrange interpolation (not constant-time by design — the threat model assumes a trusted combine-device)
|
|
32
|
+
|
|
33
|
+
## Math
|
|
34
|
+
|
|
35
|
+
Shamir Secret Sharing over GF(2^8), byte-wise. For each byte of the KEK:
|
|
36
|
+
|
|
37
|
+
1. Construct a random polynomial of degree k-1 whose constant term is the KEK byte.
|
|
38
|
+
2. Each share is the polynomial evaluated at a distinct x-coordinate (1..255; x=0 reserved because it would reveal the byte).
|
|
39
|
+
3. Lagrange interpolation at x=0 given any K points recovers the byte.
|
|
40
|
+
|
|
41
|
+
Implemented in ~120 LoC of pure TypeScript (`gf256.ts` + `shamir.ts`). Zero cryptographic dependencies — just Web Crypto's `getRandomValues` for randomness and `subtle.importKey` to rehydrate the reconstructed KEK.
|
|
42
|
+
|
|
43
|
+
Reduction polynomial: x^8 + x^4 + x^3 + x + 1 (0x11b, same as AES).
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Enroll — after primary unlock
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { splitKEK, encodeShareBase32 } from '@noy-db/on-shamir'
|
|
51
|
+
|
|
52
|
+
const shares = await splitKEK(currentKEK, { k: 2, n: 3 })
|
|
53
|
+
|
|
54
|
+
// Each share is now a RawShare structure. Serialise for distribution:
|
|
55
|
+
const shareStrings = shares.map(encodeShareBase32)
|
|
56
|
+
// Example: 'SHAMIR_S1_K2N3__AKHT-P4L7-...'
|
|
57
|
+
|
|
58
|
+
// Distribute via any on-* method:
|
|
59
|
+
// shareStrings[0] → store under a WebAuthn-protected keyring entry
|
|
60
|
+
// shareStrings[1] → store under an OIDC-protected keyring entry
|
|
61
|
+
// shareStrings[2] → print on paper, put in the corporate safe
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Unlock — collect K shares and combine
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { combineKEK, decodeShareBase32 } from '@noy-db/on-shamir'
|
|
68
|
+
|
|
69
|
+
// Unlock each share via its own on-* method (not shown — uses whichever
|
|
70
|
+
// on-webauthn / on-oidc / on-recovery / on-magic-link the holder chose).
|
|
71
|
+
const shareA = decodeShareBase32(shareStringFromCFO)
|
|
72
|
+
const shareB = decodeShareBase32(shareStringFromCOO)
|
|
73
|
+
|
|
74
|
+
// Combine — returns a non-extractable KEK ready to use
|
|
75
|
+
const kek = await combineKEK([shareA, shareB])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Low-level — for custom integrations
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { splitSecret, combineSecret } from '@noy-db/on-shamir'
|
|
82
|
+
|
|
83
|
+
const shares = splitSecret(new Uint8Array(secretBytes), 2, 3)
|
|
84
|
+
const recovered = combineSecret([shares[0], shares[1]])
|
|
85
|
+
// recovered is a Uint8Array — you handle its lifecycle
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
JSON form — store shares inside other on-* keyring entries:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { encodeShareJSON, decodeShareJSON } from '@noy-db/on-shamir'
|
|
92
|
+
|
|
93
|
+
const json = encodeShareJSON(shares[0])
|
|
94
|
+
// { v: 1, x: 1, k: 2, n: 3, y: '<base64>' }
|
|
95
|
+
await keyring.put('_recovery_share_1', json)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// High-level — wraps a CryptoKey
|
|
102
|
+
async function splitKEK(kek: CryptoKey, options: { k: number; n: number }): Promise<RawShare[]>
|
|
103
|
+
async function combineKEK(shares: readonly RawShare[]): Promise<CryptoKey>
|
|
104
|
+
|
|
105
|
+
// Low-level — operates on raw bytes
|
|
106
|
+
function splitSecret(
|
|
107
|
+
secret: Uint8Array,
|
|
108
|
+
k: number,
|
|
109
|
+
n: number,
|
|
110
|
+
randomBytes?: (count: number) => Uint8Array, // Injectable for tests
|
|
111
|
+
): RawShare[]
|
|
112
|
+
function combineSecret(shares: readonly RawShare[]): Uint8Array
|
|
113
|
+
|
|
114
|
+
// Serialisation
|
|
115
|
+
function encodeShareBytes(share: RawShare): Uint8Array
|
|
116
|
+
function decodeShareBytes(bytes: Uint8Array): RawShare
|
|
117
|
+
|
|
118
|
+
function encodeShareBase32(share: RawShare): string
|
|
119
|
+
function decodeShareBase32(input: string): RawShare
|
|
120
|
+
|
|
121
|
+
function encodeShareJSON(share: RawShare): ShareJSON
|
|
122
|
+
function decodeShareJSON(json: ShareJSON): RawShare
|
|
123
|
+
|
|
124
|
+
interface RawShare {
|
|
125
|
+
x: number // 1..255
|
|
126
|
+
y: Uint8Array // One byte per secret byte
|
|
127
|
+
k: number // Threshold
|
|
128
|
+
n: number // Total
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// GF(2^8) arithmetic — exported for composition / auditing
|
|
132
|
+
function gfAdd(a: number, b: number): number
|
|
133
|
+
function gfMul(a: number, b: number): number
|
|
134
|
+
function gfInv(a: number): number
|
|
135
|
+
function gfDiv(a: number, b: number): number
|
|
136
|
+
function gfPolyEval(coeffs: readonly number[], x: number): number
|
|
137
|
+
function lagrangeInterpolateAtZero(points: readonly [number, number][]): number
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Share format
|
|
141
|
+
|
|
142
|
+
Binary (6-byte header + y-bytes):
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
offset size field
|
|
146
|
+
0 1 version (= 1)
|
|
147
|
+
1 1 x-coordinate (1..255)
|
|
148
|
+
2 1 k (threshold)
|
|
149
|
+
3 1 n (total)
|
|
150
|
+
4 2 byteLength (big-endian uint16)
|
|
151
|
+
6+ L y-bytes (L = byteLength)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For a 32-byte KEK: 38 bytes total per share.
|
|
155
|
+
|
|
156
|
+
Base32 form includes a human-readable prefix (`SHAMIR_S{x}_K{k}N{n}__`) followed by the payload in groups of 4:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
SHAMIR_S2_K2N3__AKHT-P4L7-KDFG-H3JX-M8E...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The prefix is stripped by the decoder — metadata is recovered from the binary header. Consumer tools can show the prefix to the user for at-a-glance share identification without trusting it.
|
|
163
|
+
|
|
164
|
+
## Composability recipe — Shamir + any on-* method
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { splitKEK, encodeShareJSON } from '@noy-db/on-shamir'
|
|
168
|
+
import { createMagicLinkToken } from '@noy-db/on-magic-link'
|
|
169
|
+
// Plus whichever other on-* packages you use
|
|
170
|
+
|
|
171
|
+
const shares = await splitKEK(currentKEK, { k: 2, n: 3 })
|
|
172
|
+
|
|
173
|
+
// Share 1 — passkey
|
|
174
|
+
await webAuthn.enrollWithPayload(ceoPasskey, encodeShareJSON(shares[0]))
|
|
175
|
+
|
|
176
|
+
// Share 2 — magic link to an auditor's email
|
|
177
|
+
const link = createMagicLinkToken('escrow-vault', { ttlMs: 30 * 24 * 60 * 60 * 1000 })
|
|
178
|
+
await emailAuditor(link, encodeShareJSON(shares[1]))
|
|
179
|
+
|
|
180
|
+
// Share 3 — paper backup
|
|
181
|
+
console.log('Corporate-safe backup:', encodeShareBase32(shares[2]))
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Performance
|
|
185
|
+
|
|
186
|
+
GF(2^8) operations are table-lookup O(1) — all micro-operations run in nanoseconds. Splitting a 32-byte KEK into 3 shares takes sub-millisecond. Combining likewise. Web Crypto's `importKey` on the reconstructed KEK is the dominant cost (~1ms).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
combineKEK: () => combineKEK,
|
|
24
|
+
combineSecret: () => combineSecret,
|
|
25
|
+
decodeShareBase32: () => decodeShareBase32,
|
|
26
|
+
decodeShareBytes: () => decodeShareBytes,
|
|
27
|
+
decodeShareJSON: () => decodeShareJSON,
|
|
28
|
+
encodeShareBase32: () => encodeShareBase32,
|
|
29
|
+
encodeShareBytes: () => encodeShareBytes,
|
|
30
|
+
encodeShareJSON: () => encodeShareJSON,
|
|
31
|
+
gfAdd: () => gfAdd,
|
|
32
|
+
gfDiv: () => gfDiv,
|
|
33
|
+
gfInv: () => gfInv,
|
|
34
|
+
gfMul: () => gfMul,
|
|
35
|
+
gfPolyEval: () => gfPolyEval,
|
|
36
|
+
lagrangeInterpolateAtZero: () => lagrangeInterpolateAtZero,
|
|
37
|
+
splitKEK: () => splitKEK,
|
|
38
|
+
splitSecret: () => splitSecret
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/gf256.ts
|
|
43
|
+
var LOG = new Uint8Array(256);
|
|
44
|
+
var EXP = new Uint8Array(256);
|
|
45
|
+
{
|
|
46
|
+
let x = 1;
|
|
47
|
+
for (let i = 0; i < 255; i++) {
|
|
48
|
+
EXP[i] = x;
|
|
49
|
+
LOG[x] = i;
|
|
50
|
+
let doubled = x << 1;
|
|
51
|
+
if (doubled & 256) doubled ^= 283;
|
|
52
|
+
x = (doubled ^ x) & 255;
|
|
53
|
+
}
|
|
54
|
+
EXP[255] = EXP[0];
|
|
55
|
+
}
|
|
56
|
+
function gfAdd(a, b) {
|
|
57
|
+
return (a ^ b) & 255;
|
|
58
|
+
}
|
|
59
|
+
function gfMul(a, b) {
|
|
60
|
+
if (a === 0 || b === 0) return 0;
|
|
61
|
+
const s = (LOG[a] + LOG[b]) % 255;
|
|
62
|
+
return EXP[s];
|
|
63
|
+
}
|
|
64
|
+
function gfInv(a) {
|
|
65
|
+
if (a === 0) throw new Error("gf256: no inverse for 0");
|
|
66
|
+
return EXP[(255 - LOG[a]) % 255];
|
|
67
|
+
}
|
|
68
|
+
function gfDiv(a, b) {
|
|
69
|
+
if (b === 0) throw new Error("gf256: division by zero");
|
|
70
|
+
if (a === 0) return 0;
|
|
71
|
+
const s = (LOG[a] + 255 - LOG[b]) % 255;
|
|
72
|
+
return EXP[s];
|
|
73
|
+
}
|
|
74
|
+
function gfPolyEval(coeffs, x) {
|
|
75
|
+
let y = 0;
|
|
76
|
+
for (let i = coeffs.length - 1; i >= 0; i--) {
|
|
77
|
+
y = gfAdd(gfMul(y, x), coeffs[i]);
|
|
78
|
+
}
|
|
79
|
+
return y;
|
|
80
|
+
}
|
|
81
|
+
function lagrangeInterpolateAtZero(points) {
|
|
82
|
+
let result = 0;
|
|
83
|
+
for (let i = 0; i < points.length; i++) {
|
|
84
|
+
const [xi, yi] = points[i];
|
|
85
|
+
let numerator = 1;
|
|
86
|
+
let denominator = 1;
|
|
87
|
+
for (let j = 0; j < points.length; j++) {
|
|
88
|
+
if (i === j) continue;
|
|
89
|
+
const [xj] = points[j];
|
|
90
|
+
numerator = gfMul(numerator, xj);
|
|
91
|
+
denominator = gfMul(denominator, gfAdd(xi, xj));
|
|
92
|
+
}
|
|
93
|
+
const basis = gfDiv(numerator, denominator);
|
|
94
|
+
result = gfAdd(result, gfMul(yi, basis));
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/shamir.ts
|
|
100
|
+
function splitSecret(secret, k, n, randomBytes = defaultRandomBytes) {
|
|
101
|
+
assertSplitArgs(k, n, secret.length);
|
|
102
|
+
const xCoords = pickXCoords(n);
|
|
103
|
+
const shares = xCoords.map((x) => ({
|
|
104
|
+
x,
|
|
105
|
+
y: new Uint8Array(secret.length),
|
|
106
|
+
k,
|
|
107
|
+
n
|
|
108
|
+
}));
|
|
109
|
+
for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {
|
|
110
|
+
const coeffs = [secret[byteIdx]];
|
|
111
|
+
const rand = randomBytes(k - 1);
|
|
112
|
+
for (let j = 0; j < k - 1; j++) {
|
|
113
|
+
coeffs.push(rand[j]);
|
|
114
|
+
}
|
|
115
|
+
for (let shareIdx = 0; shareIdx < n; shareIdx++) {
|
|
116
|
+
shares[shareIdx].y[byteIdx] = gfPolyEval(coeffs, xCoords[shareIdx]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return shares;
|
|
120
|
+
}
|
|
121
|
+
function combineSecret(shares) {
|
|
122
|
+
if (shares.length === 0) {
|
|
123
|
+
throw new Error("on-shamir: no shares provided");
|
|
124
|
+
}
|
|
125
|
+
const k = shares[0].k;
|
|
126
|
+
if (shares.length < k) {
|
|
127
|
+
throw new Error(`on-shamir: insufficient shares \u2014 need ${k}, got ${shares.length}`);
|
|
128
|
+
}
|
|
129
|
+
const byteLength = shares[0].y.length;
|
|
130
|
+
for (const s of shares) {
|
|
131
|
+
if (s.y.length !== byteLength) {
|
|
132
|
+
throw new Error("on-shamir: share lengths disagree \u2014 incompatible enrollment");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const selected = shares.slice(0, k);
|
|
136
|
+
const xs = selected.map((s) => s.x);
|
|
137
|
+
if (new Set(xs).size !== xs.length) {
|
|
138
|
+
throw new Error("on-shamir: duplicate x-coordinates among provided shares");
|
|
139
|
+
}
|
|
140
|
+
const secret = new Uint8Array(byteLength);
|
|
141
|
+
for (let byteIdx = 0; byteIdx < byteLength; byteIdx++) {
|
|
142
|
+
const points = selected.map((s) => [s.x, s.y[byteIdx]]);
|
|
143
|
+
secret[byteIdx] = lagrangeInterpolateAtZero(points);
|
|
144
|
+
}
|
|
145
|
+
return secret;
|
|
146
|
+
}
|
|
147
|
+
function assertSplitArgs(k, n, secretLen) {
|
|
148
|
+
if (!Number.isInteger(k) || k < 2) {
|
|
149
|
+
throw new Error(`on-shamir: k must be an integer >= 2 (got ${k})`);
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isInteger(n) || n < k || n > 255) {
|
|
152
|
+
throw new Error(`on-shamir: n must satisfy k <= n <= 255 (got k=${k} n=${n})`);
|
|
153
|
+
}
|
|
154
|
+
if (!Number.isInteger(secretLen) || secretLen < 1) {
|
|
155
|
+
throw new Error(`on-shamir: secret must be at least 1 byte`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function pickXCoords(n) {
|
|
159
|
+
const result = [];
|
|
160
|
+
for (let i = 1; i <= n; i++) result.push(i);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
function defaultRandomBytes(count) {
|
|
164
|
+
return crypto.getRandomValues(new Uint8Array(count));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/share-format.ts
|
|
168
|
+
var SHARE_VERSION = 1;
|
|
169
|
+
var HEADER_LEN = 6;
|
|
170
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
171
|
+
function encodeShareBytes(share) {
|
|
172
|
+
const bytes = new Uint8Array(HEADER_LEN + share.y.length);
|
|
173
|
+
bytes[0] = SHARE_VERSION;
|
|
174
|
+
bytes[1] = share.x;
|
|
175
|
+
bytes[2] = share.k;
|
|
176
|
+
bytes[3] = share.n;
|
|
177
|
+
bytes[4] = share.y.length >>> 8 & 255;
|
|
178
|
+
bytes[5] = share.y.length & 255;
|
|
179
|
+
bytes.set(share.y, HEADER_LEN);
|
|
180
|
+
return bytes;
|
|
181
|
+
}
|
|
182
|
+
function decodeShareBytes(bytes) {
|
|
183
|
+
if (bytes.length < HEADER_LEN) {
|
|
184
|
+
throw new Error(`on-shamir: share bytes too short (${bytes.length} < ${HEADER_LEN})`);
|
|
185
|
+
}
|
|
186
|
+
const version = bytes[0];
|
|
187
|
+
if (version !== SHARE_VERSION) {
|
|
188
|
+
throw new Error(`on-shamir: unsupported share version ${version} (expected ${SHARE_VERSION})`);
|
|
189
|
+
}
|
|
190
|
+
const x = bytes[1];
|
|
191
|
+
const k = bytes[2];
|
|
192
|
+
const n = bytes[3];
|
|
193
|
+
const byteLength = bytes[4] << 8 | bytes[5];
|
|
194
|
+
if (bytes.length !== HEADER_LEN + byteLength) {
|
|
195
|
+
throw new Error(`on-shamir: share length mismatch \u2014 header says ${byteLength}, got ${bytes.length - HEADER_LEN}`);
|
|
196
|
+
}
|
|
197
|
+
if (x === 0) {
|
|
198
|
+
throw new Error("on-shamir: share has x=0 \u2014 malformed");
|
|
199
|
+
}
|
|
200
|
+
return { x, k, n, y: bytes.slice(HEADER_LEN) };
|
|
201
|
+
}
|
|
202
|
+
function encodeShareBase32(share) {
|
|
203
|
+
const bytes = encodeShareBytes(share);
|
|
204
|
+
const chunks = base32Encode(bytes);
|
|
205
|
+
const grouped = chunks.match(/.{1,4}/g)?.join("-") ?? chunks;
|
|
206
|
+
return `SHAMIR_S${share.x}_K${share.k}N${share.n}__${grouped}`;
|
|
207
|
+
}
|
|
208
|
+
function decodeShareBase32(input) {
|
|
209
|
+
const normalised = input.toUpperCase().replace(/[\s-]/g, "");
|
|
210
|
+
const lastDoubleUnderscore = normalised.lastIndexOf("__");
|
|
211
|
+
const payload = lastDoubleUnderscore >= 0 ? normalised.slice(lastDoubleUnderscore + 2) : normalised;
|
|
212
|
+
const stripped = payload.replace(/[^A-Z2-7]/g, "");
|
|
213
|
+
const bytes = base32Decode(stripped);
|
|
214
|
+
return decodeShareBytes(bytes);
|
|
215
|
+
}
|
|
216
|
+
function encodeShareJSON(share) {
|
|
217
|
+
return {
|
|
218
|
+
v: SHARE_VERSION,
|
|
219
|
+
x: share.x,
|
|
220
|
+
k: share.k,
|
|
221
|
+
n: share.n,
|
|
222
|
+
y: base64Encode(share.y)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function decodeShareJSON(json) {
|
|
226
|
+
if (json.v !== SHARE_VERSION) {
|
|
227
|
+
throw new Error(`on-shamir: unsupported share version ${String(json.v)}`);
|
|
228
|
+
}
|
|
229
|
+
return { x: json.x, k: json.k, n: json.n, y: base64Decode(json.y) };
|
|
230
|
+
}
|
|
231
|
+
function base32Encode(bytes) {
|
|
232
|
+
let bits = 0;
|
|
233
|
+
let value = 0;
|
|
234
|
+
let out = "";
|
|
235
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
236
|
+
value = value << 8 | bytes[i];
|
|
237
|
+
bits += 8;
|
|
238
|
+
while (bits >= 5) {
|
|
239
|
+
bits -= 5;
|
|
240
|
+
out += BASE32_ALPHABET[value >>> bits & 31];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (bits > 0) {
|
|
244
|
+
out += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function base32Decode(input) {
|
|
249
|
+
let bits = 0;
|
|
250
|
+
let value = 0;
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const ch of input) {
|
|
253
|
+
const idx = BASE32_ALPHABET.indexOf(ch);
|
|
254
|
+
if (idx < 0) {
|
|
255
|
+
throw new Error(`on-shamir: invalid Base32 character "${ch}"`);
|
|
256
|
+
}
|
|
257
|
+
value = value << 5 | idx;
|
|
258
|
+
bits += 5;
|
|
259
|
+
if (bits >= 8) {
|
|
260
|
+
bits -= 8;
|
|
261
|
+
out.push(value >>> bits & 255);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return new Uint8Array(out);
|
|
265
|
+
}
|
|
266
|
+
function base64Encode(bytes) {
|
|
267
|
+
let s = "";
|
|
268
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
269
|
+
return btoa(s);
|
|
270
|
+
}
|
|
271
|
+
function base64Decode(str) {
|
|
272
|
+
const s = atob(str);
|
|
273
|
+
const out = new Uint8Array(s.length);
|
|
274
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/index.ts
|
|
279
|
+
async function splitKEK(kek, options) {
|
|
280
|
+
const rawKek = await crypto.subtle.exportKey("raw", kek);
|
|
281
|
+
const secret = new Uint8Array(rawKek);
|
|
282
|
+
try {
|
|
283
|
+
return splitSecret(secret, options.k, options.n);
|
|
284
|
+
} finally {
|
|
285
|
+
secret.fill(0);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function combineKEK(shares) {
|
|
289
|
+
const secret = combineSecret(shares);
|
|
290
|
+
try {
|
|
291
|
+
return await crypto.subtle.importKey(
|
|
292
|
+
"raw",
|
|
293
|
+
secret,
|
|
294
|
+
{ name: "AES-GCM", length: 256 },
|
|
295
|
+
false,
|
|
296
|
+
// non-extractable by default
|
|
297
|
+
["encrypt", "decrypt"]
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
secret.fill(0);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
304
|
+
0 && (module.exports = {
|
|
305
|
+
combineKEK,
|
|
306
|
+
combineSecret,
|
|
307
|
+
decodeShareBase32,
|
|
308
|
+
decodeShareBytes,
|
|
309
|
+
decodeShareJSON,
|
|
310
|
+
encodeShareBase32,
|
|
311
|
+
encodeShareBytes,
|
|
312
|
+
encodeShareJSON,
|
|
313
|
+
gfAdd,
|
|
314
|
+
gfDiv,
|
|
315
|
+
gfInv,
|
|
316
|
+
gfMul,
|
|
317
|
+
gfPolyEval,
|
|
318
|
+
lagrangeInterpolateAtZero,
|
|
319
|
+
splitKEK,
|
|
320
|
+
splitSecret
|
|
321
|
+
});
|
|
322
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/gf256.ts","../src/shamir.ts","../src/share-format.ts"],"sourcesContent":["/**\n * **@noy-db/on-shamir** — k-of-n Shamir Secret Sharing of the vault KEK.\n *\n * Any K of N enrolled shares recombines the original KEK; fewer than\n * K leaks zero bits. Unlike naive multi-passphrase schemes, each\n * share can be protected by ANY other `@noy-db/on-*` method — share\n * 1 under a WebAuthn passkey, share 2 under an OIDC login, share 3\n * on paper in a safe. This composability is the defining feature.\n *\n * Part of the `@noy-db/on-*` authentication family.\n *\n * ## Math\n *\n * Shamir Secret Sharing over GF(2^8), byte-wise. For each byte of the\n * secret, construct a random polynomial of degree k-1 with the byte\n * as the constant term. Each share is a value of that polynomial at\n * a distinct x-coordinate. Lagrange interpolation at x=0 recovers\n * the byte. Zero cryptographic dependencies — pure math in\n * `gf256.ts` and `shamir.ts`.\n *\n * ## Threat model\n *\n * Protects against:\n * - Up to K-1 colluding share holders (mathematically — fewer than K shares reveals zero bits)\n * - Loss of up to N-K shares\n *\n * Does NOT protect against:\n * - K colluding share holders (by design — that's the threshold contract)\n * - Device compromise of the combining machine during reconstruction\n *\n * ## Usage\n *\n * ```ts\n * import {\n * splitKEK,\n * combineKEK,\n * encodeShareBase32,\n * decodeShareBase32,\n * } from '@noy-db/on-shamir'\n *\n * // ENROLL — user has unlocked the vault with passphrase; now create a 2-of-3 split\n * const shares = await splitKEK(currentKEK, { k: 2, n: 3 })\n * const shareStrings = shares.map(encodeShareBase32)\n * // Distribute each shareString to a different holder via any on-* method\n *\n * // UNLOCK — collect 2 of the 3 shares and combine\n * const collected = [decodeShareBase32(shareA), decodeShareBase32(shareB)]\n * const kek = await combineKEK(collected)\n * // kek is now a non-extractable CryptoKey usable as the vault's KEK\n * ```\n *\n * @packageDocumentation\n */\n\nexport {\n gfAdd,\n gfMul,\n gfDiv,\n gfInv,\n gfPolyEval,\n lagrangeInterpolateAtZero,\n} from './gf256.js'\n\nexport {\n splitSecret,\n combineSecret,\n type RawShare,\n} from './shamir.js'\n\nexport {\n encodeShareBytes,\n decodeShareBytes,\n encodeShareBase32,\n decodeShareBase32,\n encodeShareJSON,\n decodeShareJSON,\n type ShareJSON,\n} from './share-format.js'\n\nimport type { RawShare } from './shamir.js'\nimport { combineSecret, splitSecret } from './shamir.js'\n\n// ── High-level KEK API ──────────────────────────────────────────────────\n\nexport interface SplitKEKOptions {\n /** Threshold — minimum shares needed to reconstruct. Must be >= 2. */\n readonly k: number\n /** Total shares. Must satisfy k <= n <= 255. */\n readonly n: number\n}\n\n/**\n * Split the given KEK into N Shamir shares.\n *\n * The KEK must be extractable (so the raw bytes can be read for the\n * split operation). The returned `RawShare[]` contains the raw share\n * material — serialise each via `encodeShareBase32` / `encodeShareJSON`\n * before distributing.\n *\n * The caller is responsible for:\n * 1. Distributing the shares to their holders.\n * 2. Writing an audit-ledger entry recording the enrollment (including\n * the `k`, `n`, and share x-coordinates — not the share material).\n * 3. Securely zeroing any in-memory share/secret material after\n * serialisation.\n */\nexport async function splitKEK(kek: CryptoKey, options: SplitKEKOptions): Promise<RawShare[]> {\n const rawKek = await crypto.subtle.exportKey('raw', kek)\n const secret = new Uint8Array(rawKek)\n try {\n return splitSecret(secret, options.k, options.n)\n } finally {\n // Zero the secret buffer — best-effort, GC will also reclaim.\n secret.fill(0)\n }\n}\n\n/**\n * Reconstruct the KEK from K or more shares.\n *\n * Returns a non-extractable `CryptoKey` ready to use as the vault's\n * KEK. Internal secret bytes are zeroed after the CryptoKey is\n * imported.\n *\n * Throws:\n * - if fewer than K shares are provided\n * - if shares have mismatched lengths (likely indicating shares from\n * different enrollments were mixed)\n * - if duplicate x-coordinates are detected\n */\nexport async function combineKEK(shares: readonly RawShare[]): Promise<CryptoKey> {\n const secret = combineSecret(shares)\n try {\n return await crypto.subtle.importKey(\n 'raw',\n secret as BufferSource,\n { name: 'AES-GCM', length: 256 },\n false, // non-extractable by default\n ['encrypt', 'decrypt'],\n )\n } finally {\n secret.fill(0)\n }\n}\n","/**\n * Arithmetic in the Galois field GF(2^8), represented as bytes 0..255.\n *\n * Used by Shamir Secret Sharing for byte-wise polynomial operations.\n * Addition is XOR; multiplication uses precomputed log/exp tables\n * against the primitive element 0x03 with irreducible polynomial\n * 0x11b (x^8 + x^4 + x^3 + x + 1 — the AES polynomial).\n *\n * Constant-time is NOT a goal here — for secret sharing, the threat\n * model assumes the combining device is trusted during\n * reconstruction. If that assumption is wrong, no library-level\n * constant-time hedge protects the plaintext anyway.\n */\n\nconst LOG = new Uint8Array(256)\nconst EXP = new Uint8Array(256)\n\n// Build log/exp tables. Primitive element 0x03; reduction polynomial 0x11b.\n// Multiplication by 3 in GF(2^8) is (x << 1) ^ x, with polynomial reduction\n// when the shift carries bit 8.\n{\n let x = 1\n for (let i = 0; i < 255; i++) {\n EXP[i] = x\n LOG[x] = i\n // x <- x * 3 in GF(2^8)\n let doubled = x << 1\n if (doubled & 0x100) doubled ^= 0x11b\n x = (doubled ^ x) & 0xff\n }\n // EXP cycles with period 255 — EXP[255] = EXP[0] simplifies modulo math.\n EXP[255] = EXP[0]!\n // LOG[0] is undefined (log of 0 doesn't exist) — guarded at call sites.\n}\n\n/** Addition in GF(2^8) is XOR. */\nexport function gfAdd(a: number, b: number): number {\n return (a ^ b) & 0xff\n}\n\n/** Multiplication in GF(2^8). Returns 0 if either operand is 0. */\nexport function gfMul(a: number, b: number): number {\n if (a === 0 || b === 0) return 0\n const s = (LOG[a]! + LOG[b]!) % 255\n return EXP[s]!\n}\n\n/** Multiplicative inverse in GF(2^8). Throws on 0 (no inverse exists). */\nexport function gfInv(a: number): number {\n if (a === 0) throw new Error('gf256: no inverse for 0')\n return EXP[(255 - LOG[a]!) % 255]!\n}\n\n/** Division in GF(2^8). Throws on divide-by-zero. */\nexport function gfDiv(a: number, b: number): number {\n if (b === 0) throw new Error('gf256: division by zero')\n if (a === 0) return 0\n const s = (LOG[a]! + 255 - LOG[b]!) % 255\n return EXP[s]!\n}\n\n/**\n * Evaluate a polynomial with coefficients `coeffs[0] + coeffs[1]*x + ...`\n * at point `x` in GF(2^8) via Horner's method.\n */\nexport function gfPolyEval(coeffs: readonly number[], x: number): number {\n let y = 0\n for (let i = coeffs.length - 1; i >= 0; i--) {\n y = gfAdd(gfMul(y, x), coeffs[i]!)\n }\n return y\n}\n\n/**\n * Lagrange interpolation at x=0, given k distinct points `(xi, yi)`.\n *\n * Returns the constant term of the unique degree-(k-1) polynomial\n * through those points — equal to the original secret byte for\n * Shamir's construction.\n */\nexport function lagrangeInterpolateAtZero(points: readonly [number, number][]): number {\n let result = 0\n for (let i = 0; i < points.length; i++) {\n const [xi, yi] = points[i]!\n // Li(0) = ∏_{j≠i} xj / (xi XOR xj) (in GF(2^8), -x == x)\n let numerator = 1\n let denominator = 1\n for (let j = 0; j < points.length; j++) {\n if (i === j) continue\n const [xj] = points[j]!\n numerator = gfMul(numerator, xj)\n denominator = gfMul(denominator, gfAdd(xi, xj))\n }\n const basis = gfDiv(numerator, denominator)\n result = gfAdd(result, gfMul(yi, basis))\n }\n return result\n}\n","/**\n * Shamir Secret Sharing over GF(2^8), byte-wise.\n *\n * For each byte of the secret, construct a random polynomial of\n * degree k-1 whose constant term is the byte. Each share is a value\n * of that polynomial at a distinct x-coordinate in 1..255. Any K of\n * the N shares recombines the original bytes via Lagrange interpolation\n * at x=0.\n *\n * x=0 is reserved (it would directly reveal the secret). x-coordinates\n * are chosen from 1..255 ensuring distinctness.\n */\n\nimport { gfPolyEval, lagrangeInterpolateAtZero } from './gf256.js'\n\n/**\n * Split a secret into N shares. Any K of them reconstructs the secret;\n * fewer than K leaks zero bits.\n *\n * @param secret - The bytes to share (e.g., a 32-byte KEK).\n * @param k - Threshold (min shares needed to reconstruct). Must be >= 2.\n * @param n - Total shares to produce. Must be >= k and <= 255.\n * @param randomBytes - RNG function returning N random bytes. Defaults to\n * `crypto.getRandomValues`. Injectable for deterministic tests.\n * @returns An array of `n` shares. Each share has an x-coordinate + y-bytes\n * parallel to the secret bytes.\n */\nexport function splitSecret(\n secret: Uint8Array,\n k: number,\n n: number,\n randomBytes: (count: number) => Uint8Array = defaultRandomBytes,\n): RawShare[] {\n assertSplitArgs(k, n, secret.length)\n\n // Choose n distinct x-coordinates in 1..255.\n const xCoords = pickXCoords(n)\n\n // For each byte of the secret, build a polynomial of degree k-1 with\n // the byte as the constant term and random coefficients for x^1..x^(k-1).\n // Evaluate at each xCoord to get the share's y-byte for that position.\n const shares: RawShare[] = xCoords.map(x => ({\n x,\n y: new Uint8Array(secret.length),\n k,\n n,\n }))\n\n for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {\n const coeffs: number[] = [secret[byteIdx]!]\n const rand = randomBytes(k - 1)\n for (let j = 0; j < k - 1; j++) {\n coeffs.push(rand[j]!)\n }\n for (let shareIdx = 0; shareIdx < n; shareIdx++) {\n shares[shareIdx]!.y[byteIdx] = gfPolyEval(coeffs, xCoords[shareIdx]!)\n }\n }\n\n return shares\n}\n\n/**\n * Reconstruct a secret from K or more shares.\n *\n * Uses the first K shares provided; additional shares are ignored.\n * Shares must agree on secret length and x-coordinates must be\n * distinct (checked; throws on mismatch).\n */\nexport function combineSecret(shares: readonly RawShare[]): Uint8Array {\n if (shares.length === 0) {\n throw new Error('on-shamir: no shares provided')\n }\n const k = shares[0]!.k\n if (shares.length < k) {\n throw new Error(`on-shamir: insufficient shares — need ${k}, got ${shares.length}`)\n }\n const byteLength = shares[0]!.y.length\n for (const s of shares) {\n if (s.y.length !== byteLength) {\n throw new Error('on-shamir: share lengths disagree — incompatible enrollment')\n }\n }\n const selected = shares.slice(0, k)\n const xs = selected.map(s => s.x)\n if (new Set(xs).size !== xs.length) {\n throw new Error('on-shamir: duplicate x-coordinates among provided shares')\n }\n\n const secret = new Uint8Array(byteLength)\n for (let byteIdx = 0; byteIdx < byteLength; byteIdx++) {\n const points: [number, number][] = selected.map(s => [s.x, s.y[byteIdx]!])\n secret[byteIdx] = lagrangeInterpolateAtZero(points)\n }\n return secret\n}\n\n/** A raw Shamir share before serialisation. */\nexport interface RawShare {\n /** x-coordinate in GF(2^8), 1..255. Zero is disallowed (it would reveal the secret). */\n readonly x: number\n /** y-bytes — one per byte of the secret. */\n readonly y: Uint8Array\n /** Threshold used at split time. */\n readonly k: number\n /** Total shares at split time. */\n readonly n: number\n}\n\n// ── internals ──────────────────────────────────────────────────────────\n\nfunction assertSplitArgs(k: number, n: number, secretLen: number): void {\n if (!Number.isInteger(k) || k < 2) {\n throw new Error(`on-shamir: k must be an integer >= 2 (got ${k})`)\n }\n if (!Number.isInteger(n) || n < k || n > 255) {\n throw new Error(`on-shamir: n must satisfy k <= n <= 255 (got k=${k} n=${n})`)\n }\n if (!Number.isInteger(secretLen) || secretLen < 1) {\n throw new Error(`on-shamir: secret must be at least 1 byte`)\n }\n}\n\nfunction pickXCoords(n: number): number[] {\n // Use 1..n as x-coordinates. Simple, deterministic, well-distributed.\n // (For applications where share-position anonymity matters, shuffle at\n // serialisation time; the crypto doesn't care about the labels.)\n const result: number[] = []\n for (let i = 1; i <= n; i++) result.push(i)\n return result\n}\n\nfunction defaultRandomBytes(count: number): Uint8Array {\n return crypto.getRandomValues(new Uint8Array(count))\n}\n","/**\n * Share serialisation — raw bytes, Base32 for printing, and JSON for\n * structured transport (e.g. storing inside another on-* keyring).\n *\n * Binary layout (fixed header + secret-length y-bytes):\n *\n * ```\n * offset size field\n * 0 1 version (= 1)\n * 1 1 x-coordinate (1..255)\n * 2 1 k (threshold)\n * 3 1 n (total)\n * 4 2 byteLength (big-endian uint16) — should equal y-length\n * 6+ L y-bytes (L = byteLength)\n * ```\n *\n * Base32 adds a 26-character ULID `shareId` prefix + version/k/n\n * metadata for human-readable diagnostics.\n */\n\nimport type { RawShare } from './shamir.js'\n\nconst SHARE_VERSION = 1\nconst HEADER_LEN = 6\n\n// RFC 4648 Base32 alphabet — no confusing 0/1/8/O/I/L/B pairs.\nconst BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'\n\n/** Serialise a raw share into its canonical binary form. */\nexport function encodeShareBytes(share: RawShare): Uint8Array {\n const bytes = new Uint8Array(HEADER_LEN + share.y.length)\n bytes[0] = SHARE_VERSION\n bytes[1] = share.x\n bytes[2] = share.k\n bytes[3] = share.n\n bytes[4] = (share.y.length >>> 8) & 0xff\n bytes[5] = share.y.length & 0xff\n bytes.set(share.y, HEADER_LEN)\n return bytes\n}\n\n/** Parse binary share bytes back into a structured share. */\nexport function decodeShareBytes(bytes: Uint8Array): RawShare {\n if (bytes.length < HEADER_LEN) {\n throw new Error(`on-shamir: share bytes too short (${bytes.length} < ${HEADER_LEN})`)\n }\n const version = bytes[0]!\n if (version !== SHARE_VERSION) {\n throw new Error(`on-shamir: unsupported share version ${version} (expected ${SHARE_VERSION})`)\n }\n const x = bytes[1]!\n const k = bytes[2]!\n const n = bytes[3]!\n const byteLength = (bytes[4]! << 8) | bytes[5]!\n if (bytes.length !== HEADER_LEN + byteLength) {\n throw new Error(`on-shamir: share length mismatch — header says ${byteLength}, got ${bytes.length - HEADER_LEN}`)\n }\n if (x === 0) {\n throw new Error('on-shamir: share has x=0 — malformed')\n }\n return { x, k, n, y: bytes.slice(HEADER_LEN) }\n}\n\n/**\n * Encode a share as a Base32 string with hyphenated groups of 4.\n *\n * Output format: `SHAMIR_S{x}_K{k}N{n}__<base32-groups-of-4>`\n *\n * The prefix is not fed into the decoder — it's for eye-readability at a\n * glance (share-number / threshold / total). The `SHAMIR` tag + `_`\n * separators are non-Base32 characters (Base32 alphabet is A-Z2-7\n * only), so the decoder can strip everything up to and including the\n * last `_` before the payload. The metadata is also encoded in the\n * share bytes themselves; the prefix is redundant-but-useful.\n */\nexport function encodeShareBase32(share: RawShare): string {\n const bytes = encodeShareBytes(share)\n const chunks = base32Encode(bytes)\n const grouped = chunks.match(/.{1,4}/g)?.join('-') ?? chunks\n return `SHAMIR_S${share.x}_K${share.k}N${share.n}__${grouped}`\n}\n\n/**\n * Parse a Base32 share string back into a structured share. Tolerates\n * hyphens, whitespace, lowercase, and the optional prefix produced by\n * `encodeShareBase32`. The metadata prefix is informational; actual\n * share data comes from the binary bytes after the last `_`.\n */\nexport function decodeShareBase32(input: string): RawShare {\n // Normalise first: uppercase + remove whitespace/hyphens. Keep `_`\n // and digits so we can locate the prefix separator.\n const normalised = input.toUpperCase().replace(/[\\s-]/g, '')\n\n // If an optional SHAMIR_..._ prefix is present, strip up to the last `__`.\n const lastDoubleUnderscore = normalised.lastIndexOf('__')\n const payload = lastDoubleUnderscore >= 0 ? normalised.slice(lastDoubleUnderscore + 2) : normalised\n\n // Remove any remaining non-Base32 characters (defensive — also handles\n // cases where prefix metadata leaked into the payload somehow).\n const stripped = payload.replace(/[^A-Z2-7]/g, '')\n const bytes = base32Decode(stripped)\n return decodeShareBytes(bytes)\n}\n\n/**\n * JSON-friendly form for structured storage (e.g. stash inside another\n * on-* keyring entry, or pass over `postMessage`).\n */\nexport interface ShareJSON {\n readonly v: 1\n readonly x: number\n readonly k: number\n readonly n: number\n readonly y: string // Base64\n}\n\nexport function encodeShareJSON(share: RawShare): ShareJSON {\n return {\n v: SHARE_VERSION,\n x: share.x,\n k: share.k,\n n: share.n,\n y: base64Encode(share.y),\n }\n}\n\nexport function decodeShareJSON(json: ShareJSON): RawShare {\n if (json.v !== SHARE_VERSION) {\n throw new Error(`on-shamir: unsupported share version ${String(json.v)}`)\n }\n return { x: json.x, k: json.k, n: json.n, y: base64Decode(json.y) }\n}\n\n// ── Base32 ─────────────────────────────────────────────────────────────\n\nfunction base32Encode(bytes: Uint8Array): string {\n let bits = 0\n let value = 0\n let out = ''\n for (let i = 0; i < bytes.length; i++) {\n value = (value << 8) | bytes[i]!\n bits += 8\n while (bits >= 5) {\n bits -= 5\n out += BASE32_ALPHABET[(value >>> bits) & 0x1f]\n }\n }\n if (bits > 0) {\n out += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f]\n }\n return out\n}\n\nfunction base32Decode(input: string): Uint8Array {\n let bits = 0\n let value = 0\n const out: number[] = []\n for (const ch of input) {\n const idx = BASE32_ALPHABET.indexOf(ch)\n if (idx < 0) {\n throw new Error(`on-shamir: invalid Base32 character \"${ch}\"`)\n }\n value = (value << 5) | idx\n bits += 5\n if (bits >= 8) {\n bits -= 8\n out.push((value >>> bits) & 0xff)\n }\n }\n return new Uint8Array(out)\n}\n\n// ── Base64 ─────────────────────────────────────────────────────────────\n\nfunction base64Encode(bytes: Uint8Array): string {\n let s = ''\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)\n return btoa(s)\n}\n\nfunction base64Decode(str: string): Uint8Array {\n const s = atob(str)\n const out = new Uint8Array(s.length)\n for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)\n return out\n}\n"],"mappings":"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","names":[]}
|