@private.me/xcontinuity 2.1.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.
- package/AGENTS.md +123 -0
- package/LICENSE.md +26 -0
- package/MIGRATING.md +77 -0
- package/README.md +601 -0
- package/dist/adjudicator.d.ts +75 -0
- package/dist/adjudicator.js +184 -0
- package/dist/cascade.d.ts +157 -0
- package/dist/cascade.js +323 -0
- package/dist/chronicle.d.ts +76 -0
- package/dist/chronicle.js +173 -0
- package/dist/cjs/adjudicator.js +189 -0
- package/dist/cjs/cascade.js +328 -0
- package/dist/cjs/chronicle.js +178 -0
- package/dist/cjs/enforcement.js +108 -0
- package/dist/cjs/errors.js +72 -0
- package/dist/cjs/index.js +108 -0
- package/dist/cjs/memory-runtime.js +129 -0
- package/dist/cjs/memory-session.js +134 -0
- package/dist/cjs/mission.js +178 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/provenance.js +192 -0
- package/dist/cjs/ratification.js +322 -0
- package/dist/cjs/reverse-xorida.js +506 -0
- package/dist/cjs/session.js +273 -0
- package/dist/cjs/state-serializer.js +300 -0
- package/dist/cjs/store-memory.js +33 -0
- package/dist/cjs/trust.js +133 -0
- package/dist/cjs/types.js +59 -0
- package/dist/enforcement.d.ts +40 -0
- package/dist/enforcement.js +104 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +43 -0
- package/dist/memory-runtime.d.ts +36 -0
- package/dist/memory-runtime.js +125 -0
- package/dist/memory-session.d.ts +38 -0
- package/dist/memory-session.js +97 -0
- package/dist/mission.d.ts +68 -0
- package/dist/mission.js +172 -0
- package/dist/provenance.d.ts +54 -0
- package/dist/provenance.js +182 -0
- package/dist/ratification.d.ts +113 -0
- package/dist/ratification.js +317 -0
- package/dist/reverse-xorida.d.ts +174 -0
- package/dist/reverse-xorida.js +490 -0
- package/dist/session.d.ts +102 -0
- package/dist/session.js +269 -0
- package/dist/state-serializer.d.ts +37 -0
- package/dist/state-serializer.js +294 -0
- package/dist/store-memory.d.ts +18 -0
- package/dist/store-memory.js +29 -0
- package/dist/trust.d.ts +76 -0
- package/dist/trust.js +121 -0
- package/dist/types.d.ts +320 -0
- package/dist/types.js +56 -0
- package/llms.txt +43 -0
- package/package.json +125 -0
- package/share1.dat +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @private.me/xcontinuity — Reverse-XorIDA Core Crypto Layer
|
|
4
|
+
*
|
|
5
|
+
* The novel primitive: exploits XorIDA's linearity over GF(2) to perform
|
|
6
|
+
* incremental state updates at O(delta) cost instead of re-splitting entire state.
|
|
7
|
+
*
|
|
8
|
+
* Key insight: split(A XOR B) = split(A) XOR split(B)
|
|
9
|
+
*
|
|
10
|
+
* Therefore: newShares = oldShares XOR split(oldPadded XOR newPadded)
|
|
11
|
+
*
|
|
12
|
+
* HMAC must be recomputed fresh for the new state (not derived from delta).
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.padForSplit = padForSplit;
|
|
16
|
+
exports.splitState = splitState;
|
|
17
|
+
exports.reconstructState = reconstructState;
|
|
18
|
+
exports.computeDelta = computeDelta;
|
|
19
|
+
exports.applyDeltaShares = applyDeltaShares;
|
|
20
|
+
exports.incrementalUpdate = incrementalUpdate;
|
|
21
|
+
exports.undoDelta = undoDelta;
|
|
22
|
+
exports.branchState = branchState;
|
|
23
|
+
exports.squashDeltas = squashDeltas;
|
|
24
|
+
exports.blindUpdateShare = blindUpdateShare;
|
|
25
|
+
exports.refreshShares = refreshShares;
|
|
26
|
+
exports.blindEqual = blindEqual;
|
|
27
|
+
exports.networkCodeShares = networkCodeShares;
|
|
28
|
+
exports.networkDecodeShare = networkDecodeShare;
|
|
29
|
+
const shared_1 = require("@private.me/shared");
|
|
30
|
+
const crypto_1 = require("@private.me/crypto");
|
|
31
|
+
const types_js_1 = require("./types.js");
|
|
32
|
+
const errors_js_1 = require("./errors.js");
|
|
33
|
+
/**
|
|
34
|
+
* Pad data for XorIDA splitting.
|
|
35
|
+
* Block size = nextOddPrime(n) - 1.
|
|
36
|
+
*/
|
|
37
|
+
function padForSplit(data, n) {
|
|
38
|
+
const p = (0, crypto_1.nextOddPrime)(n);
|
|
39
|
+
const blockSize = p - 1;
|
|
40
|
+
return (0, crypto_1.pkcs7Pad)(data, blockSize);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Split a state snapshot into XorIDA threshold shares.
|
|
44
|
+
*
|
|
45
|
+
* Pipeline: pad -> HMAC -> split -> package as StateShare[]
|
|
46
|
+
*
|
|
47
|
+
* @param snapshot - Serialized state with checksum
|
|
48
|
+
* @param config - Split configuration (n, k)
|
|
49
|
+
* @returns SplitState containing all shares
|
|
50
|
+
*/
|
|
51
|
+
async function splitState(snapshot, config = types_js_1.DEFAULT_SPLIT_CONFIG) {
|
|
52
|
+
try {
|
|
53
|
+
const { n, k } = config;
|
|
54
|
+
if (n < 2 || k < 2 || k > n) {
|
|
55
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('SPLIT_FAILED', `Invalid config: n=${n}, k=${k}`));
|
|
56
|
+
}
|
|
57
|
+
const padded = padForSplit(snapshot.serializedBytes, n);
|
|
58
|
+
const { key: hmacKey, signature: hmacSignature } = await (0, crypto_1.generateHMAC)(padded);
|
|
59
|
+
const rawShares = (0, crypto_1.splitXorIDA)(padded, n, k);
|
|
60
|
+
const shares = rawShares.map((data, index) => ({
|
|
61
|
+
stateId: snapshot.stateId,
|
|
62
|
+
index,
|
|
63
|
+
n,
|
|
64
|
+
k,
|
|
65
|
+
data,
|
|
66
|
+
hmacKey,
|
|
67
|
+
hmacSignature,
|
|
68
|
+
}));
|
|
69
|
+
return (0, shared_1.ok)({
|
|
70
|
+
stateId: snapshot.stateId,
|
|
71
|
+
metadata: snapshot.metadata,
|
|
72
|
+
shares,
|
|
73
|
+
n,
|
|
74
|
+
k,
|
|
75
|
+
paddedLength: padded.length,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
const msg = e instanceof Error ? e.message : 'Split failed';
|
|
80
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('SPLIT_FAILED', msg));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reconstruct a state snapshot from threshold shares.
|
|
85
|
+
*
|
|
86
|
+
* Pipeline: validate -> reconstruct -> verify HMAC -> unpad -> return snapshot bytes
|
|
87
|
+
*
|
|
88
|
+
* @param shares - Array of k or more StateShares (uses first k)
|
|
89
|
+
* @returns Reconstructed StateSnapshot serialized bytes and checksum
|
|
90
|
+
*/
|
|
91
|
+
async function reconstructState(shares) {
|
|
92
|
+
try {
|
|
93
|
+
if (shares.length === 0) {
|
|
94
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INSUFFICIENT_SHARES', 'No shares provided'));
|
|
95
|
+
}
|
|
96
|
+
const first = shares[0];
|
|
97
|
+
const k = first.k;
|
|
98
|
+
const n = first.n;
|
|
99
|
+
if (shares.length < k) {
|
|
100
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INSUFFICIENT_SHARES', `Need ${k} shares, got ${shares.length}`));
|
|
101
|
+
}
|
|
102
|
+
// Verify all shares have same stateId
|
|
103
|
+
const stateId = first.stateId;
|
|
104
|
+
for (const share of shares) {
|
|
105
|
+
if (share.stateId !== stateId) {
|
|
106
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INVALID_SHARES', 'Share stateId mismatch'));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Take first k shares
|
|
110
|
+
const subset = shares.slice(0, k);
|
|
111
|
+
const shareData = subset.map(s => s.data);
|
|
112
|
+
const indices = subset.map(s => s.index);
|
|
113
|
+
const reconstructed = (0, crypto_1.reconstructXorIDA)(shareData, indices, n, k);
|
|
114
|
+
// Verify HMAC using the key from the first share
|
|
115
|
+
const hmacValid = await (0, crypto_1.verifyHMAC)(first.hmacKey, reconstructed, first.hmacSignature);
|
|
116
|
+
if (!hmacValid) {
|
|
117
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('HMAC_FAILURE', 'HMAC verification failed after reconstruction'));
|
|
118
|
+
}
|
|
119
|
+
// Unpad
|
|
120
|
+
const p = (0, crypto_1.nextOddPrime)(n);
|
|
121
|
+
const blockSize = p - 1;
|
|
122
|
+
const unpadResult = (0, crypto_1.pkcs7Unpad)(reconstructed, blockSize);
|
|
123
|
+
if (!unpadResult.ok) {
|
|
124
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('PADDING_ERROR', 'Failed to remove padding after reconstruction'));
|
|
125
|
+
}
|
|
126
|
+
return (0, shared_1.ok)({ serializedBytes: unpadResult.value, stateId });
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
const msg = e instanceof Error ? e.message : 'Reconstruction failed';
|
|
130
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('RECONSTRUCT_FAILED', msg));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Compute the byte-level delta between two padded state arrays.
|
|
135
|
+
* Both arrays must be the same length.
|
|
136
|
+
*
|
|
137
|
+
* delta[i] = oldPadded[i] XOR newPadded[i]
|
|
138
|
+
*/
|
|
139
|
+
function computeDelta(oldPadded, newPadded, fromStateId, toStateId, fromChecksum, toChecksum) {
|
|
140
|
+
if (oldPadded.length !== newPadded.length) {
|
|
141
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', `Padded sizes differ: ${oldPadded.length} vs ${newPadded.length}`));
|
|
142
|
+
}
|
|
143
|
+
const deltaBytes = new Uint8Array(oldPadded.length);
|
|
144
|
+
for (let i = 0; i < oldPadded.length; i++) {
|
|
145
|
+
deltaBytes[i] = oldPadded[i] ^ newPadded[i];
|
|
146
|
+
}
|
|
147
|
+
return (0, shared_1.ok)({ fromStateId, toStateId, deltaBytes, fromChecksum, toChecksum });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Apply delta shares to old split state via XOR.
|
|
151
|
+
*
|
|
152
|
+
* newShares[i].data = oldShares[i].data XOR deltaShares[i].data
|
|
153
|
+
*
|
|
154
|
+
* @param oldShares - Original shares
|
|
155
|
+
* @param deltaShares - Delta shares (from splitting the delta)
|
|
156
|
+
* @param newStateId - State ID for the new split state
|
|
157
|
+
* @param newMetadata - Metadata for the new state
|
|
158
|
+
* @param newHmacKey - Fresh HMAC key for the new state
|
|
159
|
+
* @param newHmacSignature - Fresh HMAC signature for the new padded state
|
|
160
|
+
*/
|
|
161
|
+
function applyDeltaShares(oldShares, deltaShares, newStateId, newHmacKey, newHmacSignature) {
|
|
162
|
+
if (oldShares.length !== deltaShares.length) {
|
|
163
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INVALID_SHARES', `Share count mismatch: ${oldShares.length} vs ${deltaShares.length}`));
|
|
164
|
+
}
|
|
165
|
+
const newShares = [];
|
|
166
|
+
for (let i = 0; i < oldShares.length; i++) {
|
|
167
|
+
const oldShare = oldShares[i];
|
|
168
|
+
const deltaData = deltaShares[i];
|
|
169
|
+
if (oldShare.data.length !== deltaData.length) {
|
|
170
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', `Share ${i} size mismatch: ${oldShare.data.length} vs ${deltaData.length}`));
|
|
171
|
+
}
|
|
172
|
+
const newData = new Uint8Array(oldShare.data.length);
|
|
173
|
+
for (let j = 0; j < oldShare.data.length; j++) {
|
|
174
|
+
newData[j] = oldShare.data[j] ^ deltaData[j];
|
|
175
|
+
}
|
|
176
|
+
newShares.push({
|
|
177
|
+
stateId: newStateId,
|
|
178
|
+
index: oldShare.index,
|
|
179
|
+
n: oldShare.n,
|
|
180
|
+
k: oldShare.k,
|
|
181
|
+
data: newData,
|
|
182
|
+
hmacKey: newHmacKey,
|
|
183
|
+
hmacSignature: newHmacSignature,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return (0, shared_1.ok)(newShares);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Perform an incremental state update using Reverse-XorIDA.
|
|
190
|
+
*
|
|
191
|
+
* Instead of re-splitting the entire new state:
|
|
192
|
+
* 1. Pad both old and new to the same block-aligned size
|
|
193
|
+
* 2. Compute delta = oldPadded XOR newPadded
|
|
194
|
+
* 3. Split the delta
|
|
195
|
+
* 4. XOR each old share with the corresponding delta share
|
|
196
|
+
* 5. Recompute HMAC fresh for the new padded state
|
|
197
|
+
*
|
|
198
|
+
* Falls back to fresh split when sizes differ significantly
|
|
199
|
+
* (incremental update only works when padded lengths match).
|
|
200
|
+
*
|
|
201
|
+
* @param oldSplitState - Previous split state
|
|
202
|
+
* @param oldSnapshot - Previous state snapshot
|
|
203
|
+
* @param newSnapshot - New state snapshot
|
|
204
|
+
* @returns Updated SplitState with new shares
|
|
205
|
+
*/
|
|
206
|
+
async function incrementalUpdate(oldSplitState, oldSnapshot, newSnapshot) {
|
|
207
|
+
try {
|
|
208
|
+
const { n, k } = oldSplitState;
|
|
209
|
+
// Pad both to the same block boundary
|
|
210
|
+
const oldPadded = padForSplit(oldSnapshot.serializedBytes, n);
|
|
211
|
+
const newPadded = padForSplit(newSnapshot.serializedBytes, n);
|
|
212
|
+
// If padded lengths differ, fall back to fresh split
|
|
213
|
+
if (oldPadded.length !== newPadded.length) {
|
|
214
|
+
return splitState(newSnapshot, { n, k });
|
|
215
|
+
}
|
|
216
|
+
// Compute delta
|
|
217
|
+
const deltaBytes = new Uint8Array(oldPadded.length);
|
|
218
|
+
for (let i = 0; i < oldPadded.length; i++) {
|
|
219
|
+
deltaBytes[i] = oldPadded[i] ^ newPadded[i];
|
|
220
|
+
}
|
|
221
|
+
// Split the delta
|
|
222
|
+
const deltaShares = (0, crypto_1.splitXorIDA)(deltaBytes, n, k);
|
|
223
|
+
// Fresh HMAC for the new padded state
|
|
224
|
+
const { key: newHmacKey, signature: newHmacSignature } = await (0, crypto_1.generateHMAC)(newPadded);
|
|
225
|
+
// XOR old shares with delta shares
|
|
226
|
+
const applyResult = applyDeltaShares(oldSplitState.shares, deltaShares, newSnapshot.stateId, newHmacKey, newHmacSignature);
|
|
227
|
+
if (!applyResult.ok)
|
|
228
|
+
return applyResult;
|
|
229
|
+
return (0, shared_1.ok)({
|
|
230
|
+
stateId: newSnapshot.stateId,
|
|
231
|
+
metadata: newSnapshot.metadata,
|
|
232
|
+
shares: applyResult.value,
|
|
233
|
+
n,
|
|
234
|
+
k,
|
|
235
|
+
paddedLength: newPadded.length,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
const msg = e instanceof Error ? e.message : 'Incremental update failed';
|
|
240
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('SPLIT_FAILED', msg));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/* ══════════════════════════════════════════════════════════════════
|
|
244
|
+
* Algebraic Extensions (v2.0.0)
|
|
245
|
+
*
|
|
246
|
+
* All extensions exploit XorIDA's linearity over GF(2):
|
|
247
|
+
* split(A ⊕ B) = split(A) ⊕ split(B)
|
|
248
|
+
*
|
|
249
|
+
* This property enables O(δ) operations on encrypted shares
|
|
250
|
+
* without ever reconstructing the plaintext.
|
|
251
|
+
* ══════════════════════════════════════════════════════════════════ */
|
|
252
|
+
/**
|
|
253
|
+
* Extension 1: Undo / Time-Travel
|
|
254
|
+
*
|
|
255
|
+
* Reverse a delta by re-applying it (XOR is its own inverse in GF(2)).
|
|
256
|
+
* Given shares at state B and the delta A→B, produces shares at state A.
|
|
257
|
+
*
|
|
258
|
+
* Math: shares_A = shares_B ⊕ deltaShares(A→B)
|
|
259
|
+
* because: (A ⊕ Δ) ⊕ Δ = A (XOR self-inverse)
|
|
260
|
+
*/
|
|
261
|
+
function undoDelta(currentShares, deltaShares, previousStateId, hmacKey, hmacSignature) {
|
|
262
|
+
// Undo is identical to apply — XOR is self-inverse
|
|
263
|
+
return applyDeltaShares(currentShares, deltaShares, previousStateId, hmacKey, hmacSignature);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Extension 2: Branch
|
|
267
|
+
*
|
|
268
|
+
* Fork a split state into an independent branch by copying shares.
|
|
269
|
+
* The branch can diverge independently from the original.
|
|
270
|
+
*
|
|
271
|
+
* @param source - The split state to branch from
|
|
272
|
+
* @param branchStateId - New stateId for the branch
|
|
273
|
+
* @returns Independent copy of the split state
|
|
274
|
+
*/
|
|
275
|
+
function branchState(source, branchStateId) {
|
|
276
|
+
const branchedShares = source.shares.map(share => ({
|
|
277
|
+
stateId: branchStateId,
|
|
278
|
+
index: share.index,
|
|
279
|
+
n: share.n,
|
|
280
|
+
k: share.k,
|
|
281
|
+
data: new Uint8Array(share.data),
|
|
282
|
+
hmacKey: new Uint8Array(share.hmacKey),
|
|
283
|
+
hmacSignature: new Uint8Array(share.hmacSignature),
|
|
284
|
+
}));
|
|
285
|
+
return {
|
|
286
|
+
stateId: branchStateId,
|
|
287
|
+
metadata: source.metadata,
|
|
288
|
+
shares: branchedShares,
|
|
289
|
+
n: source.n,
|
|
290
|
+
k: source.k,
|
|
291
|
+
paddedLength: source.paddedLength,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Extension 3: Delta Squashing
|
|
296
|
+
*
|
|
297
|
+
* Combine multiple sequential deltas into a single delta.
|
|
298
|
+
* Exploits GF(2) associativity: Δ_total = Δ₁ ⊕ Δ₂ ⊕ ... ⊕ Δₙ
|
|
299
|
+
*
|
|
300
|
+
* @param deltas - Ordered array of deltas to squash (must form a chain)
|
|
301
|
+
* @returns Single squashed delta from first.fromStateId to last.toStateId
|
|
302
|
+
*/
|
|
303
|
+
function squashDeltas(deltas) {
|
|
304
|
+
if (deltas.length === 0) {
|
|
305
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', 'No deltas to squash'));
|
|
306
|
+
}
|
|
307
|
+
if (deltas.length === 1) {
|
|
308
|
+
return (0, shared_1.ok)(deltas[0]);
|
|
309
|
+
}
|
|
310
|
+
const first = deltas[0];
|
|
311
|
+
const last = deltas[deltas.length - 1];
|
|
312
|
+
// Verify chain continuity
|
|
313
|
+
for (let i = 1; i < deltas.length; i++) {
|
|
314
|
+
if (deltas[i].fromStateId !== deltas[i - 1].toStateId) {
|
|
315
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', `Delta chain broken at index ${i}: expected from=${deltas[i - 1].toStateId}, got from=${deltas[i].fromStateId}`));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Verify all deltas have the same length
|
|
319
|
+
const len = first.deltaBytes.length;
|
|
320
|
+
for (const d of deltas) {
|
|
321
|
+
if (d.deltaBytes.length !== len) {
|
|
322
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', `Delta size mismatch: expected ${len}, got ${d.deltaBytes.length}`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// XOR all deltas together
|
|
326
|
+
const squashed = new Uint8Array(len);
|
|
327
|
+
for (const d of deltas) {
|
|
328
|
+
for (let i = 0; i < len; i++) {
|
|
329
|
+
squashed[i] ^= d.deltaBytes[i];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return (0, shared_1.ok)({
|
|
333
|
+
fromStateId: first.fromStateId,
|
|
334
|
+
toStateId: last.toStateId,
|
|
335
|
+
deltaBytes: squashed,
|
|
336
|
+
fromChecksum: first.fromChecksum,
|
|
337
|
+
toChecksum: last.toChecksum,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Extension 4: Blind Update
|
|
342
|
+
*
|
|
343
|
+
* Apply a delta to shares without seeing the plaintext state.
|
|
344
|
+
* The share holder never reconstructs the full state.
|
|
345
|
+
*
|
|
346
|
+
* Math: newShare_i = oldShare_i ⊕ deltaShare_i
|
|
347
|
+
*
|
|
348
|
+
* @param share - A single share to update
|
|
349
|
+
* @param deltaShareData - The corresponding delta share data
|
|
350
|
+
* @param newStateId - New state identifier
|
|
351
|
+
* @param newHmacKey - Fresh HMAC key
|
|
352
|
+
* @param newHmacSignature - Fresh HMAC signature
|
|
353
|
+
*/
|
|
354
|
+
function blindUpdateShare(share, deltaShareData, newStateId, newHmacKey, newHmacSignature) {
|
|
355
|
+
if (share.data.length !== deltaShareData.length) {
|
|
356
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', `Share data length ${share.data.length} != delta length ${deltaShareData.length}`));
|
|
357
|
+
}
|
|
358
|
+
const newData = new Uint8Array(share.data.length);
|
|
359
|
+
for (let i = 0; i < share.data.length; i++) {
|
|
360
|
+
newData[i] = share.data[i] ^ deltaShareData[i];
|
|
361
|
+
}
|
|
362
|
+
return (0, shared_1.ok)({
|
|
363
|
+
stateId: newStateId,
|
|
364
|
+
index: share.index,
|
|
365
|
+
n: share.n,
|
|
366
|
+
k: share.k,
|
|
367
|
+
data: newData,
|
|
368
|
+
hmacKey: newHmacKey,
|
|
369
|
+
hmacSignature: newHmacSignature,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Extension 5: Proactive Share Refresh
|
|
374
|
+
*
|
|
375
|
+
* Re-randomize shares while preserving reconstruction correctness.
|
|
376
|
+
* Generates a random mask, splits it, XORs with existing shares.
|
|
377
|
+
* The mask XORs to zero upon reconstruction, so the plaintext is unchanged.
|
|
378
|
+
*
|
|
379
|
+
* @param splitState - Current split state
|
|
380
|
+
* @returns Refreshed split state with re-randomized shares (same plaintext)
|
|
381
|
+
*/
|
|
382
|
+
async function refreshShares(state) {
|
|
383
|
+
try {
|
|
384
|
+
const { n, k, paddedLength } = state;
|
|
385
|
+
// Generate random mask of the same padded length
|
|
386
|
+
const mask = new Uint8Array(paddedLength);
|
|
387
|
+
crypto.getRandomValues(mask);
|
|
388
|
+
// Split the mask
|
|
389
|
+
const maskShares = (0, crypto_1.splitXorIDA)(mask, n, k);
|
|
390
|
+
// XOR each share with its mask share
|
|
391
|
+
const refreshedShares = [];
|
|
392
|
+
for (let i = 0; i < state.shares.length; i++) {
|
|
393
|
+
const share = state.shares[i];
|
|
394
|
+
const maskData = maskShares[i];
|
|
395
|
+
const newData = new Uint8Array(share.data.length);
|
|
396
|
+
for (let j = 0; j < share.data.length; j++) {
|
|
397
|
+
newData[j] = share.data[j] ^ maskData[j];
|
|
398
|
+
}
|
|
399
|
+
refreshedShares.push({
|
|
400
|
+
...share,
|
|
401
|
+
data: newData,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// Recompute HMAC for the refreshed state
|
|
405
|
+
// Note: the plaintext doesn't change, so we reconstruct and verify
|
|
406
|
+
const refreshedData = refreshedShares.map(s => s.data);
|
|
407
|
+
const indices = refreshedShares.map(s => s.index);
|
|
408
|
+
const reconstructed = (0, crypto_1.reconstructXorIDA)(refreshedData.slice(0, k), indices.slice(0, k), n, k);
|
|
409
|
+
const { key: newHmacKey, signature: newHmacSignature } = await (0, crypto_1.generateHMAC)(reconstructed);
|
|
410
|
+
const finalShares = refreshedShares.map(s => ({
|
|
411
|
+
...s,
|
|
412
|
+
hmacKey: newHmacKey,
|
|
413
|
+
hmacSignature: newHmacSignature,
|
|
414
|
+
}));
|
|
415
|
+
return (0, shared_1.ok)({
|
|
416
|
+
...state,
|
|
417
|
+
shares: finalShares,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('SPLIT_FAILED', `Share refresh failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Extension 6: Blind Equality Check
|
|
426
|
+
*
|
|
427
|
+
* Compare two split states for equality without reconstructing plaintext.
|
|
428
|
+
* Computes delta between corresponding shares — if all delta bytes are zero,
|
|
429
|
+
* the states are equal.
|
|
430
|
+
*
|
|
431
|
+
* @param a - First split state
|
|
432
|
+
* @param b - Second split state
|
|
433
|
+
* @returns true if both states encode the same plaintext
|
|
434
|
+
*/
|
|
435
|
+
function blindEqual(a, b) {
|
|
436
|
+
if (a.n !== b.n || a.k !== b.k) {
|
|
437
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INVALID_SHARES', 'Split configs differ'));
|
|
438
|
+
}
|
|
439
|
+
if (a.shares.length !== b.shares.length) {
|
|
440
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INVALID_SHARES', 'Share count differs'));
|
|
441
|
+
}
|
|
442
|
+
// Check if all corresponding share data XOR to zero
|
|
443
|
+
for (let i = 0; i < a.shares.length; i++) {
|
|
444
|
+
const aShare = a.shares[i];
|
|
445
|
+
const bShare = b.shares[i];
|
|
446
|
+
if (aShare.data.length !== bShare.data.length) {
|
|
447
|
+
return (0, shared_1.ok)(false);
|
|
448
|
+
}
|
|
449
|
+
for (let j = 0; j < aShare.data.length; j++) {
|
|
450
|
+
if ((aShare.data[j] ^ bShare.data[j]) !== 0) {
|
|
451
|
+
return (0, shared_1.ok)(false);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return (0, shared_1.ok)(true);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Extension 7: Network-Coded Sync
|
|
459
|
+
*
|
|
460
|
+
* Combine multiple shares during transit using XOR for bandwidth efficiency.
|
|
461
|
+
* The receiver can extract individual shares if they hold the complementary shares.
|
|
462
|
+
*
|
|
463
|
+
* coded = share_i ⊕ share_j
|
|
464
|
+
* share_i = coded ⊕ share_j (if receiver already holds share_j)
|
|
465
|
+
*
|
|
466
|
+
* @param shares - Array of shares to combine
|
|
467
|
+
* @returns Single coded block (XOR of all input shares)
|
|
468
|
+
*/
|
|
469
|
+
function networkCodeShares(shares) {
|
|
470
|
+
if (shares.length === 0) {
|
|
471
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('INSUFFICIENT_SHARES', 'No shares to code'));
|
|
472
|
+
}
|
|
473
|
+
const len = shares[0].data.length;
|
|
474
|
+
for (const s of shares) {
|
|
475
|
+
if (s.data.length !== len) {
|
|
476
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', 'Share data lengths differ'));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const coded = new Uint8Array(len);
|
|
480
|
+
for (const s of shares) {
|
|
481
|
+
for (let i = 0; i < len; i++) {
|
|
482
|
+
coded[i] ^= s.data[i];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return (0, shared_1.ok)(coded);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Decode a network-coded block using a held share.
|
|
489
|
+
*
|
|
490
|
+
* If coded = share_i ⊕ share_j, and you hold share_j:
|
|
491
|
+
* share_i = coded ⊕ share_j
|
|
492
|
+
*
|
|
493
|
+
* @param codedBlock - The network-coded block
|
|
494
|
+
* @param heldShare - The share the receiver already holds
|
|
495
|
+
* @returns The extracted share data
|
|
496
|
+
*/
|
|
497
|
+
function networkDecodeShare(codedBlock, heldShare) {
|
|
498
|
+
if (codedBlock.length !== heldShare.data.length) {
|
|
499
|
+
return (0, shared_1.err)((0, errors_js_1.continuityError)('DELTA_SIZE_MISMATCH', 'Coded block and share lengths differ'));
|
|
500
|
+
}
|
|
501
|
+
const decoded = new Uint8Array(codedBlock.length);
|
|
502
|
+
for (let i = 0; i < codedBlock.length; i++) {
|
|
503
|
+
decoded[i] = codedBlock[i] ^ heldShare.data[i];
|
|
504
|
+
}
|
|
505
|
+
return (0, shared_1.ok)(decoded);
|
|
506
|
+
}
|