@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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic in the Galois field GF(2^8), represented as bytes 0..255.
|
|
3
|
+
*
|
|
4
|
+
* Used by Shamir Secret Sharing for byte-wise polynomial operations.
|
|
5
|
+
* Addition is XOR; multiplication uses precomputed log/exp tables
|
|
6
|
+
* against the primitive element 0x03 with irreducible polynomial
|
|
7
|
+
* 0x11b (x^8 + x^4 + x^3 + x + 1 — the AES polynomial).
|
|
8
|
+
*
|
|
9
|
+
* Constant-time is NOT a goal here — for secret sharing, the threat
|
|
10
|
+
* model assumes the combining device is trusted during
|
|
11
|
+
* reconstruction. If that assumption is wrong, no library-level
|
|
12
|
+
* constant-time hedge protects the plaintext anyway.
|
|
13
|
+
*/
|
|
14
|
+
/** Addition in GF(2^8) is XOR. */
|
|
15
|
+
declare function gfAdd(a: number, b: number): number;
|
|
16
|
+
/** Multiplication in GF(2^8). Returns 0 if either operand is 0. */
|
|
17
|
+
declare function gfMul(a: number, b: number): number;
|
|
18
|
+
/** Multiplicative inverse in GF(2^8). Throws on 0 (no inverse exists). */
|
|
19
|
+
declare function gfInv(a: number): number;
|
|
20
|
+
/** Division in GF(2^8). Throws on divide-by-zero. */
|
|
21
|
+
declare function gfDiv(a: number, b: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Evaluate a polynomial with coefficients `coeffs[0] + coeffs[1]*x + ...`
|
|
24
|
+
* at point `x` in GF(2^8) via Horner's method.
|
|
25
|
+
*/
|
|
26
|
+
declare function gfPolyEval(coeffs: readonly number[], x: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Lagrange interpolation at x=0, given k distinct points `(xi, yi)`.
|
|
29
|
+
*
|
|
30
|
+
* Returns the constant term of the unique degree-(k-1) polynomial
|
|
31
|
+
* through those points — equal to the original secret byte for
|
|
32
|
+
* Shamir's construction.
|
|
33
|
+
*/
|
|
34
|
+
declare function lagrangeInterpolateAtZero(points: readonly [number, number][]): number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shamir Secret Sharing over GF(2^8), byte-wise.
|
|
38
|
+
*
|
|
39
|
+
* For each byte of the secret, construct a random polynomial of
|
|
40
|
+
* degree k-1 whose constant term is the byte. Each share is a value
|
|
41
|
+
* of that polynomial at a distinct x-coordinate in 1..255. Any K of
|
|
42
|
+
* the N shares recombines the original bytes via Lagrange interpolation
|
|
43
|
+
* at x=0.
|
|
44
|
+
*
|
|
45
|
+
* x=0 is reserved (it would directly reveal the secret). x-coordinates
|
|
46
|
+
* are chosen from 1..255 ensuring distinctness.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Split a secret into N shares. Any K of them reconstructs the secret;
|
|
50
|
+
* fewer than K leaks zero bits.
|
|
51
|
+
*
|
|
52
|
+
* @param secret - The bytes to share (e.g., a 32-byte KEK).
|
|
53
|
+
* @param k - Threshold (min shares needed to reconstruct). Must be >= 2.
|
|
54
|
+
* @param n - Total shares to produce. Must be >= k and <= 255.
|
|
55
|
+
* @param randomBytes - RNG function returning N random bytes. Defaults to
|
|
56
|
+
* `crypto.getRandomValues`. Injectable for deterministic tests.
|
|
57
|
+
* @returns An array of `n` shares. Each share has an x-coordinate + y-bytes
|
|
58
|
+
* parallel to the secret bytes.
|
|
59
|
+
*/
|
|
60
|
+
declare function splitSecret(secret: Uint8Array, k: number, n: number, randomBytes?: (count: number) => Uint8Array): RawShare[];
|
|
61
|
+
/**
|
|
62
|
+
* Reconstruct a secret from K or more shares.
|
|
63
|
+
*
|
|
64
|
+
* Uses the first K shares provided; additional shares are ignored.
|
|
65
|
+
* Shares must agree on secret length and x-coordinates must be
|
|
66
|
+
* distinct (checked; throws on mismatch).
|
|
67
|
+
*/
|
|
68
|
+
declare function combineSecret(shares: readonly RawShare[]): Uint8Array;
|
|
69
|
+
/** A raw Shamir share before serialisation. */
|
|
70
|
+
interface RawShare {
|
|
71
|
+
/** x-coordinate in GF(2^8), 1..255. Zero is disallowed (it would reveal the secret). */
|
|
72
|
+
readonly x: number;
|
|
73
|
+
/** y-bytes — one per byte of the secret. */
|
|
74
|
+
readonly y: Uint8Array;
|
|
75
|
+
/** Threshold used at split time. */
|
|
76
|
+
readonly k: number;
|
|
77
|
+
/** Total shares at split time. */
|
|
78
|
+
readonly n: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Share serialisation — raw bytes, Base32 for printing, and JSON for
|
|
83
|
+
* structured transport (e.g. storing inside another on-* keyring).
|
|
84
|
+
*
|
|
85
|
+
* Binary layout (fixed header + secret-length y-bytes):
|
|
86
|
+
*
|
|
87
|
+
* ```
|
|
88
|
+
* offset size field
|
|
89
|
+
* 0 1 version (= 1)
|
|
90
|
+
* 1 1 x-coordinate (1..255)
|
|
91
|
+
* 2 1 k (threshold)
|
|
92
|
+
* 3 1 n (total)
|
|
93
|
+
* 4 2 byteLength (big-endian uint16) — should equal y-length
|
|
94
|
+
* 6+ L y-bytes (L = byteLength)
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Base32 adds a 26-character ULID `shareId` prefix + version/k/n
|
|
98
|
+
* metadata for human-readable diagnostics.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/** Serialise a raw share into its canonical binary form. */
|
|
102
|
+
declare function encodeShareBytes(share: RawShare): Uint8Array;
|
|
103
|
+
/** Parse binary share bytes back into a structured share. */
|
|
104
|
+
declare function decodeShareBytes(bytes: Uint8Array): RawShare;
|
|
105
|
+
/**
|
|
106
|
+
* Encode a share as a Base32 string with hyphenated groups of 4.
|
|
107
|
+
*
|
|
108
|
+
* Output format: `SHAMIR_S{x}_K{k}N{n}__<base32-groups-of-4>`
|
|
109
|
+
*
|
|
110
|
+
* The prefix is not fed into the decoder — it's for eye-readability at a
|
|
111
|
+
* glance (share-number / threshold / total). The `SHAMIR` tag + `_`
|
|
112
|
+
* separators are non-Base32 characters (Base32 alphabet is A-Z2-7
|
|
113
|
+
* only), so the decoder can strip everything up to and including the
|
|
114
|
+
* last `_` before the payload. The metadata is also encoded in the
|
|
115
|
+
* share bytes themselves; the prefix is redundant-but-useful.
|
|
116
|
+
*/
|
|
117
|
+
declare function encodeShareBase32(share: RawShare): string;
|
|
118
|
+
/**
|
|
119
|
+
* Parse a Base32 share string back into a structured share. Tolerates
|
|
120
|
+
* hyphens, whitespace, lowercase, and the optional prefix produced by
|
|
121
|
+
* `encodeShareBase32`. The metadata prefix is informational; actual
|
|
122
|
+
* share data comes from the binary bytes after the last `_`.
|
|
123
|
+
*/
|
|
124
|
+
declare function decodeShareBase32(input: string): RawShare;
|
|
125
|
+
/**
|
|
126
|
+
* JSON-friendly form for structured storage (e.g. stash inside another
|
|
127
|
+
* on-* keyring entry, or pass over `postMessage`).
|
|
128
|
+
*/
|
|
129
|
+
interface ShareJSON {
|
|
130
|
+
readonly v: 1;
|
|
131
|
+
readonly x: number;
|
|
132
|
+
readonly k: number;
|
|
133
|
+
readonly n: number;
|
|
134
|
+
readonly y: string;
|
|
135
|
+
}
|
|
136
|
+
declare function encodeShareJSON(share: RawShare): ShareJSON;
|
|
137
|
+
declare function decodeShareJSON(json: ShareJSON): RawShare;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* **@noy-db/on-shamir** — k-of-n Shamir Secret Sharing of the vault KEK.
|
|
141
|
+
*
|
|
142
|
+
* Any K of N enrolled shares recombines the original KEK; fewer than
|
|
143
|
+
* K leaks zero bits. Unlike naive multi-passphrase schemes, each
|
|
144
|
+
* share can be protected by ANY other `@noy-db/on-*` method — share
|
|
145
|
+
* 1 under a WebAuthn passkey, share 2 under an OIDC login, share 3
|
|
146
|
+
* on paper in a safe. This composability is the defining feature.
|
|
147
|
+
*
|
|
148
|
+
* Part of the `@noy-db/on-*` authentication family.
|
|
149
|
+
*
|
|
150
|
+
* ## Math
|
|
151
|
+
*
|
|
152
|
+
* Shamir Secret Sharing over GF(2^8), byte-wise. For each byte of the
|
|
153
|
+
* secret, construct a random polynomial of degree k-1 with the byte
|
|
154
|
+
* as the constant term. Each share is a value of that polynomial at
|
|
155
|
+
* a distinct x-coordinate. Lagrange interpolation at x=0 recovers
|
|
156
|
+
* the byte. Zero cryptographic dependencies — pure math in
|
|
157
|
+
* `gf256.ts` and `shamir.ts`.
|
|
158
|
+
*
|
|
159
|
+
* ## Threat model
|
|
160
|
+
*
|
|
161
|
+
* Protects against:
|
|
162
|
+
* - Up to K-1 colluding share holders (mathematically — fewer than K shares reveals zero bits)
|
|
163
|
+
* - Loss of up to N-K shares
|
|
164
|
+
*
|
|
165
|
+
* Does NOT protect against:
|
|
166
|
+
* - K colluding share holders (by design — that's the threshold contract)
|
|
167
|
+
* - Device compromise of the combining machine during reconstruction
|
|
168
|
+
*
|
|
169
|
+
* ## Usage
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* import {
|
|
173
|
+
* splitKEK,
|
|
174
|
+
* combineKEK,
|
|
175
|
+
* encodeShareBase32,
|
|
176
|
+
* decodeShareBase32,
|
|
177
|
+
* } from '@noy-db/on-shamir'
|
|
178
|
+
*
|
|
179
|
+
* // ENROLL — user has unlocked the vault with passphrase; now create a 2-of-3 split
|
|
180
|
+
* const shares = await splitKEK(currentKEK, { k: 2, n: 3 })
|
|
181
|
+
* const shareStrings = shares.map(encodeShareBase32)
|
|
182
|
+
* // Distribute each shareString to a different holder via any on-* method
|
|
183
|
+
*
|
|
184
|
+
* // UNLOCK — collect 2 of the 3 shares and combine
|
|
185
|
+
* const collected = [decodeShareBase32(shareA), decodeShareBase32(shareB)]
|
|
186
|
+
* const kek = await combineKEK(collected)
|
|
187
|
+
* // kek is now a non-extractable CryptoKey usable as the vault's KEK
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @packageDocumentation
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
interface SplitKEKOptions {
|
|
194
|
+
/** Threshold — minimum shares needed to reconstruct. Must be >= 2. */
|
|
195
|
+
readonly k: number;
|
|
196
|
+
/** Total shares. Must satisfy k <= n <= 255. */
|
|
197
|
+
readonly n: number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Split the given KEK into N Shamir shares.
|
|
201
|
+
*
|
|
202
|
+
* The KEK must be extractable (so the raw bytes can be read for the
|
|
203
|
+
* split operation). The returned `RawShare[]` contains the raw share
|
|
204
|
+
* material — serialise each via `encodeShareBase32` / `encodeShareJSON`
|
|
205
|
+
* before distributing.
|
|
206
|
+
*
|
|
207
|
+
* The caller is responsible for:
|
|
208
|
+
* 1. Distributing the shares to their holders.
|
|
209
|
+
* 2. Writing an audit-ledger entry recording the enrollment (including
|
|
210
|
+
* the `k`, `n`, and share x-coordinates — not the share material).
|
|
211
|
+
* 3. Securely zeroing any in-memory share/secret material after
|
|
212
|
+
* serialisation.
|
|
213
|
+
*/
|
|
214
|
+
declare function splitKEK(kek: CryptoKey, options: SplitKEKOptions): Promise<RawShare[]>;
|
|
215
|
+
/**
|
|
216
|
+
* Reconstruct the KEK from K or more shares.
|
|
217
|
+
*
|
|
218
|
+
* Returns a non-extractable `CryptoKey` ready to use as the vault's
|
|
219
|
+
* KEK. Internal secret bytes are zeroed after the CryptoKey is
|
|
220
|
+
* imported.
|
|
221
|
+
*
|
|
222
|
+
* Throws:
|
|
223
|
+
* - if fewer than K shares are provided
|
|
224
|
+
* - if shares have mismatched lengths (likely indicating shares from
|
|
225
|
+
* different enrollments were mixed)
|
|
226
|
+
* - if duplicate x-coordinates are detected
|
|
227
|
+
*/
|
|
228
|
+
declare function combineKEK(shares: readonly RawShare[]): Promise<CryptoKey>;
|
|
229
|
+
|
|
230
|
+
export { type RawShare, type ShareJSON, type SplitKEKOptions, combineKEK, combineSecret, decodeShareBase32, decodeShareBytes, decodeShareJSON, encodeShareBase32, encodeShareBytes, encodeShareJSON, gfAdd, gfDiv, gfInv, gfMul, gfPolyEval, lagrangeInterpolateAtZero, splitKEK, splitSecret };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic in the Galois field GF(2^8), represented as bytes 0..255.
|
|
3
|
+
*
|
|
4
|
+
* Used by Shamir Secret Sharing for byte-wise polynomial operations.
|
|
5
|
+
* Addition is XOR; multiplication uses precomputed log/exp tables
|
|
6
|
+
* against the primitive element 0x03 with irreducible polynomial
|
|
7
|
+
* 0x11b (x^8 + x^4 + x^3 + x + 1 — the AES polynomial).
|
|
8
|
+
*
|
|
9
|
+
* Constant-time is NOT a goal here — for secret sharing, the threat
|
|
10
|
+
* model assumes the combining device is trusted during
|
|
11
|
+
* reconstruction. If that assumption is wrong, no library-level
|
|
12
|
+
* constant-time hedge protects the plaintext anyway.
|
|
13
|
+
*/
|
|
14
|
+
/** Addition in GF(2^8) is XOR. */
|
|
15
|
+
declare function gfAdd(a: number, b: number): number;
|
|
16
|
+
/** Multiplication in GF(2^8). Returns 0 if either operand is 0. */
|
|
17
|
+
declare function gfMul(a: number, b: number): number;
|
|
18
|
+
/** Multiplicative inverse in GF(2^8). Throws on 0 (no inverse exists). */
|
|
19
|
+
declare function gfInv(a: number): number;
|
|
20
|
+
/** Division in GF(2^8). Throws on divide-by-zero. */
|
|
21
|
+
declare function gfDiv(a: number, b: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Evaluate a polynomial with coefficients `coeffs[0] + coeffs[1]*x + ...`
|
|
24
|
+
* at point `x` in GF(2^8) via Horner's method.
|
|
25
|
+
*/
|
|
26
|
+
declare function gfPolyEval(coeffs: readonly number[], x: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Lagrange interpolation at x=0, given k distinct points `(xi, yi)`.
|
|
29
|
+
*
|
|
30
|
+
* Returns the constant term of the unique degree-(k-1) polynomial
|
|
31
|
+
* through those points — equal to the original secret byte for
|
|
32
|
+
* Shamir's construction.
|
|
33
|
+
*/
|
|
34
|
+
declare function lagrangeInterpolateAtZero(points: readonly [number, number][]): number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Shamir Secret Sharing over GF(2^8), byte-wise.
|
|
38
|
+
*
|
|
39
|
+
* For each byte of the secret, construct a random polynomial of
|
|
40
|
+
* degree k-1 whose constant term is the byte. Each share is a value
|
|
41
|
+
* of that polynomial at a distinct x-coordinate in 1..255. Any K of
|
|
42
|
+
* the N shares recombines the original bytes via Lagrange interpolation
|
|
43
|
+
* at x=0.
|
|
44
|
+
*
|
|
45
|
+
* x=0 is reserved (it would directly reveal the secret). x-coordinates
|
|
46
|
+
* are chosen from 1..255 ensuring distinctness.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Split a secret into N shares. Any K of them reconstructs the secret;
|
|
50
|
+
* fewer than K leaks zero bits.
|
|
51
|
+
*
|
|
52
|
+
* @param secret - The bytes to share (e.g., a 32-byte KEK).
|
|
53
|
+
* @param k - Threshold (min shares needed to reconstruct). Must be >= 2.
|
|
54
|
+
* @param n - Total shares to produce. Must be >= k and <= 255.
|
|
55
|
+
* @param randomBytes - RNG function returning N random bytes. Defaults to
|
|
56
|
+
* `crypto.getRandomValues`. Injectable for deterministic tests.
|
|
57
|
+
* @returns An array of `n` shares. Each share has an x-coordinate + y-bytes
|
|
58
|
+
* parallel to the secret bytes.
|
|
59
|
+
*/
|
|
60
|
+
declare function splitSecret(secret: Uint8Array, k: number, n: number, randomBytes?: (count: number) => Uint8Array): RawShare[];
|
|
61
|
+
/**
|
|
62
|
+
* Reconstruct a secret from K or more shares.
|
|
63
|
+
*
|
|
64
|
+
* Uses the first K shares provided; additional shares are ignored.
|
|
65
|
+
* Shares must agree on secret length and x-coordinates must be
|
|
66
|
+
* distinct (checked; throws on mismatch).
|
|
67
|
+
*/
|
|
68
|
+
declare function combineSecret(shares: readonly RawShare[]): Uint8Array;
|
|
69
|
+
/** A raw Shamir share before serialisation. */
|
|
70
|
+
interface RawShare {
|
|
71
|
+
/** x-coordinate in GF(2^8), 1..255. Zero is disallowed (it would reveal the secret). */
|
|
72
|
+
readonly x: number;
|
|
73
|
+
/** y-bytes — one per byte of the secret. */
|
|
74
|
+
readonly y: Uint8Array;
|
|
75
|
+
/** Threshold used at split time. */
|
|
76
|
+
readonly k: number;
|
|
77
|
+
/** Total shares at split time. */
|
|
78
|
+
readonly n: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Share serialisation — raw bytes, Base32 for printing, and JSON for
|
|
83
|
+
* structured transport (e.g. storing inside another on-* keyring).
|
|
84
|
+
*
|
|
85
|
+
* Binary layout (fixed header + secret-length y-bytes):
|
|
86
|
+
*
|
|
87
|
+
* ```
|
|
88
|
+
* offset size field
|
|
89
|
+
* 0 1 version (= 1)
|
|
90
|
+
* 1 1 x-coordinate (1..255)
|
|
91
|
+
* 2 1 k (threshold)
|
|
92
|
+
* 3 1 n (total)
|
|
93
|
+
* 4 2 byteLength (big-endian uint16) — should equal y-length
|
|
94
|
+
* 6+ L y-bytes (L = byteLength)
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Base32 adds a 26-character ULID `shareId` prefix + version/k/n
|
|
98
|
+
* metadata for human-readable diagnostics.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/** Serialise a raw share into its canonical binary form. */
|
|
102
|
+
declare function encodeShareBytes(share: RawShare): Uint8Array;
|
|
103
|
+
/** Parse binary share bytes back into a structured share. */
|
|
104
|
+
declare function decodeShareBytes(bytes: Uint8Array): RawShare;
|
|
105
|
+
/**
|
|
106
|
+
* Encode a share as a Base32 string with hyphenated groups of 4.
|
|
107
|
+
*
|
|
108
|
+
* Output format: `SHAMIR_S{x}_K{k}N{n}__<base32-groups-of-4>`
|
|
109
|
+
*
|
|
110
|
+
* The prefix is not fed into the decoder — it's for eye-readability at a
|
|
111
|
+
* glance (share-number / threshold / total). The `SHAMIR` tag + `_`
|
|
112
|
+
* separators are non-Base32 characters (Base32 alphabet is A-Z2-7
|
|
113
|
+
* only), so the decoder can strip everything up to and including the
|
|
114
|
+
* last `_` before the payload. The metadata is also encoded in the
|
|
115
|
+
* share bytes themselves; the prefix is redundant-but-useful.
|
|
116
|
+
*/
|
|
117
|
+
declare function encodeShareBase32(share: RawShare): string;
|
|
118
|
+
/**
|
|
119
|
+
* Parse a Base32 share string back into a structured share. Tolerates
|
|
120
|
+
* hyphens, whitespace, lowercase, and the optional prefix produced by
|
|
121
|
+
* `encodeShareBase32`. The metadata prefix is informational; actual
|
|
122
|
+
* share data comes from the binary bytes after the last `_`.
|
|
123
|
+
*/
|
|
124
|
+
declare function decodeShareBase32(input: string): RawShare;
|
|
125
|
+
/**
|
|
126
|
+
* JSON-friendly form for structured storage (e.g. stash inside another
|
|
127
|
+
* on-* keyring entry, or pass over `postMessage`).
|
|
128
|
+
*/
|
|
129
|
+
interface ShareJSON {
|
|
130
|
+
readonly v: 1;
|
|
131
|
+
readonly x: number;
|
|
132
|
+
readonly k: number;
|
|
133
|
+
readonly n: number;
|
|
134
|
+
readonly y: string;
|
|
135
|
+
}
|
|
136
|
+
declare function encodeShareJSON(share: RawShare): ShareJSON;
|
|
137
|
+
declare function decodeShareJSON(json: ShareJSON): RawShare;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* **@noy-db/on-shamir** — k-of-n Shamir Secret Sharing of the vault KEK.
|
|
141
|
+
*
|
|
142
|
+
* Any K of N enrolled shares recombines the original KEK; fewer than
|
|
143
|
+
* K leaks zero bits. Unlike naive multi-passphrase schemes, each
|
|
144
|
+
* share can be protected by ANY other `@noy-db/on-*` method — share
|
|
145
|
+
* 1 under a WebAuthn passkey, share 2 under an OIDC login, share 3
|
|
146
|
+
* on paper in a safe. This composability is the defining feature.
|
|
147
|
+
*
|
|
148
|
+
* Part of the `@noy-db/on-*` authentication family.
|
|
149
|
+
*
|
|
150
|
+
* ## Math
|
|
151
|
+
*
|
|
152
|
+
* Shamir Secret Sharing over GF(2^8), byte-wise. For each byte of the
|
|
153
|
+
* secret, construct a random polynomial of degree k-1 with the byte
|
|
154
|
+
* as the constant term. Each share is a value of that polynomial at
|
|
155
|
+
* a distinct x-coordinate. Lagrange interpolation at x=0 recovers
|
|
156
|
+
* the byte. Zero cryptographic dependencies — pure math in
|
|
157
|
+
* `gf256.ts` and `shamir.ts`.
|
|
158
|
+
*
|
|
159
|
+
* ## Threat model
|
|
160
|
+
*
|
|
161
|
+
* Protects against:
|
|
162
|
+
* - Up to K-1 colluding share holders (mathematically — fewer than K shares reveals zero bits)
|
|
163
|
+
* - Loss of up to N-K shares
|
|
164
|
+
*
|
|
165
|
+
* Does NOT protect against:
|
|
166
|
+
* - K colluding share holders (by design — that's the threshold contract)
|
|
167
|
+
* - Device compromise of the combining machine during reconstruction
|
|
168
|
+
*
|
|
169
|
+
* ## Usage
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* import {
|
|
173
|
+
* splitKEK,
|
|
174
|
+
* combineKEK,
|
|
175
|
+
* encodeShareBase32,
|
|
176
|
+
* decodeShareBase32,
|
|
177
|
+
* } from '@noy-db/on-shamir'
|
|
178
|
+
*
|
|
179
|
+
* // ENROLL — user has unlocked the vault with passphrase; now create a 2-of-3 split
|
|
180
|
+
* const shares = await splitKEK(currentKEK, { k: 2, n: 3 })
|
|
181
|
+
* const shareStrings = shares.map(encodeShareBase32)
|
|
182
|
+
* // Distribute each shareString to a different holder via any on-* method
|
|
183
|
+
*
|
|
184
|
+
* // UNLOCK — collect 2 of the 3 shares and combine
|
|
185
|
+
* const collected = [decodeShareBase32(shareA), decodeShareBase32(shareB)]
|
|
186
|
+
* const kek = await combineKEK(collected)
|
|
187
|
+
* // kek is now a non-extractable CryptoKey usable as the vault's KEK
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @packageDocumentation
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
interface SplitKEKOptions {
|
|
194
|
+
/** Threshold — minimum shares needed to reconstruct. Must be >= 2. */
|
|
195
|
+
readonly k: number;
|
|
196
|
+
/** Total shares. Must satisfy k <= n <= 255. */
|
|
197
|
+
readonly n: number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Split the given KEK into N Shamir shares.
|
|
201
|
+
*
|
|
202
|
+
* The KEK must be extractable (so the raw bytes can be read for the
|
|
203
|
+
* split operation). The returned `RawShare[]` contains the raw share
|
|
204
|
+
* material — serialise each via `encodeShareBase32` / `encodeShareJSON`
|
|
205
|
+
* before distributing.
|
|
206
|
+
*
|
|
207
|
+
* The caller is responsible for:
|
|
208
|
+
* 1. Distributing the shares to their holders.
|
|
209
|
+
* 2. Writing an audit-ledger entry recording the enrollment (including
|
|
210
|
+
* the `k`, `n`, and share x-coordinates — not the share material).
|
|
211
|
+
* 3. Securely zeroing any in-memory share/secret material after
|
|
212
|
+
* serialisation.
|
|
213
|
+
*/
|
|
214
|
+
declare function splitKEK(kek: CryptoKey, options: SplitKEKOptions): Promise<RawShare[]>;
|
|
215
|
+
/**
|
|
216
|
+
* Reconstruct the KEK from K or more shares.
|
|
217
|
+
*
|
|
218
|
+
* Returns a non-extractable `CryptoKey` ready to use as the vault's
|
|
219
|
+
* KEK. Internal secret bytes are zeroed after the CryptoKey is
|
|
220
|
+
* imported.
|
|
221
|
+
*
|
|
222
|
+
* Throws:
|
|
223
|
+
* - if fewer than K shares are provided
|
|
224
|
+
* - if shares have mismatched lengths (likely indicating shares from
|
|
225
|
+
* different enrollments were mixed)
|
|
226
|
+
* - if duplicate x-coordinates are detected
|
|
227
|
+
*/
|
|
228
|
+
declare function combineKEK(shares: readonly RawShare[]): Promise<CryptoKey>;
|
|
229
|
+
|
|
230
|
+
export { type RawShare, type ShareJSON, type SplitKEKOptions, combineKEK, combineSecret, decodeShareBase32, decodeShareBytes, decodeShareJSON, encodeShareBase32, encodeShareBytes, encodeShareJSON, gfAdd, gfDiv, gfInv, gfMul, gfPolyEval, lagrangeInterpolateAtZero, splitKEK, splitSecret };
|