@kangwifi-pro/waliwa 1.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/LICENSE +21 -0
- package/README.md +600 -0
- package/dist/advanced/features.d.ts +211 -0
- package/dist/advanced/features.d.ts.map +1 -0
- package/dist/advanced/features.js +671 -0
- package/dist/advanced/features.js.map +1 -0
- package/dist/auth/auth-state.d.ts +89 -0
- package/dist/auth/auth-state.d.ts.map +1 -0
- package/dist/auth/auth-state.js +330 -0
- package/dist/auth/auth-state.js.map +1 -0
- package/dist/auth/pairing-code.d.ts +78 -0
- package/dist/auth/pairing-code.d.ts.map +1 -0
- package/dist/auth/pairing-code.js +191 -0
- package/dist/auth/pairing-code.js.map +1 -0
- package/dist/auth/qr-code.d.ts +60 -0
- package/dist/auth/qr-code.d.ts.map +1 -0
- package/dist/auth/qr-code.js +151 -0
- package/dist/auth/qr-code.js.map +1 -0
- package/dist/calls/handler.d.ts +61 -0
- package/dist/calls/handler.d.ts.map +1 -0
- package/dist/calls/handler.js +117 -0
- package/dist/calls/handler.js.map +1 -0
- package/dist/core/binary.d.ts +74 -0
- package/dist/core/binary.d.ts.map +1 -0
- package/dist/core/binary.js +448 -0
- package/dist/core/binary.js.map +1 -0
- package/dist/core/crypto.d.ts +60 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +170 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/noise.d.ts +106 -0
- package/dist/core/noise.d.ts.map +1 -0
- package/dist/core/noise.js +307 -0
- package/dist/core/noise.js.map +1 -0
- package/dist/events/emitter.d.ts +44 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +79 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/features/index.d.ts +342 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +755 -0
- package/dist/features/index.js.map +1 -0
- package/dist/fixes/index.d.ts +333 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +762 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/groups/management.d.ts +86 -0
- package/dist/groups/management.d.ts.map +1 -0
- package/dist/groups/management.js +443 -0
- package/dist/groups/management.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/media.d.ts +93 -0
- package/dist/messages/media.d.ts.map +1 -0
- package/dist/messages/media.js +252 -0
- package/dist/messages/media.js.map +1 -0
- package/dist/messages/message-encoder.d.ts +70 -0
- package/dist/messages/message-encoder.d.ts.map +1 -0
- package/dist/messages/message-encoder.js +453 -0
- package/dist/messages/message-encoder.js.map +1 -0
- package/dist/messages/send.d.ts +101 -0
- package/dist/messages/send.d.ts.map +1 -0
- package/dist/messages/send.js +409 -0
- package/dist/messages/send.js.map +1 -0
- package/dist/recovery/index.d.ts +125 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +584 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/skdm/index.d.ts +220 -0
- package/dist/skdm/index.d.ts.map +1 -0
- package/dist/skdm/index.js +600 -0
- package/dist/skdm/index.js.map +1 -0
- package/dist/socket/hybrid.d.ts +118 -0
- package/dist/socket/hybrid.d.ts.map +1 -0
- package/dist/socket/hybrid.js +352 -0
- package/dist/socket/hybrid.js.map +1 -0
- package/dist/socket/wa-socket.d.ts +300 -0
- package/dist/socket/wa-socket.d.ts.map +1 -0
- package/dist/socket/wa-socket.js +1094 -0
- package/dist/socket/wa-socket.js.map +1 -0
- package/dist/socket/ws-socket.d.ts +96 -0
- package/dist/socket/ws-socket.d.ts.map +1 -0
- package/dist/socket/ws-socket.js +302 -0
- package/dist/socket/ws-socket.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/jid.d.ts +46 -0
- package/dist/utils/jid.d.ts.map +1 -0
- package/dist/utils/jid.js +152 -0
- package/dist/utils/jid.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +175 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/API.md +745 -0
- package/docs/ARCHITECTURE.md +307 -0
- package/docs/BAILEYS_FIXES.md +360 -0
- package/docs/FEATURES.md +532 -0
- package/docs/PROTOCOL.md +489 -0
- package/docs/RECOVERY.md +409 -0
- package/docs/SKDM.md +233 -0
- package/examples/ai-bot/index.ts +479 -0
- package/examples/ai-bot/package.json +17 -0
- package/examples/echo-bot/index.ts +171 -0
- package/examples/echo-bot/package.json +18 -0
- package/examples/group-bot/index.ts +557 -0
- package/examples/group-bot/package.json +17 -0
- package/examples/rest-gateway/index.ts +499 -0
- package/examples/rest-gateway/package.json +19 -0
- package/package.json +75 -0
- package/src/advanced/features.ts +817 -0
- package/src/auth/auth-state.ts +342 -0
- package/src/auth/pairing-code.ts +246 -0
- package/src/auth/qr-code.ts +191 -0
- package/src/calls/handler.ts +153 -0
- package/src/core/binary.ts +464 -0
- package/src/core/crypto.ts +189 -0
- package/src/core/noise.ts +406 -0
- package/src/events/emitter.ts +88 -0
- package/src/features/index.ts +921 -0
- package/src/fixes/index.ts +882 -0
- package/src/groups/management.ts +497 -0
- package/src/index.ts +274 -0
- package/src/messages/media.ts +372 -0
- package/src/messages/message-encoder.ts +520 -0
- package/src/messages/send.ts +521 -0
- package/src/recovery/index.ts +704 -0
- package/src/skdm/index.ts +693 -0
- package/src/socket/hybrid.ts +414 -0
- package/src/socket/wa-socket.ts +1347 -0
- package/src/socket/ws-socket.ts +355 -0
- package/src/types/index.ts +457 -0
- package/src/utils/jid.ts +156 -0
- package/src/utils/logger.ts +84 -0
- package/src/utils/retry.ts +205 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Noise Protocol XX handshake implementation
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp Web Multi-Device menggunakan Noise Protocol Framework (XX pattern)
|
|
5
|
+
* untuk establish secure channel antara client dan WhatsApp server.
|
|
6
|
+
*
|
|
7
|
+
* Pattern XX (lihat noiseprotocol.com):
|
|
8
|
+
* <- e
|
|
9
|
+
* -> e, ee, s, es
|
|
10
|
+
* <- s, se
|
|
11
|
+
*
|
|
12
|
+
* Handshake ini menghasilkan encryption key untuk semua komunikasi WebSocket selanjutnya.
|
|
13
|
+
*
|
|
14
|
+
* Original implementation - tidak copy dari Baileys/ZapoJS.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
generateCurveKeyPair,
|
|
19
|
+
curveSharedSecret,
|
|
20
|
+
hkdfDerive,
|
|
21
|
+
hmacSha256,
|
|
22
|
+
sha256Hash,
|
|
23
|
+
createAESCipher,
|
|
24
|
+
getRandomBytes,
|
|
25
|
+
concatBytes,
|
|
26
|
+
areBytesEqual,
|
|
27
|
+
KeyPair,
|
|
28
|
+
AESCipher
|
|
29
|
+
} from './crypto.js';
|
|
30
|
+
|
|
31
|
+
// ============== Noise Constants ==============
|
|
32
|
+
|
|
33
|
+
/** Noise protocol name - WhatsApp menggunakan "Noise_XX_25519_AESGCM_SHA256" */
|
|
34
|
+
const NOISE_PROTOCOL_NAME = 'Noise_XX_25519_AESGCM_SHA256';
|
|
35
|
+
const NOISE_HANDSHAKE_DONE = 'Noise_XX_25519_AESGCM_SHA256_handshake_done';
|
|
36
|
+
const HKDF_INFO = new TextEncoder().encode(NOISE_PROTOCOL_NAME);
|
|
37
|
+
|
|
38
|
+
// WhatsApp specific constants
|
|
39
|
+
const WA_NOISE_HEADER = new Uint8Array([0x57, 0x41]); // "WA" - 2 bytes prefix
|
|
40
|
+
const WA_VERSION = new Uint8Array([0x05, 0x2d]); // Version 2.45 encoded
|
|
41
|
+
|
|
42
|
+
// Max ciphertext size untuk WA frames
|
|
43
|
+
const MAX_FRAME_SIZE = 1024 * 1024; // 1 MB
|
|
44
|
+
|
|
45
|
+
// ============== Cipher State ==============
|
|
46
|
+
|
|
47
|
+
interface CipherState {
|
|
48
|
+
key: Uint8Array | null;
|
|
49
|
+
nonce: number; // Counter-based nonce, increments per message
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SymmetricState {
|
|
53
|
+
ck: Uint8Array; // Chaining key
|
|
54
|
+
h: Uint8Array; // Handshake hash
|
|
55
|
+
cipher: CipherState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface HandshakeState {
|
|
59
|
+
ss: SymmetricState;
|
|
60
|
+
s: KeyPair; // Static key pair
|
|
61
|
+
e: KeyPair; // Ephemeral key pair
|
|
62
|
+
rs: Uint8Array | null; // Remote static public key
|
|
63
|
+
re: Uint8Array | null; // Remote ephemeral public key
|
|
64
|
+
initiator: boolean;
|
|
65
|
+
messagePattern: ('e' | 's' | 'ee' | 'es' | 'se' | 'ss')[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============== Noise Handshake ==============
|
|
69
|
+
|
|
70
|
+
export class NoiseHandshake {
|
|
71
|
+
private state: HandshakeState;
|
|
72
|
+
private cipher: AESCipher;
|
|
73
|
+
private readonly cipherKeyLen = 32;
|
|
74
|
+
private readonly nonceLen = 12;
|
|
75
|
+
|
|
76
|
+
constructor(staticKey?: KeyPair, initiator: boolean = true) {
|
|
77
|
+
this.state = {
|
|
78
|
+
ss: this.initializeSymmetric(),
|
|
79
|
+
s: staticKey ?? generateCurveKeyPair(),
|
|
80
|
+
e: generateCurveKeyPair(),
|
|
81
|
+
rs: null,
|
|
82
|
+
re: null,
|
|
83
|
+
initiator,
|
|
84
|
+
messagePattern: ['e', 'e', 'ee', 's', 'es', 's', 'se']
|
|
85
|
+
};
|
|
86
|
+
this.cipher = createAESCipher(new Uint8Array(32));
|
|
87
|
+
// Mix protocol name into hash
|
|
88
|
+
this.state.ss.h = sha256Hash(new TextEncoder().encode(NOISE_PROTOCOL_NAME));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private initializeSymmetric(): SymmetricState {
|
|
92
|
+
const protocolHash = sha256Hash(new TextEncoder().encode(NOISE_PROTOCOL_NAME));
|
|
93
|
+
return {
|
|
94
|
+
ck: protocolHash,
|
|
95
|
+
h: protocolHash,
|
|
96
|
+
cipher: { key: null, nonce: 0 }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mix key into chaining key + derive new cipher key
|
|
102
|
+
*/
|
|
103
|
+
private mixKey(inputKey: Uint8Array): void {
|
|
104
|
+
const derived = hkdfDerive(
|
|
105
|
+
inputKey,
|
|
106
|
+
this.state.ss.ck,
|
|
107
|
+
new Uint8Array(0),
|
|
108
|
+
64
|
|
109
|
+
);
|
|
110
|
+
// Split into 32-byte ck and 32-byte key
|
|
111
|
+
this.state.ss.ck = derived.subarray(0, 32);
|
|
112
|
+
this.state.ss.cipher.key = derived.subarray(32, 64);
|
|
113
|
+
this.state.ss.cipher.nonce = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mix hash data into handshake hash
|
|
118
|
+
*/
|
|
119
|
+
private mixHash(data: Uint8Array): void {
|
|
120
|
+
this.state.ss.h = sha256Hash(concatBytes(this.state.ss.h, data));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Encrypt dengan handshake key
|
|
125
|
+
*/
|
|
126
|
+
private encryptWithHash(plaintext: Uint8Array): Uint8Array {
|
|
127
|
+
if (!this.state.ss.cipher.key) {
|
|
128
|
+
return plaintext;
|
|
129
|
+
}
|
|
130
|
+
const iv = this.generateNonce(this.state.ss.cipher.nonce);
|
|
131
|
+
const { ciphertext, tag } = this.cipher.encrypt(
|
|
132
|
+
plaintext,
|
|
133
|
+
iv,
|
|
134
|
+
this.state.ss.h
|
|
135
|
+
);
|
|
136
|
+
this.state.ss.cipher.nonce++;
|
|
137
|
+
return concatBytes(ciphertext, tag);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decrypt dengan handshake key
|
|
142
|
+
*/
|
|
143
|
+
private decryptWithHash(ciphertext: Uint8Array): Uint8Array {
|
|
144
|
+
if (!this.state.ss.cipher.key) {
|
|
145
|
+
return ciphertext;
|
|
146
|
+
}
|
|
147
|
+
if (ciphertext.length < 16) {
|
|
148
|
+
throw new Error('Ciphertext too short');
|
|
149
|
+
}
|
|
150
|
+
const iv = this.generateNonce(this.state.ss.cipher.nonce);
|
|
151
|
+
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
152
|
+
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
153
|
+
const plaintext = this.cipher.decrypt(ct, iv, tag, this.state.ss.h);
|
|
154
|
+
this.state.ss.cipher.nonce++;
|
|
155
|
+
return plaintext;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate GCM nonce dari counter (big-endian, 4-byte prefix zero)
|
|
160
|
+
*/
|
|
161
|
+
private generateNonce(counter: number): Uint8Array {
|
|
162
|
+
const nonce = new Uint8Array(12);
|
|
163
|
+
// Last 8 bytes = counter (big-endian)
|
|
164
|
+
const view = new DataView(nonce.buffer);
|
|
165
|
+
view.setUint32(4, Math.floor(counter / 0x100000000));
|
|
166
|
+
view.setUint32(8, counter & 0xffffffff);
|
|
167
|
+
return nonce;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============== Public Handshake Methods ==============
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Step 1: Send ephemeral key (initiator -> responder)
|
|
174
|
+
* Output: bytes untuk dikirim ke server
|
|
175
|
+
*/
|
|
176
|
+
public writeEphemeral(): Uint8Array {
|
|
177
|
+
const e = generateCurveKeyPair();
|
|
178
|
+
this.state.e = e;
|
|
179
|
+
this.mixHash(e.public);
|
|
180
|
+
this.mixKey(e.public);
|
|
181
|
+
return e.public;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Step 2: Receive ephemeral + static + EE + ES from responder
|
|
186
|
+
*/
|
|
187
|
+
public readResponderHandshake(payload: Uint8Array): { plaintext: Uint8Array } {
|
|
188
|
+
// First 32 bytes = ephemeral key
|
|
189
|
+
if (payload.length < 32) throw new Error('Handshake too short');
|
|
190
|
+
const re = payload.subarray(0, 32);
|
|
191
|
+
this.state.re = re;
|
|
192
|
+
this.mixHash(re);
|
|
193
|
+
this.mixKey(re);
|
|
194
|
+
|
|
195
|
+
// Ephemeral-ephemeral DH
|
|
196
|
+
const ee = curveSharedSecret(this.state.e.private, re);
|
|
197
|
+
this.mixKey(ee);
|
|
198
|
+
|
|
199
|
+
// Next: encrypted static key + payload
|
|
200
|
+
const encryptedStatic = payload.subarray(32);
|
|
201
|
+
const remoteStatic = this.decryptWithHash(encryptedStatic);
|
|
202
|
+
this.state.rs = remoteStatic.subarray(0, 32);
|
|
203
|
+
this.mixHash(remoteStatic);
|
|
204
|
+
|
|
205
|
+
// Ephemeral-static DH
|
|
206
|
+
const es = curveSharedSecret(this.state.e.private, remoteStatic);
|
|
207
|
+
this.mixKey(es);
|
|
208
|
+
|
|
209
|
+
// Remaining: encrypted payload
|
|
210
|
+
const encryptedPayload = remoteStatic.subarray(32);
|
|
211
|
+
const payload2 = this.decryptWithHash(encryptedPayload);
|
|
212
|
+
|
|
213
|
+
return { plaintext: payload2 };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Step 3: Send our static + ES + payload (initiator -> responder)
|
|
218
|
+
*/
|
|
219
|
+
public writeInitiatorHandshake(payload: Uint8Array): Uint8Array {
|
|
220
|
+
// Send ephemeral already done in step 1
|
|
221
|
+
// Now send static + ES + payload, all encrypted with current handshake key
|
|
222
|
+
|
|
223
|
+
// Encrypt our static key
|
|
224
|
+
const encryptedStatic = this.encryptWithHash(this.state.s.public);
|
|
225
|
+
this.mixHash(this.state.s.public);
|
|
226
|
+
|
|
227
|
+
// Static-ephemeral DH (us.remote ephemeral)
|
|
228
|
+
if (!this.state.re) throw new Error('No remote ephemeral');
|
|
229
|
+
const se = curveSharedSecret(this.state.s.private, this.state.re);
|
|
230
|
+
this.mixKey(se);
|
|
231
|
+
|
|
232
|
+
// Encrypt payload
|
|
233
|
+
const encryptedPayload = this.encryptWithHash(payload);
|
|
234
|
+
|
|
235
|
+
return concatBytes(encryptedStatic, encryptedPayload);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Finalize handshake - split prk into send/recv keys
|
|
240
|
+
*/
|
|
241
|
+
public finalize(): { sendKey: Uint8Array; recvKey: Uint8Array } {
|
|
242
|
+
const derived = hkdfDerive(
|
|
243
|
+
new Uint8Array(0),
|
|
244
|
+
this.state.ss.ck,
|
|
245
|
+
new TextEncoder().encode(NOISE_HANDSHAKE_DONE),
|
|
246
|
+
64
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
sendKey: derived.subarray(0, 32),
|
|
250
|
+
recvKey: derived.subarray(32, 64)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============== Post-handshake AEAD ==============
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Encrypt a frame post-handshake menggunakan traffic keys
|
|
258
|
+
*/
|
|
259
|
+
public static encryptFrame(
|
|
260
|
+
key: Uint8Array,
|
|
261
|
+
counter: number,
|
|
262
|
+
plaintext: Uint8Array
|
|
263
|
+
): Uint8Array {
|
|
264
|
+
const cipher = createAESCipher(key);
|
|
265
|
+
const iv = new Uint8Array(12);
|
|
266
|
+
const view = new DataView(iv.buffer);
|
|
267
|
+
view.setUint32(4, Math.floor(counter / 0x100000000));
|
|
268
|
+
view.setUint32(8, counter & 0xffffffff);
|
|
269
|
+
const { ciphertext, tag } = cipher.encrypt(plaintext, iv);
|
|
270
|
+
return concatBytes(ciphertext, tag);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Decrypt a frame post-handshake
|
|
275
|
+
*/
|
|
276
|
+
public static decryptFrame(
|
|
277
|
+
key: Uint8Array,
|
|
278
|
+
counter: number,
|
|
279
|
+
ciphertext: Uint8Array
|
|
280
|
+
): Uint8Array {
|
|
281
|
+
if (ciphertext.length < 16) throw new Error('Frame too short');
|
|
282
|
+
const cipher = createAESCipher(key);
|
|
283
|
+
const iv = new Uint8Array(12);
|
|
284
|
+
const view = new DataView(iv.buffer);
|
|
285
|
+
view.setUint32(4, Math.floor(counter / 0x100000000));
|
|
286
|
+
view.setUint32(8, counter & 0xffffffff);
|
|
287
|
+
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
288
|
+
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
289
|
+
return cipher.decrypt(ct, iv, tag);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============== Frame helpers ==============
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Encode WA noise frame: [WA_HEADER] [VERSION] [LENGTH:3] [PAYLOAD]
|
|
296
|
+
*/
|
|
297
|
+
public static encodeFrame(payload: Uint8Array): Uint8Array {
|
|
298
|
+
const len = payload.length;
|
|
299
|
+
const header = new Uint8Array(3 + 2 + 2); // WA + version + 16-bit length placeholder (3 byte for >64KB)
|
|
300
|
+
|
|
301
|
+
// WA header
|
|
302
|
+
header[0] = 0x57; // 'W'
|
|
303
|
+
header[1] = 0x41; // 'A'
|
|
304
|
+
// Version
|
|
305
|
+
header[2] = 0x05;
|
|
306
|
+
header[3] = 0x2d;
|
|
307
|
+
// Length (3 bytes big-endian for up to 16MB)
|
|
308
|
+
header[4] = (len >> 16) & 0xff;
|
|
309
|
+
header[5] = (len >> 8) & 0xff;
|
|
310
|
+
header[6] = len & 0xff;
|
|
311
|
+
|
|
312
|
+
return concatBytes(header.subarray(0, 7), payload);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Encode WA client payload (untuk handshake init message)
|
|
317
|
+
* Format: [ephemeral_pubkey:32] [static_encrypted] [payload_encrypted]
|
|
318
|
+
*/
|
|
319
|
+
public static encodeClientHello(
|
|
320
|
+
ephemeralPub: Uint8Array,
|
|
321
|
+
encryptedStatic: Uint8Array,
|
|
322
|
+
encryptedPayload: Uint8Array
|
|
323
|
+
): Uint8Array {
|
|
324
|
+
return concatBytes(ephemeralPub, encryptedStatic, encryptedPayload);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Build client hello untuk initial connection
|
|
329
|
+
*/
|
|
330
|
+
public buildClientHello(clientPayload: Uint8Array): Uint8Array {
|
|
331
|
+
// Step 1 + 3 combined for client hello
|
|
332
|
+
const ephemeral = this.writeEphemeral();
|
|
333
|
+
const encrypted = this.writeInitiatorHandshake(clientPayload);
|
|
334
|
+
return concatBytes(ephemeral, encrypted);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============== Traffic Cipher (post-handshake) ==============
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Traffic cipher untuk encrypt/decrypt frames setelah handshake done
|
|
342
|
+
*/
|
|
343
|
+
export class TrafficCipher {
|
|
344
|
+
private sendKey: Uint8Array;
|
|
345
|
+
private recvKey: Uint8Array;
|
|
346
|
+
private sendCounter: number = 0;
|
|
347
|
+
private recvCounter: number = 0;
|
|
348
|
+
|
|
349
|
+
constructor(sendKey: Uint8Array, recvKey: Uint8Array) {
|
|
350
|
+
this.sendKey = sendKey;
|
|
351
|
+
this.recvKey = recvKey;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
encrypt(plaintext: Uint8Array): Uint8Array {
|
|
355
|
+
const result = NoiseHandshake.encryptFrame(this.sendKey, this.sendCounter, plaintext);
|
|
356
|
+
this.sendCounter++;
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
decrypt(ciphertext: Uint8Array): Uint8Array {
|
|
361
|
+
const plaintext = NoiseHandshake.decryptFrame(this.recvKey, this.recvCounter, ciphertext);
|
|
362
|
+
this.recvCounter++;
|
|
363
|
+
return plaintext;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
reset() {
|
|
367
|
+
this.sendCounter = 0;
|
|
368
|
+
this.recvCounter = 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============== Client Payload Builder ==============
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Build initial client payload untuk WA Web multi-device connect
|
|
376
|
+
* Berisi info browser, version, dan props
|
|
377
|
+
*/
|
|
378
|
+
export function buildClientPayload(browser: [string, string, string]): Uint8Array {
|
|
379
|
+
// Encoded sebagai WA binary node
|
|
380
|
+
const enc = new TextEncoder();
|
|
381
|
+
const parts: Uint8Array[] = [];
|
|
382
|
+
|
|
383
|
+
// Browser info: [name, short_name, version]
|
|
384
|
+
parts.push(enc.encode(browser[0]));
|
|
385
|
+
parts.push(enc.encode(browser[1]));
|
|
386
|
+
parts.push(enc.encode(browser[2]));
|
|
387
|
+
|
|
388
|
+
// Pairing static key (32 bytes) - atau empty jika belum ada
|
|
389
|
+
// This is generated at first connection and stored in creds
|
|
390
|
+
// For first-time: generate new
|
|
391
|
+
const pairingKey = generateCurveKeyPair();
|
|
392
|
+
parts.push(pairingKey.public);
|
|
393
|
+
|
|
394
|
+
// Compute total length
|
|
395
|
+
let totalLen = 0;
|
|
396
|
+
for (const p of parts) totalLen += p.length;
|
|
397
|
+
|
|
398
|
+
// Prepend each part with length byte (assuming < 256)
|
|
399
|
+
const result: number[] = [];
|
|
400
|
+
for (const p of parts) {
|
|
401
|
+
result.push(p.length);
|
|
402
|
+
for (let i = 0; i < p.length; i++) result.push(p[i]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return new Uint8Array(result);
|
|
406
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Event emitter untuk WebSocket events
|
|
3
|
+
* Typed event emitter untuk WhatsApp events (messages, presence, calls, dll)
|
|
4
|
+
*
|
|
5
|
+
* RAM optimization: gunakan Map untuk handlers, weakly referenced jika possible
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import type { WAEventMap } from '../types/index.js';
|
|
10
|
+
|
|
11
|
+
export class WAEventEmitter extends EventEmitter {
|
|
12
|
+
/**
|
|
13
|
+
* Listen untuk specific event
|
|
14
|
+
*/
|
|
15
|
+
on<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
|
|
16
|
+
return super.on(event, listener as any);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Listen once
|
|
21
|
+
*/
|
|
22
|
+
once<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
|
|
23
|
+
return super.once(event, listener as any);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Listen dengan off support
|
|
28
|
+
*/
|
|
29
|
+
addListener<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
|
|
30
|
+
return super.addListener(event, listener as any);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Emit event ke all listeners
|
|
35
|
+
*/
|
|
36
|
+
emit<K extends keyof WAEventMap>(event: K, payload: WAEventMap[K]): boolean {
|
|
37
|
+
return super.emit(event, payload);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Remove listener
|
|
42
|
+
*/
|
|
43
|
+
off<K extends keyof WAEventMap>(event: K, listener: (payload: WAEventMap[K]) => void): this {
|
|
44
|
+
return super.off(event, listener as any);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wait for event dengan timeout
|
|
49
|
+
*/
|
|
50
|
+
async waitFor<K extends keyof WAEventMap>(
|
|
51
|
+
event: K,
|
|
52
|
+
timeoutMs: number = 30000
|
|
53
|
+
): Promise<WAEventMap[K]> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
this.off(event, handler);
|
|
57
|
+
reject(new Error(`Timeout waiting for ${String(event)}`));
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
|
|
60
|
+
const handler = (payload: WAEventMap[K]) => {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
this.off(event, handler);
|
|
63
|
+
resolve(payload);
|
|
64
|
+
};
|
|
65
|
+
this.on(event, handler);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strict event emitter - hanya registered events yang di-allow
|
|
72
|
+
* Berguna untuk catching typos dalam event names
|
|
73
|
+
*/
|
|
74
|
+
export class StrictEventEmitter extends WAEventEmitter {
|
|
75
|
+
private knownEvents: Set<string>;
|
|
76
|
+
|
|
77
|
+
constructor(events: (keyof WAEventMap)[]) {
|
|
78
|
+
super();
|
|
79
|
+
this.knownEvents = new Set(events as string[]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
emit<K extends keyof WAEventMap>(event: K, payload: WAEventMap[K]): boolean {
|
|
83
|
+
if (!this.knownEvents.has(event as string)) {
|
|
84
|
+
throw new Error(`Unknown event: ${String(event)}`);
|
|
85
|
+
}
|
|
86
|
+
return super.emit(event, payload);
|
|
87
|
+
}
|
|
88
|
+
}
|