@derec-alliance/nodejs 0.0.1-alpha.6 → 0.0.1-alpha.8
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/README.md +401 -71
- package/derec_library.d.ts +369 -221
- package/derec_library.js +1376 -400
- package/derec_library_bg.wasm +0 -0
- package/derec_library_bg.wasm.d.ts +77 -11
- package/index.d.ts +1214 -0
- package/index.js +106 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -53,68 +53,210 @@ These represent **opaque wire-level protocol messages**.
|
|
|
53
53
|
## Quick Example
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
import
|
|
56
|
+
import { primitives } from "@derec-alliance/nodejs";
|
|
57
57
|
|
|
58
|
-
const
|
|
58
|
+
const channelId = 1n; // u64 → bigint
|
|
59
|
+
const secretId = 42n; // u64 → bigint
|
|
60
|
+
const version = 1; // u32 → number
|
|
61
|
+
const sharedKey = new Uint8Array(32); // established during pairing
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
const result = primitives.verification.request.produce(channelId, secretId, version, sharedKey);
|
|
64
|
+
// result carries the encoded DeRecMessage envelope, ready to send over transport.
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
---
|
|
64
68
|
|
|
65
69
|
## Pairing Flow
|
|
66
70
|
|
|
71
|
+
The `ContactMessage` is exchanged out-of-band (QR codes, existing messaging
|
|
72
|
+
channels, etc.). Two `ContactMode` values select how the public encryption
|
|
73
|
+
material is delivered:
|
|
74
|
+
|
|
75
|
+
| Mode | What the contact carries | Use when |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `InlineKeys` (default) | Full ML-KEM encapsulation key + ECIES public key | Out-of-band channel can carry the keys (NFC, messaging). |
|
|
78
|
+
| `HashedKeys` | Only a SHA-384 commitment to the keys | Channel is size-constrained (QR codes). Scanner fetches the actual keys via a plaintext `PrePair` round-trip and verifies them against the hash. |
|
|
79
|
+
|
|
80
|
+
After the handshake completes, **both modes** rekey the channel id. The
|
|
81
|
+
responder derives `SHA-384(u64_be(originalId) || sharedKey)[..8]` as a
|
|
82
|
+
`bigint`, includes it in the encrypted `PairResponseMessage`, and both sides
|
|
83
|
+
switch their local state to the new id. The new id never appears in plaintext
|
|
84
|
+
on the wire, so a passive observer who only saw pre-rekey traffic cannot link
|
|
85
|
+
the long-running channel to its pairing-time id.
|
|
86
|
+
|
|
87
|
+
### `InlineKeys` flow
|
|
88
|
+
|
|
67
89
|
```ts
|
|
68
|
-
import
|
|
90
|
+
import { ContactMode, primitives, SenderKind } from "@derec-alliance/nodejs";
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
const channelId = 1n;
|
|
93
|
+
|
|
94
|
+
// Step 1: Initiator creates the out-of-band ContactMessage.
|
|
95
|
+
const contact = primitives.pairing.request.create_contact(
|
|
96
|
+
channelId,
|
|
97
|
+
ContactMode.InlineKeys,
|
|
98
|
+
{ protocol: 0, uri: "https://owner.example.com" },
|
|
74
99
|
);
|
|
75
100
|
|
|
76
|
-
// Step 2:
|
|
77
|
-
const request =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
contact.
|
|
101
|
+
// Step 2: Responder produces a pairing request from the contact.
|
|
102
|
+
const request = primitives.pairing.request.produce(
|
|
103
|
+
SenderKind.Helper,
|
|
104
|
+
{ protocol: 0, uri: "https://helper.example.com" },
|
|
105
|
+
contact.contact_message,
|
|
106
|
+
null, // optional CommunicationInfo
|
|
81
107
|
);
|
|
82
108
|
|
|
83
|
-
// Step 3:
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
// Step 3: Initiator extracts the request and produces the response.
|
|
110
|
+
const { request: pairRequest } =
|
|
111
|
+
primitives.pairing.request.extract(request.envelope, contact.secret_key);
|
|
112
|
+
const produced = primitives.pairing.response.produce(
|
|
113
|
+
channelId,
|
|
114
|
+
pairRequest,
|
|
115
|
+
contact.secret_key,
|
|
116
|
+
null,
|
|
88
117
|
);
|
|
89
118
|
|
|
90
|
-
// Step 4:
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
request.
|
|
119
|
+
// Step 4: Responder extracts and processes the response.
|
|
120
|
+
const { response: pairResponse } =
|
|
121
|
+
primitives.pairing.response.extract(produced.envelope, request.secret_key);
|
|
122
|
+
const processed = primitives.pairing.response.process(
|
|
123
|
+
request.initiator_contact_message,
|
|
124
|
+
pairResponse,
|
|
125
|
+
request.secret_key,
|
|
95
126
|
);
|
|
96
127
|
|
|
97
|
-
|
|
128
|
+
// Both sides hold the same shared key and rekeyed channel id.
|
|
129
|
+
// produced.shared_key == processed.shared_key
|
|
130
|
+
// produced.channel_id == processed.channel_id !== channelId
|
|
131
|
+
//
|
|
132
|
+
// Rename local channel state from `channelId` to `produced.channel_id`
|
|
133
|
+
// before sending any further traffic.
|
|
98
134
|
```
|
|
99
135
|
|
|
100
|
-
|
|
136
|
+
To reject the request, build a `PairResponseMessage` with a non-OK status and
|
|
137
|
+
encrypt it against `request.ecies_public_key` using the WASM-exposed pairing
|
|
138
|
+
envelope helpers. The higher-level `DeRecProtocol` orchestrator's `reject`
|
|
139
|
+
method does this for you. Rejected responses do not carry a meaningful
|
|
140
|
+
`channel_id` — the rekey only takes effect on `Ok` responses.
|
|
101
141
|
|
|
102
|
-
|
|
142
|
+
### `HashedKeys` flow (PrePair)
|
|
143
|
+
|
|
144
|
+
`HashedKeys` adds one plaintext round-trip before the regular `InlineKeys`
|
|
145
|
+
handshake. The scanner fetches the actual keys via `PrePair`, verifies them
|
|
146
|
+
against `contact.contact_binding_hash`, and then runs the normal pairing
|
|
147
|
+
flow on a synthesized contact with the keys filled in.
|
|
103
148
|
|
|
104
149
|
```ts
|
|
105
|
-
import
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
import { ContactMode, primitives, SenderKind, type ContactMessage } from "@derec-alliance/nodejs";
|
|
151
|
+
|
|
152
|
+
const channelId = 7n;
|
|
153
|
+
|
|
154
|
+
// Initiator: HASHED_KEYS contact (no inline keys, only the binding hash).
|
|
155
|
+
// Transport URI MUST be ephemeral — PrePair envelopes are plaintext.
|
|
156
|
+
const contact = primitives.pairing.request.create_contact(
|
|
157
|
+
channelId,
|
|
158
|
+
ContactMode.HashedKeys,
|
|
159
|
+
{ protocol: 0, uri: "https://relay.example.com/ephemeral" },
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Scanner: fetch keys via PrePair.
|
|
163
|
+
const prePairReqEnv = primitives.pairing.request.produce_pre_pair(
|
|
164
|
+
{ protocol: 0, uri: "https://scanner.example.com/ephemeral" },
|
|
165
|
+
contact.contact_message,
|
|
113
166
|
);
|
|
167
|
+
const { request: prePairReq } =
|
|
168
|
+
primitives.pairing.request.extract_pre_pair(prePairReqEnv.envelope);
|
|
169
|
+
const prePairRespEnv = primitives.pairing.response.produce_pre_pair(
|
|
170
|
+
channelId, prePairReq, contact.secret_key,
|
|
171
|
+
);
|
|
172
|
+
const { response: prePairResp } =
|
|
173
|
+
primitives.pairing.response.extract_pre_pair(prePairRespEnv.envelope);
|
|
174
|
+
|
|
175
|
+
// Scanner validates the published keys against contact.contact_binding_hash.
|
|
176
|
+
// Throws on mismatch (returns the keys + echoed nonce on match).
|
|
177
|
+
const validated = primitives.pairing.response.process_pre_pair(
|
|
178
|
+
contact.contact_message, prePairResp,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Synthesize a "filled-in" contact and run the regular pairing flow. The
|
|
182
|
+
// mode flip is required — `primitives.pairing.request.produce` enforces
|
|
183
|
+
// `InlineKeys` and rejects a contact that still advertises `HashedKeys`.
|
|
184
|
+
const { contact_binding_hash: _omitBindingHash, ...contactBase } =
|
|
185
|
+
contact.contact_message;
|
|
186
|
+
const filledInContact: ContactMessage = {
|
|
187
|
+
...contactBase,
|
|
188
|
+
contact_mode: ContactMode.InlineKeys,
|
|
189
|
+
mlkem_encapsulation_key: validated.mlkem_encapsulation_key,
|
|
190
|
+
ecies_public_key: validated.ecies_public_key,
|
|
191
|
+
};
|
|
192
|
+
// ... continue with primitives.pairing.request.produce / extract /
|
|
193
|
+
// primitives.pairing.response.produce / process against `filledInContact`
|
|
194
|
+
// exactly as in the InlineKeys example.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
After the PrePair exchange the application **must** swap the transport
|
|
198
|
+
endpoint to a long-term one via `UpdateChannelInfo`. The ephemeral endpoint
|
|
199
|
+
advertised in the `HashedKeys` contact is intended to be retired immediately
|
|
200
|
+
after pairing.
|
|
201
|
+
|
|
202
|
+
#### Using `DeRecProtocol` instead
|
|
203
|
+
|
|
204
|
+
The orchestrator handles the whole chain automatically:
|
|
205
|
+
|
|
206
|
+
- **Contact creator** — `protocol.createContact(channelId, ContactMode.HashedKeys)`
|
|
207
|
+
returns the small contact (binding hash only). When the scanner's
|
|
208
|
+
`PrePairRequest` arrives, `protocol.process(bytes)` emits an
|
|
209
|
+
`ActionRequired` event with `action_kind: "PrePair"`. Call
|
|
210
|
+
`protocol.accept(action)` to publish the keys (the library builds the
|
|
211
|
+
response and routes it), or `protocol.reject(action, status, memo)` to
|
|
212
|
+
refuse.
|
|
213
|
+
- **Scanner** — `protocol.start(FlowKind.Pairing, { kind, contact })` kicks
|
|
214
|
+
off the plaintext PrePair leg. `start()` returns a `DeRecEvent[]`
|
|
215
|
+
containing one `PairingStarted { channel_id, kind }` event that
|
|
216
|
+
describes the dispatched handshake; the scanner auto-proceeds to
|
|
217
|
+
`PairRequest`, and the application sees `PairingCompleted` only when
|
|
218
|
+
the final response lands via `process()`. Failure modes:
|
|
219
|
+
- Contact creator rejected → `DeRecEvent` with
|
|
220
|
+
`type: "PrePairRejected"`, plus `status` / `memo`.
|
|
221
|
+
- Binding-hash mismatch → `protocol.process(...)` throws a
|
|
222
|
+
`DeRecException`-shape error whose `message` carries
|
|
223
|
+
`"contact binding hash mismatch"`. This is security-relevant — the
|
|
224
|
+
keys published by the peer do not match the commitment the scanner
|
|
225
|
+
originally accepted.
|
|
226
|
+
|
|
227
|
+
End-to-end orchestrator-level coverage is in
|
|
228
|
+
`bindings/nodejs/protocol.ts::runHashedKeysPairingFlow` (happy path +
|
|
229
|
+
tampered-hash assertion).
|
|
230
|
+
|
|
231
|
+
---
|
|
114
232
|
|
|
115
|
-
|
|
233
|
+
## Share Distribution (Sharing Flow)
|
|
116
234
|
|
|
117
|
-
|
|
235
|
+
```ts
|
|
236
|
+
import { primitives } from "@derec-alliance/nodejs";
|
|
237
|
+
|
|
238
|
+
const secretId = 42n; // u64
|
|
239
|
+
const secretData = new TextEncoder().encode("super-secret");
|
|
240
|
+
const channelIds = [1n, 2n, 3n];
|
|
241
|
+
const threshold = 2; // must be 2 <= threshold <= channelIds.length
|
|
242
|
+
const version = 1;
|
|
243
|
+
// sharedKeys: Map<bigint, Uint8Array> with the 32-byte channel keys
|
|
244
|
+
|
|
245
|
+
const splitResult = primitives.sharing.request.split(
|
|
246
|
+
secretId,
|
|
247
|
+
secretData,
|
|
248
|
+
channelIds,
|
|
249
|
+
threshold,
|
|
250
|
+
version,
|
|
251
|
+
);
|
|
252
|
+
// splitResult.value: Map<bigint, Uint8Array> — one CommittedDeRecShare per helper.
|
|
253
|
+
|
|
254
|
+
// Wrap each share into an encrypted delivery envelope.
|
|
255
|
+
for (const [channelId, committedShare] of splitResult.value) {
|
|
256
|
+
const envelope = primitives.sharing.request.produce(
|
|
257
|
+
channelId, version, secretId, committedShare, [], "", sharedKeys.get(channelId)!,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
118
260
|
```
|
|
119
261
|
|
|
120
262
|
---
|
|
@@ -122,54 +264,86 @@ console.log(shareMessages);
|
|
|
122
264
|
## Recovery Flow
|
|
123
265
|
|
|
124
266
|
```ts
|
|
125
|
-
import
|
|
267
|
+
import { primitives } from "@derec-alliance/nodejs";
|
|
268
|
+
|
|
269
|
+
const secretId = 42n; // u64
|
|
270
|
+
const version = 1; // u32
|
|
126
271
|
|
|
127
|
-
// Owner
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
272
|
+
// Owner side: produce the recovery request.
|
|
273
|
+
const shareRequest = primitives.recovery.request.produce(
|
|
274
|
+
1n, // channel ID
|
|
275
|
+
secretId,
|
|
276
|
+
version,
|
|
277
|
+
sharedKey,
|
|
131
278
|
);
|
|
132
279
|
|
|
133
|
-
// Helper
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
280
|
+
// Helper side: produce the response using the StoreShareRequest it persisted
|
|
281
|
+
// at sharing time.
|
|
282
|
+
const shareResponse = primitives.recovery.response.produce(
|
|
283
|
+
secretId,
|
|
284
|
+
1n, // channel ID
|
|
285
|
+
storedShareEnvelope,
|
|
286
|
+
shareRequest,
|
|
287
|
+
sharedKey,
|
|
137
288
|
);
|
|
138
289
|
|
|
139
|
-
// Owner
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
290
|
+
// Owner side: collect at least `threshold` responses and reconstruct.
|
|
291
|
+
const recovered = primitives.recovery.response.recover(
|
|
292
|
+
[
|
|
293
|
+
{ response: shareResponse, shared_key: sharedKey },
|
|
294
|
+
// …additional helper responses…
|
|
295
|
+
],
|
|
296
|
+
secretId,
|
|
297
|
+
version,
|
|
144
298
|
);
|
|
299
|
+
// `recovered` is a Uint8Array carrying the reconstructed secret payload.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
When driving the protocol layer instead of the primitives, the recovering
|
|
303
|
+
device receives a `SecretRecovered` event carrying the typed `secret`. Pass it
|
|
304
|
+
to `protocol.restore(secret, version)` on a fresh `DeRecProtocol` instance to
|
|
305
|
+
commit canonical helper / replica state and wipe the throwaway recovery-mode
|
|
306
|
+
channels — at that point the device resumes normal operation as if the secret
|
|
307
|
+
had been protected here originally.
|
|
145
308
|
|
|
146
|
-
|
|
309
|
+
```ts
|
|
310
|
+
const events = await protocol.process(responseBytes);
|
|
311
|
+
for (const ev of events) {
|
|
312
|
+
if (ev.type === "SecretRecovered") {
|
|
313
|
+
await freshProtocol.restore(ev.secret, recoveredVersion);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
147
316
|
```
|
|
148
317
|
|
|
318
|
+
Errors surface as objects with a `code` field — `ALREADY_RESTORED`,
|
|
319
|
+
`CONFLICT` (with `channel_ids`), `INVARIANT`, or `STORAGE`.
|
|
320
|
+
|
|
149
321
|
---
|
|
150
322
|
|
|
151
323
|
## Verification Flow
|
|
152
324
|
|
|
153
325
|
```ts
|
|
154
|
-
import
|
|
155
|
-
|
|
156
|
-
// Owner
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
326
|
+
import { primitives } from "@derec-alliance/nodejs";
|
|
327
|
+
|
|
328
|
+
// Owner side: produce the verification request.
|
|
329
|
+
const requestEnvelope = primitives.verification.request.produce(channelId, secretId, version, sharedKey);
|
|
330
|
+
|
|
331
|
+
// Helper side: decrypt and extract the challenge fields.
|
|
332
|
+
const req = primitives.verification.request.extract(requestEnvelope, sharedKey);
|
|
333
|
+
// req.channel_id, req.secret_id, req.version, req.nonce
|
|
334
|
+
|
|
335
|
+
// Helper side: produce the response.
|
|
336
|
+
const responseEnvelope = primitives.verification.response.produce(
|
|
337
|
+
channelId,
|
|
338
|
+
req.secret_id,
|
|
339
|
+
req.version,
|
|
340
|
+
req.nonce,
|
|
341
|
+
sharedKey,
|
|
342
|
+
storedShareEnvelope
|
|
166
343
|
);
|
|
167
344
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
request,
|
|
171
|
-
response
|
|
172
|
-
);
|
|
345
|
+
// Owner side: verify the response.
|
|
346
|
+
const isValid = primitives.verification.response.process(responseEnvelope, sharedKey, storedShareEnvelope);
|
|
173
347
|
|
|
174
348
|
console.log("Valid:", isValid);
|
|
175
349
|
```
|
|
@@ -185,23 +359,179 @@ console.log("Valid:", isValid);
|
|
|
185
359
|
|
|
186
360
|
---
|
|
187
361
|
|
|
362
|
+
## Replica flows
|
|
363
|
+
|
|
364
|
+
Replicas mirror an Owner's secret onto a second device so the same secrets
|
|
365
|
+
remain reachable after device loss. Pairings are **unidirectional** — one
|
|
366
|
+
side runs as `SenderKind.ReplicaSource` (owns the secret), the other as
|
|
367
|
+
`SenderKind.ReplicaDestination` (receives it). Both must be constructed
|
|
368
|
+
with a stable `replicaId`:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
const owner = new DeRecProtocol(
|
|
372
|
+
channelStore, shareStore, secretStore, transport,
|
|
373
|
+
"https://owner.example.com", "https",
|
|
374
|
+
/* threshold */ 2, /* keepVersionsCount */ 3,
|
|
375
|
+
{ name: "Owner" },
|
|
376
|
+
null, null, null, null,
|
|
377
|
+
/* replicaId */ 0xAAAA_AAAA_AAAA_AAAAn,
|
|
378
|
+
);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
A typical Source↔Destination handshake:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const contact = await owner.createContact(channelId, ContactMode.InlineKeys);
|
|
385
|
+
await destination.start(FlowKind.Pairing, {
|
|
386
|
+
kind: SenderKind.ReplicaDestination,
|
|
387
|
+
contact,
|
|
388
|
+
});
|
|
389
|
+
// pump messages between the two protocols (drain transport → process)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The channel ends up in `Pending` and is NOT eligible as a
|
|
393
|
+
`ProtectSecret` target until both sides confirm a deterministic
|
|
394
|
+
fingerprint derived from the shared key:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
const localFp = await owner.getFingerprint(channelId);
|
|
398
|
+
const peerFp = await destination.getFingerprint(channelId); // out of band
|
|
399
|
+
|
|
400
|
+
await owner.verifyFingerprint(channelId, peerFp); // → true
|
|
401
|
+
await destination.verifyFingerprint(channelId, localFp); // → true
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Once paired, the Source includes the Destination as a `ProtectSecret`
|
|
405
|
+
target alongside helpers. Helpers receive the usual VSS share via
|
|
406
|
+
`StoreShareRequest`; the Destination receives the full secret as a
|
|
407
|
+
typed `ReplicaSecretReceived` event:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
{
|
|
411
|
+
type: "ReplicaSecretReceived",
|
|
412
|
+
channel_id, from_replica_id, secret_id, version,
|
|
413
|
+
secret: {
|
|
414
|
+
helpers: [...], // every paired helper (channel_id, transport_uri, shared_key, ...)
|
|
415
|
+
secrets: [{ id, name, data }],
|
|
416
|
+
replicas: [...], // every paired destination (replica_id, sender_kind, ...)
|
|
417
|
+
owner_replica_id, // the Source's replica_id
|
|
418
|
+
},
|
|
419
|
+
shares: [{ channel_id, committed_share }, ...], // helper channel_id → share bytes
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
`secret` + `shares` give the Destination everything it needs to act in the
|
|
424
|
+
Source's place during recovery.
|
|
425
|
+
|
|
426
|
+
End-to-end coverage lives in
|
|
427
|
+
[`runReplicaPairingAndSecretSyncFlow`](../../bindings/nodejs/protocol.ts).
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Correlation and routing
|
|
432
|
+
|
|
433
|
+
Two cross-cutting metadata fields appear on every channel-mode exchange:
|
|
434
|
+
|
|
435
|
+
- **`traceId`** — opaque `bigint` on the outer envelope, used to correlate
|
|
436
|
+
responses with requests. The `DeRecProtocol` orchestrator handles this
|
|
437
|
+
end-to-end (random token on every outbound request, echo on every
|
|
438
|
+
response). Primitive-only callers can manipulate it directly via
|
|
439
|
+
`envelope.apply_trace_id(bytes, traceId)` and `envelope.read_trace_id(bytes)`.
|
|
440
|
+
- **`replyTo`** — optional `TransportProtocol` on request bodies, telling
|
|
441
|
+
the responder to route this exchange's response to an alternate endpoint.
|
|
442
|
+
Set it per call (every `primitives.*.request.produce` takes a trailing
|
|
443
|
+
`reply_to` arg) or protocol-wide with the `autoReplyTo` constructor flag
|
|
444
|
+
on `DeRecProtocol` (stamps `replyTo = ownTransport` on every outbound
|
|
445
|
+
request). Excludes pairing and `UpdateChannelInfo`, which already carry
|
|
446
|
+
their own `transportProtocol` field.
|
|
447
|
+
|
|
448
|
+
The motivating case for `replyTo` is replicas: when Replica A sends a
|
|
449
|
+
request on a channel the helper paired with sibling Replica B, the
|
|
450
|
+
helper's stored peer endpoint points at B. `replyTo` lets A say "send the
|
|
451
|
+
response back to me," without rewriting channel state.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
188
455
|
## Package Contents
|
|
189
456
|
|
|
190
457
|
```text
|
|
191
458
|
derec_library_bg.wasm
|
|
192
459
|
derec_library.js
|
|
193
460
|
derec_library.d.ts
|
|
461
|
+
index.js
|
|
462
|
+
index.d.ts
|
|
194
463
|
```
|
|
195
464
|
|
|
196
465
|
- `.wasm` — compiled Rust core
|
|
197
|
-
-
|
|
198
|
-
-
|
|
466
|
+
- `derec_library.js` / `derec_library.d.ts` — raw wasm-bindgen bindings
|
|
467
|
+
- `index.js` / `index.d.ts` — `primitives.*` namespace assembly and TypeScript declarations
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Security considerations
|
|
472
|
+
|
|
473
|
+
### Replica destinations inherit Source trust
|
|
474
|
+
|
|
475
|
+
`ReplicaSecretReceived.secret` carries the full secret, which
|
|
476
|
+
embeds every helper's `channel_id` and `shared_key`. Anyone holding the
|
|
477
|
+
secret can therefore authenticate as the Source toward every helper.
|
|
478
|
+
This is intentional — it is what makes Destination-driven recovery
|
|
479
|
+
work — but it means a compromised Destination can impersonate the
|
|
480
|
+
Source against every helper paired at the time the secret was sent.
|
|
481
|
+
Pick Destinations with at least the trust level of the Source device
|
|
482
|
+
itself; do not treat them as opaque backups.
|
|
483
|
+
|
|
484
|
+
All replicas of one `secret_id` also share a single **group channel
|
|
485
|
+
key**: every replica channel's `SharedKey` entry in the secret store
|
|
486
|
+
holds the same 32 bytes, established at the first replica pair and
|
|
487
|
+
handed to every subsequent joiner via the
|
|
488
|
+
`ReplicaSecretPayload.shared_key` field on its first sync round.
|
|
489
|
+
Compromise of any one Destination therefore exposes that single key;
|
|
490
|
+
the protocol does not provide per-pair forward secrecy across replicas.
|
|
491
|
+
|
|
492
|
+
### `ContactMode.HashedKeys` requires an ephemeral transport URI
|
|
493
|
+
|
|
494
|
+
`HashedKeys` ships only a SHA-384 binding hash in the contact and
|
|
495
|
+
serves the actual public keys through a plaintext PrePair round-trip
|
|
496
|
+
on the contact creator's own transport. Any party that can reach that
|
|
497
|
+
URI before the legitimate scanner gets the keys. Use `HashedKeys` only
|
|
498
|
+
with a transport endpoint that is freshly minted for the pairing and
|
|
499
|
+
that you can retire as soon as the PrePair leg completes.
|
|
500
|
+
`ContactMode.InlineKeys` has no such constraint.
|
|
501
|
+
|
|
502
|
+
The recommended pattern is: pair on the ephemeral URI, then — as soon
|
|
503
|
+
as the pairing completes on the contact creator side — call
|
|
504
|
+
`setOwnTransport` with the permanent endpoint and start an
|
|
505
|
+
`UpdateChannelInfo` flow against the peer to announce the swap. Once
|
|
506
|
+
the peer acknowledges, retire the ephemeral URI. This keeps the
|
|
507
|
+
plaintext PrePair window tight while letting subsequent traffic ride
|
|
508
|
+
on the long-lived endpoint.
|
|
509
|
+
|
|
510
|
+
### Replica fingerprint verification is mandatory
|
|
511
|
+
|
|
512
|
+
Replica channels are created with `status: "Pending"` and remain there
|
|
513
|
+
until both sides call `verifyFingerprint` with the value the peer
|
|
514
|
+
derived from the shared key — confirmed out of band. The orchestrator
|
|
515
|
+
enforces this: `start(FlowKind.ProtectSecret, ...)` throws when a
|
|
516
|
+
target is still `Pending`. Treat verification as a required step in
|
|
517
|
+
the pairing UX — a scanner that auto-pairs without it accepts a
|
|
518
|
+
MITM-vulnerable replica.
|
|
519
|
+
|
|
520
|
+
### The `derec.*` namespace in `communicationInfo` is library-owned
|
|
521
|
+
|
|
522
|
+
`communicationInfo` is otherwise an opaque app-defined map, but every
|
|
523
|
+
key under the `derec.` prefix is reserved for the protocol. Today the
|
|
524
|
+
library owns `derec.replica_id`; future protocol additions will use
|
|
525
|
+
the same namespace. Application code must not write any `derec.*`
|
|
526
|
+
entry — the orchestrator silently overwrites or strips library-owned
|
|
527
|
+
keys at the protocol boundary, and app-set values are lost without
|
|
528
|
+
warning.
|
|
199
529
|
|
|
200
530
|
---
|
|
201
531
|
|
|
202
532
|
## Documentation
|
|
203
533
|
|
|
204
|
-
- DeRec Alliance: https://
|
|
534
|
+
- DeRec Alliance: https://derec.org
|
|
205
535
|
- Protocol specification: https://derec-alliance.gitbook.io/docs/protocol-specification/protocol-overview
|
|
206
536
|
- Rust SDK: https://github.com/derecalliance/lib-derec
|
|
207
537
|
|