@fairfox/polly 0.70.0 → 0.71.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/dist/src/mesh.js +407 -156
- package/dist/src/mesh.js.map +9 -8
- package/dist/src/peer.js +92 -5
- package/dist/src/peer.js.map +4 -4
- package/dist/src/shared/lib/mesh-client.d.ts +29 -0
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +31 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +91 -0
- package/dist/src/shared/lib/revocation-summary.d.ts +54 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +1 -1
- package/dist/tools/test/src/e2e-mesh/index.js +95 -1
- package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +29 -1
- package/package.json +2 -2
package/dist/src/mesh.js
CHANGED
|
@@ -975,12 +975,6 @@ function base64ToBytes(b64) {
|
|
|
975
975
|
// src/shared/lib/mesh-client.ts
|
|
976
976
|
import { Repo } from "@automerge/automerge-repo/slim";
|
|
977
977
|
|
|
978
|
-
// src/shared/lib/mesh-network-adapter.ts
|
|
979
|
-
init_encryption();
|
|
980
|
-
import {
|
|
981
|
-
NetworkAdapter
|
|
982
|
-
} from "@automerge/automerge-repo/slim";
|
|
983
|
-
|
|
984
978
|
// src/shared/lib/mesh-diagnostics.ts
|
|
985
979
|
var listeners = new Set;
|
|
986
980
|
function emitMeshDiagnostic(diagnostic) {
|
|
@@ -1005,6 +999,12 @@ function recordMeshDiagnostics() {
|
|
|
1005
999
|
return { events: captured, stop };
|
|
1006
1000
|
}
|
|
1007
1001
|
|
|
1002
|
+
// src/shared/lib/mesh-network-adapter.ts
|
|
1003
|
+
init_encryption();
|
|
1004
|
+
import {
|
|
1005
|
+
NetworkAdapter
|
|
1006
|
+
} from "@automerge/automerge-repo/slim";
|
|
1007
|
+
|
|
1008
1008
|
// src/shared/lib/signing.ts
|
|
1009
1009
|
import nacl2 from "tweetnacl";
|
|
1010
1010
|
var PUBLIC_KEY_BYTES = 32;
|
|
@@ -1090,11 +1090,17 @@ function decodeSignedEnvelope(bytes) {
|
|
|
1090
1090
|
|
|
1091
1091
|
// src/shared/lib/mesh-network-adapter.ts
|
|
1092
1092
|
var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
|
|
1093
|
+
var MESH_CONTROL_TYPE = {
|
|
1094
|
+
Sync: 0,
|
|
1095
|
+
Revocation: 1,
|
|
1096
|
+
RevocationSummary: 2
|
|
1097
|
+
};
|
|
1093
1098
|
|
|
1094
1099
|
class MeshNetworkAdapter extends NetworkAdapter {
|
|
1095
1100
|
base;
|
|
1096
1101
|
keyringSource;
|
|
1097
1102
|
encryptionEnabled;
|
|
1103
|
+
onControlMessage;
|
|
1098
1104
|
get keyring() {
|
|
1099
1105
|
return this.keyringSource();
|
|
1100
1106
|
}
|
|
@@ -1103,6 +1109,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1103
1109
|
this.base = options.base;
|
|
1104
1110
|
this.keyringSource = options.keyringSource;
|
|
1105
1111
|
this.encryptionEnabled = options.encryptionEnabled ?? true;
|
|
1112
|
+
this.onControlMessage = options.onControlMessage;
|
|
1106
1113
|
this.base.on("close", () => this.emit("close"));
|
|
1107
1114
|
this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
|
|
1108
1115
|
this.base.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
|
|
@@ -1136,16 +1143,17 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1136
1143
|
wrap(message) {
|
|
1137
1144
|
const keyring = this.keyringSource();
|
|
1138
1145
|
const serialised = serialiseMessage(message);
|
|
1146
|
+
const tagged = prependControlTag(MESH_CONTROL_TYPE.Sync, serialised);
|
|
1139
1147
|
let payloadToSign;
|
|
1140
1148
|
if (this.encryptionEnabled) {
|
|
1141
1149
|
const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
1142
1150
|
if (!docKey) {
|
|
1143
1151
|
throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
|
|
1144
1152
|
}
|
|
1145
|
-
const encrypted = sealEnvelope(
|
|
1153
|
+
const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
|
|
1146
1154
|
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
1147
1155
|
} else {
|
|
1148
|
-
payloadToSign =
|
|
1156
|
+
payloadToSign = tagged;
|
|
1149
1157
|
}
|
|
1150
1158
|
const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
|
|
1151
1159
|
const signedBytes = encodeSignedEnvelope(signed);
|
|
@@ -1201,7 +1209,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1201
1209
|
return;
|
|
1202
1210
|
}
|
|
1203
1211
|
if (!this.encryptionEnabled) {
|
|
1204
|
-
return
|
|
1212
|
+
return this.dispatchTaggedPayload(verifiedPayload, signed.senderId);
|
|
1205
1213
|
}
|
|
1206
1214
|
let encrypted;
|
|
1207
1215
|
try {
|
|
@@ -1235,9 +1243,88 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1235
1243
|
});
|
|
1236
1244
|
return;
|
|
1237
1245
|
}
|
|
1238
|
-
return
|
|
1246
|
+
return this.dispatchTaggedPayload(plaintext, signed.senderId);
|
|
1247
|
+
}
|
|
1248
|
+
dispatchTaggedPayload(payload, senderId) {
|
|
1249
|
+
if (payload.byteLength < 1) {
|
|
1250
|
+
emitMeshDiagnostic({ kind: "drop:empty-control-payload", senderId });
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const tag = payload[0];
|
|
1254
|
+
const body = payload.subarray(1);
|
|
1255
|
+
switch (tag) {
|
|
1256
|
+
case MESH_CONTROL_TYPE.Sync:
|
|
1257
|
+
return deserialiseMessage(body);
|
|
1258
|
+
case MESH_CONTROL_TYPE.Revocation:
|
|
1259
|
+
emitMeshDiagnostic({ kind: "ctrl:revocation-received", senderId });
|
|
1260
|
+
this.invokeControlHandler(tag, body, senderId);
|
|
1261
|
+
return;
|
|
1262
|
+
case MESH_CONTROL_TYPE.RevocationSummary:
|
|
1263
|
+
emitMeshDiagnostic({
|
|
1264
|
+
kind: "ctrl:revocation-summary-received",
|
|
1265
|
+
senderId
|
|
1266
|
+
});
|
|
1267
|
+
this.invokeControlHandler(tag, body, senderId);
|
|
1268
|
+
return;
|
|
1269
|
+
default:
|
|
1270
|
+
emitMeshDiagnostic({
|
|
1271
|
+
kind: "drop:unknown-control-type",
|
|
1272
|
+
senderId,
|
|
1273
|
+
tag
|
|
1274
|
+
});
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
invokeControlHandler(tag, body, senderId) {
|
|
1279
|
+
if (!this.onControlMessage)
|
|
1280
|
+
return;
|
|
1281
|
+
try {
|
|
1282
|
+
this.onControlMessage(tag, body, senderId);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
emitMeshDiagnostic({
|
|
1285
|
+
kind: "drop:control-handler-threw",
|
|
1286
|
+
senderId,
|
|
1287
|
+
tag,
|
|
1288
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
sendControlMessage(tag, body, targetPeerIds) {
|
|
1293
|
+
if (targetPeerIds.length === 0)
|
|
1294
|
+
return;
|
|
1295
|
+
const keyring = this.keyringSource();
|
|
1296
|
+
const tagged = prependControlTag(tag, body);
|
|
1297
|
+
let payloadToSign;
|
|
1298
|
+
if (this.encryptionEnabled) {
|
|
1299
|
+
const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
1300
|
+
if (!docKey) {
|
|
1301
|
+
throw new Error(`MeshNetworkAdapter.sendControlMessage: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}".`);
|
|
1302
|
+
}
|
|
1303
|
+
const encrypted = sealEnvelope(tagged, DEFAULT_MESH_KEY_ID, docKey);
|
|
1304
|
+
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
1305
|
+
} else {
|
|
1306
|
+
payloadToSign = tagged;
|
|
1307
|
+
}
|
|
1308
|
+
const senderId = this.peerId ?? "";
|
|
1309
|
+
const signed = signEnvelope(payloadToSign, senderId, keyring.identity.secretKey);
|
|
1310
|
+
const signedBytes = encodeSignedEnvelope(signed);
|
|
1311
|
+
for (const targetId of targetPeerIds) {
|
|
1312
|
+
const outer = {
|
|
1313
|
+
type: "sync",
|
|
1314
|
+
senderId,
|
|
1315
|
+
targetId,
|
|
1316
|
+
data: signedBytes
|
|
1317
|
+
};
|
|
1318
|
+
this.base.send(outer);
|
|
1319
|
+
}
|
|
1239
1320
|
}
|
|
1240
1321
|
}
|
|
1322
|
+
function prependControlTag(tag, body) {
|
|
1323
|
+
const out = new Uint8Array(body.byteLength + 1);
|
|
1324
|
+
out[0] = tag;
|
|
1325
|
+
out.set(body, 1);
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1241
1328
|
function serialiseMessage(message) {
|
|
1242
1329
|
const headerObj = {
|
|
1243
1330
|
type: message.type,
|
|
@@ -3353,6 +3440,193 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
3353
3440
|
}
|
|
3354
3441
|
}
|
|
3355
3442
|
|
|
3443
|
+
// src/shared/lib/revocation.ts
|
|
3444
|
+
var REVOCATION_RECORD_VERSION = 1;
|
|
3445
|
+
var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
|
|
3446
|
+
|
|
3447
|
+
class RevocationError extends Error {
|
|
3448
|
+
code;
|
|
3449
|
+
constructor(message, code) {
|
|
3450
|
+
super(message);
|
|
3451
|
+
this.name = "RevocationError";
|
|
3452
|
+
this.code = code;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function createRevocation(options) {
|
|
3456
|
+
const now = options.now ? options.now() : Date.now();
|
|
3457
|
+
return {
|
|
3458
|
+
version: REVOCATION_RECORD_VERSION,
|
|
3459
|
+
issuerPeerId: options.issuerPeerId,
|
|
3460
|
+
revokedPeerId: options.revokedPeerId,
|
|
3461
|
+
issuedAt: now,
|
|
3462
|
+
...options.reason === undefined ? {} : { reason: options.reason }
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
function applyRevocation(record, keyring) {
|
|
3466
|
+
keyring.revokedPeers.add(record.revokedPeerId);
|
|
3467
|
+
}
|
|
3468
|
+
function revokePeerLocally(peerId, keyring) {
|
|
3469
|
+
keyring.revokedPeers.add(peerId);
|
|
3470
|
+
}
|
|
3471
|
+
function serialiseRevocationPayload(record) {
|
|
3472
|
+
const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
|
|
3473
|
+
const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
|
|
3474
|
+
const reasonBytes = new TextEncoder().encode(record.reason ?? "");
|
|
3475
|
+
const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
|
|
3476
|
+
const out = new Uint8Array(total);
|
|
3477
|
+
let offset = 0;
|
|
3478
|
+
out.set(REVOCATION_MAGIC, offset);
|
|
3479
|
+
offset += REVOCATION_MAGIC.length;
|
|
3480
|
+
out[offset] = record.version;
|
|
3481
|
+
offset += 1;
|
|
3482
|
+
const view = new DataView(out.buffer);
|
|
3483
|
+
view.setUint32(offset, issuerBytes.length, false);
|
|
3484
|
+
offset += 4;
|
|
3485
|
+
out.set(issuerBytes, offset);
|
|
3486
|
+
offset += issuerBytes.length;
|
|
3487
|
+
view.setUint32(offset, revokedBytes.length, false);
|
|
3488
|
+
offset += 4;
|
|
3489
|
+
out.set(revokedBytes, offset);
|
|
3490
|
+
offset += revokedBytes.length;
|
|
3491
|
+
view.setBigUint64(offset, BigInt(record.issuedAt), false);
|
|
3492
|
+
offset += 8;
|
|
3493
|
+
view.setUint32(offset, reasonBytes.length, false);
|
|
3494
|
+
offset += 4;
|
|
3495
|
+
out.set(reasonBytes, offset);
|
|
3496
|
+
return out;
|
|
3497
|
+
}
|
|
3498
|
+
function parseRevocationPayload(bytes) {
|
|
3499
|
+
let offset = 0;
|
|
3500
|
+
if (bytes.length < REVOCATION_MAGIC.length) {
|
|
3501
|
+
throw new RevocationError("Revocation record too short for magic.", "truncated");
|
|
3502
|
+
}
|
|
3503
|
+
for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
|
|
3504
|
+
if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
|
|
3505
|
+
throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
offset += REVOCATION_MAGIC.length;
|
|
3509
|
+
if (bytes.length < offset + 1) {
|
|
3510
|
+
throw new RevocationError("Revocation record truncated at version.", "truncated");
|
|
3511
|
+
}
|
|
3512
|
+
const version = bytes[offset];
|
|
3513
|
+
offset += 1;
|
|
3514
|
+
if (version !== REVOCATION_RECORD_VERSION) {
|
|
3515
|
+
throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
|
|
3516
|
+
}
|
|
3517
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3518
|
+
if (bytes.length < offset + 4) {
|
|
3519
|
+
throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
|
|
3520
|
+
}
|
|
3521
|
+
const issuerLen = view.getUint32(offset, false);
|
|
3522
|
+
offset += 4;
|
|
3523
|
+
if (bytes.length < offset + issuerLen) {
|
|
3524
|
+
throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
|
|
3525
|
+
}
|
|
3526
|
+
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3527
|
+
offset += issuerLen;
|
|
3528
|
+
if (bytes.length < offset + 4) {
|
|
3529
|
+
throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
|
|
3530
|
+
}
|
|
3531
|
+
const revokedLen = view.getUint32(offset, false);
|
|
3532
|
+
offset += 4;
|
|
3533
|
+
if (bytes.length < offset + revokedLen) {
|
|
3534
|
+
throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
|
|
3535
|
+
}
|
|
3536
|
+
const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
|
|
3537
|
+
offset += revokedLen;
|
|
3538
|
+
if (bytes.length < offset + 8) {
|
|
3539
|
+
throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
|
|
3540
|
+
}
|
|
3541
|
+
const issuedAt = Number(view.getBigUint64(offset, false));
|
|
3542
|
+
offset += 8;
|
|
3543
|
+
if (bytes.length < offset + 4) {
|
|
3544
|
+
throw new RevocationError("Revocation record truncated at reason length.", "truncated");
|
|
3545
|
+
}
|
|
3546
|
+
const reasonLen = view.getUint32(offset, false);
|
|
3547
|
+
offset += 4;
|
|
3548
|
+
if (bytes.length < offset + reasonLen) {
|
|
3549
|
+
throw new RevocationError("Revocation record truncated at reason.", "truncated");
|
|
3550
|
+
}
|
|
3551
|
+
const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
|
|
3552
|
+
offset += reasonLen;
|
|
3553
|
+
return {
|
|
3554
|
+
version,
|
|
3555
|
+
issuerPeerId,
|
|
3556
|
+
revokedPeerId,
|
|
3557
|
+
issuedAt,
|
|
3558
|
+
...reason ? { reason } : {}
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
function encodeRevocation(record, issuer) {
|
|
3562
|
+
const payload = serialiseRevocationPayload(record);
|
|
3563
|
+
const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
|
|
3564
|
+
return encodeSignedEnvelope(envelope);
|
|
3565
|
+
}
|
|
3566
|
+
function decodeRevocation(bytes, keyring) {
|
|
3567
|
+
const envelope = decodeSignedEnvelope(bytes);
|
|
3568
|
+
const issuerKey = keyring.knownPeers.get(envelope.senderId);
|
|
3569
|
+
if (!issuerKey) {
|
|
3570
|
+
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
|
|
3571
|
+
}
|
|
3572
|
+
if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
|
|
3573
|
+
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
|
|
3574
|
+
}
|
|
3575
|
+
let payloadBytes;
|
|
3576
|
+
try {
|
|
3577
|
+
payloadBytes = openEnvelope2(envelope, issuerKey);
|
|
3578
|
+
} catch {
|
|
3579
|
+
throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
|
|
3580
|
+
}
|
|
3581
|
+
const record = parseRevocationPayload(payloadBytes);
|
|
3582
|
+
if (record.issuerPeerId !== envelope.senderId) {
|
|
3583
|
+
throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
|
|
3584
|
+
}
|
|
3585
|
+
return record;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// src/shared/lib/revocation-summary.ts
|
|
3589
|
+
function encodeRevocationSummary(entries) {
|
|
3590
|
+
const sorted = [...entries].sort(compareSummaryEntries);
|
|
3591
|
+
const json = JSON.stringify(sorted.map((entry) => ({
|
|
3592
|
+
r: entry.revokedPeerId,
|
|
3593
|
+
i: entry.issuerPeerId,
|
|
3594
|
+
t: entry.issuedAt
|
|
3595
|
+
})));
|
|
3596
|
+
return new TextEncoder().encode(json);
|
|
3597
|
+
}
|
|
3598
|
+
function decodeRevocationSummary(bytes) {
|
|
3599
|
+
const text = new TextDecoder().decode(bytes);
|
|
3600
|
+
let raw;
|
|
3601
|
+
try {
|
|
3602
|
+
raw = JSON.parse(text);
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
throw new Error(`decodeRevocationSummary: invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
3605
|
+
}
|
|
3606
|
+
if (!Array.isArray(raw)) {
|
|
3607
|
+
throw new Error("decodeRevocationSummary: expected an array");
|
|
3608
|
+
}
|
|
3609
|
+
return raw.map((item, index) => {
|
|
3610
|
+
if (!item || typeof item !== "object") {
|
|
3611
|
+
throw new Error(`decodeRevocationSummary: entry ${index} is not an object`);
|
|
3612
|
+
}
|
|
3613
|
+
const e = item;
|
|
3614
|
+
if (typeof e.r !== "string" || typeof e.i !== "string" || typeof e.t !== "number") {
|
|
3615
|
+
throw new Error(`decodeRevocationSummary: entry ${index} has missing or wrong-typed fields`);
|
|
3616
|
+
}
|
|
3617
|
+
return { revokedPeerId: e.r, issuerPeerId: e.i, issuedAt: e.t };
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
function compareSummaryEntries(a, b) {
|
|
3621
|
+
if (a.revokedPeerId !== b.revokedPeerId) {
|
|
3622
|
+
return a.revokedPeerId < b.revokedPeerId ? -1 : 1;
|
|
3623
|
+
}
|
|
3624
|
+
if (a.issuerPeerId !== b.issuerPeerId) {
|
|
3625
|
+
return a.issuerPeerId < b.issuerPeerId ? -1 : 1;
|
|
3626
|
+
}
|
|
3627
|
+
return a.issuedAt - b.issuedAt;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3356
3630
|
// src/shared/lib/mesh-client.ts
|
|
3357
3631
|
async function resolveIceServers(rtc) {
|
|
3358
3632
|
if (rtc?.iceCredentialResolver) {
|
|
@@ -3539,10 +3813,107 @@ async function createMeshClient(options) {
|
|
|
3539
3813
|
});
|
|
3540
3814
|
webrtcAdapterOptions.signaling = signaling;
|
|
3541
3815
|
webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
|
|
3816
|
+
const connectedPeerIds = new Set;
|
|
3817
|
+
let selfRevocation;
|
|
3818
|
+
const localPeerId = options.signaling.peerId;
|
|
3819
|
+
const revocationStore = new Map;
|
|
3820
|
+
function storeRevocation(record, bytes) {
|
|
3821
|
+
const existing = revocationStore.get(record.revokedPeerId);
|
|
3822
|
+
if (existing && existing.entry.issuedAt >= record.issuedAt)
|
|
3823
|
+
return;
|
|
3824
|
+
revocationStore.set(record.revokedPeerId, {
|
|
3825
|
+
bytes,
|
|
3826
|
+
entry: {
|
|
3827
|
+
revokedPeerId: record.revokedPeerId,
|
|
3828
|
+
issuerPeerId: record.issuerPeerId,
|
|
3829
|
+
issuedAt: record.issuedAt
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
const handleRevocationControl = (body, senderId) => {
|
|
3834
|
+
const keyring2 = keyringSource();
|
|
3835
|
+
let record;
|
|
3836
|
+
try {
|
|
3837
|
+
record = decodeRevocation(body, keyring2);
|
|
3838
|
+
} catch (err) {
|
|
3839
|
+
const reason = err instanceof RevocationError ? err.code : err instanceof Error ? err.message : String(err);
|
|
3840
|
+
emitMeshDiagnostic({ kind: "revoke:rejected", senderId, reason });
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (record.revokedPeerId === localPeerId) {
|
|
3844
|
+
selfRevocation = record;
|
|
3845
|
+
emitMeshDiagnostic({
|
|
3846
|
+
kind: "revoke:self-targeted",
|
|
3847
|
+
issuerId: record.issuerPeerId,
|
|
3848
|
+
...record.reason !== undefined && { reason: record.reason },
|
|
3849
|
+
issuedAt: record.issuedAt
|
|
3850
|
+
});
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
if (keyring2.revokedPeers.has(record.revokedPeerId)) {
|
|
3854
|
+
storeRevocation(record, body);
|
|
3855
|
+
emitMeshDiagnostic({
|
|
3856
|
+
kind: "revoke:duplicate",
|
|
3857
|
+
revokedPeerId: record.revokedPeerId,
|
|
3858
|
+
issuerId: record.issuerPeerId
|
|
3859
|
+
});
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
applyRevocation(record, keyring2);
|
|
3863
|
+
storeRevocation(record, body);
|
|
3864
|
+
emitMeshDiagnostic({ kind: "revoke:applied", revokedPeerId: record.revokedPeerId });
|
|
3865
|
+
};
|
|
3866
|
+
const handleRevocationSummary = (body, senderId) => {
|
|
3867
|
+
let summary;
|
|
3868
|
+
try {
|
|
3869
|
+
summary = decodeRevocationSummary(body);
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
emitMeshDiagnostic({
|
|
3872
|
+
kind: "revoke:rejected",
|
|
3873
|
+
senderId,
|
|
3874
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3875
|
+
});
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
const remoteKeys = new Set(summary.map((entry) => entry.revokedPeerId));
|
|
3879
|
+
const senderPeerId = senderId;
|
|
3880
|
+
for (const stored of revocationStore.values()) {
|
|
3881
|
+
if (remoteKeys.has(stored.entry.revokedPeerId))
|
|
3882
|
+
continue;
|
|
3883
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.Revocation, stored.bytes, [senderPeerId]);
|
|
3884
|
+
}
|
|
3885
|
+
};
|
|
3542
3886
|
const networkAdapter = new MeshNetworkAdapter({
|
|
3543
3887
|
base: webrtcAdapter,
|
|
3544
3888
|
keyringSource,
|
|
3545
|
-
encryptionEnabled
|
|
3889
|
+
encryptionEnabled,
|
|
3890
|
+
onControlMessage: (tag, body, senderId) => {
|
|
3891
|
+
if (tag === MESH_CONTROL_TYPE.Revocation) {
|
|
3892
|
+
handleRevocationControl(body, senderId);
|
|
3893
|
+
} else if (tag === MESH_CONTROL_TYPE.RevocationSummary) {
|
|
3894
|
+
handleRevocationSummary(body, senderId);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
function sendSummaryTo(peerId) {
|
|
3899
|
+
const entries = [...revocationStore.values()].map((stored) => stored.entry);
|
|
3900
|
+
const body = encodeRevocationSummary(entries);
|
|
3901
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.RevocationSummary, body, [peerId]);
|
|
3902
|
+
}
|
|
3903
|
+
networkAdapter.on("peer-candidate", (event) => {
|
|
3904
|
+
connectedPeerIds.add(event.peerId);
|
|
3905
|
+
try {
|
|
3906
|
+
sendSummaryTo(event.peerId);
|
|
3907
|
+
} catch (err) {
|
|
3908
|
+
emitMeshDiagnostic({
|
|
3909
|
+
kind: "revoke:rejected",
|
|
3910
|
+
senderId: event.peerId,
|
|
3911
|
+
reason: `summary-send-failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
networkAdapter.on("peer-disconnected", (event) => {
|
|
3916
|
+
connectedPeerIds.delete(event.peerId);
|
|
3546
3917
|
});
|
|
3547
3918
|
const repo = new Repo({
|
|
3548
3919
|
network: [networkAdapter],
|
|
@@ -3609,6 +3980,30 @@ async function createMeshClient(options) {
|
|
|
3609
3980
|
return;
|
|
3610
3981
|
await webrtcAdapter.refreshAllTransportStats();
|
|
3611
3982
|
},
|
|
3983
|
+
revokePeer: async (targetPeerId, reason) => {
|
|
3984
|
+
const keyring2 = keyringSource();
|
|
3985
|
+
const record = createRevocation({
|
|
3986
|
+
issuer: keyring2.identity,
|
|
3987
|
+
issuerPeerId: localPeerId,
|
|
3988
|
+
revokedPeerId: targetPeerId,
|
|
3989
|
+
...reason !== undefined && { reason }
|
|
3990
|
+
});
|
|
3991
|
+
const bytes = encodeRevocation(record, keyring2.identity);
|
|
3992
|
+
applyRevocation(record, keyring2);
|
|
3993
|
+
storeRevocation(record, bytes);
|
|
3994
|
+
emitMeshDiagnostic({
|
|
3995
|
+
kind: "revoke:issued",
|
|
3996
|
+
revokedPeerId: targetPeerId,
|
|
3997
|
+
issuerId: localPeerId
|
|
3998
|
+
});
|
|
3999
|
+
const targets = [...connectedPeerIds];
|
|
4000
|
+
if (targets.length > 0) {
|
|
4001
|
+
networkAdapter.sendControlMessage(MESH_CONTROL_TYPE.Revocation, bytes, targets);
|
|
4002
|
+
}
|
|
4003
|
+
},
|
|
4004
|
+
get selfRevocation() {
|
|
4005
|
+
return selfRevocation;
|
|
4006
|
+
},
|
|
3612
4007
|
close: async () => {
|
|
3613
4008
|
signaling.close();
|
|
3614
4009
|
webrtcAdapter?.disconnect();
|
|
@@ -3819,150 +4214,6 @@ function randomBytes(n) {
|
|
|
3819
4214
|
crypto.getRandomValues(out);
|
|
3820
4215
|
return out;
|
|
3821
4216
|
}
|
|
3822
|
-
// src/shared/lib/revocation.ts
|
|
3823
|
-
var REVOCATION_RECORD_VERSION = 1;
|
|
3824
|
-
var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
|
|
3825
|
-
|
|
3826
|
-
class RevocationError extends Error {
|
|
3827
|
-
code;
|
|
3828
|
-
constructor(message, code) {
|
|
3829
|
-
super(message);
|
|
3830
|
-
this.name = "RevocationError";
|
|
3831
|
-
this.code = code;
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
function createRevocation(options) {
|
|
3835
|
-
const now = options.now ? options.now() : Date.now();
|
|
3836
|
-
return {
|
|
3837
|
-
version: REVOCATION_RECORD_VERSION,
|
|
3838
|
-
issuerPeerId: options.issuerPeerId,
|
|
3839
|
-
revokedPeerId: options.revokedPeerId,
|
|
3840
|
-
issuedAt: now,
|
|
3841
|
-
...options.reason === undefined ? {} : { reason: options.reason }
|
|
3842
|
-
};
|
|
3843
|
-
}
|
|
3844
|
-
function applyRevocation(record, keyring) {
|
|
3845
|
-
keyring.revokedPeers.add(record.revokedPeerId);
|
|
3846
|
-
}
|
|
3847
|
-
function revokePeerLocally(peerId, keyring) {
|
|
3848
|
-
keyring.revokedPeers.add(peerId);
|
|
3849
|
-
}
|
|
3850
|
-
function serialiseRevocationPayload(record) {
|
|
3851
|
-
const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
|
|
3852
|
-
const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
|
|
3853
|
-
const reasonBytes = new TextEncoder().encode(record.reason ?? "");
|
|
3854
|
-
const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
|
|
3855
|
-
const out = new Uint8Array(total);
|
|
3856
|
-
let offset = 0;
|
|
3857
|
-
out.set(REVOCATION_MAGIC, offset);
|
|
3858
|
-
offset += REVOCATION_MAGIC.length;
|
|
3859
|
-
out[offset] = record.version;
|
|
3860
|
-
offset += 1;
|
|
3861
|
-
const view = new DataView(out.buffer);
|
|
3862
|
-
view.setUint32(offset, issuerBytes.length, false);
|
|
3863
|
-
offset += 4;
|
|
3864
|
-
out.set(issuerBytes, offset);
|
|
3865
|
-
offset += issuerBytes.length;
|
|
3866
|
-
view.setUint32(offset, revokedBytes.length, false);
|
|
3867
|
-
offset += 4;
|
|
3868
|
-
out.set(revokedBytes, offset);
|
|
3869
|
-
offset += revokedBytes.length;
|
|
3870
|
-
view.setBigUint64(offset, BigInt(record.issuedAt), false);
|
|
3871
|
-
offset += 8;
|
|
3872
|
-
view.setUint32(offset, reasonBytes.length, false);
|
|
3873
|
-
offset += 4;
|
|
3874
|
-
out.set(reasonBytes, offset);
|
|
3875
|
-
return out;
|
|
3876
|
-
}
|
|
3877
|
-
function parseRevocationPayload(bytes) {
|
|
3878
|
-
let offset = 0;
|
|
3879
|
-
if (bytes.length < REVOCATION_MAGIC.length) {
|
|
3880
|
-
throw new RevocationError("Revocation record too short for magic.", "truncated");
|
|
3881
|
-
}
|
|
3882
|
-
for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
|
|
3883
|
-
if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
|
|
3884
|
-
throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
offset += REVOCATION_MAGIC.length;
|
|
3888
|
-
if (bytes.length < offset + 1) {
|
|
3889
|
-
throw new RevocationError("Revocation record truncated at version.", "truncated");
|
|
3890
|
-
}
|
|
3891
|
-
const version = bytes[offset];
|
|
3892
|
-
offset += 1;
|
|
3893
|
-
if (version !== REVOCATION_RECORD_VERSION) {
|
|
3894
|
-
throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
|
|
3895
|
-
}
|
|
3896
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3897
|
-
if (bytes.length < offset + 4) {
|
|
3898
|
-
throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
|
|
3899
|
-
}
|
|
3900
|
-
const issuerLen = view.getUint32(offset, false);
|
|
3901
|
-
offset += 4;
|
|
3902
|
-
if (bytes.length < offset + issuerLen) {
|
|
3903
|
-
throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
|
|
3904
|
-
}
|
|
3905
|
-
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3906
|
-
offset += issuerLen;
|
|
3907
|
-
if (bytes.length < offset + 4) {
|
|
3908
|
-
throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
|
|
3909
|
-
}
|
|
3910
|
-
const revokedLen = view.getUint32(offset, false);
|
|
3911
|
-
offset += 4;
|
|
3912
|
-
if (bytes.length < offset + revokedLen) {
|
|
3913
|
-
throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
|
|
3914
|
-
}
|
|
3915
|
-
const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
|
|
3916
|
-
offset += revokedLen;
|
|
3917
|
-
if (bytes.length < offset + 8) {
|
|
3918
|
-
throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
|
|
3919
|
-
}
|
|
3920
|
-
const issuedAt = Number(view.getBigUint64(offset, false));
|
|
3921
|
-
offset += 8;
|
|
3922
|
-
if (bytes.length < offset + 4) {
|
|
3923
|
-
throw new RevocationError("Revocation record truncated at reason length.", "truncated");
|
|
3924
|
-
}
|
|
3925
|
-
const reasonLen = view.getUint32(offset, false);
|
|
3926
|
-
offset += 4;
|
|
3927
|
-
if (bytes.length < offset + reasonLen) {
|
|
3928
|
-
throw new RevocationError("Revocation record truncated at reason.", "truncated");
|
|
3929
|
-
}
|
|
3930
|
-
const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
|
|
3931
|
-
offset += reasonLen;
|
|
3932
|
-
return {
|
|
3933
|
-
version,
|
|
3934
|
-
issuerPeerId,
|
|
3935
|
-
revokedPeerId,
|
|
3936
|
-
issuedAt,
|
|
3937
|
-
...reason ? { reason } : {}
|
|
3938
|
-
};
|
|
3939
|
-
}
|
|
3940
|
-
function encodeRevocation(record, issuer) {
|
|
3941
|
-
const payload = serialiseRevocationPayload(record);
|
|
3942
|
-
const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
|
|
3943
|
-
return encodeSignedEnvelope(envelope);
|
|
3944
|
-
}
|
|
3945
|
-
function decodeRevocation(bytes, keyring) {
|
|
3946
|
-
const envelope = decodeSignedEnvelope(bytes);
|
|
3947
|
-
const issuerKey = keyring.knownPeers.get(envelope.senderId);
|
|
3948
|
-
if (!issuerKey) {
|
|
3949
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
|
|
3950
|
-
}
|
|
3951
|
-
if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
|
|
3952
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
|
|
3953
|
-
}
|
|
3954
|
-
let payloadBytes;
|
|
3955
|
-
try {
|
|
3956
|
-
payloadBytes = openEnvelope2(envelope, issuerKey);
|
|
3957
|
-
} catch {
|
|
3958
|
-
throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
|
|
3959
|
-
}
|
|
3960
|
-
const record = parseRevocationPayload(payloadBytes);
|
|
3961
|
-
if (record.issuerPeerId !== envelope.senderId) {
|
|
3962
|
-
throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
|
|
3963
|
-
}
|
|
3964
|
-
return record;
|
|
3965
|
-
}
|
|
3966
4217
|
export {
|
|
3967
4218
|
wasMeshStateResolved,
|
|
3968
4219
|
verify,
|
|
@@ -4039,4 +4290,4 @@ export {
|
|
|
4039
4290
|
$meshCounter
|
|
4040
4291
|
};
|
|
4041
4292
|
|
|
4042
|
-
//# debugId=
|
|
4293
|
+
//# debugId=E57D210FDE704BCC64756E2164756E21
|