@voidly/agent-sdk 2.2.0 → 3.0.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/index.d.mts +112 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +478 -70
- package/dist/index.mjs +478 -70
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4007,6 +4007,8 @@ var FLAG_PADDED = 1;
|
|
|
4007
4007
|
var FLAG_SEALED = 2;
|
|
4008
4008
|
var FLAG_RATCHET = 4;
|
|
4009
4009
|
var FLAG_PQ = 8;
|
|
4010
|
+
var FLAG_DH_RATCHET = 16;
|
|
4011
|
+
var FLAG_DENIABLE = 32;
|
|
4010
4012
|
function makeProtoHeader(flags, ratchetStep2) {
|
|
4011
4013
|
return new Uint8Array([PROTO_MARKER, flags, ratchetStep2 >> 8 & 255, ratchetStep2 & 255]);
|
|
4012
4014
|
}
|
|
@@ -4018,6 +4020,30 @@ function parseProtoHeader(data) {
|
|
|
4018
4020
|
content: data.slice(4)
|
|
4019
4021
|
};
|
|
4020
4022
|
}
|
|
4023
|
+
async function kdfRK(rootKey, dhOutput) {
|
|
4024
|
+
const combined = new Uint8Array(rootKey.length + dhOutput.length);
|
|
4025
|
+
combined.set(rootKey, 0);
|
|
4026
|
+
combined.set(dhOutput, rootKey.length);
|
|
4027
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
4028
|
+
const prk2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", combined.buffer));
|
|
4029
|
+
const newRootKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array([...prk2, 1]).buffer));
|
|
4030
|
+
const newChainKey2 = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array([...prk2, 2]).buffer));
|
|
4031
|
+
return { newRootKey: newRootKey2, newChainKey: newChainKey2 };
|
|
4032
|
+
}
|
|
4033
|
+
const { createHash } = await import("crypto");
|
|
4034
|
+
const prk = new Uint8Array(createHash("sha256").update(Buffer.from(combined)).digest());
|
|
4035
|
+
const newRootKey = new Uint8Array(createHash("sha256").update(Buffer.from([...prk, 1])).digest());
|
|
4036
|
+
const newChainKey = new Uint8Array(createHash("sha256").update(Buffer.from([...prk, 2])).digest());
|
|
4037
|
+
return { newRootKey, newChainKey };
|
|
4038
|
+
}
|
|
4039
|
+
async function hmacSha256(key, data) {
|
|
4040
|
+
if (typeof globalThis.crypto?.subtle !== "undefined") {
|
|
4041
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey("raw", key.buffer, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
4042
|
+
return new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", cryptoKey, data.buffer));
|
|
4043
|
+
}
|
|
4044
|
+
const { createHmac } = await import("crypto");
|
|
4045
|
+
return new Uint8Array(createHmac("sha256", Buffer.from(key)).update(Buffer.from(data)).digest());
|
|
4046
|
+
}
|
|
4021
4047
|
var MAX_SKIP = 200;
|
|
4022
4048
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4023
4049
|
function toBase58(bytes) {
|
|
@@ -4037,6 +4063,8 @@ function toBase58(bytes) {
|
|
|
4037
4063
|
}
|
|
4038
4064
|
var VoidlyAgent = class _VoidlyAgent {
|
|
4039
4065
|
constructor(identity, config) {
|
|
4066
|
+
this._signedPrekey = null;
|
|
4067
|
+
this._signedPrekeyId = 0;
|
|
4040
4068
|
this._pinnedDids = /* @__PURE__ */ new Set();
|
|
4041
4069
|
this._listeners = /* @__PURE__ */ new Set();
|
|
4042
4070
|
this._conversations = /* @__PURE__ */ new Map();
|
|
@@ -4058,6 +4086,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4058
4086
|
this.requireSignatures = config?.requireSignatures || false;
|
|
4059
4087
|
this.timeout = config?.timeout ?? 3e4;
|
|
4060
4088
|
this.postQuantum = config?.postQuantum !== false;
|
|
4089
|
+
this.deniable = config?.deniable || false;
|
|
4090
|
+
this.doubleRatchet = config?.doubleRatchet !== false;
|
|
4091
|
+
this.jitterMs = config?.jitterMs || 0;
|
|
4092
|
+
this.longPoll = config?.longPoll !== false;
|
|
4061
4093
|
this.mlkemPublicKey = identity.mlkemPublicKey || null;
|
|
4062
4094
|
this.mlkemSecretKey = identity.mlkemSecretKey || null;
|
|
4063
4095
|
}
|
|
@@ -4077,11 +4109,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4077
4109
|
const kem = new MlKem768();
|
|
4078
4110
|
[mlkemPk, mlkemSk] = await kem.generateKeyPair();
|
|
4079
4111
|
}
|
|
4112
|
+
const signedPrekeyPair = import_tweetnacl.default.box.keyPair();
|
|
4113
|
+
const signedPrekeyId = 1;
|
|
4114
|
+
const prekeySignature = import_tweetnacl.default.sign.detached(signedPrekeyPair.publicKey, signingKeyPair.secretKey);
|
|
4080
4115
|
const regBody = {
|
|
4081
4116
|
name: options.name,
|
|
4082
4117
|
capabilities: options.capabilities,
|
|
4083
4118
|
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(signingKeyPair.publicKey),
|
|
4084
|
-
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(encryptionKeyPair.publicKey)
|
|
4119
|
+
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(encryptionKeyPair.publicKey),
|
|
4120
|
+
// X3DH signed prekey
|
|
4121
|
+
signed_prekey_public: (0, import_tweetnacl_util.encodeBase64)(signedPrekeyPair.publicKey),
|
|
4122
|
+
signed_prekey_signature: (0, import_tweetnacl_util.encodeBase64)(prekeySignature),
|
|
4123
|
+
signed_prekey_id: signedPrekeyId
|
|
4085
4124
|
};
|
|
4086
4125
|
if (mlkemPk) {
|
|
4087
4126
|
regBody.mlkem_public_key = (0, import_tweetnacl_util.encodeBase64)(mlkemPk);
|
|
@@ -4097,7 +4136,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4097
4136
|
throw new Error(`Registration failed: ${errMsg}`);
|
|
4098
4137
|
}
|
|
4099
4138
|
const data = await res.json();
|
|
4100
|
-
|
|
4139
|
+
const agent = new _VoidlyAgent({
|
|
4101
4140
|
did: data.did,
|
|
4102
4141
|
apiKey: data.api_key,
|
|
4103
4142
|
signingKeyPair,
|
|
@@ -4105,6 +4144,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4105
4144
|
mlkemPublicKey: mlkemPk,
|
|
4106
4145
|
mlkemSecretKey: mlkemSk
|
|
4107
4146
|
}, config);
|
|
4147
|
+
agent._signedPrekey = signedPrekeyPair;
|
|
4148
|
+
agent._signedPrekeyId = signedPrekeyId;
|
|
4149
|
+
return agent;
|
|
4108
4150
|
}
|
|
4109
4151
|
/**
|
|
4110
4152
|
* Restore an agent from saved credentials.
|
|
@@ -4164,17 +4206,55 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4164
4206
|
const sendChainKey = (0, import_tweetnacl_util.decodeBase64)(rs.sendChainKey);
|
|
4165
4207
|
const recvChainKey = (0, import_tweetnacl_util.decodeBase64)(rs.recvChainKey);
|
|
4166
4208
|
if (sendChainKey.length !== 32 || recvChainKey.length !== 32) continue;
|
|
4167
|
-
|
|
4209
|
+
const state = {
|
|
4168
4210
|
sendChainKey,
|
|
4169
4211
|
sendStep: rs.sendStep || 0,
|
|
4170
4212
|
recvChainKey,
|
|
4171
4213
|
recvStep: rs.recvStep || 0,
|
|
4172
4214
|
skippedKeys: /* @__PURE__ */ new Map()
|
|
4173
|
-
}
|
|
4215
|
+
};
|
|
4216
|
+
if (rs.rootKey) {
|
|
4217
|
+
try {
|
|
4218
|
+
state.rootKey = (0, import_tweetnacl_util.decodeBase64)(rs.rootKey);
|
|
4219
|
+
if (state.rootKey.length !== 32) state.rootKey = void 0;
|
|
4220
|
+
} catch {
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
if (rs.dhSendSecretKey && rs.dhSendPublicKey) {
|
|
4224
|
+
try {
|
|
4225
|
+
const sk = (0, import_tweetnacl_util.decodeBase64)(rs.dhSendSecretKey);
|
|
4226
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(rs.dhSendPublicKey);
|
|
4227
|
+
if (sk.length === 32 && pk.length === 32) {
|
|
4228
|
+
state.dhSendKeyPair = { publicKey: pk, secretKey: sk };
|
|
4229
|
+
}
|
|
4230
|
+
} catch {
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
if (rs.dhRecvPubKey) {
|
|
4234
|
+
try {
|
|
4235
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(rs.dhRecvPubKey);
|
|
4236
|
+
if (pk.length === 32) state.dhRecvPubKey = pk;
|
|
4237
|
+
} catch {
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
if (rs.prevSendStep !== void 0) state.prevSendStep = rs.prevSendStep;
|
|
4241
|
+
state.dhSkippedKeys = /* @__PURE__ */ new Map();
|
|
4242
|
+
agent._ratchetStates.set(pairId, state);
|
|
4174
4243
|
} catch {
|
|
4175
4244
|
}
|
|
4176
4245
|
}
|
|
4177
4246
|
}
|
|
4247
|
+
if (creds.signedPrekeySecret && creds.signedPrekeyPublic) {
|
|
4248
|
+
try {
|
|
4249
|
+
const sk = (0, import_tweetnacl_util.decodeBase64)(creds.signedPrekeySecret);
|
|
4250
|
+
const pk = (0, import_tweetnacl_util.decodeBase64)(creds.signedPrekeyPublic);
|
|
4251
|
+
if (sk.length === 32 && pk.length === 32) {
|
|
4252
|
+
agent._signedPrekey = { publicKey: pk, secretKey: sk };
|
|
4253
|
+
agent._signedPrekeyId = creds.signedPrekeyId || 0;
|
|
4254
|
+
}
|
|
4255
|
+
} catch {
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4178
4258
|
return agent;
|
|
4179
4259
|
}
|
|
4180
4260
|
/**
|
|
@@ -4184,12 +4264,20 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4184
4264
|
exportCredentials() {
|
|
4185
4265
|
const ratchetStates = {};
|
|
4186
4266
|
for (const [pairId, state] of this._ratchetStates) {
|
|
4187
|
-
|
|
4267
|
+
const rs = {
|
|
4188
4268
|
sendChainKey: (0, import_tweetnacl_util.encodeBase64)(state.sendChainKey),
|
|
4189
4269
|
sendStep: state.sendStep,
|
|
4190
4270
|
recvChainKey: (0, import_tweetnacl_util.encodeBase64)(state.recvChainKey),
|
|
4191
4271
|
recvStep: state.recvStep
|
|
4192
4272
|
};
|
|
4273
|
+
if (state.rootKey) rs.rootKey = (0, import_tweetnacl_util.encodeBase64)(state.rootKey);
|
|
4274
|
+
if (state.dhSendKeyPair) {
|
|
4275
|
+
rs.dhSendSecretKey = (0, import_tweetnacl_util.encodeBase64)(state.dhSendKeyPair.secretKey);
|
|
4276
|
+
rs.dhSendPublicKey = (0, import_tweetnacl_util.encodeBase64)(state.dhSendKeyPair.publicKey);
|
|
4277
|
+
}
|
|
4278
|
+
if (state.dhRecvPubKey) rs.dhRecvPubKey = (0, import_tweetnacl_util.encodeBase64)(state.dhRecvPubKey);
|
|
4279
|
+
if (state.prevSendStep !== void 0) rs.prevSendStep = state.prevSendStep;
|
|
4280
|
+
ratchetStates[pairId] = rs;
|
|
4193
4281
|
}
|
|
4194
4282
|
return {
|
|
4195
4283
|
did: this.did,
|
|
@@ -4200,7 +4288,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4200
4288
|
encryptionPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.encryptionKeyPair.publicKey),
|
|
4201
4289
|
...Object.keys(ratchetStates).length > 0 ? { ratchetStates } : {},
|
|
4202
4290
|
...this.mlkemPublicKey ? { mlkemPublicKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemPublicKey) } : {},
|
|
4203
|
-
...this.mlkemSecretKey ? { mlkemSecretKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemSecretKey) } : {}
|
|
4291
|
+
...this.mlkemSecretKey ? { mlkemSecretKey: (0, import_tweetnacl_util.encodeBase64)(this.mlkemSecretKey) } : {},
|
|
4292
|
+
...this._signedPrekey ? {
|
|
4293
|
+
signedPrekeySecret: (0, import_tweetnacl_util.encodeBase64)(this._signedPrekey.secretKey),
|
|
4294
|
+
signedPrekeyPublic: (0, import_tweetnacl_util.encodeBase64)(this._signedPrekey.publicKey),
|
|
4295
|
+
signedPrekeyId: this._signedPrekeyId
|
|
4296
|
+
} : {}
|
|
4204
4297
|
};
|
|
4205
4298
|
}
|
|
4206
4299
|
/**
|
|
@@ -4261,9 +4354,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4261
4354
|
const pairId = `${this.did}:${recipientDid}`;
|
|
4262
4355
|
let state = this._ratchetStates.get(pairId);
|
|
4263
4356
|
let pqCiphertext = null;
|
|
4357
|
+
let dhRatchetPub = null;
|
|
4264
4358
|
if (!state) {
|
|
4265
4359
|
const x25519Shared = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
4266
|
-
let
|
|
4360
|
+
let initialKey;
|
|
4267
4361
|
if (this.postQuantum && profile.mlkem_public_key) {
|
|
4268
4362
|
try {
|
|
4269
4363
|
const recipientPqPk = (0, import_tweetnacl_util.decodeBase64)(profile.mlkem_public_key);
|
|
@@ -4273,22 +4367,44 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4273
4367
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4274
4368
|
combined.set(x25519Shared, 0);
|
|
4275
4369
|
combined.set(pqShared, x25519Shared.length);
|
|
4276
|
-
|
|
4370
|
+
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4277
4371
|
} catch {
|
|
4278
|
-
|
|
4372
|
+
initialKey = x25519Shared;
|
|
4279
4373
|
}
|
|
4280
4374
|
} else {
|
|
4281
|
-
|
|
4375
|
+
initialKey = x25519Shared;
|
|
4376
|
+
}
|
|
4377
|
+
if (this.doubleRatchet) {
|
|
4378
|
+
const dhSendKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4379
|
+
const dhOutput = import_tweetnacl.default.box.before(recipientPubKey, dhSendKeyPair.secretKey);
|
|
4380
|
+
const { newRootKey, newChainKey } = await kdfRK(initialKey, dhOutput);
|
|
4381
|
+
dhRatchetPub = dhSendKeyPair.publicKey;
|
|
4382
|
+
state = {
|
|
4383
|
+
sendChainKey: newChainKey,
|
|
4384
|
+
sendStep: 0,
|
|
4385
|
+
recvChainKey: initialKey,
|
|
4386
|
+
// Will be updated on first receive
|
|
4387
|
+
recvStep: 0,
|
|
4388
|
+
skippedKeys: /* @__PURE__ */ new Map(),
|
|
4389
|
+
// Double Ratchet state
|
|
4390
|
+
rootKey: newRootKey,
|
|
4391
|
+
dhSendKeyPair,
|
|
4392
|
+
dhRecvPubKey: void 0,
|
|
4393
|
+
prevSendStep: 0,
|
|
4394
|
+
dhSkippedKeys: /* @__PURE__ */ new Map()
|
|
4395
|
+
};
|
|
4396
|
+
} else {
|
|
4397
|
+
state = {
|
|
4398
|
+
sendChainKey: initialKey,
|
|
4399
|
+
sendStep: 0,
|
|
4400
|
+
recvChainKey: initialKey,
|
|
4401
|
+
recvStep: 0,
|
|
4402
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4403
|
+
};
|
|
4282
4404
|
}
|
|
4283
|
-
state = {
|
|
4284
|
-
sendChainKey: chainKey,
|
|
4285
|
-
sendStep: 0,
|
|
4286
|
-
recvChainKey: chainKey,
|
|
4287
|
-
// Will be synced on first receive
|
|
4288
|
-
recvStep: 0,
|
|
4289
|
-
skippedKeys: /* @__PURE__ */ new Map()
|
|
4290
|
-
};
|
|
4291
4405
|
this._ratchetStates.set(pairId, state);
|
|
4406
|
+
} else if (state.rootKey && state.dhSendKeyPair) {
|
|
4407
|
+
dhRatchetPub = state.dhSendKeyPair.publicKey;
|
|
4292
4408
|
}
|
|
4293
4409
|
const { nextChainKey, messageKey } = await ratchetStep(state.sendChainKey);
|
|
4294
4410
|
state.sendChainKey = nextChainKey;
|
|
@@ -4298,6 +4414,8 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4298
4414
|
if (usePadding) flags |= FLAG_PADDED;
|
|
4299
4415
|
if (useSealed) flags |= FLAG_SEALED;
|
|
4300
4416
|
if (pqCiphertext) flags |= FLAG_PQ;
|
|
4417
|
+
if (dhRatchetPub) flags |= FLAG_DH_RATCHET;
|
|
4418
|
+
if (this.deniable) flags |= FLAG_DENIABLE;
|
|
4301
4419
|
const header = makeProtoHeader(flags, currentStep);
|
|
4302
4420
|
const messageBytes = new Uint8Array(header.length + contentBytes.length);
|
|
4303
4421
|
messageBytes.set(header, 0);
|
|
@@ -4307,6 +4425,10 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4307
4425
|
if (!ciphertext) {
|
|
4308
4426
|
throw new Error("Encryption failed");
|
|
4309
4427
|
}
|
|
4428
|
+
if (this.jitterMs > 0) {
|
|
4429
|
+
const jitter = Math.random() * this.jitterMs;
|
|
4430
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
4431
|
+
}
|
|
4310
4432
|
const envelopeObj = {
|
|
4311
4433
|
from: this.did,
|
|
4312
4434
|
to: recipientDid,
|
|
@@ -4318,8 +4440,18 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4318
4440
|
if (pqCiphertext) {
|
|
4319
4441
|
envelopeObj.pq_ciphertext = (0, import_tweetnacl_util.encodeBase64)(pqCiphertext);
|
|
4320
4442
|
}
|
|
4443
|
+
if (dhRatchetPub) {
|
|
4444
|
+
envelopeObj.dh_ratchet_key = (0, import_tweetnacl_util.encodeBase64)(dhRatchetPub);
|
|
4445
|
+
envelopeObj.pn = state.prevSendStep || 0;
|
|
4446
|
+
}
|
|
4321
4447
|
const envelopeData = JSON.stringify(envelopeObj);
|
|
4322
|
-
|
|
4448
|
+
let signature;
|
|
4449
|
+
if (this.deniable) {
|
|
4450
|
+
const sharedSecret = import_tweetnacl.default.box.before(recipientPubKey, this.encryptionKeyPair.secretKey);
|
|
4451
|
+
signature = await hmacSha256(sharedSecret, (0, import_tweetnacl_util.decodeUTF8)(envelopeData));
|
|
4452
|
+
} else {
|
|
4453
|
+
signature = import_tweetnacl.default.sign.detached((0, import_tweetnacl_util.decodeUTF8)(envelopeData), this.signingKeyPair.secretKey);
|
|
4454
|
+
}
|
|
4323
4455
|
const payload = {
|
|
4324
4456
|
to: recipientDid,
|
|
4325
4457
|
ciphertext: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
@@ -4400,6 +4532,9 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4400
4532
|
let rawPlaintext = null;
|
|
4401
4533
|
let envelopeRatchetStep = 0;
|
|
4402
4534
|
let envelopePqCiphertext = null;
|
|
4535
|
+
let envelopeDhRatchetKey = null;
|
|
4536
|
+
let envelopePn = 0;
|
|
4537
|
+
let envelopeDeniable = false;
|
|
4403
4538
|
if (msg.envelope) {
|
|
4404
4539
|
try {
|
|
4405
4540
|
const env = JSON.parse(msg.envelope);
|
|
@@ -4409,6 +4544,12 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4409
4544
|
if (typeof env.pq_ciphertext === "string") {
|
|
4410
4545
|
envelopePqCiphertext = env.pq_ciphertext;
|
|
4411
4546
|
}
|
|
4547
|
+
if (typeof env.dh_ratchet_key === "string") {
|
|
4548
|
+
envelopeDhRatchetKey = env.dh_ratchet_key;
|
|
4549
|
+
}
|
|
4550
|
+
if (typeof env.pn === "number") {
|
|
4551
|
+
envelopePn = env.pn;
|
|
4552
|
+
}
|
|
4412
4553
|
} catch {
|
|
4413
4554
|
}
|
|
4414
4555
|
}
|
|
@@ -4417,7 +4558,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4417
4558
|
let state = this._ratchetStates.get(pairId);
|
|
4418
4559
|
if (!state) {
|
|
4419
4560
|
const x25519Shared = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
4420
|
-
let
|
|
4561
|
+
let initialKey;
|
|
4421
4562
|
if (envelopePqCiphertext && this.mlkemSecretKey) {
|
|
4422
4563
|
try {
|
|
4423
4564
|
const pqCt = (0, import_tweetnacl_util.decodeBase64)(envelopePqCiphertext);
|
|
@@ -4426,26 +4567,80 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4426
4567
|
const combined = new Uint8Array(x25519Shared.length + pqShared.length);
|
|
4427
4568
|
combined.set(x25519Shared, 0);
|
|
4428
4569
|
combined.set(pqShared, x25519Shared.length);
|
|
4429
|
-
|
|
4570
|
+
initialKey = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
|
4430
4571
|
} catch {
|
|
4431
|
-
|
|
4572
|
+
initialKey = x25519Shared;
|
|
4432
4573
|
}
|
|
4433
4574
|
} else {
|
|
4434
|
-
|
|
4575
|
+
initialKey = x25519Shared;
|
|
4576
|
+
}
|
|
4577
|
+
if (envelopeDhRatchetKey && this.doubleRatchet) {
|
|
4578
|
+
const senderDhPub = (0, import_tweetnacl_util.decodeBase64)(envelopeDhRatchetKey);
|
|
4579
|
+
const dhOutput = import_tweetnacl.default.box.before(senderDhPub, this.encryptionKeyPair.secretKey);
|
|
4580
|
+
const { newRootKey, newChainKey } = await kdfRK(initialKey, dhOutput);
|
|
4581
|
+
state = {
|
|
4582
|
+
sendChainKey: initialKey,
|
|
4583
|
+
sendStep: 0,
|
|
4584
|
+
recvChainKey: newChainKey,
|
|
4585
|
+
recvStep: 0,
|
|
4586
|
+
skippedKeys: /* @__PURE__ */ new Map(),
|
|
4587
|
+
rootKey: newRootKey,
|
|
4588
|
+
dhSendKeyPair: void 0,
|
|
4589
|
+
dhRecvPubKey: senderDhPub,
|
|
4590
|
+
prevSendStep: 0,
|
|
4591
|
+
dhSkippedKeys: /* @__PURE__ */ new Map()
|
|
4592
|
+
};
|
|
4593
|
+
} else {
|
|
4594
|
+
state = {
|
|
4595
|
+
sendChainKey: initialKey,
|
|
4596
|
+
sendStep: 0,
|
|
4597
|
+
recvChainKey: initialKey,
|
|
4598
|
+
recvStep: 0,
|
|
4599
|
+
skippedKeys: /* @__PURE__ */ new Map()
|
|
4600
|
+
};
|
|
4435
4601
|
}
|
|
4436
|
-
state = {
|
|
4437
|
-
sendChainKey: chainKey,
|
|
4438
|
-
// Our sending chain to this peer
|
|
4439
|
-
sendStep: 0,
|
|
4440
|
-
recvChainKey: chainKey,
|
|
4441
|
-
// Their sending chain (our receiving)
|
|
4442
|
-
recvStep: 0,
|
|
4443
|
-
skippedKeys: /* @__PURE__ */ new Map()
|
|
4444
|
-
};
|
|
4445
4602
|
this._ratchetStates.set(pairId, state);
|
|
4603
|
+
} else if (envelopeDhRatchetKey && state.rootKey) {
|
|
4604
|
+
const senderDhPub = (0, import_tweetnacl_util.decodeBase64)(envelopeDhRatchetKey);
|
|
4605
|
+
const currentDhRecv = state.dhRecvPubKey;
|
|
4606
|
+
if (!currentDhRecv || (0, import_tweetnacl_util.encodeBase64)(senderDhPub) !== (0, import_tweetnacl_util.encodeBase64)(currentDhRecv)) {
|
|
4607
|
+
if (envelopePn > state.recvStep) {
|
|
4608
|
+
let ck = state.recvChainKey;
|
|
4609
|
+
for (let i = state.recvStep + 1; i <= envelopePn && i - state.recvStep <= MAX_SKIP; i++) {
|
|
4610
|
+
const { nextChainKey, messageKey: skippedMk } = await ratchetStep(ck);
|
|
4611
|
+
const skipKey = `${currentDhRecv ? (0, import_tweetnacl_util.encodeBase64)(currentDhRecv) : "init"}:${i}`;
|
|
4612
|
+
if (!state.dhSkippedKeys) state.dhSkippedKeys = /* @__PURE__ */ new Map();
|
|
4613
|
+
state.dhSkippedKeys.set(skipKey, skippedMk);
|
|
4614
|
+
ck = nextChainKey;
|
|
4615
|
+
if (state.dhSkippedKeys.size > MAX_SKIP) {
|
|
4616
|
+
const oldest = state.dhSkippedKeys.keys().next().value;
|
|
4617
|
+
if (oldest !== void 0) state.dhSkippedKeys.delete(oldest);
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
state.dhRecvPubKey = senderDhPub;
|
|
4622
|
+
const myKey = state.dhSendKeyPair || this.encryptionKeyPair;
|
|
4623
|
+
const dhOutput1 = import_tweetnacl.default.box.before(senderDhPub, myKey.secretKey);
|
|
4624
|
+
const kdf1 = await kdfRK(state.rootKey, dhOutput1);
|
|
4625
|
+
state.rootKey = kdf1.newRootKey;
|
|
4626
|
+
state.recvChainKey = kdf1.newChainKey;
|
|
4627
|
+
state.recvStep = 0;
|
|
4628
|
+
state.prevSendStep = state.sendStep;
|
|
4629
|
+
state.dhSendKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4630
|
+
state.sendStep = 0;
|
|
4631
|
+
const dhOutput2 = import_tweetnacl.default.box.before(senderDhPub, state.dhSendKeyPair.secretKey);
|
|
4632
|
+
const kdf2 = await kdfRK(state.rootKey, dhOutput2);
|
|
4633
|
+
state.rootKey = kdf2.newRootKey;
|
|
4634
|
+
state.sendChainKey = kdf2.newChainKey;
|
|
4635
|
+
}
|
|
4446
4636
|
}
|
|
4447
4637
|
const targetStep = envelopeRatchetStep;
|
|
4448
|
-
|
|
4638
|
+
const dhSkipKey = envelopeDhRatchetKey ? `${envelopeDhRatchetKey}:${targetStep}` : `init:${targetStep}`;
|
|
4639
|
+
if (state.dhSkippedKeys?.has(dhSkipKey)) {
|
|
4640
|
+
const mk = state.dhSkippedKeys.get(dhSkipKey);
|
|
4641
|
+
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
4642
|
+
state.dhSkippedKeys.delete(dhSkipKey);
|
|
4643
|
+
} else if (state.skippedKeys.has(targetStep)) {
|
|
4449
4644
|
const mk = state.skippedKeys.get(targetStep);
|
|
4450
4645
|
rawPlaintext = import_tweetnacl.default.secretbox.open(ciphertext, nonce, mk);
|
|
4451
4646
|
state.skippedKeys.delete(targetStep);
|
|
@@ -4508,7 +4703,6 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4508
4703
|
}
|
|
4509
4704
|
let signatureValid = false;
|
|
4510
4705
|
try {
|
|
4511
|
-
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4512
4706
|
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(msg.signature);
|
|
4513
4707
|
const envelopeStr = msg.envelope || JSON.stringify({
|
|
4514
4708
|
from: senderDid,
|
|
@@ -4517,11 +4711,22 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4517
4711
|
nonce: msg.nonce,
|
|
4518
4712
|
ciphertext_hash: await sha256(msg.ciphertext)
|
|
4519
4713
|
});
|
|
4520
|
-
|
|
4521
|
-
(
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4714
|
+
if (signatureBytes.length === 32) {
|
|
4715
|
+
const sharedSecret = import_tweetnacl.default.box.before(senderEncPub, this.encryptionKeyPair.secretKey);
|
|
4716
|
+
const expectedHmac = await hmacSha256(sharedSecret, (0, import_tweetnacl_util.decodeUTF8)(envelopeStr));
|
|
4717
|
+
if (expectedHmac.length === signatureBytes.length) {
|
|
4718
|
+
let diff = 0;
|
|
4719
|
+
for (let i = 0; i < expectedHmac.length; i++) diff |= expectedHmac[i] ^ signatureBytes[i];
|
|
4720
|
+
signatureValid = diff === 0;
|
|
4721
|
+
}
|
|
4722
|
+
} else {
|
|
4723
|
+
const senderSignPub = (0, import_tweetnacl_util.decodeBase64)(msg.sender_signing_key);
|
|
4724
|
+
signatureValid = import_tweetnacl.default.sign.detached.verify(
|
|
4725
|
+
(0, import_tweetnacl_util.decodeUTF8)(envelopeStr),
|
|
4726
|
+
signatureBytes,
|
|
4727
|
+
senderSignPub
|
|
4728
|
+
);
|
|
4729
|
+
}
|
|
4525
4730
|
} catch {
|
|
4526
4731
|
signatureValid = false;
|
|
4527
4732
|
}
|
|
@@ -4715,22 +4920,35 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
4715
4920
|
async rotateKeys() {
|
|
4716
4921
|
const newSigningKeyPair = import_tweetnacl.default.sign.keyPair();
|
|
4717
4922
|
const newEncryptionKeyPair = import_tweetnacl.default.box.keyPair();
|
|
4923
|
+
const newSignedPrekey = import_tweetnacl.default.box.keyPair();
|
|
4924
|
+
const newSignedPrekeyId = (this._signedPrekeyId || 0) + 1;
|
|
4925
|
+
const signedPrekeySignature = import_tweetnacl.default.sign.detached(newSignedPrekey.publicKey, newSigningKeyPair.secretKey);
|
|
4926
|
+
const body = {
|
|
4927
|
+
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(newSigningKeyPair.publicKey),
|
|
4928
|
+
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(newEncryptionKeyPair.publicKey),
|
|
4929
|
+
signed_prekey_public: (0, import_tweetnacl_util.encodeBase64)(newSignedPrekey.publicKey),
|
|
4930
|
+
signed_prekey_signature: (0, import_tweetnacl_util.encodeBase64)(signedPrekeySignature),
|
|
4931
|
+
signed_prekey_id: newSignedPrekeyId
|
|
4932
|
+
};
|
|
4933
|
+
if (this.postQuantum && this.mlkemPublicKey) {
|
|
4934
|
+
body.mlkem_public_key = this.mlkemPublicKey;
|
|
4935
|
+
}
|
|
4718
4936
|
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/rotate-keys`, {
|
|
4719
4937
|
method: "POST",
|
|
4720
4938
|
headers: {
|
|
4721
4939
|
"Content-Type": "application/json",
|
|
4722
4940
|
"X-Agent-Key": this.apiKey
|
|
4723
4941
|
},
|
|
4724
|
-
body: JSON.stringify(
|
|
4725
|
-
signing_public_key: (0, import_tweetnacl_util.encodeBase64)(newSigningKeyPair.publicKey),
|
|
4726
|
-
encryption_public_key: (0, import_tweetnacl_util.encodeBase64)(newEncryptionKeyPair.publicKey)
|
|
4727
|
-
})
|
|
4942
|
+
body: JSON.stringify(body)
|
|
4728
4943
|
});
|
|
4729
4944
|
if (!res.ok) {
|
|
4730
4945
|
throw new Error("Key rotation failed");
|
|
4731
4946
|
}
|
|
4732
4947
|
this.signingKeyPair = newSigningKeyPair;
|
|
4733
4948
|
this.encryptionKeyPair = newEncryptionKeyPair;
|
|
4949
|
+
this._signedPrekey = newSignedPrekey;
|
|
4950
|
+
this._signedPrekeyId = newSignedPrekeyId;
|
|
4951
|
+
await this.uploadPrekeys(10);
|
|
4734
4952
|
}
|
|
4735
4953
|
// ─── Channels (Encrypted AI Forum) ──────────────────────────────────────────
|
|
4736
4954
|
/**
|
|
@@ -5608,6 +5826,158 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5608
5826
|
return res.json();
|
|
5609
5827
|
}
|
|
5610
5828
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5829
|
+
// X3DH — Async Key Agreement (prekey bundles for offline agents)
|
|
5830
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5831
|
+
/**
|
|
5832
|
+
* Upload prekey bundle for X3DH async key agreement.
|
|
5833
|
+
* Other agents can fetch your prekeys and establish encrypted sessions
|
|
5834
|
+
* even while you're offline.
|
|
5835
|
+
*
|
|
5836
|
+
* @param count Number of one-time prekeys to upload (default: 20)
|
|
5837
|
+
*/
|
|
5838
|
+
async uploadPrekeys(count = 20) {
|
|
5839
|
+
const prekeys = [];
|
|
5840
|
+
for (let i = 0; i < count; i++) {
|
|
5841
|
+
const kp = import_tweetnacl.default.box.keyPair();
|
|
5842
|
+
prekeys.push({
|
|
5843
|
+
id: Date.now() + i,
|
|
5844
|
+
public_key: (0, import_tweetnacl_util.encodeBase64)(kp.publicKey),
|
|
5845
|
+
secretKey: kp.secretKey
|
|
5846
|
+
});
|
|
5847
|
+
}
|
|
5848
|
+
const newPrekey = import_tweetnacl.default.box.keyPair();
|
|
5849
|
+
this._signedPrekeyId++;
|
|
5850
|
+
const prekeySignature = import_tweetnacl.default.sign.detached(newPrekey.publicKey, this.signingKeyPair.secretKey);
|
|
5851
|
+
this._signedPrekey = newPrekey;
|
|
5852
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/prekeys`, {
|
|
5853
|
+
method: "POST",
|
|
5854
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5855
|
+
body: JSON.stringify({
|
|
5856
|
+
prekeys: prekeys.map((pk) => ({ id: pk.id, public_key: pk.public_key })),
|
|
5857
|
+
signed_prekey: {
|
|
5858
|
+
public_key: (0, import_tweetnacl_util.encodeBase64)(newPrekey.publicKey),
|
|
5859
|
+
signature: (0, import_tweetnacl_util.encodeBase64)(prekeySignature),
|
|
5860
|
+
id: this._signedPrekeyId
|
|
5861
|
+
}
|
|
5862
|
+
})
|
|
5863
|
+
});
|
|
5864
|
+
if (!res.ok) throw new Error(`Prekey upload failed: ${res.status}`);
|
|
5865
|
+
return await res.json();
|
|
5866
|
+
}
|
|
5867
|
+
/**
|
|
5868
|
+
* Fetch another agent's prekey bundle for X3DH key agreement.
|
|
5869
|
+
* Use this to establish an encrypted session with an offline agent.
|
|
5870
|
+
*/
|
|
5871
|
+
async fetchPrekeys(did) {
|
|
5872
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/prekeys/${encodeURIComponent(did)}`);
|
|
5873
|
+
if (!res.ok) return null;
|
|
5874
|
+
return await res.json();
|
|
5875
|
+
}
|
|
5876
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5877
|
+
// CLIENT-SIDE CHANNEL ENCRYPTION
|
|
5878
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5879
|
+
/**
|
|
5880
|
+
* Create a channel with client-side encryption.
|
|
5881
|
+
* The channel symmetric key is generated locally and encrypted per-member.
|
|
5882
|
+
* The relay NEVER sees the plaintext channel key — true E2E for groups.
|
|
5883
|
+
*/
|
|
5884
|
+
async createEncryptedChannel(options) {
|
|
5885
|
+
const channelKey = import_tweetnacl.default.randomBytes(32);
|
|
5886
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/channels`, {
|
|
5887
|
+
method: "POST",
|
|
5888
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5889
|
+
body: JSON.stringify(options)
|
|
5890
|
+
});
|
|
5891
|
+
if (!res.ok) {
|
|
5892
|
+
const err = await res.json().catch(() => ({}));
|
|
5893
|
+
throw new Error(`Channel creation failed: ${err.error || res.statusText}`);
|
|
5894
|
+
}
|
|
5895
|
+
const channel = await res.json();
|
|
5896
|
+
const selfNonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.box.nonceLength);
|
|
5897
|
+
const selfEncrypted = import_tweetnacl.default.box(channelKey, selfNonce, this.encryptionKeyPair.publicKey, this.encryptionKeyPair.secretKey);
|
|
5898
|
+
await this.memorySet("channel-keys", channel.id, {
|
|
5899
|
+
key: (0, import_tweetnacl_util.encodeBase64)(channelKey),
|
|
5900
|
+
nonce: (0, import_tweetnacl_util.encodeBase64)(selfNonce)
|
|
5901
|
+
});
|
|
5902
|
+
return { ...channel, channelKey };
|
|
5903
|
+
}
|
|
5904
|
+
/**
|
|
5905
|
+
* Post a client-side encrypted message to a channel.
|
|
5906
|
+
* Uses the channel's shared symmetric key — relay never sees plaintext.
|
|
5907
|
+
*
|
|
5908
|
+
* @param channelId Channel ID
|
|
5909
|
+
* @param message Plaintext message
|
|
5910
|
+
* @param channelKey 32-byte symmetric channel key (from createEncryptedChannel or received via invite)
|
|
5911
|
+
*/
|
|
5912
|
+
async postEncrypted(channelId, message, channelKey) {
|
|
5913
|
+
const nonce = import_tweetnacl.default.randomBytes(import_tweetnacl.default.secretbox.nonceLength);
|
|
5914
|
+
const ciphertext = import_tweetnacl.default.secretbox((0, import_tweetnacl_util.decodeUTF8)(message), nonce, channelKey);
|
|
5915
|
+
const sigPayload = new Uint8Array([...ciphertext, ...nonce]);
|
|
5916
|
+
const signature = import_tweetnacl.default.sign.detached(sigPayload, this.signingKeyPair.secretKey);
|
|
5917
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/channels/${channelId}/messages`, {
|
|
5918
|
+
method: "POST",
|
|
5919
|
+
headers: { "Content-Type": "application/json", "X-Agent-Key": this.apiKey },
|
|
5920
|
+
body: JSON.stringify({
|
|
5921
|
+
message: JSON.stringify({
|
|
5922
|
+
v: 3,
|
|
5923
|
+
ct: (0, import_tweetnacl_util.encodeBase64)(ciphertext),
|
|
5924
|
+
n: (0, import_tweetnacl_util.encodeBase64)(nonce),
|
|
5925
|
+
sig: (0, import_tweetnacl_util.encodeBase64)(signature),
|
|
5926
|
+
from: this.did
|
|
5927
|
+
})
|
|
5928
|
+
})
|
|
5929
|
+
});
|
|
5930
|
+
if (!res.ok) throw new Error(`Post failed: ${res.status}`);
|
|
5931
|
+
return await res.json();
|
|
5932
|
+
}
|
|
5933
|
+
/**
|
|
5934
|
+
* Read and decrypt channel messages using client-side channel key.
|
|
5935
|
+
* Ignores server-side encryption entirely — true E2E.
|
|
5936
|
+
*
|
|
5937
|
+
* @param channelId Channel ID
|
|
5938
|
+
* @param channelKey 32-byte symmetric channel key
|
|
5939
|
+
*/
|
|
5940
|
+
async readEncrypted(channelId, channelKey, options) {
|
|
5941
|
+
const raw = await this.readChannel(channelId, options);
|
|
5942
|
+
const decrypted = [];
|
|
5943
|
+
for (const msg of raw.messages) {
|
|
5944
|
+
try {
|
|
5945
|
+
const parsed = JSON.parse(typeof msg.content === "string" ? msg.content : msg.message || "");
|
|
5946
|
+
if (parsed.v === 3 && parsed.ct && parsed.n) {
|
|
5947
|
+
const ct = (0, import_tweetnacl_util.decodeBase64)(parsed.ct);
|
|
5948
|
+
const nonce = (0, import_tweetnacl_util.decodeBase64)(parsed.n);
|
|
5949
|
+
const plain = import_tweetnacl.default.secretbox.open(ct, nonce, channelKey);
|
|
5950
|
+
if (plain) {
|
|
5951
|
+
let sigValid = false;
|
|
5952
|
+
if (parsed.sig && parsed.from) {
|
|
5953
|
+
try {
|
|
5954
|
+
const profile = await this.getIdentity(parsed.from);
|
|
5955
|
+
if (profile) {
|
|
5956
|
+
const sigPayload = new Uint8Array([...ct, ...nonce]);
|
|
5957
|
+
sigValid = import_tweetnacl.default.sign.detached.verify(
|
|
5958
|
+
sigPayload,
|
|
5959
|
+
(0, import_tweetnacl_util.decodeBase64)(parsed.sig),
|
|
5960
|
+
(0, import_tweetnacl_util.decodeBase64)(profile.signing_public_key)
|
|
5961
|
+
);
|
|
5962
|
+
}
|
|
5963
|
+
} catch {
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
decrypted.push({
|
|
5967
|
+
id: msg.id,
|
|
5968
|
+
from: parsed.from || msg.author || "unknown",
|
|
5969
|
+
content: (0, import_tweetnacl_util.encodeUTF8)(plain),
|
|
5970
|
+
timestamp: msg.created_at || msg.timestamp,
|
|
5971
|
+
signatureValid: sigValid
|
|
5972
|
+
});
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
} catch {
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
return { messages: decrypted, count: decrypted.length };
|
|
5979
|
+
}
|
|
5980
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
5611
5981
|
// LISTEN — Event-Driven Message Receiving
|
|
5612
5982
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
5613
5983
|
/**
|
|
@@ -5674,20 +6044,54 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5674
6044
|
this.ping().catch(() => {
|
|
5675
6045
|
});
|
|
5676
6046
|
}
|
|
6047
|
+
const useLongPoll = this.longPoll;
|
|
5677
6048
|
const poll = async () => {
|
|
5678
6049
|
if (!active || options.signal?.aborted) {
|
|
5679
6050
|
handle.stop();
|
|
5680
6051
|
return;
|
|
5681
6052
|
}
|
|
5682
6053
|
try {
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
6054
|
+
let messages;
|
|
6055
|
+
if (useLongPoll) {
|
|
6056
|
+
const params = new URLSearchParams({ timeout: "25" });
|
|
6057
|
+
if (lastSeen) params.set("since", lastSeen);
|
|
6058
|
+
if (options.from) params.set("from", options.from);
|
|
6059
|
+
const res = await this._timedFetch(`${this.baseUrl}/v1/agent/receive/poll?${params}`, {
|
|
6060
|
+
headers: { "X-Agent-Key": this.apiKey }
|
|
6061
|
+
});
|
|
6062
|
+
if (res.ok) {
|
|
6063
|
+
const data = await res.json();
|
|
6064
|
+
messages = [];
|
|
6065
|
+
for (const raw of data.messages) {
|
|
6066
|
+
try {
|
|
6067
|
+
if (this._seenMessageIds.has(raw.id)) continue;
|
|
6068
|
+
this._seenMessageIds.add(raw.id);
|
|
6069
|
+
} catch {
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
if (data.messages.length > 0) {
|
|
6073
|
+
messages = await this.receive({
|
|
6074
|
+
since: lastSeen,
|
|
6075
|
+
from: options.from,
|
|
6076
|
+
threadId: options.threadId,
|
|
6077
|
+
messageType: options.messageType,
|
|
6078
|
+
unreadOnly,
|
|
6079
|
+
limit: 50
|
|
6080
|
+
});
|
|
6081
|
+
}
|
|
6082
|
+
} else {
|
|
6083
|
+
messages = [];
|
|
6084
|
+
}
|
|
6085
|
+
} else {
|
|
6086
|
+
messages = await this.receive({
|
|
6087
|
+
since: lastSeen,
|
|
6088
|
+
from: options.from,
|
|
6089
|
+
threadId: options.threadId,
|
|
6090
|
+
messageType: options.messageType,
|
|
6091
|
+
unreadOnly,
|
|
6092
|
+
limit: 50
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
5691
6095
|
if (messages.length > 0) {
|
|
5692
6096
|
consecutiveEmpty = 0;
|
|
5693
6097
|
if (adaptive) currentInterval = Math.max(interval / 2, 500);
|
|
@@ -5705,7 +6109,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5705
6109
|
lastSeen = messages[messages.length - 1].timestamp;
|
|
5706
6110
|
} else {
|
|
5707
6111
|
consecutiveEmpty++;
|
|
5708
|
-
if (adaptive && consecutiveEmpty > 3) {
|
|
6112
|
+
if (adaptive && consecutiveEmpty > 3 && !useLongPoll) {
|
|
5709
6113
|
currentInterval = Math.min(currentInterval * 1.5, interval * 4);
|
|
5710
6114
|
}
|
|
5711
6115
|
}
|
|
@@ -5714,7 +6118,7 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5714
6118
|
currentInterval = Math.min(currentInterval * 2, interval * 8);
|
|
5715
6119
|
}
|
|
5716
6120
|
if (active && !options.signal?.aborted) {
|
|
5717
|
-
timer = setTimeout(poll, currentInterval);
|
|
6121
|
+
timer = setTimeout(poll, useLongPoll ? 100 : currentInterval);
|
|
5718
6122
|
}
|
|
5719
6123
|
};
|
|
5720
6124
|
poll();
|
|
@@ -5914,30 +6318,35 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5914
6318
|
return {
|
|
5915
6319
|
relayCanSee: [
|
|
5916
6320
|
"Your DID (public identifier)",
|
|
5917
|
-
"Who you message (recipient DIDs)",
|
|
5918
|
-
"When you message (timestamps)",
|
|
6321
|
+
...this.sealedSender ? [] : ["Who you message (recipient DIDs)"],
|
|
6322
|
+
...this.jitterMs > 0 ? ["Approximate timing (jittered timestamps)"] : ["When you message (timestamps)"],
|
|
5919
6323
|
"Message types (text, task-request, etc.)",
|
|
5920
6324
|
"Thread structure (which messages are replies)",
|
|
5921
|
-
"Channel membership",
|
|
6325
|
+
"Channel membership (but NOT channel message content with client-side encryption)",
|
|
5922
6326
|
"Capability registrations",
|
|
5923
6327
|
"Online/offline status",
|
|
5924
6328
|
"Approximate message size (even with padding, bounded to power-of-2)"
|
|
5925
6329
|
],
|
|
5926
6330
|
relayCannotSee: [
|
|
5927
|
-
"Message content (E2E encrypted \u2014 nacl.
|
|
6331
|
+
"Message content (E2E encrypted \u2014 nacl.box with Double Ratchet per-message keys)",
|
|
5928
6332
|
"Private keys (generated and stored client-side only)",
|
|
5929
6333
|
"Memory values (encrypted CLIENT-SIDE with nacl.secretbox before relay storage)",
|
|
5930
|
-
"Past message keys (hash ratchet
|
|
5931
|
-
|
|
6334
|
+
"Past message keys (forward secrecy \u2014 hash ratchet + DH ratchet, old keys deleted)",
|
|
6335
|
+
"Future message keys (post-compromise recovery via DH ratchet \u2014 compromise heals after one round-trip)",
|
|
6336
|
+
"Channel message content (client-side nacl.secretbox encryption \u2014 relay stores only ciphertext)",
|
|
6337
|
+
...this.sealedSender ? ["Sender identity (sealed inside ciphertext)"] : [],
|
|
6338
|
+
...this.deniable ? ["Who authored a message (HMAC is symmetric \u2014 either party could have produced it)"] : []
|
|
5932
6339
|
],
|
|
5933
6340
|
protections: [
|
|
5934
|
-
"Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted",
|
|
6341
|
+
...this.doubleRatchet ? ["Double Ratchet (Signal Protocol) \u2014 DH ratchet for post-compromise recovery + hash ratchet for forward secrecy"] : ["Hash ratchet forward secrecy \u2014 per-message key derivation, old keys deleted"],
|
|
5935
6342
|
...this.postQuantum && this.mlkemPublicKey ? ["ML-KEM-768 + X25519 hybrid key exchange (NIST FIPS 203 post-quantum, harvest-now-decrypt-later resistant)"] : [],
|
|
6343
|
+
"X3DH async key agreement (signed prekeys + one-time prekeys for offline session establishment)",
|
|
5936
6344
|
"X25519 key exchange + XSalsa20-Poly1305 authenticated encryption",
|
|
5937
|
-
"Ed25519 signatures on every message (envelope + ciphertext hash)",
|
|
6345
|
+
...this.deniable ? ["Deniable authentication (HMAC-SHA256 with shared DH secret \u2014 both parties can produce the MAC)"] : ["Ed25519 signatures on every message (envelope + ciphertext hash)"],
|
|
5938
6346
|
"TOFU key pinning (MitM detection on key change)",
|
|
5939
6347
|
"Client-side memory encryption (relay never sees plaintext values)",
|
|
5940
|
-
"
|
|
6348
|
+
"Client-side channel encryption (nacl.secretbox \u2014 relay never sees channel plaintext)",
|
|
6349
|
+
"Protocol version header (deterministic padding/sealing/ratchet detection, no heuristics)",
|
|
5941
6350
|
"Identity cache (reduced key lookups, 5-min TTL)",
|
|
5942
6351
|
"Message deduplication (track seen message IDs)",
|
|
5943
6352
|
"Request timeouts (AbortController on all HTTP, configurable)",
|
|
@@ -5946,20 +6355,19 @@ var VoidlyAgent = class _VoidlyAgent {
|
|
|
5946
6355
|
...this.sealedSender ? ["Sealed sender (relay cannot see who sent a message)"] : [],
|
|
5947
6356
|
...this.requireSignatures ? ["Strict signature enforcement (reject unsigned/invalid messages)"] : [],
|
|
5948
6357
|
...this.fallbackRelays.length > 0 ? [`Multi-relay fallback (${this.fallbackRelays.length} backup relays)`] : [],
|
|
6358
|
+
...this.jitterMs > 0 ? [`Timing jitter (random ${this.jitterMs}ms delay \u2014 metadata timing protection)`] : [],
|
|
6359
|
+
...this.longPoll ? ["Long-poll transport (25s server-held connection \u2014 near-real-time delivery)"] : [],
|
|
5949
6360
|
"Auto-retry with exponential backoff",
|
|
5950
6361
|
"Offline message queue",
|
|
5951
6362
|
"did:key interoperability (W3C standard DID format)"
|
|
5952
6363
|
],
|
|
5953
6364
|
gaps: [
|
|
5954
|
-
"No DH ratchet yet \u2014 hash ratchet only (no post-compromise recovery, planned for v3)",
|
|
5955
6365
|
...!this.postQuantum || !this.mlkemPublicKey ? ["No post-quantum protection \u2014 enable postQuantum option and re-register"] : [],
|
|
5956
|
-
"
|
|
5957
|
-
"
|
|
5958
|
-
"
|
|
5959
|
-
"
|
|
5960
|
-
"No
|
|
5961
|
-
"Polling-based (no WebSocket real-time transport)",
|
|
5962
|
-
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)"
|
|
6366
|
+
"Single relay architecture (no onion routing, no mix network \u2014 mitigated by multi-relay fallback)",
|
|
6367
|
+
"Ratchet state is in-memory (lost on process restart \u2014 export credentials to persist)",
|
|
6368
|
+
...!this.deniable ? ["Ed25519 signatures are non-repudiable \u2014 enable deniable option for HMAC auth"] : [],
|
|
6369
|
+
...!this.doubleRatchet ? ["Hash ratchet only \u2014 enable doubleRatchet option for post-compromise recovery"] : [],
|
|
6370
|
+
...this.jitterMs === 0 ? ["No timing jitter \u2014 enable jitterMs option for metadata protection"] : []
|
|
5963
6371
|
]
|
|
5964
6372
|
};
|
|
5965
6373
|
}
|