@purecore/one-jwt-4-all 1.2.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/CHANGELOG.md +87 -0
- package/LICENSE-COGFULNESS +117 -0
- package/examples/MTLS_AGENTS.md +288 -0
- package/examples/SELF_HEALING_AGENTS.md +269 -0
- package/examples/SIGNAL_E2EE.md +457 -0
- package/examples/mtls-agents.ts +539 -0
- package/examples/self-healing-agents.ts +355 -0
- package/examples/signal-e2ee-agents.ts +827 -0
- package/package.json +20 -0
- package/readme.md +507 -0
- package/reports/22-12-2024_01-50.md +165 -0
- package/src/examples.mcps.ts +86 -0
- package/src/examples.ts +81 -0
- package/src/index.ts +286 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal Protocol End-to-End Encryption para Sistema de Agentes
|
|
3
|
+
*
|
|
4
|
+
* Implementação do Double Ratchet Algorithm do Signal Protocol para
|
|
5
|
+
* comunicação end-to-end encriptada entre agentes.
|
|
6
|
+
*
|
|
7
|
+
* Características:
|
|
8
|
+
* - X3DH (Extended Triple Diffie-Hellman) para key agreement inicial
|
|
9
|
+
* - Double Ratchet para forward secrecy e break-in recovery
|
|
10
|
+
* - Cada mensagem usa uma chave única derivada
|
|
11
|
+
* - Perfect Forward Secrecy (PFS)
|
|
12
|
+
* - Post-Compromise Security (PCS)
|
|
13
|
+
*
|
|
14
|
+
* @see https://signal.org/docs/specifications/doubleratchet/
|
|
15
|
+
* @see https://signal.org/docs/specifications/x3dh/
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { SignJWT, jwtVerify, generateKeyPair } from '../src/index';
|
|
19
|
+
import * as crypto from 'node:crypto';
|
|
20
|
+
import { EventEmitter } from 'node:events';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Constantes e Tipos
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const SYMMETRIC_KEY_LENGTH = 32; // 256 bits
|
|
27
|
+
const MAX_SKIP = 1000; // Máximo de message keys puladas para guardar
|
|
28
|
+
|
|
29
|
+
interface KeyBundle {
|
|
30
|
+
identityKey: Buffer; // Chave de identidade de longo prazo (pública)
|
|
31
|
+
signedPreKey: Buffer; // Pre-key assinada (pública)
|
|
32
|
+
signedPreKeySignature: Buffer;
|
|
33
|
+
oneTimePreKey?: Buffer; // Pre-key única (pública, opcional)
|
|
34
|
+
ephemeralKey?: Buffer; // Chave efêmera do remetente
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface X3DHKeyPair {
|
|
38
|
+
publicKey: Buffer;
|
|
39
|
+
privateKey: Buffer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RatchetState {
|
|
43
|
+
DHs: X3DHKeyPair; // Ratchet key pair próprio
|
|
44
|
+
DHr: Buffer | null; // Ratchet public key do peer
|
|
45
|
+
RK: Buffer; // Root key
|
|
46
|
+
CKs: Buffer | null; // Sending chain key
|
|
47
|
+
CKr: Buffer | null; // Receiving chain key
|
|
48
|
+
Ns: number; // Número de mensagens enviadas na chain atual
|
|
49
|
+
Nr: number; // Número de mensagens recebidas na chain atual
|
|
50
|
+
PN: number; // Número de mensagens da chain anterior
|
|
51
|
+
MKSKIPPED: Map<string, Buffer>; // Skipped message keys
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SignalMessage {
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
messageId: string;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
header: {
|
|
60
|
+
dh: string; // Ratchet public key (hex)
|
|
61
|
+
pn: number; // Previous chain length
|
|
62
|
+
n: number; // Message number
|
|
63
|
+
};
|
|
64
|
+
ciphertext: string; // Encrypted content (hex)
|
|
65
|
+
nonce: string; // Nonce usado (hex)
|
|
66
|
+
jwt?: string; // JWT opcional para contexto
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Funções Criptográficas
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gera par de chaves X25519 para Diffie-Hellman
|
|
75
|
+
*/
|
|
76
|
+
function generateX25519KeyPair(): X3DHKeyPair {
|
|
77
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519');
|
|
78
|
+
return {
|
|
79
|
+
publicKey: Buffer.from(publicKey.export({ type: 'spki', format: 'der' }).slice(-32)),
|
|
80
|
+
privateKey: Buffer.from(privateKey.export({ type: 'pkcs8', format: 'der' }).slice(-32))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Computa Diffie-Hellman shared secret
|
|
86
|
+
*/
|
|
87
|
+
function computeDH(privateKey: Buffer, publicKey: Buffer): Buffer {
|
|
88
|
+
const privKeyObj = crypto.createPrivateKey({
|
|
89
|
+
key: Buffer.concat([
|
|
90
|
+
Buffer.from('302e020100300506032b656e04220420', 'hex'),
|
|
91
|
+
privateKey
|
|
92
|
+
]),
|
|
93
|
+
format: 'der',
|
|
94
|
+
type: 'pkcs8'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const pubKeyObj = crypto.createPublicKey({
|
|
98
|
+
key: Buffer.concat([
|
|
99
|
+
Buffer.from('302a300506032b656e032100', 'hex'),
|
|
100
|
+
publicKey
|
|
101
|
+
]),
|
|
102
|
+
format: 'der',
|
|
103
|
+
type: 'spki'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return crypto.diffieHellman({
|
|
107
|
+
privateKey: privKeyObj,
|
|
108
|
+
publicKey: pubKeyObj
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* HKDF - Hash-based Key Derivation Function
|
|
114
|
+
*/
|
|
115
|
+
function hkdf(
|
|
116
|
+
inputKeyMaterial: Buffer,
|
|
117
|
+
salt: Buffer,
|
|
118
|
+
info: Buffer,
|
|
119
|
+
length: number
|
|
120
|
+
): Buffer {
|
|
121
|
+
return crypto.hkdfSync('sha256', inputKeyMaterial, salt, info, length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* KDF para Root Chain - deriva novo RK e Chain Key
|
|
126
|
+
*/
|
|
127
|
+
function kdfRK(rk: Buffer, dhOut: Buffer): { rootKey: Buffer; chainKey: Buffer } {
|
|
128
|
+
const output = hkdf(dhOut, rk, Buffer.from('SignalRootRatchet'), 64);
|
|
129
|
+
return {
|
|
130
|
+
rootKey: output.slice(0, 32),
|
|
131
|
+
chainKey: output.slice(32, 64)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* KDF para Chain Key - deriva novo CK e Message Key
|
|
137
|
+
*/
|
|
138
|
+
function kdfCK(ck: Buffer): { chainKey: Buffer; messageKey: Buffer } {
|
|
139
|
+
const chainKey = crypto.createHmac('sha256', ck).update(Buffer.from([0x01])).digest();
|
|
140
|
+
const messageKey = crypto.createHmac('sha256', ck).update(Buffer.from([0x02])).digest();
|
|
141
|
+
return { chainKey, messageKey };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Encripta mensagem usando AES-256-GCM
|
|
146
|
+
*/
|
|
147
|
+
function encrypt(plaintext: string, key: Buffer): { ciphertext: Buffer; nonce: Buffer } {
|
|
148
|
+
const nonce = crypto.randomBytes(12);
|
|
149
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
150
|
+
const encrypted = Buffer.concat([
|
|
151
|
+
cipher.update(plaintext, 'utf8'),
|
|
152
|
+
cipher.final()
|
|
153
|
+
]);
|
|
154
|
+
const authTag = cipher.getAuthTag();
|
|
155
|
+
return {
|
|
156
|
+
ciphertext: Buffer.concat([encrypted, authTag]),
|
|
157
|
+
nonce
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Decripta mensagem usando AES-256-GCM
|
|
163
|
+
*/
|
|
164
|
+
function decrypt(ciphertext: Buffer, key: Buffer, nonce: Buffer): string {
|
|
165
|
+
const authTag = ciphertext.slice(-16);
|
|
166
|
+
const encrypted = ciphertext.slice(0, -16);
|
|
167
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
168
|
+
decipher.setAuthTag(authTag);
|
|
169
|
+
return decipher.update(encrypted) + decipher.final('utf8');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// X3DH Key Agreement Protocol
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
class X3DHKeyBundle {
|
|
177
|
+
readonly identityKey: X3DHKeyPair;
|
|
178
|
+
readonly signedPreKey: X3DHKeyPair;
|
|
179
|
+
readonly signedPreKeySignature: Buffer;
|
|
180
|
+
readonly oneTimePreKeys: X3DHKeyPair[];
|
|
181
|
+
private readonly signingKey: crypto.KeyObject;
|
|
182
|
+
|
|
183
|
+
constructor() {
|
|
184
|
+
// Gerar Identity Key (longo prazo)
|
|
185
|
+
this.identityKey = generateX25519KeyPair();
|
|
186
|
+
|
|
187
|
+
// Gerar Signed Pre-Key
|
|
188
|
+
this.signedPreKey = generateX25519KeyPair();
|
|
189
|
+
|
|
190
|
+
// Assinar a Pre-Key com Ed25519
|
|
191
|
+
const { publicKey: edPub, privateKey: edPriv } = crypto.generateKeyPairSync('ed25519');
|
|
192
|
+
this.signingKey = edPriv;
|
|
193
|
+
this.signedPreKeySignature = crypto.sign(null, this.signedPreKey.publicKey, edPriv);
|
|
194
|
+
|
|
195
|
+
// Gerar One-Time Pre-Keys
|
|
196
|
+
this.oneTimePreKeys = Array.from({ length: 10 }, () => generateX25519KeyPair());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getPublicBundle(): KeyBundle {
|
|
200
|
+
const oneTimePreKey = this.oneTimePreKeys.shift();
|
|
201
|
+
return {
|
|
202
|
+
identityKey: this.identityKey.publicKey,
|
|
203
|
+
signedPreKey: this.signedPreKey.publicKey,
|
|
204
|
+
signedPreKeySignature: this.signedPreKeySignature,
|
|
205
|
+
oneTimePreKey: oneTimePreKey?.publicKey
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Executa X3DH como receptor (Bob)
|
|
211
|
+
*/
|
|
212
|
+
performX3DHAsReceiver(
|
|
213
|
+
ephemeralKey: Buffer,
|
|
214
|
+
senderIdentityKey: Buffer,
|
|
215
|
+
usedOneTimePreKey: boolean
|
|
216
|
+
): Buffer {
|
|
217
|
+
// DH1: IKa, SPKb
|
|
218
|
+
const dh1 = computeDH(this.signedPreKey.privateKey, senderIdentityKey);
|
|
219
|
+
|
|
220
|
+
// DH2: EKa, IKb
|
|
221
|
+
const dh2 = computeDH(this.identityKey.privateKey, ephemeralKey);
|
|
222
|
+
|
|
223
|
+
// DH3: EKa, SPKb
|
|
224
|
+
const dh3 = computeDH(this.signedPreKey.privateKey, ephemeralKey);
|
|
225
|
+
|
|
226
|
+
let masterSecret: Buffer;
|
|
227
|
+
if (usedOneTimePreKey && this.oneTimePreKeys.length > 0) {
|
|
228
|
+
// DH4: EKa, OPKb (se one-time pre-key foi usada)
|
|
229
|
+
const otpk = this.oneTimePreKeys[0];
|
|
230
|
+
const dh4 = computeDH(otpk.privateKey, ephemeralKey);
|
|
231
|
+
masterSecret = Buffer.concat([dh1, dh2, dh3, dh4]);
|
|
232
|
+
} else {
|
|
233
|
+
masterSecret = Buffer.concat([dh1, dh2, dh3]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return hkdf(masterSecret, Buffer.alloc(32), Buffer.from('X3DH'), 32);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Executa X3DH como iniciador (Alice)
|
|
242
|
+
*/
|
|
243
|
+
function performX3DHAsInitiator(
|
|
244
|
+
identityKey: X3DHKeyPair,
|
|
245
|
+
ephemeralKey: X3DHKeyPair,
|
|
246
|
+
receiverBundle: KeyBundle
|
|
247
|
+
): Buffer {
|
|
248
|
+
// DH1: IKa, SPKb
|
|
249
|
+
const dh1 = computeDH(identityKey.privateKey, receiverBundle.signedPreKey);
|
|
250
|
+
|
|
251
|
+
// DH2: EKa, IKb
|
|
252
|
+
const dh2 = computeDH(ephemeralKey.privateKey, receiverBundle.identityKey);
|
|
253
|
+
|
|
254
|
+
// DH3: EKa, SPKb
|
|
255
|
+
const dh3 = computeDH(ephemeralKey.privateKey, receiverBundle.signedPreKey);
|
|
256
|
+
|
|
257
|
+
let masterSecret: Buffer;
|
|
258
|
+
if (receiverBundle.oneTimePreKey) {
|
|
259
|
+
// DH4: EKa, OPKb
|
|
260
|
+
const dh4 = computeDH(ephemeralKey.privateKey, receiverBundle.oneTimePreKey);
|
|
261
|
+
masterSecret = Buffer.concat([dh1, dh2, dh3, dh4]);
|
|
262
|
+
} else {
|
|
263
|
+
masterSecret = Buffer.concat([dh1, dh2, dh3]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return hkdf(masterSecret, Buffer.alloc(32), Buffer.from('X3DH'), 32);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Double Ratchet State Machine
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
class DoubleRatchet {
|
|
274
|
+
private state: RatchetState;
|
|
275
|
+
|
|
276
|
+
constructor() {
|
|
277
|
+
this.state = {
|
|
278
|
+
DHs: generateX25519KeyPair(),
|
|
279
|
+
DHr: null,
|
|
280
|
+
RK: Buffer.alloc(32),
|
|
281
|
+
CKs: null,
|
|
282
|
+
CKr: null,
|
|
283
|
+
Ns: 0,
|
|
284
|
+
Nr: 0,
|
|
285
|
+
PN: 0,
|
|
286
|
+
MKSKIPPED: new Map()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Inicializa como Alice (iniciadora da sessão)
|
|
292
|
+
*/
|
|
293
|
+
initializeAsAlice(sharedSecret: Buffer, bobRatchetPublicKey: Buffer): void {
|
|
294
|
+
this.state.DHs = generateX25519KeyPair();
|
|
295
|
+
this.state.DHr = bobRatchetPublicKey;
|
|
296
|
+
|
|
297
|
+
// Deriva root key e sending chain key iniciais
|
|
298
|
+
const dhOutput = computeDH(this.state.DHs.privateKey, this.state.DHr);
|
|
299
|
+
const { rootKey, chainKey } = kdfRK(sharedSecret, dhOutput);
|
|
300
|
+
|
|
301
|
+
this.state.RK = rootKey;
|
|
302
|
+
this.state.CKs = chainKey;
|
|
303
|
+
this.state.CKr = null;
|
|
304
|
+
this.state.Ns = 0;
|
|
305
|
+
this.state.Nr = 0;
|
|
306
|
+
this.state.PN = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Inicializa como Bob (receptor da sessão)
|
|
311
|
+
*/
|
|
312
|
+
initializeAsBob(sharedSecret: Buffer): void {
|
|
313
|
+
this.state.DHs = generateX25519KeyPair();
|
|
314
|
+
this.state.DHr = null;
|
|
315
|
+
this.state.RK = sharedSecret;
|
|
316
|
+
this.state.CKs = null;
|
|
317
|
+
this.state.CKr = null;
|
|
318
|
+
this.state.Ns = 0;
|
|
319
|
+
this.state.Nr = 0;
|
|
320
|
+
this.state.PN = 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Encripta uma mensagem
|
|
325
|
+
*/
|
|
326
|
+
ratchetEncrypt(plaintext: string): { header: SignalMessage['header']; ciphertext: Buffer; nonce: Buffer } {
|
|
327
|
+
if (!this.state.CKs) {
|
|
328
|
+
throw new Error('Sending chain não inicializada');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { chainKey, messageKey } = kdfCK(this.state.CKs);
|
|
332
|
+
this.state.CKs = chainKey;
|
|
333
|
+
|
|
334
|
+
const header = {
|
|
335
|
+
dh: this.state.DHs.publicKey.toString('hex'),
|
|
336
|
+
pn: this.state.PN,
|
|
337
|
+
n: this.state.Ns
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.state.Ns++;
|
|
341
|
+
|
|
342
|
+
const { ciphertext, nonce } = encrypt(plaintext, messageKey);
|
|
343
|
+
|
|
344
|
+
// Limpa message key da memória
|
|
345
|
+
messageKey.fill(0);
|
|
346
|
+
|
|
347
|
+
return { header, ciphertext, nonce };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Decripta uma mensagem
|
|
352
|
+
*/
|
|
353
|
+
ratchetDecrypt(header: SignalMessage['header'], ciphertext: Buffer, nonce: Buffer): string {
|
|
354
|
+
const dhPublicKey = Buffer.from(header.dh, 'hex');
|
|
355
|
+
|
|
356
|
+
// Tenta chaves puladas primeiro
|
|
357
|
+
const skippedKey = this.trySkippedMessageKeys(header, dhPublicKey);
|
|
358
|
+
if (skippedKey) {
|
|
359
|
+
const plaintext = decrypt(ciphertext, skippedKey, nonce);
|
|
360
|
+
skippedKey.fill(0);
|
|
361
|
+
return plaintext;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Verifica se precisamos fazer DH ratchet
|
|
365
|
+
if (!this.state.DHr || !dhPublicKey.equals(this.state.DHr)) {
|
|
366
|
+
this.skipMessageKeys(header.pn);
|
|
367
|
+
this.dhRatchet(dhPublicKey);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.skipMessageKeys(header.n);
|
|
371
|
+
|
|
372
|
+
if (!this.state.CKr) {
|
|
373
|
+
throw new Error('Receiving chain não inicializada');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { chainKey, messageKey } = kdfCK(this.state.CKr);
|
|
377
|
+
this.state.CKr = chainKey;
|
|
378
|
+
this.state.Nr++;
|
|
379
|
+
|
|
380
|
+
const plaintext = decrypt(ciphertext, messageKey, nonce);
|
|
381
|
+
messageKey.fill(0);
|
|
382
|
+
|
|
383
|
+
return plaintext;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Realiza DH Ratchet step
|
|
388
|
+
*/
|
|
389
|
+
private dhRatchet(dhPublicKey: Buffer): void {
|
|
390
|
+
this.state.PN = this.state.Ns;
|
|
391
|
+
this.state.Ns = 0;
|
|
392
|
+
this.state.Nr = 0;
|
|
393
|
+
this.state.DHr = dhPublicKey;
|
|
394
|
+
|
|
395
|
+
// Deriva receiving chain
|
|
396
|
+
const dhOutput1 = computeDH(this.state.DHs.privateKey, this.state.DHr);
|
|
397
|
+
const { rootKey: rk1, chainKey: ckr } = kdfRK(this.state.RK, dhOutput1);
|
|
398
|
+
this.state.RK = rk1;
|
|
399
|
+
this.state.CKr = ckr;
|
|
400
|
+
|
|
401
|
+
// Gera novo DH key pair e deriva sending chain
|
|
402
|
+
this.state.DHs = generateX25519KeyPair();
|
|
403
|
+
const dhOutput2 = computeDH(this.state.DHs.privateKey, this.state.DHr);
|
|
404
|
+
const { rootKey: rk2, chainKey: cks } = kdfRK(this.state.RK, dhOutput2);
|
|
405
|
+
this.state.RK = rk2;
|
|
406
|
+
this.state.CKs = cks;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Armazena message keys puladas
|
|
411
|
+
*/
|
|
412
|
+
private skipMessageKeys(until: number): void {
|
|
413
|
+
if (!this.state.CKr) return;
|
|
414
|
+
|
|
415
|
+
if (this.state.Nr + MAX_SKIP < until) {
|
|
416
|
+
throw new Error('Muitas mensagens puladas');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
while (this.state.Nr < until) {
|
|
420
|
+
const { chainKey, messageKey } = kdfCK(this.state.CKr);
|
|
421
|
+
this.state.CKr = chainKey;
|
|
422
|
+
|
|
423
|
+
const key = `${this.state.DHr?.toString('hex')}-${this.state.Nr}`;
|
|
424
|
+
this.state.MKSKIPPED.set(key, messageKey);
|
|
425
|
+
|
|
426
|
+
this.state.Nr++;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Tenta usar uma message key pulada
|
|
432
|
+
*/
|
|
433
|
+
private trySkippedMessageKeys(header: SignalMessage['header'], dhPublicKey: Buffer): Buffer | null {
|
|
434
|
+
const key = `${dhPublicKey.toString('hex')}-${header.n}`;
|
|
435
|
+
const messageKey = this.state.MKSKIPPED.get(key);
|
|
436
|
+
|
|
437
|
+
if (messageKey) {
|
|
438
|
+
this.state.MKSKIPPED.delete(key);
|
|
439
|
+
return messageKey;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Retorna public key atual para inicialização do peer
|
|
447
|
+
*/
|
|
448
|
+
getPublicKey(): Buffer {
|
|
449
|
+
return this.state.DHs.publicKey;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// Token Authority (integração com JWT)
|
|
455
|
+
// ============================================================================
|
|
456
|
+
|
|
457
|
+
class TokenAuthority {
|
|
458
|
+
private privateKey: crypto.KeyObject;
|
|
459
|
+
public publicKey: crypto.KeyObject;
|
|
460
|
+
private issuer = 'urn:agentic-system:authority';
|
|
461
|
+
private audience = 'urn:agentic-system:agents';
|
|
462
|
+
|
|
463
|
+
constructor() {
|
|
464
|
+
const keys = generateKeyPair();
|
|
465
|
+
this.privateKey = crypto.createPrivateKey(keys.privateKey);
|
|
466
|
+
this.publicKey = crypto.createPublicKey(keys.publicKey);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async issueAgentToken(
|
|
470
|
+
agentId: string,
|
|
471
|
+
conversationId: string,
|
|
472
|
+
capabilities: string[] = []
|
|
473
|
+
): Promise<string> {
|
|
474
|
+
return await new SignJWT({
|
|
475
|
+
agentId,
|
|
476
|
+
conversationId,
|
|
477
|
+
capabilities,
|
|
478
|
+
encryptionProtocol: 'signal-e2ee',
|
|
479
|
+
issuedAt: Date.now()
|
|
480
|
+
})
|
|
481
|
+
.setProtectedHeader({ alg: 'EdDSA', typ: 'JWT' })
|
|
482
|
+
.setIssuedAt()
|
|
483
|
+
.setIssuer(this.issuer)
|
|
484
|
+
.setAudience(this.audience)
|
|
485
|
+
.setSubject(agentId)
|
|
486
|
+
.setExpirationTime('5m')
|
|
487
|
+
.sign(this.privateKey);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async verifyToken(token: string): Promise<any> {
|
|
491
|
+
const { payload } = await jwtVerify(token, this.publicKey, {
|
|
492
|
+
issuer: this.issuer,
|
|
493
|
+
audience: this.audience
|
|
494
|
+
});
|
|
495
|
+
return payload;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ============================================================================
|
|
500
|
+
// Agente com Signal E2EE
|
|
501
|
+
// ============================================================================
|
|
502
|
+
|
|
503
|
+
class SignalE2EEAgent extends EventEmitter {
|
|
504
|
+
readonly agentId: string;
|
|
505
|
+
private keyBundle: X3DHKeyBundle;
|
|
506
|
+
private sessions: Map<string, DoubleRatchet> = new Map();
|
|
507
|
+
private messageHistory: SignalMessage[] = [];
|
|
508
|
+
private token: string | null = null;
|
|
509
|
+
private authority: TokenAuthority;
|
|
510
|
+
private conversationId: string;
|
|
511
|
+
private peerPublicBundles: Map<string, KeyBundle> = new Map();
|
|
512
|
+
private identityKey: X3DHKeyPair;
|
|
513
|
+
|
|
514
|
+
constructor(
|
|
515
|
+
agentId: string,
|
|
516
|
+
authority: TokenAuthority,
|
|
517
|
+
capabilities: string[] = []
|
|
518
|
+
) {
|
|
519
|
+
super();
|
|
520
|
+
this.agentId = agentId;
|
|
521
|
+
this.authority = authority;
|
|
522
|
+
this.keyBundle = new X3DHKeyBundle();
|
|
523
|
+
this.identityKey = generateX25519KeyPair();
|
|
524
|
+
this.conversationId = `conv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async initialize(): Promise<void> {
|
|
528
|
+
this.token = await this.authority.issueAgentToken(
|
|
529
|
+
this.agentId,
|
|
530
|
+
this.conversationId
|
|
531
|
+
);
|
|
532
|
+
console.log(`🔐 [${this.agentId}] Agente Signal E2EE inicializado`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Retorna bundle público para troca de chaves
|
|
537
|
+
*/
|
|
538
|
+
getPublicKeyBundle(): KeyBundle {
|
|
539
|
+
return this.keyBundle.getPublicBundle();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Armazena bundle público de um peer
|
|
544
|
+
*/
|
|
545
|
+
registerPeerBundle(peerId: string, bundle: KeyBundle): void {
|
|
546
|
+
this.peerPublicBundles.set(peerId, bundle);
|
|
547
|
+
console.log(`📋 [${this.agentId}] Bundle de ${peerId} registrado`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Estabelece sessão segura com peer (como iniciador)
|
|
552
|
+
*/
|
|
553
|
+
async establishSession(peerId: string): Promise<void> {
|
|
554
|
+
const peerBundle = this.peerPublicBundles.get(peerId);
|
|
555
|
+
if (!peerBundle) {
|
|
556
|
+
throw new Error(`Bundle de ${peerId} não encontrado`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Gerar chave efêmera para X3DH
|
|
560
|
+
const ephemeralKey = generateX25519KeyPair();
|
|
561
|
+
|
|
562
|
+
// Executar X3DH como Alice
|
|
563
|
+
const sharedSecret = performX3DHAsInitiator(
|
|
564
|
+
this.identityKey,
|
|
565
|
+
ephemeralKey,
|
|
566
|
+
peerBundle
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Inicializar Double Ratchet
|
|
570
|
+
const ratchet = new DoubleRatchet();
|
|
571
|
+
ratchet.initializeAsAlice(sharedSecret, peerBundle.signedPreKey);
|
|
572
|
+
|
|
573
|
+
this.sessions.set(peerId, ratchet);
|
|
574
|
+
|
|
575
|
+
// Limpar chaves temporárias
|
|
576
|
+
ephemeralKey.privateKey.fill(0);
|
|
577
|
+
|
|
578
|
+
console.log(`🔗 [${this.agentId}] Sessão E2EE estabelecida com ${peerId}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Aceita sessão segura (como receptor)
|
|
583
|
+
*/
|
|
584
|
+
async acceptSession(
|
|
585
|
+
peerId: string,
|
|
586
|
+
senderIdentityKey: Buffer,
|
|
587
|
+
senderEphemeralKey: Buffer
|
|
588
|
+
): Promise<Buffer> {
|
|
589
|
+
// Executar X3DH como Bob
|
|
590
|
+
const sharedSecret = this.keyBundle.performX3DHAsReceiver(
|
|
591
|
+
senderEphemeralKey,
|
|
592
|
+
senderIdentityKey,
|
|
593
|
+
true
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// Inicializar Double Ratchet
|
|
597
|
+
const ratchet = new DoubleRatchet();
|
|
598
|
+
ratchet.initializeAsBob(sharedSecret);
|
|
599
|
+
|
|
600
|
+
this.sessions.set(peerId, ratchet);
|
|
601
|
+
|
|
602
|
+
console.log(`🔗 [${this.agentId}] Sessão E2EE aceita de ${peerId}`);
|
|
603
|
+
|
|
604
|
+
return ratchet.getPublicKey();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Envia mensagem encriptada
|
|
609
|
+
*/
|
|
610
|
+
async sendMessage(peerId: string, content: string): Promise<SignalMessage> {
|
|
611
|
+
const session = this.sessions.get(peerId);
|
|
612
|
+
if (!session) {
|
|
613
|
+
throw new Error(`Sessão com ${peerId} não estabelecida`);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const { header, ciphertext, nonce } = session.ratchetEncrypt(content);
|
|
617
|
+
|
|
618
|
+
const message: SignalMessage = {
|
|
619
|
+
from: this.agentId,
|
|
620
|
+
to: peerId,
|
|
621
|
+
messageId: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
622
|
+
timestamp: Date.now(),
|
|
623
|
+
header,
|
|
624
|
+
ciphertext: ciphertext.toString('hex'),
|
|
625
|
+
nonce: nonce.toString('hex'),
|
|
626
|
+
jwt: this.token || undefined
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
this.messageHistory.push(message);
|
|
630
|
+
console.log(`📤 [${this.agentId}] → [${peerId}] (E2EE): [${content.length} chars encrypted]`);
|
|
631
|
+
|
|
632
|
+
return message;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Recebe e decripta mensagem
|
|
637
|
+
*/
|
|
638
|
+
async receiveMessage(message: SignalMessage): Promise<string> {
|
|
639
|
+
// Verificar JWT se presente
|
|
640
|
+
if (message.jwt) {
|
|
641
|
+
try {
|
|
642
|
+
await this.authority.verifyToken(message.jwt);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
console.warn(`⚠️ [${this.agentId}] JWT inválido de ${message.from}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const session = this.sessions.get(message.from);
|
|
649
|
+
if (!session) {
|
|
650
|
+
throw new Error(`Sessão com ${message.from} não encontrada`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const ciphertext = Buffer.from(message.ciphertext, 'hex');
|
|
654
|
+
const nonce = Buffer.from(message.nonce, 'hex');
|
|
655
|
+
|
|
656
|
+
const plaintext = session.ratchetDecrypt(message.header, ciphertext, nonce);
|
|
657
|
+
|
|
658
|
+
this.messageHistory.push(message);
|
|
659
|
+
console.log(`📥 [${this.agentId}] ← [${message.from}] (E2EE): ${plaintext}`);
|
|
660
|
+
|
|
661
|
+
this.emit('message', { from: message.from, content: plaintext, message });
|
|
662
|
+
|
|
663
|
+
return plaintext;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
getMessageHistory(): SignalMessage[] {
|
|
667
|
+
return [...this.messageHistory];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
getIdentityPublicKey(): Buffer {
|
|
671
|
+
return this.identityKey.publicKey;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ============================================================================
|
|
676
|
+
// Demonstração
|
|
677
|
+
// ============================================================================
|
|
678
|
+
|
|
679
|
+
async function demonstrateSignalE2EE() {
|
|
680
|
+
console.log('🚀 Demonstração de Signal Protocol E2EE para Agentes\n');
|
|
681
|
+
console.log('═'.repeat(60));
|
|
682
|
+
|
|
683
|
+
// 1. Criar autoridade de tokens
|
|
684
|
+
const tokenAuthority = new TokenAuthority();
|
|
685
|
+
console.log('✅ Token Authority criada\n');
|
|
686
|
+
|
|
687
|
+
// 2. Criar agentes
|
|
688
|
+
const agentAlpha = new SignalE2EEAgent('agent-alpha', tokenAuthority, ['reasoning']);
|
|
689
|
+
const agentBeta = new SignalE2EEAgent('agent-beta', tokenAuthority, ['analysis']);
|
|
690
|
+
|
|
691
|
+
await agentAlpha.initialize();
|
|
692
|
+
await agentBeta.initialize();
|
|
693
|
+
console.log('');
|
|
694
|
+
|
|
695
|
+
// 3. Trocar bundles públicos (simulando um servidor de key distribution)
|
|
696
|
+
console.log('📦 Trocando bundles de chaves públicas...');
|
|
697
|
+
const alphaBundle = agentAlpha.getPublicKeyBundle();
|
|
698
|
+
const betaBundle = agentBeta.getPublicKeyBundle();
|
|
699
|
+
|
|
700
|
+
agentAlpha.registerPeerBundle('agent-beta', betaBundle);
|
|
701
|
+
agentBeta.registerPeerBundle('agent-alpha', alphaBundle);
|
|
702
|
+
console.log('');
|
|
703
|
+
|
|
704
|
+
// 4. Alpha estabelece sessão com Beta
|
|
705
|
+
console.log('🔐 Estabelecendo sessão E2EE...');
|
|
706
|
+
await agentAlpha.establishSession('agent-beta');
|
|
707
|
+
|
|
708
|
+
// Beta aceita a sessão (em produção, isso seria feito via primeira mensagem)
|
|
709
|
+
await agentBeta.acceptSession(
|
|
710
|
+
'agent-alpha',
|
|
711
|
+
agentAlpha.getIdentityPublicKey(),
|
|
712
|
+
agentAlpha.getPublicKeyBundle().signedPreKey
|
|
713
|
+
);
|
|
714
|
+
console.log('');
|
|
715
|
+
|
|
716
|
+
// 5. Trocar mensagens encriptadas
|
|
717
|
+
console.log('💬 Iniciando conversa E2EE...\n');
|
|
718
|
+
console.log('─'.repeat(60));
|
|
719
|
+
|
|
720
|
+
// Alpha envia primeira mensagem
|
|
721
|
+
const msg1 = await agentAlpha.sendMessage(
|
|
722
|
+
'agent-beta',
|
|
723
|
+
'Olá Beta! Esta mensagem está encriptada com Signal Protocol.'
|
|
724
|
+
);
|
|
725
|
+
await agentBeta.receiveMessage(msg1);
|
|
726
|
+
|
|
727
|
+
console.log('');
|
|
728
|
+
|
|
729
|
+
// Beta responde
|
|
730
|
+
const msg2 = await agentBeta.sendMessage(
|
|
731
|
+
'agent-alpha',
|
|
732
|
+
'Olá Alpha! Recebi sua mensagem com Perfect Forward Secrecy!'
|
|
733
|
+
);
|
|
734
|
+
await agentAlpha.receiveMessage(msg2);
|
|
735
|
+
|
|
736
|
+
console.log('');
|
|
737
|
+
|
|
738
|
+
// Alpha envia outra mensagem
|
|
739
|
+
const msg3 = await agentAlpha.sendMessage(
|
|
740
|
+
'agent-beta',
|
|
741
|
+
'O Double Ratchet garante que cada mensagem usa uma chave única.'
|
|
742
|
+
);
|
|
743
|
+
await agentBeta.receiveMessage(msg3);
|
|
744
|
+
|
|
745
|
+
console.log('');
|
|
746
|
+
|
|
747
|
+
// Beta responde novamente
|
|
748
|
+
const msg4 = await agentBeta.sendMessage(
|
|
749
|
+
'agent-alpha',
|
|
750
|
+
'Exatamente! E temos Post-Compromise Security também.'
|
|
751
|
+
);
|
|
752
|
+
await agentAlpha.receiveMessage(msg4);
|
|
753
|
+
|
|
754
|
+
console.log('');
|
|
755
|
+
console.log('─'.repeat(60));
|
|
756
|
+
|
|
757
|
+
// 6. Mostrar resumo
|
|
758
|
+
console.log('\n📊 Resumo da Demonstração:\n');
|
|
759
|
+
console.log('🔒 Propriedades de Segurança:');
|
|
760
|
+
console.log(' • Perfect Forward Secrecy (PFS)');
|
|
761
|
+
console.log(' • Post-Compromise Security (PCS)');
|
|
762
|
+
console.log(' • Chaves únicas por mensagem');
|
|
763
|
+
console.log(' • Deniability (negabilidade)');
|
|
764
|
+
console.log('');
|
|
765
|
+
console.log('🔧 Algoritmos Utilizados:');
|
|
766
|
+
console.log(' • X3DH para key agreement inicial');
|
|
767
|
+
console.log(' • X25519 para Diffie-Hellman');
|
|
768
|
+
console.log(' • HKDF-SHA256 para derivação de chaves');
|
|
769
|
+
console.log(' • AES-256-GCM para encriptação');
|
|
770
|
+
console.log(' • Double Ratchet para gestão de chaves');
|
|
771
|
+
console.log('');
|
|
772
|
+
console.log(`📨 Total de mensagens trocadas: ${agentAlpha.getMessageHistory().length * 2}`);
|
|
773
|
+
console.log('');
|
|
774
|
+
console.log('✅ Demonstração concluída!');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// Combinação com mTLS (demonstração conceitual)
|
|
779
|
+
// ============================================================================
|
|
780
|
+
|
|
781
|
+
async function demonstrateCombinedSecurity() {
|
|
782
|
+
console.log('\n');
|
|
783
|
+
console.log('═'.repeat(60));
|
|
784
|
+
console.log('🔐 Signal E2EE + mTLS: Defesa em Profundidade');
|
|
785
|
+
console.log('═'.repeat(60));
|
|
786
|
+
console.log('');
|
|
787
|
+
console.log('Quando combinados, você obtém:');
|
|
788
|
+
console.log('');
|
|
789
|
+
console.log('┌─────────────────────────────────────────────────────────┐');
|
|
790
|
+
console.log('│ CAMADA │ PROTOCOLO │ PROTEÇÃO │');
|
|
791
|
+
console.log('├─────────────────────────────────────────────────────────┤');
|
|
792
|
+
console.log('│ Transporte │ mTLS │ Autenticação mútua │');
|
|
793
|
+
console.log('│ │ │ Canal seguro │');
|
|
794
|
+
console.log('│ │ │ Anti-MITM │');
|
|
795
|
+
console.log('├─────────────────────────────────────────────────────────┤');
|
|
796
|
+
console.log('│ Aplicação │ Signal E2E │ Forward Secrecy │');
|
|
797
|
+
console.log('│ │ │ Post-Compromise Sec │');
|
|
798
|
+
console.log('│ │ │ Deniability │');
|
|
799
|
+
console.log('├─────────────────────────────────────────────────────────┤');
|
|
800
|
+
console.log('│ Contexto │ JWT │ Identity claims │');
|
|
801
|
+
console.log('│ │ │ Authorization │');
|
|
802
|
+
console.log('│ │ │ Expiration │');
|
|
803
|
+
console.log('└─────────────────────────────────────────────────────────┘');
|
|
804
|
+
console.log('');
|
|
805
|
+
console.log('💡 Benefícios da combinação:');
|
|
806
|
+
console.log(' 1. Mesmo que mTLS seja comprometido, E2EE protege o conteúdo');
|
|
807
|
+
console.log(' 2. Mesmo que E2EE seja comprometido, mTLS protege o canal');
|
|
808
|
+
console.log(' 3. JWT adiciona contexto e autorização independente');
|
|
809
|
+
console.log(' 4. Defesa em profundidade reduz superfície de ataque');
|
|
810
|
+
console.log('');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Executar demonstrações
|
|
814
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
815
|
+
demonstrateSignalE2EE()
|
|
816
|
+
.then(demonstrateCombinedSecurity)
|
|
817
|
+
.catch(console.error);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export {
|
|
821
|
+
SignalE2EEAgent,
|
|
822
|
+
DoubleRatchet,
|
|
823
|
+
X3DHKeyBundle,
|
|
824
|
+
TokenAuthority,
|
|
825
|
+
SignalMessage,
|
|
826
|
+
KeyBundle
|
|
827
|
+
};
|