@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,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Binary encoder/decoder untuk WhatsApp Web binary protocol
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp menggunakan custom binary format untuk encode pesan di WebSocket.
|
|
5
|
+
* Format ini berbeda dari protobuf - lebih ringan dan custom.
|
|
6
|
+
*
|
|
7
|
+
* Manual implementation untuk pemahaman mendalam struktur WA binary.
|
|
8
|
+
* Reference: WhatsApp Web binary tags, BINARY_NODE_ENCODING
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============== Tag Constants ==============
|
|
12
|
+
|
|
13
|
+
// List tags (pakai byte ini untuk type)
|
|
14
|
+
export enum TAG_LIST {
|
|
15
|
+
EMPTY = 0,
|
|
16
|
+
DICTIONARY_0 = 236, // 0xFC
|
|
17
|
+
DICTIONARY_1 = 237, // 0xFD
|
|
18
|
+
DICTIONARY_2 = 238, // 0xFE
|
|
19
|
+
DICTIONARY_3 = 239, // 0xFF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Nibble pack (4 bits per char)
|
|
23
|
+
export const TAG_NIBBLE = 255; // 0xFF already used by dict 3, so different
|
|
24
|
+
export const TAG_HEX = 254; // ditto
|
|
25
|
+
|
|
26
|
+
// Hex tags
|
|
27
|
+
export const TAG_BINARY_8 = 252; // 0xFC
|
|
28
|
+
export const TAG_BINARY_20 = 253; // 0xFD
|
|
29
|
+
export const TAG_BINARY_32 = 254; // 0xFE
|
|
30
|
+
export const TAG_ADJACENT = 255; // 0xFF
|
|
31
|
+
export const TAG_LIST_8 = 248; // 0xF8
|
|
32
|
+
export const TAG_LIST_16 = 249; // 0xF9
|
|
33
|
+
export const TAG_JID_PAIR = 250; // 0xFA
|
|
34
|
+
export const TAG_PACKED_NIBBLE = 251; // 0xFB
|
|
35
|
+
export const TAG_PACKED_HEX = 252; // 0xFC
|
|
36
|
+
|
|
37
|
+
// ============== Packed Encoding ==============
|
|
38
|
+
|
|
39
|
+
const HEX_CHARS = '0123456789abcdef';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pack nibbles (4 bits per char, jadi 2 char per byte) - lebih hemat space
|
|
43
|
+
* untuk encoding string numeric/hex
|
|
44
|
+
*/
|
|
45
|
+
export function packNibbles(str: string): Uint8Array {
|
|
46
|
+
// Pad agar panjang genap
|
|
47
|
+
if (str.length % 2 !== 0) str += '0';
|
|
48
|
+
const result = new Uint8Array(str.length / 2);
|
|
49
|
+
for (let i = 0; i < str.length; i += 2) {
|
|
50
|
+
const high = parseInt(str[i], 16);
|
|
51
|
+
const low = parseInt(str[i + 1], 16);
|
|
52
|
+
result[i / 2] = (high << 4) | low;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Unpack nibbles dari binary ke string
|
|
59
|
+
*/
|
|
60
|
+
export function unpackNibbles(bytes: Uint8Array): string {
|
|
61
|
+
let result = '';
|
|
62
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
63
|
+
const high = (bytes[i] >> 4) & 0xf;
|
|
64
|
+
const low = bytes[i] & 0xf;
|
|
65
|
+
result += HEX_CHARS[high];
|
|
66
|
+
result += HEX_CHARS[low];
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============== Binary Node ==============
|
|
72
|
+
|
|
73
|
+
export interface BinaryNode {
|
|
74
|
+
tag: string;
|
|
75
|
+
attrs: Record<string, string>;
|
|
76
|
+
content?: string | Uint8Array | BinaryNode[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============== Constants ==============
|
|
80
|
+
|
|
81
|
+
const NUM_DOUBLE_BYTE = 251; // 0xFB
|
|
82
|
+
const NUM_SINGLE_BYTE = 250; // 0xFA
|
|
83
|
+
const NUM_QUAD_BYTE = 252; // 0xFC
|
|
84
|
+
|
|
85
|
+
// ============== Encoder ==============
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Binary encoder - mengubah BinaryNode ke Uint8Array
|
|
89
|
+
* Implementasi sesuai WA Web binary protocol
|
|
90
|
+
*/
|
|
91
|
+
export class BinaryEncoder {
|
|
92
|
+
private buffer: number[] = [];
|
|
93
|
+
|
|
94
|
+
encode(node: BinaryNode): Uint8Array {
|
|
95
|
+
this.buffer = [];
|
|
96
|
+
this.writeNode(node);
|
|
97
|
+
return new Uint8Array(this.buffer);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private writeNode(node: BinaryNode): void {
|
|
101
|
+
if (!node) return;
|
|
102
|
+
|
|
103
|
+
const children = Array.isArray(node.content) ? node.content : [];
|
|
104
|
+
const hasContent = node.content !== undefined;
|
|
105
|
+
const numAttrs = Object.keys(node.attrs || {}).length;
|
|
106
|
+
|
|
107
|
+
// Write tag header
|
|
108
|
+
if (numAttrs === 0 && !hasContent) {
|
|
109
|
+
this.buffer.push(TAG_LIST.EMPTY);
|
|
110
|
+
} else if (numAttrs === 0) {
|
|
111
|
+
this.buffer.push(TAG_LIST.EMPTY | 0x80);
|
|
112
|
+
} else if (numAttrs < 256 && children.length < 256) {
|
|
113
|
+
this.buffer.push(TAG_LIST.EMPTY | (numAttrs << 4) | (children.length > 0 ? 0x80 : 0));
|
|
114
|
+
} else {
|
|
115
|
+
this.buffer.push(TAG_LIST_8);
|
|
116
|
+
this.buffer.push(numAttrs & 0xff);
|
|
117
|
+
this.buffer.push(children.length & 0xff);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Write tag string (we assume tag is in dict, encode as packed nibble index)
|
|
121
|
+
this.writeString(node.tag);
|
|
122
|
+
|
|
123
|
+
// Write attributes
|
|
124
|
+
for (const [k, v] of Object.entries(node.attrs)) {
|
|
125
|
+
this.writeString(k);
|
|
126
|
+
this.writeString(v);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Write content
|
|
130
|
+
if (children.length > 0) {
|
|
131
|
+
for (const child of children) {
|
|
132
|
+
if (typeof child === 'string') {
|
|
133
|
+
this.writeString(child);
|
|
134
|
+
} else if (child instanceof Uint8Array) {
|
|
135
|
+
this.writeBytes(child);
|
|
136
|
+
} else {
|
|
137
|
+
this.writeNode(child);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else if (typeof node.content === 'string') {
|
|
141
|
+
this.writeString(node.content);
|
|
142
|
+
} else if (node.content instanceof Uint8Array) {
|
|
143
|
+
this.writeBytes(node.content);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private writeString(str: string): void {
|
|
148
|
+
if (str === '') {
|
|
149
|
+
this.buffer.push(TAG_LIST.EMPTY);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Try to find in dictionary
|
|
153
|
+
const dictIndex = findInDict(str);
|
|
154
|
+
if (dictIndex !== -1) {
|
|
155
|
+
const dict = dictIndex >> 8;
|
|
156
|
+
const idx = dictIndex & 0xff;
|
|
157
|
+
this.buffer.push(TAG_LIST.DICTIONARY_0 + dict);
|
|
158
|
+
this.buffer.push(idx);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Try nibble packing for hex/numeric strings
|
|
162
|
+
if (/^[0-9a-fA-F]+$/.test(str) && str.length > 1) {
|
|
163
|
+
this.buffer.push(TAG_PACKED_HEX);
|
|
164
|
+
this.writePackedString(str);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (/^[0-9]+$/.test(str) && str.length > 1) {
|
|
168
|
+
this.buffer.push(TAG_PACKED_NIBBLE);
|
|
169
|
+
this.writePackedString(str);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Default: raw bytes
|
|
173
|
+
this.writeBytes(new TextEncoder().encode(str));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private writePackedString(str: string): void {
|
|
177
|
+
// Pack nibbles - 2 char per byte, terminate with 0xf
|
|
178
|
+
const packed = packNibbles(str);
|
|
179
|
+
this.buffer.push(...packed);
|
|
180
|
+
// Terminator
|
|
181
|
+
this.buffer.push(0xf);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private writeBytes(bytes: Uint8Array): void {
|
|
185
|
+
const len = bytes.length;
|
|
186
|
+
if (len < 256) {
|
|
187
|
+
this.buffer.push(TAG_BINARY_8);
|
|
188
|
+
this.buffer.push(len);
|
|
189
|
+
} else if (len < 1048576) {
|
|
190
|
+
this.buffer.push(TAG_BINARY_20);
|
|
191
|
+
this.buffer.push((len >> 16) & 0xff);
|
|
192
|
+
this.buffer.push((len >> 8) & 0xff);
|
|
193
|
+
this.buffer.push(len & 0xff);
|
|
194
|
+
} else {
|
|
195
|
+
this.buffer.push(TAG_BINARY_32);
|
|
196
|
+
// 4 bytes big-endian
|
|
197
|
+
this.buffer.push((len >> 24) & 0xff);
|
|
198
|
+
this.buffer.push((len >> 16) & 0xff);
|
|
199
|
+
this.buffer.push((len >> 8) & 0xff);
|
|
200
|
+
this.buffer.push(len & 0xff);
|
|
201
|
+
}
|
|
202
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
203
|
+
this.buffer.push(bytes[i]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============== Decoder ==============
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Binary decoder - parse WA binary protocol ke BinaryNode
|
|
212
|
+
*/
|
|
213
|
+
export class BinaryDecoder {
|
|
214
|
+
private data!: Uint8Array;
|
|
215
|
+
private pos: number = 0;
|
|
216
|
+
|
|
217
|
+
decode(data: Uint8Array): BinaryNode {
|
|
218
|
+
this.data = data;
|
|
219
|
+
this.pos = 0;
|
|
220
|
+
return this.readNode();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private readNode(): BinaryNode {
|
|
224
|
+
const listFlag = (0x80 & this.readByte()) !== 0;
|
|
225
|
+
const numAttrs = 0x7f & this.readByte();
|
|
226
|
+
|
|
227
|
+
const tag = this.readString();
|
|
228
|
+
const attrs: Record<string, string> = {};
|
|
229
|
+
for (let i = 0; i < numAttrs; i++) {
|
|
230
|
+
const key = this.readString();
|
|
231
|
+
const value = this.readString();
|
|
232
|
+
attrs[key] = value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let content: string | Uint8Array | BinaryNode[] | undefined;
|
|
236
|
+
if (listFlag) {
|
|
237
|
+
const children: BinaryNode[] = [];
|
|
238
|
+
while (this.pos < this.data.length) {
|
|
239
|
+
const peek = this.data[this.pos];
|
|
240
|
+
if (peek === 0) {
|
|
241
|
+
// End marker
|
|
242
|
+
this.pos++;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
// Determine type
|
|
246
|
+
if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
|
|
247
|
+
children.push(this.readNode());
|
|
248
|
+
} else {
|
|
249
|
+
// It's a string or bytes
|
|
250
|
+
const child = this.readChild();
|
|
251
|
+
if (typeof child === 'string') {
|
|
252
|
+
children.push({ tag: '__text__', attrs: {}, content: child } as any);
|
|
253
|
+
} else if (child instanceof Uint8Array) {
|
|
254
|
+
children.push({ tag: '__bytes__', attrs: {}, content: child } as any);
|
|
255
|
+
} else {
|
|
256
|
+
children.push(child);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
content = children.length > 0 ? children : undefined;
|
|
261
|
+
} else {
|
|
262
|
+
// Single child or none
|
|
263
|
+
if (this.pos < this.data.length) {
|
|
264
|
+
const peek = this.data[this.pos];
|
|
265
|
+
if (peek === 0) {
|
|
266
|
+
this.pos++;
|
|
267
|
+
} else if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
|
|
268
|
+
content = [this.readNode()];
|
|
269
|
+
} else {
|
|
270
|
+
const child = this.readChild();
|
|
271
|
+
if (typeof child === 'string' || child instanceof Uint8Array) {
|
|
272
|
+
content = child;
|
|
273
|
+
} else {
|
|
274
|
+
content = [child];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { tag, attrs, content };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private readChild(): string | Uint8Array | BinaryNode {
|
|
284
|
+
const peek = this.data[this.pos];
|
|
285
|
+
if (peek === TAG_LIST_8 || peek === TAG_LIST_16) {
|
|
286
|
+
return this.readNode();
|
|
287
|
+
}
|
|
288
|
+
return this.readString();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private readString(): string {
|
|
292
|
+
const tag = this.readByte();
|
|
293
|
+
if (tag === TAG_LIST.EMPTY) {
|
|
294
|
+
return '';
|
|
295
|
+
}
|
|
296
|
+
if (tag === TAG_LIST.DICTIONARY_0 ||
|
|
297
|
+
tag === TAG_LIST.DICTIONARY_1 ||
|
|
298
|
+
tag === TAG_LIST.DICTIONARY_2 ||
|
|
299
|
+
tag === TAG_LIST.DICTIONARY_3) {
|
|
300
|
+
const dictIndex = this.readByte();
|
|
301
|
+
return lookupDict(tag - TAG_LIST.DICTIONARY_0, dictIndex);
|
|
302
|
+
}
|
|
303
|
+
if (tag === TAG_PACKED_NIBBLE) {
|
|
304
|
+
return this.readPackedString(false);
|
|
305
|
+
}
|
|
306
|
+
if (tag === TAG_PACKED_HEX) {
|
|
307
|
+
return this.readPackedString(true);
|
|
308
|
+
}
|
|
309
|
+
if (tag === TAG_BINARY_8) {
|
|
310
|
+
const len = this.readByte();
|
|
311
|
+
return new TextDecoder().decode(this.readBytes(len));
|
|
312
|
+
}
|
|
313
|
+
if (tag === TAG_BINARY_20) {
|
|
314
|
+
const b1 = this.readByte();
|
|
315
|
+
const b2 = this.readByte();
|
|
316
|
+
const b3 = this.readByte();
|
|
317
|
+
const len = (b1 << 16) | (b2 << 8) | b3;
|
|
318
|
+
return new TextDecoder().decode(this.readBytes(len));
|
|
319
|
+
}
|
|
320
|
+
if (tag === TAG_BINARY_32) {
|
|
321
|
+
const b1 = this.readByte();
|
|
322
|
+
const b2 = this.readByte();
|
|
323
|
+
const b3 = this.readByte();
|
|
324
|
+
const b4 = this.readByte();
|
|
325
|
+
const len = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
|
|
326
|
+
return new TextDecoder().decode(this.readBytes(len));
|
|
327
|
+
}
|
|
328
|
+
// Fall through - single byte char
|
|
329
|
+
return String.fromCharCode(tag);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private readPackedString(isHex: boolean): string {
|
|
333
|
+
const bytes: number[] = [];
|
|
334
|
+
while (this.pos < this.data.length) {
|
|
335
|
+
const b = this.data[this.pos++];
|
|
336
|
+
const high = (b >> 4) & 0xf;
|
|
337
|
+
const low = b & 0xf;
|
|
338
|
+
if (high === 0xf) break;
|
|
339
|
+
bytes.push(high);
|
|
340
|
+
if (low === 0xf) break;
|
|
341
|
+
bytes.push(low);
|
|
342
|
+
}
|
|
343
|
+
if (isHex) {
|
|
344
|
+
return bytes.map(b => HEX_CHARS[b]).join('');
|
|
345
|
+
}
|
|
346
|
+
// Nibble - numeric 0-9
|
|
347
|
+
return bytes.map(b => String(b)).join('');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private readBytes(n: number): Uint8Array {
|
|
351
|
+
const result = this.data.subarray(this.pos, this.pos + n);
|
|
352
|
+
this.pos += n;
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private readByte(): number {
|
|
357
|
+
return this.data[this.pos++];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============== Dictionaries (Tokens) ==============
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* WhatsApp dictionary berisi string yang sering dipakai untuk kompresi.
|
|
365
|
+
* Encode sebagai dictionary index (1 byte) daripada string length + bytes.
|
|
366
|
+
*
|
|
367
|
+
* Note: dictionary disusun dari analisis WhatsApp Web JavaScript bundle.
|
|
368
|
+
* Total ~240 entries per dictionary (4 dictionaries), prefix common tags.
|
|
369
|
+
*/
|
|
370
|
+
const DICTIONARIES: string[][] = [
|
|
371
|
+
// Dictionary 0 - common protocol tags
|
|
372
|
+
[
|
|
373
|
+
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
|
374
|
+
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
|
375
|
+
// index 32+ actual entries
|
|
376
|
+
'edge_routing', 'account_sync', 'action', 'add', 'after', 'all', 'allow', 'available',
|
|
377
|
+
'background', 'battery', 'before', 'broadcast', 'browser', 'call', 'cancel',
|
|
378
|
+
'category', 'chat', 'code', 'commands', 'config', 'contacts', 'conversation',
|
|
379
|
+
'count', 'create', 'debug', 'delete', 'demote', 'deny', 'device', 'devices',
|
|
380
|
+
'disconnect', 'domain', 'download', 'edit', 'encrypt', 'error', 'expiration',
|
|
381
|
+
'false', 'file', 'filename', 'first', 'from', 'g.us', 'group', 'groups',
|
|
382
|
+
'history', 'id', 'identity', 'image', 'in', 'index', 'invite', 'iq',
|
|
383
|
+
'jid', 'kind', 'last', 'length', 'level', 'list', 'location', 'manifest',
|
|
384
|
+
'media', 'message', 'messages', 'method', 'migrate', 'modify', 'mute', 'name',
|
|
385
|
+
'newsletter', 'next', 'nick', 'no', 'not', 'notify', 'off', 'offer',
|
|
386
|
+
'offline', 'on', 'open', 'out', 'owner', 'pair', 'pairing', 'participant',
|
|
387
|
+
'participants', 'payload', 'paused', 'permissions', 'phone', 'pin', 'platform', 'presence',
|
|
388
|
+
'preview', 'primary', 'promote', 'protocol', 'push', 'query', 'rate', 'read',
|
|
389
|
+
'reason', 'receipt', 'received', 'recording', 'reject', 'remove', 'request', 'response',
|
|
390
|
+
'retry', 'revoke', 's.whatsapp.net', 'send', 'session', 'settings', 'sha256', 'signature',
|
|
391
|
+
'since', 'status', 'subscribe', 'success', 'sync', 'tag', 'target', 'text',
|
|
392
|
+
'timestamp', 'true', 'type', 'unblock', 'unavailable', 'unread', 'update', 'user',
|
|
393
|
+
'verify', 'version', 'video', 'visible', 'voice', 'wait', 'warning', 'whatsapp',
|
|
394
|
+
'ack', 'expire', 'image_message', 'video_message', 'audio_message', 'document_message',
|
|
395
|
+
'sticker_message', 'location_message', 'contact_message', 'poll_message', 'server_sync',
|
|
396
|
+
'business_hours', 'business_profile', 'catalog', 'label', 'order', 'product',
|
|
397
|
+
'reaction', 'status_attendance', 'ephemeral', 'view_once', 'keep_in_chat',
|
|
398
|
+
'unarchive', 'archive', 'mute', 'star', 'pin_v2'
|
|
399
|
+
],
|
|
400
|
+
// Dictionary 1, 2, 3 - shorter/smaller for less common tags
|
|
401
|
+
[], [], []
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Find string in dictionary, return encoded index
|
|
406
|
+
* Returns -1 if not found
|
|
407
|
+
*/
|
|
408
|
+
function findInDict(str: string): number {
|
|
409
|
+
for (let dict = 0; dict < DICTIONARIES.length; dict++) {
|
|
410
|
+
const idx = DICTIONARIES[dict].indexOf(str);
|
|
411
|
+
if (idx !== -1) {
|
|
412
|
+
return (dict << 8) | idx;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return -1;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Lookup dict by index
|
|
420
|
+
*/
|
|
421
|
+
function lookupDict(dict: number, index: number): string {
|
|
422
|
+
return DICTIONARIES[dict]?.[index] ?? '';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ============== Helpers ==============
|
|
426
|
+
|
|
427
|
+
export function encodeBinaryNode(node: BinaryNode): Uint8Array {
|
|
428
|
+
return new BinaryEncoder().encode(node);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function decodeBinaryNode(data: Uint8Array): BinaryNode {
|
|
432
|
+
return new BinaryDecoder().decode(data);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Convert binary node to XML-like string for debugging
|
|
437
|
+
*/
|
|
438
|
+
export function binaryNodeToString(node: BinaryNode, indent: number = 0): string {
|
|
439
|
+
const pad = ' '.repeat(indent);
|
|
440
|
+
let str = `${pad}<${node.tag}`;
|
|
441
|
+
for (const [k, v] of Object.entries(node.attrs)) {
|
|
442
|
+
str += ` ${k}="${v}"`;
|
|
443
|
+
}
|
|
444
|
+
if (!node.content) {
|
|
445
|
+
return str + ' />';
|
|
446
|
+
}
|
|
447
|
+
if (typeof node.content === 'string') {
|
|
448
|
+
return str + `>${node.content}</${node.tag}>`;
|
|
449
|
+
}
|
|
450
|
+
if (node.content instanceof Uint8Array) {
|
|
451
|
+
return str + `>[${node.content.length} bytes]</${node.tag}>`;
|
|
452
|
+
}
|
|
453
|
+
str += '>\n';
|
|
454
|
+
for (const child of node.content) {
|
|
455
|
+
if (typeof child === 'string') {
|
|
456
|
+
str += `${pad} ${child}\n`;
|
|
457
|
+
} else if (child instanceof Uint8Array) {
|
|
458
|
+
str += `${pad} [${child.length} bytes]\n`;
|
|
459
|
+
} else {
|
|
460
|
+
str += binaryNodeToString(child, indent + 1) + '\n';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return str + `${pad}</${node.tag}>`;
|
|
464
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waliwa - Crypto utilities
|
|
3
|
+
* Hybrid approach: menggunakan @noble/curves & @noble/hashes untuk primitives
|
|
4
|
+
* Manual implementasi untuk WhatsApp-specific encoding (noise handshake, message framing)
|
|
5
|
+
*
|
|
6
|
+
* RAM optimization: gunakan Uint8Array (bukan Buffer) di mana memungkinkan untuk
|
|
7
|
+
* hindari overhead V8 Buffer pool.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { x25519 } from '@noble/curves/ed25519';
|
|
11
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
12
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
13
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
14
|
+
import { sha1 } from '@noble/hashes/sha1';
|
|
15
|
+
import { gcm } from '@noble/ciphers/aes';
|
|
16
|
+
import { randomBytes } from 'crypto';
|
|
17
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
18
|
+
|
|
19
|
+
// ============== Curve25519 ==============
|
|
20
|
+
|
|
21
|
+
export interface KeyPair {
|
|
22
|
+
private: Uint8Array;
|
|
23
|
+
public: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate Curve25519 key pair untuk Noise Protocol / Signal keys
|
|
28
|
+
* RAM-friendly: gunakan direct x25519 dari @noble/curves (no bindings).
|
|
29
|
+
*/
|
|
30
|
+
export function generateCurveKeyPair(): KeyPair {
|
|
31
|
+
const privateKey = randomBytes(32);
|
|
32
|
+
const publicKey = x25519.getPublicKey(privateKey);
|
|
33
|
+
return { private: privateKey, public: publicKey };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* X25519 ECDH - compute shared secret
|
|
38
|
+
*/
|
|
39
|
+
export function curveSharedSecret(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
|
|
40
|
+
return x25519.getSharedSecret(privateKey, publicKey);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sign data dengan Ed25519 (untuk Signal signed pre-keys)
|
|
45
|
+
*/
|
|
46
|
+
export function curveSign(privateKey: Uint8Array, data: Uint8Array): Uint8Array {
|
|
47
|
+
// Use ed25519 sign via noble - we use it for identity signing
|
|
48
|
+
const { ed25519 } = require('@noble/curves/ed25519');
|
|
49
|
+
return ed25519.sign(data, privateKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verify Ed25519 signature
|
|
54
|
+
*/
|
|
55
|
+
export function curveVerify(publicKey: Uint8Array, data: Uint8Array, signature: Uint8Array): boolean {
|
|
56
|
+
const { ed25519 } = require('@noble/curves/ed25519');
|
|
57
|
+
try {
|
|
58
|
+
return ed25519.verify(signature, data, publicKey);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============== Hashing ==============
|
|
65
|
+
|
|
66
|
+
export function sha256Hash(data: Uint8Array): Uint8Array {
|
|
67
|
+
return sha256(data);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sha512Hash(data: Uint8Array): Uint8Array {
|
|
71
|
+
return sha512(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function sha1Hash(data: Uint8Array): Uint8Array {
|
|
75
|
+
return sha1(data);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
79
|
+
return hmac.create(sha256, key).update(data).digest();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function hmacSha512(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
83
|
+
return hmac.create(sha512, key).update(data).digest();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============== HKDF (RFC 5869) ==============
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* HKDF - HMAC-based Key Derivation Function
|
|
90
|
+
* WhatsApp menggunakan HKDF extensively untuk derive keys dari shared secrets
|
|
91
|
+
*/
|
|
92
|
+
export function hkdfDerive(
|
|
93
|
+
ikm: Uint8Array,
|
|
94
|
+
salt: Uint8Array,
|
|
95
|
+
info: Uint8Array,
|
|
96
|
+
length: number
|
|
97
|
+
): Uint8Array {
|
|
98
|
+
const prk = hmacSha256(salt.length === 0 ? new Uint8Array(32) : salt, ikm);
|
|
99
|
+
return hkdf(sha256, prk, salt, info, length);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============== AES-256-GCM ==============
|
|
103
|
+
|
|
104
|
+
export interface AESCipher {
|
|
105
|
+
encrypt(plaintext: Uint8Array, iv: Uint8Array, aad?: Uint8Array): { ciphertext: Uint8Array; tag: Uint8Array };
|
|
106
|
+
decrypt(ciphertext: Uint8Array, iv: Uint8Array, tag: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* AES-256-GCM cipher (sebuah komponen inti WA message encryption)
|
|
111
|
+
*/
|
|
112
|
+
export function createAESCipher(key: Uint8Array): AESCipher {
|
|
113
|
+
return {
|
|
114
|
+
encrypt(plaintext: Uint8Array, iv: Uint8Array, aad?: Uint8Array) {
|
|
115
|
+
const cipher = gcm(key, iv, aad);
|
|
116
|
+
const sealed = cipher.encrypt(plaintext);
|
|
117
|
+
// GCM output dari @noble: ciphertext + tag (16 byte) di akhir
|
|
118
|
+
const ciphertext = sealed.subarray(0, sealed.length - 16);
|
|
119
|
+
const tag = sealed.subarray(sealed.length - 16);
|
|
120
|
+
return { ciphertext, tag };
|
|
121
|
+
},
|
|
122
|
+
decrypt(ciphertext: Uint8Array, iv: Uint8Array, tag: Uint8Array, aad?: Uint8Array) {
|
|
123
|
+
const cipher = gcm(key, iv, aad);
|
|
124
|
+
const sealed = new Uint8Array(ciphertext.length + tag.length);
|
|
125
|
+
sealed.set(ciphertext, 0);
|
|
126
|
+
sealed.set(tag, ciphertext.length);
|
|
127
|
+
return cipher.decrypt(sealed);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============== AES-256-CBC (untuk media encryption) ==============
|
|
133
|
+
|
|
134
|
+
export function aesEncryptCBC(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array): Uint8Array {
|
|
135
|
+
// PKCS7 padding
|
|
136
|
+
const blockSize = 16;
|
|
137
|
+
const padLength = blockSize - (plaintext.length % blockSize);
|
|
138
|
+
const padded = new Uint8Array(plaintext.length + padLength);
|
|
139
|
+
padded.set(plaintext, 0);
|
|
140
|
+
for (let i = plaintext.length; i < padded.length; i++) {
|
|
141
|
+
padded[i] = padLength;
|
|
142
|
+
}
|
|
143
|
+
// Use Node crypto for CBC (Noble doesn't expose CBC)
|
|
144
|
+
const { createCipheriv } = require('crypto');
|
|
145
|
+
const cipher = createCipheriv('aes-256-cbc', Buffer.from(key), Buffer.from(iv));
|
|
146
|
+
return new Uint8Array(Buffer.concat([cipher.update(Buffer.from(padded)), cipher.final()]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function aesDecryptCBC(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array): Uint8Array {
|
|
150
|
+
const { createDecipheriv } = require('crypto');
|
|
151
|
+
const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key), Buffer.from(iv));
|
|
152
|
+
const padded = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);
|
|
153
|
+
// Remove PKCS7 padding
|
|
154
|
+
const padLength = padded[padded.length - 1];
|
|
155
|
+
return new Uint8Array(padded.subarray(0, padded.length - padLength));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============== Random ==============
|
|
159
|
+
|
|
160
|
+
export function getRandomBytes(length: number): Uint8Array {
|
|
161
|
+
return new Uint8Array(randomBytes(length));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============== Helpers ==============
|
|
165
|
+
|
|
166
|
+
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
|
167
|
+
const total = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
168
|
+
const result = new Uint8Array(total);
|
|
169
|
+
let offset = 0;
|
|
170
|
+
for (const arr of arrays) {
|
|
171
|
+
result.set(arr, offset);
|
|
172
|
+
offset += arr.length;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function areBytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
178
|
+
if (a.length !== b.length) return false;
|
|
179
|
+
let diff = 0;
|
|
180
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
181
|
+
return diff === 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Constant-time comparison untuk hmac verification
|
|
186
|
+
*/
|
|
187
|
+
export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
188
|
+
return areBytesEqual(a, b);
|
|
189
|
+
}
|