@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.
@@ -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 };
@@ -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 };