@cdteam/cdwa 0.0.0-stage → 0.1.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 +112 -2
- package/dist/auth/index.cjs +191 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +25 -0
- package/dist/auth/index.d.ts +25 -0
- package/dist/auth/index.js +183 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/binary/index.cjs +414 -0
- package/dist/binary/index.cjs.map +1 -0
- package/dist/binary/index.d.cts +92 -0
- package/dist/binary/index.d.ts +92 -0
- package/dist/binary/index.js +407 -0
- package/dist/binary/index.js.map +1 -0
- package/dist/crypto/index.cjs +207 -0
- package/dist/crypto/index.cjs.map +1 -0
- package/dist/crypto/index.d.cts +91 -0
- package/dist/crypto/index.d.ts +91 -0
- package/dist/crypto/index.js +201 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/index-CcMoHfPz.d.cts +70 -0
- package/dist/index-CcMoHfPz.d.ts +70 -0
- package/dist/index.cjs +1254 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +97 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +1224 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/index.cjs +662 -0
- package/dist/messages/index.cjs.map +1 -0
- package/dist/messages/index.d.cts +122 -0
- package/dist/messages/index.d.ts +122 -0
- package/dist/messages/index.js +659 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/signal/index.cjs +38 -0
- package/dist/signal/index.cjs.map +1 -0
- package/dist/signal/index.d.cts +30 -0
- package/dist/signal/index.d.ts +30 -0
- package/dist/signal/index.js +36 -0
- package/dist/signal/index.js.map +1 -0
- package/package.json +91 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var ed25519 = require('@noble/curves/ed25519');
|
|
6
|
+
var hkdf = require('@noble/hashes/hkdf');
|
|
7
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
8
|
+
var aes = require('@noble/ciphers/aes');
|
|
9
|
+
var crypto = require('crypto');
|
|
10
|
+
var fs = require('fs');
|
|
11
|
+
var path = require('path');
|
|
12
|
+
var WebSocket = require('ws');
|
|
13
|
+
var events = require('events');
|
|
14
|
+
|
|
15
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
|
|
17
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
18
|
+
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
19
|
+
|
|
20
|
+
// src/crypto/index.ts
|
|
21
|
+
var NOISE_PROTOCOL_NAME = "Noise_XX_25519_AESGCM_SHA256";
|
|
22
|
+
var WA_NOISE_HEADER = new Uint8Array([87, 65, 6, 2]);
|
|
23
|
+
var NoiseCipherState = class {
|
|
24
|
+
key = null;
|
|
25
|
+
nonce = 0;
|
|
26
|
+
constructor(key) {
|
|
27
|
+
if (key) {
|
|
28
|
+
this.key = key;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
hasKey() {
|
|
32
|
+
return this.key !== null;
|
|
33
|
+
}
|
|
34
|
+
setKey(key) {
|
|
35
|
+
this.key = key;
|
|
36
|
+
this.nonce = 0;
|
|
37
|
+
}
|
|
38
|
+
encryptWithAd(ad, plaintext) {
|
|
39
|
+
if (!this.key) {
|
|
40
|
+
return plaintext;
|
|
41
|
+
}
|
|
42
|
+
const iv = this.generateNonceBuffer(this.nonce);
|
|
43
|
+
this.nonce += 1;
|
|
44
|
+
return CryptoEngine.aesGcmEncrypt(this.key, plaintext, iv, ad);
|
|
45
|
+
}
|
|
46
|
+
decryptWithAd(ad, ciphertext) {
|
|
47
|
+
if (!this.key) {
|
|
48
|
+
return ciphertext;
|
|
49
|
+
}
|
|
50
|
+
const iv = this.generateNonceBuffer(this.nonce);
|
|
51
|
+
this.nonce += 1;
|
|
52
|
+
return CryptoEngine.aesGcmDecrypt(this.key, ciphertext, iv, ad);
|
|
53
|
+
}
|
|
54
|
+
generateNonceBuffer(nonce) {
|
|
55
|
+
const buffer = new Uint8Array(12);
|
|
56
|
+
const view = new DataView(buffer.buffer);
|
|
57
|
+
view.setUint32(8, nonce, false);
|
|
58
|
+
return buffer;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var NoiseHandshake = class {
|
|
62
|
+
symmetricState;
|
|
63
|
+
localEphemeral;
|
|
64
|
+
localStatic;
|
|
65
|
+
remoteEphemeral = null;
|
|
66
|
+
remoteStatic = null;
|
|
67
|
+
constructor(localStaticKey) {
|
|
68
|
+
this.localStatic = localStaticKey;
|
|
69
|
+
this.localEphemeral = CryptoEngine.generateKeyPair();
|
|
70
|
+
const protocolHash = sha256.sha256(new TextEncoder().encode(NOISE_PROTOCOL_NAME));
|
|
71
|
+
this.symmetricState = {
|
|
72
|
+
h: new Uint8Array(protocolHash),
|
|
73
|
+
ck: new Uint8Array(protocolHash),
|
|
74
|
+
cipher: new NoiseCipherState()
|
|
75
|
+
};
|
|
76
|
+
this.mixHash(WA_NOISE_HEADER);
|
|
77
|
+
}
|
|
78
|
+
mixHash(data) {
|
|
79
|
+
const merged = new Uint8Array(this.symmetricState.h.length + data.length);
|
|
80
|
+
merged.set(this.symmetricState.h, 0);
|
|
81
|
+
merged.set(data, this.symmetricState.h.length);
|
|
82
|
+
this.symmetricState.h = sha256.sha256(merged);
|
|
83
|
+
}
|
|
84
|
+
mixKey(data) {
|
|
85
|
+
const derived = CryptoEngine.deriveKey(data, "", this.symmetricState.ck, 64);
|
|
86
|
+
this.symmetricState.ck = derived.slice(0, 32);
|
|
87
|
+
const tempK = derived.slice(32, 64);
|
|
88
|
+
this.symmetricState.cipher.setKey(tempK);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Passo 1: Início do Handshake -> Envia chave efêmera local (e)
|
|
92
|
+
*/
|
|
93
|
+
startHandshake() {
|
|
94
|
+
this.mixHash(this.localEphemeral.public);
|
|
95
|
+
const message = new Uint8Array(WA_NOISE_HEADER.length + this.localEphemeral.public.length);
|
|
96
|
+
message.set(WA_NOISE_HEADER, 0);
|
|
97
|
+
message.set(this.localEphemeral.public, WA_NOISE_HEADER.length);
|
|
98
|
+
return message;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Passo 2: Recebe resposta do servidor WhatsApp <- (e, ee, s, es)
|
|
102
|
+
*/
|
|
103
|
+
processServerHello(serverBuffer) {
|
|
104
|
+
this.remoteEphemeral = serverBuffer.slice(0, 32);
|
|
105
|
+
this.mixHash(this.remoteEphemeral);
|
|
106
|
+
const ee = CryptoEngine.sharedKey(this.localEphemeral.private, this.remoteEphemeral);
|
|
107
|
+
this.mixKey(ee);
|
|
108
|
+
const encryptedStatic = serverBuffer.slice(32, 80);
|
|
109
|
+
this.remoteStatic = this.symmetricState.cipher.decryptWithAd(this.symmetricState.h, encryptedStatic);
|
|
110
|
+
this.mixHash(encryptedStatic);
|
|
111
|
+
const es = CryptoEngine.sharedKey(this.localEphemeral.private, this.remoteStatic);
|
|
112
|
+
this.mixKey(es);
|
|
113
|
+
const encryptedPayload = serverBuffer.slice(80);
|
|
114
|
+
const decryptedPayload = this.symmetricState.cipher.decryptWithAd(this.symmetricState.h, encryptedPayload);
|
|
115
|
+
this.mixHash(encryptedPayload);
|
|
116
|
+
return decryptedPayload;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Passo 3: Envia resposta final de autenticação do cliente -> (s, se) + payload
|
|
120
|
+
* Retorna os CipherStates de leitura e escrita para uso no WebSocket
|
|
121
|
+
*/
|
|
122
|
+
finishHandshake(payload) {
|
|
123
|
+
const encryptedStatic = this.symmetricState.cipher.encryptWithAd(this.symmetricState.h, this.localStatic.public);
|
|
124
|
+
this.mixHash(encryptedStatic);
|
|
125
|
+
if (!this.remoteEphemeral) {
|
|
126
|
+
throw new Error("Remote ephemeral key is missing in Noise Handshake");
|
|
127
|
+
}
|
|
128
|
+
const se = CryptoEngine.sharedKey(this.localStatic.private, this.remoteEphemeral);
|
|
129
|
+
this.mixKey(se);
|
|
130
|
+
const encryptedPayload = this.symmetricState.cipher.encryptWithAd(this.symmetricState.h, payload);
|
|
131
|
+
this.mixHash(encryptedPayload);
|
|
132
|
+
const handshakeMessage = new Uint8Array(encryptedStatic.length + encryptedPayload.length);
|
|
133
|
+
handshakeMessage.set(encryptedStatic, 0);
|
|
134
|
+
handshakeMessage.set(encryptedPayload, encryptedStatic.length);
|
|
135
|
+
const transportKeys = CryptoEngine.deriveKey(new Uint8Array(0), "", this.symmetricState.ck, 64);
|
|
136
|
+
const writeKey = transportKeys.slice(0, 32);
|
|
137
|
+
const readKey = transportKeys.slice(32, 64);
|
|
138
|
+
return {
|
|
139
|
+
handshakeMessage,
|
|
140
|
+
writeCipher: new NoiseCipherState(writeKey),
|
|
141
|
+
readCipher: new NoiseCipherState(readKey)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/crypto/index.ts
|
|
147
|
+
var CryptoEngine = class {
|
|
148
|
+
/**
|
|
149
|
+
* Gera um novo par de chaves Curve25519 (Diffie-Hellman)
|
|
150
|
+
*/
|
|
151
|
+
static generateKeyPair() {
|
|
152
|
+
const priv = crypto.randomBytes(32);
|
|
153
|
+
const pub = ed25519.x25519.getPublicKey(priv);
|
|
154
|
+
return {
|
|
155
|
+
private: new Uint8Array(priv),
|
|
156
|
+
public: new Uint8Array(pub)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Executa Diffie-Hellman compartilhado (Shared Secret) usando Curve25519
|
|
161
|
+
*/
|
|
162
|
+
static sharedKey(privateKey, publicKey) {
|
|
163
|
+
return ed25519.x25519.getSharedSecret(privateKey, publicKey);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* HKDF baseado em SHA-256 para derivação segura de chaves (RFC 5869)
|
|
167
|
+
*/
|
|
168
|
+
static deriveKey(secret, info, salt, length = 32) {
|
|
169
|
+
const infoBytes = typeof info === "string" ? new TextEncoder().encode(info) : info;
|
|
170
|
+
return hkdf.hkdf(sha256.sha256, secret, salt, infoBytes, length);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Encriptação autenticada com AES-256-GCM
|
|
174
|
+
*/
|
|
175
|
+
static aesGcmEncrypt(key, plaintext, iv, additionalData) {
|
|
176
|
+
const cipher = aes.gcm(key, iv, additionalData);
|
|
177
|
+
return cipher.encrypt(plaintext);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Decriptação autenticada com AES-256-GCM
|
|
181
|
+
*/
|
|
182
|
+
static aesGcmDecrypt(key, ciphertext, iv, additionalData) {
|
|
183
|
+
const cipher = aes.gcm(key, iv, additionalData);
|
|
184
|
+
return cipher.decrypt(ciphertext);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Encripta um payload com uma senha mestra (para proteger credenciais at-rest)
|
|
188
|
+
*/
|
|
189
|
+
static encryptAtRest(payload, masterPassphrase) {
|
|
190
|
+
const salt = crypto.randomBytes(16);
|
|
191
|
+
const iv = crypto.randomBytes(12);
|
|
192
|
+
const key = hkdf.hkdf(sha256.sha256, new TextEncoder().encode(masterPassphrase), salt, new TextEncoder().encode("cdwa-at-rest"), 32);
|
|
193
|
+
const encrypted = this.aesGcmEncrypt(key, payload, iv);
|
|
194
|
+
return {
|
|
195
|
+
salt: Buffer.from(salt).toString("hex"),
|
|
196
|
+
iv: Buffer.from(iv).toString("hex"),
|
|
197
|
+
data: Buffer.from(encrypted).toString("base64")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Decripta um payload protegido com a senha mestra
|
|
202
|
+
*/
|
|
203
|
+
static decryptAtRest(encryptedBundle, masterPassphrase) {
|
|
204
|
+
const salt = Buffer.from(encryptedBundle.salt, "hex");
|
|
205
|
+
const iv = Buffer.from(encryptedBundle.iv, "hex");
|
|
206
|
+
const data = Buffer.from(encryptedBundle.data, "base64");
|
|
207
|
+
const key = hkdf.hkdf(sha256.sha256, new TextEncoder().encode(masterPassphrase), salt, new TextEncoder().encode("cdwa-at-rest"), 32);
|
|
208
|
+
return this.aesGcmDecrypt(key, data, iv);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
function initAuthCreds() {
|
|
212
|
+
const identityKeyPair = CryptoEngine.generateKeyPair();
|
|
213
|
+
const noiseKey = CryptoEngine.generateKeyPair();
|
|
214
|
+
const pairingEphemeral = CryptoEngine.generateKeyPair();
|
|
215
|
+
const signedPreKey = CryptoEngine.generateKeyPair();
|
|
216
|
+
const signedSignature = CryptoEngine.sharedKey(identityKeyPair.private, signedPreKey.public);
|
|
217
|
+
return {
|
|
218
|
+
noiseKey,
|
|
219
|
+
pairingEphemeralKeyPair: pairingEphemeral,
|
|
220
|
+
signedIdentityKey: identityKeyPair,
|
|
221
|
+
signedPreKey: {
|
|
222
|
+
...signedPreKey,
|
|
223
|
+
keyId: 1,
|
|
224
|
+
signature: signedSignature
|
|
225
|
+
},
|
|
226
|
+
registrationId: Math.floor(Math.random() * 16384),
|
|
227
|
+
advSecretKey: Buffer.from(crypto.randomBytes(32)).toString("base64"),
|
|
228
|
+
firstUnuploadedPreKeyId: 1,
|
|
229
|
+
nextPreKeyId: 1
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
var EncryptedFileAuthState = class {
|
|
233
|
+
folderPath;
|
|
234
|
+
passphrase;
|
|
235
|
+
constructor(folderPath, passphrase) {
|
|
236
|
+
this.folderPath = folderPath;
|
|
237
|
+
this.passphrase = passphrase;
|
|
238
|
+
}
|
|
239
|
+
async ensureDir() {
|
|
240
|
+
await fs.promises.mkdir(this.folderPath, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
getFilePath(filename) {
|
|
243
|
+
return path__default.default.join(this.folderPath, `${filename}.json`);
|
|
244
|
+
}
|
|
245
|
+
async save(key, data) {
|
|
246
|
+
await this.ensureDir();
|
|
247
|
+
const filePath = this.getFilePath(key);
|
|
248
|
+
const jsonString = JSON.stringify(data, (_, value) => {
|
|
249
|
+
if (value instanceof Uint8Array) {
|
|
250
|
+
return { __type: "Buffer", data: Buffer.from(value).toString("base64") };
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
});
|
|
254
|
+
if (this.passphrase) {
|
|
255
|
+
const encrypted = CryptoEngine.encryptAtRest(new TextEncoder().encode(jsonString), this.passphrase);
|
|
256
|
+
await fs.promises.writeFile(filePath, JSON.stringify(encrypted), "utf-8");
|
|
257
|
+
} else {
|
|
258
|
+
await fs.promises.writeFile(filePath, jsonString, "utf-8");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async load(key) {
|
|
262
|
+
const filePath = this.getFilePath(key);
|
|
263
|
+
try {
|
|
264
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
265
|
+
let jsonString = content;
|
|
266
|
+
if (this.passphrase) {
|
|
267
|
+
const bundle = JSON.parse(content);
|
|
268
|
+
const decryptedBytes = CryptoEngine.decryptAtRest(bundle, this.passphrase);
|
|
269
|
+
jsonString = new TextDecoder().decode(decryptedBytes);
|
|
270
|
+
}
|
|
271
|
+
return JSON.parse(jsonString, (_, value) => {
|
|
272
|
+
if (value && value.__type === "Buffer") {
|
|
273
|
+
return new Uint8Array(Buffer.from(value.data, "base64"));
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
});
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
createKeyStore() {
|
|
282
|
+
return {
|
|
283
|
+
get: async (type, ids) => {
|
|
284
|
+
const result = {};
|
|
285
|
+
for (const id of ids) {
|
|
286
|
+
const item = await this.load(`${type}-${id}`);
|
|
287
|
+
if (item) {
|
|
288
|
+
result[id] = item;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
},
|
|
293
|
+
set: async (data) => {
|
|
294
|
+
for (const [type, entries] of Object.entries(data)) {
|
|
295
|
+
for (const [id, value] of Object.entries(entries)) {
|
|
296
|
+
await this.save(`${type}-${id}`, value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
async function useSecureAuthState(folderPath, passphrase) {
|
|
304
|
+
const storage = new EncryptedFileAuthState(folderPath, passphrase);
|
|
305
|
+
let creds = await storage.load("creds");
|
|
306
|
+
if (!creds) {
|
|
307
|
+
creds = initAuthCreds();
|
|
308
|
+
await storage.save("creds", creds);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
creds,
|
|
312
|
+
keys: storage.createKeyStore()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/signal/index.ts
|
|
317
|
+
var SignalEngine = class {
|
|
318
|
+
keyStore;
|
|
319
|
+
constructor(keyStore) {
|
|
320
|
+
this.keyStore = keyStore;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Inicializa ou recupera uma sessão de Double Ratchet com um contato/dispositivo
|
|
324
|
+
*/
|
|
325
|
+
async getSession(address) {
|
|
326
|
+
const sessions = await this.keyStore.get("session", [address]);
|
|
327
|
+
return sessions[address] || null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Salva uma sessão atualizada após troca de chaves
|
|
331
|
+
*/
|
|
332
|
+
async saveSession(address, sessionData) {
|
|
333
|
+
await this.keyStore.set({
|
|
334
|
+
session: {
|
|
335
|
+
[address]: sessionData
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Gerenciamento de chaves de remetente (Sender Keys) para envio otimizado em grupos
|
|
341
|
+
*/
|
|
342
|
+
async getGroupSenderKey(groupId, senderId) {
|
|
343
|
+
const keyKey = `${groupId}::${senderId}`;
|
|
344
|
+
const keys = await this.keyStore.get("sender-key", [keyKey]);
|
|
345
|
+
return keys[keyKey] || null;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/binary/constants.ts
|
|
350
|
+
var WABinaryConstants = {
|
|
351
|
+
LIST_EMPTY: 0,
|
|
352
|
+
STREAM_END: 2,
|
|
353
|
+
DICTIONARY_0: 236,
|
|
354
|
+
DICTIONARY_1: 237,
|
|
355
|
+
DICTIONARY_2: 238,
|
|
356
|
+
DICTIONARY_3: 239,
|
|
357
|
+
LIST_8: 248,
|
|
358
|
+
LIST_16: 249,
|
|
359
|
+
JID_PAIR: 250,
|
|
360
|
+
HEX_8: 251,
|
|
361
|
+
BINARY_8: 252,
|
|
362
|
+
BINARY_20: 253,
|
|
363
|
+
BINARY_32: 254,
|
|
364
|
+
NIBBLE_8: 255,
|
|
365
|
+
SINGLE_BYTE_MAX: 256
|
|
366
|
+
};
|
|
367
|
+
var SINGLE_BYTE_TOKENS = [
|
|
368
|
+
"",
|
|
369
|
+
"",
|
|
370
|
+
"",
|
|
371
|
+
"200",
|
|
372
|
+
"400",
|
|
373
|
+
"404",
|
|
374
|
+
"500",
|
|
375
|
+
"501",
|
|
376
|
+
"502",
|
|
377
|
+
"action",
|
|
378
|
+
"add",
|
|
379
|
+
"after",
|
|
380
|
+
"archive",
|
|
381
|
+
"author",
|
|
382
|
+
"available",
|
|
383
|
+
"battery",
|
|
384
|
+
"before",
|
|
385
|
+
"body",
|
|
386
|
+
"broadcast",
|
|
387
|
+
"chat",
|
|
388
|
+
"clear",
|
|
389
|
+
"code",
|
|
390
|
+
"composing",
|
|
391
|
+
"config",
|
|
392
|
+
"contacts",
|
|
393
|
+
"count",
|
|
394
|
+
"create",
|
|
395
|
+
"debug",
|
|
396
|
+
"delete",
|
|
397
|
+
"demote",
|
|
398
|
+
"duplicate",
|
|
399
|
+
"encoding",
|
|
400
|
+
"error",
|
|
401
|
+
"false",
|
|
402
|
+
"filehash",
|
|
403
|
+
"from",
|
|
404
|
+
"g.us",
|
|
405
|
+
"group",
|
|
406
|
+
"groups_v2",
|
|
407
|
+
"height",
|
|
408
|
+
"ib",
|
|
409
|
+
"id",
|
|
410
|
+
"image",
|
|
411
|
+
"in",
|
|
412
|
+
"index",
|
|
413
|
+
"invis",
|
|
414
|
+
"item",
|
|
415
|
+
"jid",
|
|
416
|
+
"kind",
|
|
417
|
+
"last",
|
|
418
|
+
"leave",
|
|
419
|
+
"live",
|
|
420
|
+
"log",
|
|
421
|
+
"media",
|
|
422
|
+
"message",
|
|
423
|
+
"mimetype",
|
|
424
|
+
"missing",
|
|
425
|
+
"modify",
|
|
426
|
+
"name",
|
|
427
|
+
"notification",
|
|
428
|
+
"notify",
|
|
429
|
+
"out",
|
|
430
|
+
"owner",
|
|
431
|
+
"participant",
|
|
432
|
+
"paused",
|
|
433
|
+
"picture",
|
|
434
|
+
"played",
|
|
435
|
+
"presence",
|
|
436
|
+
"preview",
|
|
437
|
+
"promote",
|
|
438
|
+
"query",
|
|
439
|
+
"raw",
|
|
440
|
+
"read",
|
|
441
|
+
"receipt",
|
|
442
|
+
"received",
|
|
443
|
+
"recipient",
|
|
444
|
+
"recording",
|
|
445
|
+
"relay",
|
|
446
|
+
"remove",
|
|
447
|
+
"response",
|
|
448
|
+
"resume",
|
|
449
|
+
"retry",
|
|
450
|
+
"s.whatsapp.net",
|
|
451
|
+
"seconds",
|
|
452
|
+
"set",
|
|
453
|
+
"size",
|
|
454
|
+
"status",
|
|
455
|
+
"subject",
|
|
456
|
+
"subscribe",
|
|
457
|
+
"t",
|
|
458
|
+
"text",
|
|
459
|
+
"to",
|
|
460
|
+
"true",
|
|
461
|
+
"type",
|
|
462
|
+
"unarchive",
|
|
463
|
+
"unavailable",
|
|
464
|
+
"url",
|
|
465
|
+
"user",
|
|
466
|
+
"value",
|
|
467
|
+
"web",
|
|
468
|
+
"width",
|
|
469
|
+
"mute",
|
|
470
|
+
"read_only",
|
|
471
|
+
"admin",
|
|
472
|
+
"creator",
|
|
473
|
+
"short",
|
|
474
|
+
"update",
|
|
475
|
+
"powersave",
|
|
476
|
+
"rich",
|
|
477
|
+
"modify_tag",
|
|
478
|
+
"message_info",
|
|
479
|
+
"opaque",
|
|
480
|
+
"contact",
|
|
481
|
+
"unlink",
|
|
482
|
+
"iq",
|
|
483
|
+
"ping",
|
|
484
|
+
"version",
|
|
485
|
+
"client",
|
|
486
|
+
"pair-device",
|
|
487
|
+
"pair-success",
|
|
488
|
+
"success",
|
|
489
|
+
"failure"
|
|
490
|
+
];
|
|
491
|
+
var TOKEN_MAP = /* @__PURE__ */ new Map();
|
|
492
|
+
SINGLE_BYTE_TOKENS.forEach((token, index) => {
|
|
493
|
+
if (token) {
|
|
494
|
+
TOKEN_MAP.set(token, index);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// src/binary/encoder.ts
|
|
499
|
+
var BinaryEncoder = class {
|
|
500
|
+
buffer = [];
|
|
501
|
+
encode(node) {
|
|
502
|
+
this.buffer = [];
|
|
503
|
+
this.writeNode(node);
|
|
504
|
+
return new Uint8Array(this.buffer);
|
|
505
|
+
}
|
|
506
|
+
writeByte(byte) {
|
|
507
|
+
this.buffer.push(byte & 255);
|
|
508
|
+
}
|
|
509
|
+
writeBytes(bytes) {
|
|
510
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
511
|
+
this.writeByte(bytes[i]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
writeInt16(value) {
|
|
515
|
+
this.writeByte(value >> 8 & 255);
|
|
516
|
+
this.writeByte(value & 255);
|
|
517
|
+
}
|
|
518
|
+
writeInt32(value) {
|
|
519
|
+
this.writeByte(value >> 24 & 255);
|
|
520
|
+
this.writeByte(value >> 16 & 255);
|
|
521
|
+
this.writeByte(value >> 8 & 255);
|
|
522
|
+
this.writeByte(value & 255);
|
|
523
|
+
}
|
|
524
|
+
writeListStart(size) {
|
|
525
|
+
if (size === 0) {
|
|
526
|
+
this.writeByte(WABinaryConstants.LIST_EMPTY);
|
|
527
|
+
} else if (size < 256) {
|
|
528
|
+
this.writeByte(WABinaryConstants.LIST_8);
|
|
529
|
+
this.writeByte(size);
|
|
530
|
+
} else {
|
|
531
|
+
this.writeByte(WABinaryConstants.LIST_16);
|
|
532
|
+
this.writeInt16(size);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
writeString(str) {
|
|
536
|
+
const tokenIndex = TOKEN_MAP.get(str);
|
|
537
|
+
if (tokenIndex !== void 0) {
|
|
538
|
+
this.writeByte(tokenIndex);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const encoded = new TextEncoder().encode(str);
|
|
542
|
+
if (encoded.length < 256) {
|
|
543
|
+
this.writeByte(WABinaryConstants.BINARY_8);
|
|
544
|
+
this.writeByte(encoded.length);
|
|
545
|
+
} else {
|
|
546
|
+
this.writeByte(WABinaryConstants.BINARY_32);
|
|
547
|
+
this.writeInt32(encoded.length);
|
|
548
|
+
}
|
|
549
|
+
this.writeBytes(encoded);
|
|
550
|
+
}
|
|
551
|
+
writeNode(node) {
|
|
552
|
+
const numAttrs = Object.keys(node.attrs).length;
|
|
553
|
+
const hasContent = node.content !== void 0;
|
|
554
|
+
const listSize = 1 + numAttrs * 2 + (hasContent ? 1 : 0);
|
|
555
|
+
this.writeListStart(listSize);
|
|
556
|
+
this.writeString(node.tag);
|
|
557
|
+
for (const [key, value] of Object.entries(node.attrs)) {
|
|
558
|
+
if (value !== void 0) {
|
|
559
|
+
this.writeString(key);
|
|
560
|
+
this.writeString(String(value));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (hasContent) {
|
|
564
|
+
if (Array.isArray(node.content)) {
|
|
565
|
+
this.writeListStart(node.content.length);
|
|
566
|
+
for (const child of node.content) {
|
|
567
|
+
this.writeNode(child);
|
|
568
|
+
}
|
|
569
|
+
} else if (typeof node.content === "string") {
|
|
570
|
+
this.writeString(node.content);
|
|
571
|
+
} else if (node.content instanceof Uint8Array) {
|
|
572
|
+
if (node.content.length < 256) {
|
|
573
|
+
this.writeByte(WABinaryConstants.BINARY_8);
|
|
574
|
+
this.writeByte(node.content.length);
|
|
575
|
+
} else {
|
|
576
|
+
this.writeByte(WABinaryConstants.BINARY_32);
|
|
577
|
+
this.writeInt32(node.content.length);
|
|
578
|
+
}
|
|
579
|
+
this.writeBytes(node.content);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// src/binary/decoder.ts
|
|
586
|
+
var BinaryDecoder = class {
|
|
587
|
+
buffer = new Uint8Array(0);
|
|
588
|
+
offset = 0;
|
|
589
|
+
decode(buffer) {
|
|
590
|
+
this.buffer = buffer;
|
|
591
|
+
this.offset = 0;
|
|
592
|
+
return this.readNode();
|
|
593
|
+
}
|
|
594
|
+
readByte() {
|
|
595
|
+
if (this.offset >= this.buffer.length) {
|
|
596
|
+
throw new Error(`Unexpected EOF in WABinary stream at offset ${this.offset}`);
|
|
597
|
+
}
|
|
598
|
+
const b = this.buffer[this.offset];
|
|
599
|
+
this.offset += 1;
|
|
600
|
+
return b;
|
|
601
|
+
}
|
|
602
|
+
readBytes(length) {
|
|
603
|
+
if (this.offset + length > this.buffer.length) {
|
|
604
|
+
throw new Error(`Buffer underrun: requested ${length} bytes, have ${this.buffer.length - this.offset}`);
|
|
605
|
+
}
|
|
606
|
+
const slice = this.buffer.slice(this.offset, this.offset + length);
|
|
607
|
+
this.offset += length;
|
|
608
|
+
return slice;
|
|
609
|
+
}
|
|
610
|
+
readInt16() {
|
|
611
|
+
const b0 = this.readByte();
|
|
612
|
+
const b1 = this.readByte();
|
|
613
|
+
return b0 << 8 | b1;
|
|
614
|
+
}
|
|
615
|
+
readInt32() {
|
|
616
|
+
const b0 = this.readByte();
|
|
617
|
+
const b1 = this.readByte();
|
|
618
|
+
const b2 = this.readByte();
|
|
619
|
+
const b3 = this.readByte();
|
|
620
|
+
return b0 << 24 | b1 << 16 | b2 << 8 | b3;
|
|
621
|
+
}
|
|
622
|
+
readListSize() {
|
|
623
|
+
const tag = this.readByte();
|
|
624
|
+
if (tag === WABinaryConstants.LIST_EMPTY) {
|
|
625
|
+
return 0;
|
|
626
|
+
}
|
|
627
|
+
if (tag === WABinaryConstants.LIST_8) {
|
|
628
|
+
return this.readByte();
|
|
629
|
+
}
|
|
630
|
+
if (tag === WABinaryConstants.LIST_16) {
|
|
631
|
+
return this.readInt16();
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Invalid list tag byte: ${tag}`);
|
|
634
|
+
}
|
|
635
|
+
readString() {
|
|
636
|
+
const tag = this.readByte();
|
|
637
|
+
if (tag < SINGLE_BYTE_TOKENS.length && SINGLE_BYTE_TOKENS[tag]) {
|
|
638
|
+
return SINGLE_BYTE_TOKENS[tag];
|
|
639
|
+
}
|
|
640
|
+
if (tag === WABinaryConstants.BINARY_8) {
|
|
641
|
+
const length = this.readByte();
|
|
642
|
+
const bytes = this.readBytes(length);
|
|
643
|
+
return new TextDecoder().decode(bytes);
|
|
644
|
+
}
|
|
645
|
+
if (tag === WABinaryConstants.BINARY_32) {
|
|
646
|
+
const length = this.readInt32();
|
|
647
|
+
const bytes = this.readBytes(length);
|
|
648
|
+
return new TextDecoder().decode(bytes);
|
|
649
|
+
}
|
|
650
|
+
throw new Error(`Unsupported string tag: ${tag} at offset ${this.offset - 1}`);
|
|
651
|
+
}
|
|
652
|
+
readNode() {
|
|
653
|
+
const listSize = this.readListSize();
|
|
654
|
+
if (listSize === 0) {
|
|
655
|
+
throw new Error("Invalid empty node");
|
|
656
|
+
}
|
|
657
|
+
const tag = this.readString();
|
|
658
|
+
const attrs = {};
|
|
659
|
+
let remaining = listSize - 1;
|
|
660
|
+
while (remaining > 1) {
|
|
661
|
+
const key = this.readString();
|
|
662
|
+
const val = this.readString();
|
|
663
|
+
attrs[key] = val;
|
|
664
|
+
remaining -= 2;
|
|
665
|
+
}
|
|
666
|
+
let content;
|
|
667
|
+
if (remaining === 1) {
|
|
668
|
+
const peekByte = this.buffer[this.offset];
|
|
669
|
+
if (peekByte === WABinaryConstants.LIST_EMPTY || peekByte === WABinaryConstants.LIST_8 || peekByte === WABinaryConstants.LIST_16) {
|
|
670
|
+
const childCount = this.readListSize();
|
|
671
|
+
const children = [];
|
|
672
|
+
for (let i = 0; i < childCount; i++) {
|
|
673
|
+
children.push(this.readNode());
|
|
674
|
+
}
|
|
675
|
+
content = children;
|
|
676
|
+
} else if (peekByte === WABinaryConstants.BINARY_8 || peekByte === WABinaryConstants.BINARY_32) {
|
|
677
|
+
const nextTag = this.readByte();
|
|
678
|
+
const length = nextTag === WABinaryConstants.BINARY_8 ? this.readByte() : this.readInt32();
|
|
679
|
+
content = this.readBytes(length);
|
|
680
|
+
} else {
|
|
681
|
+
content = this.readString();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { tag, attrs, content };
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/binary/index.ts
|
|
689
|
+
var BinaryProtocol = class {
|
|
690
|
+
static encoder = new BinaryEncoder();
|
|
691
|
+
static decoder = new BinaryDecoder();
|
|
692
|
+
/**
|
|
693
|
+
* Converte um BinaryNode para bytes WABinary
|
|
694
|
+
*/
|
|
695
|
+
static encode(node) {
|
|
696
|
+
return this.encoder.encode(node);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Converte bytes WABinary de volta para um BinaryNode estruturado
|
|
700
|
+
*/
|
|
701
|
+
static decode(buffer) {
|
|
702
|
+
return this.decoder.decode(buffer);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Constrói um nó binário com validação de formato
|
|
706
|
+
*/
|
|
707
|
+
static createNode(tag, attrs = {}, content) {
|
|
708
|
+
return {
|
|
709
|
+
tag,
|
|
710
|
+
attrs,
|
|
711
|
+
content
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Localiza o primeiro filho correspondente à tag fornecida
|
|
716
|
+
*/
|
|
717
|
+
static getChildNode(node, childTag) {
|
|
718
|
+
if (Array.isArray(node.content)) {
|
|
719
|
+
return node.content.find((child) => child.tag === childTag);
|
|
720
|
+
}
|
|
721
|
+
return void 0;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Extrai todos os nós filhos que batem com uma determinada tag
|
|
725
|
+
*/
|
|
726
|
+
static getChildrenNodes(node, childTag) {
|
|
727
|
+
if (Array.isArray(node.content)) {
|
|
728
|
+
return node.content.filter((child) => child.tag === childTag);
|
|
729
|
+
}
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Converte o nó para representação em string legível para debug
|
|
734
|
+
*/
|
|
735
|
+
static stringify(node, indent = 0) {
|
|
736
|
+
const spaces = " ".repeat(indent);
|
|
737
|
+
const attrsStr = Object.entries(node.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
738
|
+
const openTag = attrsStr ? `<${node.tag} ${attrsStr}>` : `<${node.tag}>`;
|
|
739
|
+
if (!node.content) {
|
|
740
|
+
return `${spaces}<${node.tag} ${attrsStr} />`;
|
|
741
|
+
}
|
|
742
|
+
if (Array.isArray(node.content)) {
|
|
743
|
+
const children = node.content.map((c) => this.stringify(c, indent + 1)).join("\n");
|
|
744
|
+
return `${spaces}${openTag}
|
|
745
|
+
${children}
|
|
746
|
+
${spaces}</${node.tag}>`;
|
|
747
|
+
}
|
|
748
|
+
const contentStr = node.content instanceof Uint8Array ? `[Buffer ${node.content.length}b]` : String(node.content);
|
|
749
|
+
return `${spaces}${openTag}${contentStr}</${node.tag}>`;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
var MEDIA_HKDF_INFO = {
|
|
753
|
+
image: "WhatsApp Image Keys",
|
|
754
|
+
video: "WhatsApp Video Keys",
|
|
755
|
+
audio: "WhatsApp Audio Keys",
|
|
756
|
+
document: "WhatsApp Document Keys"
|
|
757
|
+
};
|
|
758
|
+
var MediaCrypto = class {
|
|
759
|
+
/**
|
|
760
|
+
* Criptografa o arquivo de mídia de acordo com o padrão RFC do WhatsApp
|
|
761
|
+
*/
|
|
762
|
+
static encryptMedia(mediaBuffer, type) {
|
|
763
|
+
const mediaKey = crypto.randomBytes(32);
|
|
764
|
+
const info = MEDIA_HKDF_INFO[type];
|
|
765
|
+
const expandedKeys = CryptoEngine.deriveKey(new Uint8Array(mediaKey), info, void 0, 112);
|
|
766
|
+
const iv = expandedKeys.slice(0, 16);
|
|
767
|
+
const cipherKey = expandedKeys.slice(16, 48);
|
|
768
|
+
const macKey = expandedKeys.slice(48, 80);
|
|
769
|
+
const fileSha256 = sha256.sha256(mediaBuffer);
|
|
770
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.from(cipherKey), Buffer.from(iv));
|
|
771
|
+
const encryptedBody = Buffer.concat([cipher.update(Buffer.from(mediaBuffer)), cipher.final()]);
|
|
772
|
+
const hmac = crypto.createHmac("sha256", Buffer.from(macKey));
|
|
773
|
+
hmac.update(Buffer.from(iv));
|
|
774
|
+
hmac.update(encryptedBody);
|
|
775
|
+
const fullMac = hmac.digest();
|
|
776
|
+
const mac = fullMac.subarray(0, 10);
|
|
777
|
+
const finalCiphertext = new Uint8Array(encryptedBody.length + mac.length);
|
|
778
|
+
finalCiphertext.set(encryptedBody, 0);
|
|
779
|
+
finalCiphertext.set(mac, encryptedBody.length);
|
|
780
|
+
const fileEncSha256 = sha256.sha256(finalCiphertext);
|
|
781
|
+
return {
|
|
782
|
+
mediaKey: new Uint8Array(mediaKey),
|
|
783
|
+
ciphertext: finalCiphertext,
|
|
784
|
+
fileSha256: new Uint8Array(fileSha256),
|
|
785
|
+
fileEncSha256: new Uint8Array(fileEncSha256),
|
|
786
|
+
mac: new Uint8Array(mac)
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// src/messages/builder.ts
|
|
792
|
+
var MessageBuilder = class {
|
|
793
|
+
/**
|
|
794
|
+
* Converte qualquer conteúdo de mensagem em uma stanza <message> pronta para transmissão
|
|
795
|
+
*/
|
|
796
|
+
static async buildMessageNode(jid, content, options = {}) {
|
|
797
|
+
const messageId = `3EB0${Date.now().toString(16).toUpperCase()}${Math.floor(Math.random() * 1e4)}`;
|
|
798
|
+
const timestamp = options.timestamp || Math.floor(Date.now() / 1e3);
|
|
799
|
+
const messageAttrs = {
|
|
800
|
+
id: messageId,
|
|
801
|
+
to: jid,
|
|
802
|
+
type: "text",
|
|
803
|
+
t: timestamp
|
|
804
|
+
};
|
|
805
|
+
let payloadContent = new Uint8Array(0);
|
|
806
|
+
if ("text" in content) {
|
|
807
|
+
payloadContent = new TextEncoder().encode(content.text);
|
|
808
|
+
} else if ("image" in content) {
|
|
809
|
+
messageAttrs.type = "media";
|
|
810
|
+
messageAttrs.mediatype = "image";
|
|
811
|
+
const buffer = await this.resolveMediaBuffer(content.image);
|
|
812
|
+
const encrypted = MediaCrypto.encryptMedia(buffer, "image");
|
|
813
|
+
payloadContent = [
|
|
814
|
+
BinaryProtocol.createNode("image", {
|
|
815
|
+
mimetype: content.mimetype || "image/jpeg",
|
|
816
|
+
caption: content.caption,
|
|
817
|
+
fileLength: buffer.length,
|
|
818
|
+
fileSha256: Buffer.from(encrypted.fileSha256).toString("base64"),
|
|
819
|
+
fileEncSha256: Buffer.from(encrypted.fileEncSha256).toString("base64"),
|
|
820
|
+
mediaKey: Buffer.from(encrypted.mediaKey).toString("base64")
|
|
821
|
+
})
|
|
822
|
+
];
|
|
823
|
+
} else if ("video" in content) {
|
|
824
|
+
messageAttrs.type = "media";
|
|
825
|
+
messageAttrs.mediatype = "video";
|
|
826
|
+
const buffer = await this.resolveMediaBuffer(content.video);
|
|
827
|
+
const encrypted = MediaCrypto.encryptMedia(buffer, "video");
|
|
828
|
+
payloadContent = [
|
|
829
|
+
BinaryProtocol.createNode("video", {
|
|
830
|
+
mimetype: content.mimetype || "video/mp4",
|
|
831
|
+
caption: content.caption,
|
|
832
|
+
gifPlayback: content.gifPlayback ? "true" : void 0,
|
|
833
|
+
ptv: content.ptv ? "true" : void 0,
|
|
834
|
+
fileLength: buffer.length,
|
|
835
|
+
fileSha256: Buffer.from(encrypted.fileSha256).toString("base64"),
|
|
836
|
+
fileEncSha256: Buffer.from(encrypted.fileEncSha256).toString("base64"),
|
|
837
|
+
mediaKey: Buffer.from(encrypted.mediaKey).toString("base64")
|
|
838
|
+
})
|
|
839
|
+
];
|
|
840
|
+
} else if ("audio" in content) {
|
|
841
|
+
messageAttrs.type = "media";
|
|
842
|
+
messageAttrs.mediatype = "audio";
|
|
843
|
+
const buffer = await this.resolveMediaBuffer(content.audio);
|
|
844
|
+
const encrypted = MediaCrypto.encryptMedia(buffer, "audio");
|
|
845
|
+
payloadContent = [
|
|
846
|
+
BinaryProtocol.createNode("audio", {
|
|
847
|
+
mimetype: content.mimetype || (content.ptt ? "audio/ogg; codecs=opus" : "audio/mp4"),
|
|
848
|
+
ptt: content.ptt ? "true" : "false",
|
|
849
|
+
fileLength: buffer.length,
|
|
850
|
+
fileSha256: Buffer.from(encrypted.fileSha256).toString("base64"),
|
|
851
|
+
fileEncSha256: Buffer.from(encrypted.fileEncSha256).toString("base64"),
|
|
852
|
+
mediaKey: Buffer.from(encrypted.mediaKey).toString("base64")
|
|
853
|
+
})
|
|
854
|
+
];
|
|
855
|
+
} else if ("document" in content) {
|
|
856
|
+
messageAttrs.type = "media";
|
|
857
|
+
messageAttrs.mediatype = "document";
|
|
858
|
+
const buffer = await this.resolveMediaBuffer(content.document);
|
|
859
|
+
const encrypted = MediaCrypto.encryptMedia(buffer, "document");
|
|
860
|
+
payloadContent = [
|
|
861
|
+
BinaryProtocol.createNode("document", {
|
|
862
|
+
mimetype: content.mimetype,
|
|
863
|
+
fileName: content.fileName || "file",
|
|
864
|
+
caption: content.caption,
|
|
865
|
+
fileLength: buffer.length,
|
|
866
|
+
fileSha256: Buffer.from(encrypted.fileSha256).toString("base64"),
|
|
867
|
+
fileEncSha256: Buffer.from(encrypted.fileEncSha256).toString("base64"),
|
|
868
|
+
mediaKey: Buffer.from(encrypted.mediaKey).toString("base64")
|
|
869
|
+
})
|
|
870
|
+
];
|
|
871
|
+
} else if ("location" in content) {
|
|
872
|
+
messageAttrs.type = "location";
|
|
873
|
+
payloadContent = [
|
|
874
|
+
BinaryProtocol.createNode("location", {
|
|
875
|
+
degreesLatitude: content.location.degreesLatitude,
|
|
876
|
+
degreesLongitude: content.location.degreesLongitude,
|
|
877
|
+
name: content.location.name,
|
|
878
|
+
address: content.location.address,
|
|
879
|
+
url: content.location.url
|
|
880
|
+
})
|
|
881
|
+
];
|
|
882
|
+
} else if ("contact" in content) {
|
|
883
|
+
messageAttrs.type = "contact";
|
|
884
|
+
payloadContent = [
|
|
885
|
+
BinaryProtocol.createNode("contact", {
|
|
886
|
+
displayName: content.contact.displayName,
|
|
887
|
+
vcard: content.contact.vcard
|
|
888
|
+
})
|
|
889
|
+
];
|
|
890
|
+
} else if ("contacts" in content) {
|
|
891
|
+
messageAttrs.type = "contacts";
|
|
892
|
+
const contactNodes = content.contacts.contacts.map(
|
|
893
|
+
(c) => BinaryProtocol.createNode("contact", {
|
|
894
|
+
displayName: c.displayName,
|
|
895
|
+
vcard: c.vcard
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
payloadContent = [
|
|
899
|
+
BinaryProtocol.createNode(
|
|
900
|
+
"contacts",
|
|
901
|
+
{ displayName: content.contacts.displayName },
|
|
902
|
+
contactNodes
|
|
903
|
+
)
|
|
904
|
+
];
|
|
905
|
+
} else if ("react" in content) {
|
|
906
|
+
messageAttrs.type = "reaction";
|
|
907
|
+
payloadContent = [
|
|
908
|
+
BinaryProtocol.createNode("reaction", {
|
|
909
|
+
text: content.react.text,
|
|
910
|
+
targetId: content.react.key.id,
|
|
911
|
+
targetJid: content.react.key.remoteJid
|
|
912
|
+
})
|
|
913
|
+
];
|
|
914
|
+
}
|
|
915
|
+
const node = BinaryProtocol.createNode("message", messageAttrs, payloadContent);
|
|
916
|
+
return {
|
|
917
|
+
node,
|
|
918
|
+
messageId
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Resolve diferentes fontes de mídia (Buffer, Uint8Array ou URL) para um Uint8Array em memória
|
|
923
|
+
*/
|
|
924
|
+
static async resolveMediaBuffer(source) {
|
|
925
|
+
if (source instanceof Uint8Array) {
|
|
926
|
+
return source;
|
|
927
|
+
}
|
|
928
|
+
if (Buffer.isBuffer(source)) {
|
|
929
|
+
return new Uint8Array(source);
|
|
930
|
+
}
|
|
931
|
+
if (typeof source === "object" && source !== null && "url" in source) {
|
|
932
|
+
const res = await fetch(source.url);
|
|
933
|
+
const arrayBuf = await res.arrayBuffer();
|
|
934
|
+
return new Uint8Array(arrayBuf);
|
|
935
|
+
}
|
|
936
|
+
throw new Error("Unsupported media source format");
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
var DEFAULT_WA_WS_URL = "wss://web.whatsapp.com/ws/chat";
|
|
940
|
+
var WhatsAppSocket = class extends events.EventEmitter {
|
|
941
|
+
ws = null;
|
|
942
|
+
config;
|
|
943
|
+
pingInterval = null;
|
|
944
|
+
state = "close";
|
|
945
|
+
// Noise Handshake & Ciphers
|
|
946
|
+
noise = null;
|
|
947
|
+
writeCipher = null;
|
|
948
|
+
readCipher = null;
|
|
949
|
+
isHandshakeComplete = false;
|
|
950
|
+
constructor(config = {}) {
|
|
951
|
+
super();
|
|
952
|
+
this.config = {
|
|
953
|
+
waWebSocketUrl: DEFAULT_WA_WS_URL,
|
|
954
|
+
connectTimeoutMs: 2e4,
|
|
955
|
+
keepAliveIntervalMs: 3e4,
|
|
956
|
+
...config
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
get connectionState() {
|
|
960
|
+
return this.state;
|
|
961
|
+
}
|
|
962
|
+
updateConnection(update) {
|
|
963
|
+
if (update.connection) {
|
|
964
|
+
this.state = update.connection;
|
|
965
|
+
}
|
|
966
|
+
this.emit("connection.update", update);
|
|
967
|
+
}
|
|
968
|
+
async connect(staticKey) {
|
|
969
|
+
if (this.ws && (this.ws.readyState === WebSocket__default.default.OPEN || this.ws.readyState === WebSocket__default.default.CONNECTING)) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this.isHandshakeComplete = false;
|
|
973
|
+
this.writeCipher = null;
|
|
974
|
+
this.readCipher = null;
|
|
975
|
+
this.noise = new NoiseHandshake(staticKey);
|
|
976
|
+
this.updateConnection({ connection: "connecting" });
|
|
977
|
+
const wsUrl = this.config.waWebSocketUrl || DEFAULT_WA_WS_URL;
|
|
978
|
+
this.ws = new WebSocket__default.default(wsUrl, {
|
|
979
|
+
origin: "https://web.whatsapp.com",
|
|
980
|
+
headers: {
|
|
981
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
this.ws.on("open", () => {
|
|
985
|
+
this.startKeepAlive();
|
|
986
|
+
this.emit("open");
|
|
987
|
+
this.initiateNoiseHandshake();
|
|
988
|
+
});
|
|
989
|
+
this.ws.on("message", (data) => {
|
|
990
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
991
|
+
this.handleIncomingFrame(new Uint8Array(buffer));
|
|
992
|
+
});
|
|
993
|
+
this.ws.on("close", (code, reason) => {
|
|
994
|
+
this.cleanup();
|
|
995
|
+
this.updateConnection({
|
|
996
|
+
connection: "close",
|
|
997
|
+
lastDisconnect: {
|
|
998
|
+
error: new Error(`WebSocket closed with code ${code}: ${reason.toString()}`),
|
|
999
|
+
date: /* @__PURE__ */ new Date()
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
this.ws.on("error", (err) => {
|
|
1004
|
+
this.emit("error", err);
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Envia a mensagem inicial do Noise Handshake
|
|
1009
|
+
*/
|
|
1010
|
+
initiateNoiseHandshake() {
|
|
1011
|
+
if (!this.noise) return;
|
|
1012
|
+
const initialPayload = this.noise.startHandshake();
|
|
1013
|
+
this.sendRaw(initialPayload);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Trata frames recebidos pelo WebSocket
|
|
1017
|
+
*/
|
|
1018
|
+
handleIncomingFrame(frame) {
|
|
1019
|
+
if (!this.isHandshakeComplete) {
|
|
1020
|
+
this.handleHandshakeFrame(frame);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (this.readCipher && frame.length > 3) {
|
|
1024
|
+
const ciphertext = frame.slice(3);
|
|
1025
|
+
const decrypted = this.readCipher.decryptWithAd(new Uint8Array(0), ciphertext);
|
|
1026
|
+
try {
|
|
1027
|
+
const node = BinaryProtocol.decode(decrypted);
|
|
1028
|
+
this.emit("node", node);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
this.emit("error", new Error(`Failed to decode WABinary stanza: ${err.message}`));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Conclui as etapas do Noise Handshake
|
|
1036
|
+
*/
|
|
1037
|
+
handleHandshakeFrame(frame) {
|
|
1038
|
+
if (!this.noise) return;
|
|
1039
|
+
try {
|
|
1040
|
+
this.noise.processServerHello(frame);
|
|
1041
|
+
const clientPayload = new Uint8Array([0]);
|
|
1042
|
+
const finish = this.noise.finishHandshake(clientPayload);
|
|
1043
|
+
this.writeCipher = finish.writeCipher;
|
|
1044
|
+
this.readCipher = finish.readCipher;
|
|
1045
|
+
this.isHandshakeComplete = true;
|
|
1046
|
+
this.sendRaw(finish.handshakeMessage);
|
|
1047
|
+
this.updateConnection({ connection: "open" });
|
|
1048
|
+
this.emit("handshake.complete");
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
this.emit("error", new Error(`Noise Handshake failed: ${err.message}`));
|
|
1051
|
+
this.disconnect();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Envia uma stanza estruturada, codificando em WABinary e criptografando com o Noise Write Cipher
|
|
1056
|
+
*/
|
|
1057
|
+
sendNode(node) {
|
|
1058
|
+
if (!this.isHandshakeComplete || !this.writeCipher) {
|
|
1059
|
+
throw new Error("Cannot send stanza: Noise Handshake is not complete");
|
|
1060
|
+
}
|
|
1061
|
+
const encodedNode = BinaryProtocol.encode(node);
|
|
1062
|
+
const encrypted = this.writeCipher.encryptWithAd(new Uint8Array(0), encodedNode);
|
|
1063
|
+
const length = encrypted.length;
|
|
1064
|
+
const frame = new Uint8Array(3 + length);
|
|
1065
|
+
frame[0] = length >> 16 & 255;
|
|
1066
|
+
frame[1] = length >> 8 & 255;
|
|
1067
|
+
frame[2] = length & 255;
|
|
1068
|
+
frame.set(encrypted, 3);
|
|
1069
|
+
this.sendRaw(frame);
|
|
1070
|
+
}
|
|
1071
|
+
sendRaw(data) {
|
|
1072
|
+
if (!this.ws || this.ws.readyState !== WebSocket__default.default.OPEN) {
|
|
1073
|
+
throw new Error("Cannot send raw data: WebSocket is not open");
|
|
1074
|
+
}
|
|
1075
|
+
this.ws.send(data);
|
|
1076
|
+
}
|
|
1077
|
+
startKeepAlive() {
|
|
1078
|
+
this.stopKeepAlive();
|
|
1079
|
+
this.pingInterval = setInterval(() => {
|
|
1080
|
+
if (this.ws && this.ws.readyState === WebSocket__default.default.OPEN) {
|
|
1081
|
+
this.ws.ping();
|
|
1082
|
+
}
|
|
1083
|
+
}, this.config.keepAliveIntervalMs);
|
|
1084
|
+
}
|
|
1085
|
+
stopKeepAlive() {
|
|
1086
|
+
if (this.pingInterval) {
|
|
1087
|
+
clearInterval(this.pingInterval);
|
|
1088
|
+
this.pingInterval = null;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
cleanup() {
|
|
1092
|
+
this.stopKeepAlive();
|
|
1093
|
+
this.isHandshakeComplete = false;
|
|
1094
|
+
this.writeCipher = null;
|
|
1095
|
+
this.readCipher = null;
|
|
1096
|
+
this.noise = null;
|
|
1097
|
+
if (this.ws) {
|
|
1098
|
+
this.ws.removeAllListeners();
|
|
1099
|
+
this.ws = null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
disconnect() {
|
|
1103
|
+
this.cleanup();
|
|
1104
|
+
this.updateConnection({ connection: "close" });
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
var WhatsAppClient = class extends events.EventEmitter {
|
|
1108
|
+
socket;
|
|
1109
|
+
auth = null;
|
|
1110
|
+
signal = null;
|
|
1111
|
+
config;
|
|
1112
|
+
constructor(config = {}) {
|
|
1113
|
+
super();
|
|
1114
|
+
this.config = config;
|
|
1115
|
+
this.socket = new WhatsAppSocket(config);
|
|
1116
|
+
if (config.auth) {
|
|
1117
|
+
this.setAuthState(config.auth);
|
|
1118
|
+
}
|
|
1119
|
+
this.setupListeners();
|
|
1120
|
+
}
|
|
1121
|
+
setAuthState(auth) {
|
|
1122
|
+
this.auth = auth;
|
|
1123
|
+
this.signal = new SignalEngine(auth.keys);
|
|
1124
|
+
}
|
|
1125
|
+
setupListeners() {
|
|
1126
|
+
this.socket.on("connection.update", (update) => {
|
|
1127
|
+
this.emit("connection.update", update);
|
|
1128
|
+
});
|
|
1129
|
+
this.socket.on("handshake.complete", () => {
|
|
1130
|
+
this.onHandshakeComplete();
|
|
1131
|
+
});
|
|
1132
|
+
this.socket.on("node", (node) => {
|
|
1133
|
+
this.handleIncomingNode(node);
|
|
1134
|
+
});
|
|
1135
|
+
this.socket.on("error", (err) => {
|
|
1136
|
+
this.emit("error", err);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Disparado após a conexão criptografada (Noise) estar pronta
|
|
1141
|
+
*/
|
|
1142
|
+
async onHandshakeComplete() {
|
|
1143
|
+
if (!this.auth) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (!this.auth.creds.me) {
|
|
1147
|
+
await this.requestPairingCode();
|
|
1148
|
+
} else {
|
|
1149
|
+
this.emit("connection.update", { connection: "open", isNewLogin: false });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Envia stanza solicitando o QR Code de pareamento
|
|
1154
|
+
*/
|
|
1155
|
+
async requestPairingCode() {
|
|
1156
|
+
if (!this.auth) return;
|
|
1157
|
+
const pairNode = BinaryProtocol.createNode(
|
|
1158
|
+
"iq",
|
|
1159
|
+
{
|
|
1160
|
+
to: "s.whatsapp.net",
|
|
1161
|
+
type: "set",
|
|
1162
|
+
xmlns: "pair-device",
|
|
1163
|
+
id: `pair-${Date.now()}`
|
|
1164
|
+
},
|
|
1165
|
+
[
|
|
1166
|
+
BinaryProtocol.createNode("pair-device", {}, [
|
|
1167
|
+
BinaryProtocol.createNode("ref", {}, void 0)
|
|
1168
|
+
])
|
|
1169
|
+
]
|
|
1170
|
+
);
|
|
1171
|
+
this.socket.sendNode(pairNode);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Processa stanzas recebidas do WhatsApp
|
|
1175
|
+
*/
|
|
1176
|
+
handleIncomingNode(node) {
|
|
1177
|
+
this.emit("raw.node", node);
|
|
1178
|
+
if (node.tag === "iq" && node.attrs.type === "result") {
|
|
1179
|
+
const pairDevice = BinaryProtocol.getChildNode(node, "pair-device");
|
|
1180
|
+
if (pairDevice) {
|
|
1181
|
+
const refNode = BinaryProtocol.getChildNode(pairDevice, "ref");
|
|
1182
|
+
if (refNode && typeof refNode.content === "string" && this.auth) {
|
|
1183
|
+
const ref = refNode.content;
|
|
1184
|
+
const pubKey = Buffer.from(this.auth.creds.signedIdentityKey.public).toString("base64");
|
|
1185
|
+
const noisePub = Buffer.from(this.auth.creds.noiseKey.public).toString("base64");
|
|
1186
|
+
const qrString = `${ref},${noisePub},${pubKey},${this.auth.creds.advSecretKey}`;
|
|
1187
|
+
this.emit("connection.update", { qr: qrString });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (node.tag === "iq" && node.attrs.xmlns === "pair-success") {
|
|
1192
|
+
this.emit("connection.update", { connection: "open", isNewLogin: true });
|
|
1193
|
+
}
|
|
1194
|
+
if (node.tag === "message") {
|
|
1195
|
+
this.emit("messages.upsert", { node });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Inicia o fluxo de conexão com o WhatsApp Web
|
|
1200
|
+
*/
|
|
1201
|
+
async connect() {
|
|
1202
|
+
if (!this.auth) {
|
|
1203
|
+
throw new Error("AuthenticationState must be provided before calling connect()");
|
|
1204
|
+
}
|
|
1205
|
+
await this.socket.connect(this.auth.creds.noiseKey);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Envia qualquer tipo de mensagem (Texto, Imagem, Vídeo, Áudio, Localização, Contato, Reação)
|
|
1209
|
+
*/
|
|
1210
|
+
async sendMessage(jid, content, options = {}) {
|
|
1211
|
+
const normalizedContent = typeof content === "string" ? { text: content } : content;
|
|
1212
|
+
const { node, messageId } = await MessageBuilder.buildMessageNode(
|
|
1213
|
+
jid,
|
|
1214
|
+
normalizedContent,
|
|
1215
|
+
options
|
|
1216
|
+
);
|
|
1217
|
+
this.socket.sendNode(node);
|
|
1218
|
+
return { messageId };
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Encerra a conexão
|
|
1222
|
+
*/
|
|
1223
|
+
disconnect() {
|
|
1224
|
+
this.socket.disconnect();
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
function makeWASocket(config = {}) {
|
|
1228
|
+
return new WhatsAppClient(config);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
exports.BinaryDecoder = BinaryDecoder;
|
|
1232
|
+
exports.BinaryEncoder = BinaryEncoder;
|
|
1233
|
+
exports.BinaryProtocol = BinaryProtocol;
|
|
1234
|
+
exports.CryptoEngine = CryptoEngine;
|
|
1235
|
+
exports.DEFAULT_WA_WS_URL = DEFAULT_WA_WS_URL;
|
|
1236
|
+
exports.EncryptedFileAuthState = EncryptedFileAuthState;
|
|
1237
|
+
exports.MediaCrypto = MediaCrypto;
|
|
1238
|
+
exports.MessageBuilder = MessageBuilder;
|
|
1239
|
+
exports.NOISE_PROTOCOL_NAME = NOISE_PROTOCOL_NAME;
|
|
1240
|
+
exports.NoiseCipherState = NoiseCipherState;
|
|
1241
|
+
exports.NoiseHandshake = NoiseHandshake;
|
|
1242
|
+
exports.SINGLE_BYTE_TOKENS = SINGLE_BYTE_TOKENS;
|
|
1243
|
+
exports.SignalEngine = SignalEngine;
|
|
1244
|
+
exports.TOKEN_MAP = TOKEN_MAP;
|
|
1245
|
+
exports.WABinaryConstants = WABinaryConstants;
|
|
1246
|
+
exports.WA_NOISE_HEADER = WA_NOISE_HEADER;
|
|
1247
|
+
exports.WhatsAppClient = WhatsAppClient;
|
|
1248
|
+
exports.WhatsAppSocket = WhatsAppSocket;
|
|
1249
|
+
exports.default = makeWASocket;
|
|
1250
|
+
exports.initAuthCreds = initAuthCreds;
|
|
1251
|
+
exports.makeWASocket = makeWASocket;
|
|
1252
|
+
exports.useSecureAuthState = useSecureAuthState;
|
|
1253
|
+
//# sourceMappingURL=index.cjs.map
|
|
1254
|
+
//# sourceMappingURL=index.cjs.map
|