crownx-decoder 1.0.0__tar.gz
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.
- crownx_decoder-1.0.0/PKG-INFO +22 -0
- crownx_decoder-1.0.0/README.md +7 -0
- crownx_decoder-1.0.0/crownx_decoder/__init__.py +0 -0
- crownx_decoder-1.0.0/crownx_decoder/decoder.py +638 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/PKG-INFO +22 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/SOURCES.txt +10 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/dependency_links.txt +1 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/entry_points.txt +2 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/requires.txt +1 -0
- crownx_decoder-1.0.0/crownx_decoder.egg-info/top_level.txt +1 -0
- crownx_decoder-1.0.0/setup.cfg +4 -0
- crownx_decoder-1.0.0/setup.py +20 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crownx-decoder
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Protobuf raw and AES decoder CLI tool
|
|
5
|
+
Author: CrownX
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pycryptodome
|
|
9
|
+
Dynamic: author
|
|
10
|
+
Dynamic: description
|
|
11
|
+
Dynamic: description-content-type
|
|
12
|
+
Dynamic: requires-dist
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
Dynamic: summary
|
|
15
|
+
|
|
16
|
+
# Crownx Decoder
|
|
17
|
+
|
|
18
|
+
Protobuf & AES Decoder CLI Tool.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
```bash
|
|
22
|
+
pip install crownx-decoder
|
|
File without changes
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Protobuf Decoder - JSON Output Version
|
|
4
|
+
Decodes a raw protobuf message given as hex or base64 text, and prints
|
|
5
|
+
the result as beautifully formatted JSON. Nested messages are expanded automatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import struct
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
import os
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from Crypto.Cipher import AES
|
|
17
|
+
from Crypto.Util.Padding import unpad
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Wire types (same constants as protobufDecoder.js)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
class TYPES:
|
|
23
|
+
MSG_LEN_DELIMITER = -1
|
|
24
|
+
VARINT = 0
|
|
25
|
+
FIXED64 = 1
|
|
26
|
+
LENDELIM = 2
|
|
27
|
+
FIXED32 = 5
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Input parsing (hexUtils.js)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
def is_hex(s):
|
|
33
|
+
return bool(s) and all(c in "0123456789abcdef" for c in s) and len(s) % 2 == 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_input(input_str, force_base64=False):
|
|
37
|
+
normalized = re.sub(r"\s", "", input_str)
|
|
38
|
+
normalized_hex = re.sub(r"0x", "", normalized, flags=re.IGNORECASE).lower()
|
|
39
|
+
if not force_base64 and is_hex(normalized_hex):
|
|
40
|
+
return bytes.fromhex(normalized_hex)
|
|
41
|
+
else:
|
|
42
|
+
# pad base64 if needed
|
|
43
|
+
padded = normalized + "=" * (-len(normalized) % 4)
|
|
44
|
+
return base64.b64decode(padded)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def buffer_to_pretty_hex(b):
|
|
48
|
+
return " ".join(f"{c:02x}" for c in b)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def buffer_le_to_be_hex(b):
|
|
52
|
+
output = ""
|
|
53
|
+
for v in b:
|
|
54
|
+
h = f"{v:x}"
|
|
55
|
+
if len(h) == 1:
|
|
56
|
+
h = "0" + h
|
|
57
|
+
output = h + output
|
|
58
|
+
return output
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Varint decoding (varintUtils.js)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
def decode_varint(buffer, offset):
|
|
64
|
+
result = 0
|
|
65
|
+
shift = 0
|
|
66
|
+
byte = 0
|
|
67
|
+
start = offset
|
|
68
|
+
while True:
|
|
69
|
+
if offset >= len(buffer):
|
|
70
|
+
raise IndexError("Index out of bound decoding varint")
|
|
71
|
+
byte = buffer[offset]
|
|
72
|
+
offset += 1
|
|
73
|
+
result += (byte & 0x7F) << shift
|
|
74
|
+
shift += 7
|
|
75
|
+
if byte < 0x80:
|
|
76
|
+
break
|
|
77
|
+
return {"value": result, "length": offset - start}
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Signed/twos-complement helpers (intUtils.js)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
def interpret_as_signed_type(n):
|
|
83
|
+
"""ZigZag decode (protobuf sint semantics)."""
|
|
84
|
+
if n % 2 == 0:
|
|
85
|
+
return n // 2
|
|
86
|
+
else:
|
|
87
|
+
return -((n + 1) // 2)
|
|
88
|
+
|
|
89
|
+
def interpret_as_twos_complement(n, bits):
|
|
90
|
+
"""If the top bit within `bits` is set, interpret as a negative
|
|
91
|
+
two's-complement number of that bit width."""
|
|
92
|
+
sign_bit = (n >> (bits - 1)) & 1
|
|
93
|
+
if sign_bit == 1:
|
|
94
|
+
return n - (1 << bits)
|
|
95
|
+
return n
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# BufferReader (protobufDecoder.js)
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
class BufferReader:
|
|
101
|
+
def __init__(self, buffer):
|
|
102
|
+
self.buffer = buffer
|
|
103
|
+
self.offset = 0
|
|
104
|
+
self.saved_offset = 0
|
|
105
|
+
|
|
106
|
+
def read_varint(self):
|
|
107
|
+
result = decode_varint(self.buffer, self.offset)
|
|
108
|
+
self.offset += result["length"]
|
|
109
|
+
return result["value"]
|
|
110
|
+
|
|
111
|
+
def read_buffer(self, length):
|
|
112
|
+
self.check_bytes(length)
|
|
113
|
+
result = self.buffer[self.offset:self.offset + length]
|
|
114
|
+
self.offset += length
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
def try_skip_grpc_header(self):
|
|
118
|
+
backup_offset = self.offset
|
|
119
|
+
if self.left_bytes() >= 5 and self.buffer[self.offset] == 0:
|
|
120
|
+
self.offset += 1
|
|
121
|
+
length = struct.unpack_from(">I", self.buffer, self.offset)[0]
|
|
122
|
+
self.offset += 4
|
|
123
|
+
if length > self.left_bytes():
|
|
124
|
+
self.offset = backup_offset
|
|
125
|
+
|
|
126
|
+
def left_bytes(self):
|
|
127
|
+
return len(self.buffer) - self.offset
|
|
128
|
+
|
|
129
|
+
def check_bytes(self, length):
|
|
130
|
+
available = self.left_bytes()
|
|
131
|
+
if length > available:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Not enough bytes left. Requested: {length} left: {available}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def checkpoint(self):
|
|
137
|
+
self.saved_offset = self.offset
|
|
138
|
+
|
|
139
|
+
def reset_to_checkpoint(self):
|
|
140
|
+
self.offset = self.saved_offset
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Core decode (decodeProto in protobufDecoder.js)
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
def decode_proto(buffer, parse_delimited=False, skip_grpc_header=False, scan_embedded=True):
|
|
146
|
+
reader = BufferReader(buffer)
|
|
147
|
+
parts = []
|
|
148
|
+
|
|
149
|
+
if skip_grpc_header:
|
|
150
|
+
reader.try_skip_grpc_header()
|
|
151
|
+
|
|
152
|
+
proto_buf_msg_length = 0
|
|
153
|
+
proto_buf_msg_end = 0
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------
|
|
156
|
+
# Normal protobuf parser
|
|
157
|
+
# ---------------------------------------------------------------
|
|
158
|
+
while reader.left_bytes() > 0:
|
|
159
|
+
reader.checkpoint()
|
|
160
|
+
field_start = reader.offset
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
if parse_delimited and proto_buf_msg_end == reader.offset:
|
|
164
|
+
proto_buf_msg_length = reader.read_varint()
|
|
165
|
+
proto_buf_msg_end = reader.offset + proto_buf_msg_length
|
|
166
|
+
|
|
167
|
+
parts.append({
|
|
168
|
+
"index": -1,
|
|
169
|
+
"type": TYPES.MSG_LEN_DELIMITER,
|
|
170
|
+
"value": proto_buf_msg_length,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
index_type = reader.read_varint()
|
|
174
|
+
|
|
175
|
+
type_ = index_type & 0b111
|
|
176
|
+
index = index_type >> 3
|
|
177
|
+
|
|
178
|
+
# -------------------------------------------------------
|
|
179
|
+
# Invalid protobuf field number
|
|
180
|
+
# -------------------------------------------------------
|
|
181
|
+
if index <= 0:
|
|
182
|
+
raise ValueError("Invalid protobuf field number")
|
|
183
|
+
|
|
184
|
+
# -------------------------------------------------------
|
|
185
|
+
# VARINT
|
|
186
|
+
# -------------------------------------------------------
|
|
187
|
+
if type_ == TYPES.VARINT:
|
|
188
|
+
value = reader.read_varint()
|
|
189
|
+
|
|
190
|
+
# -------------------------------------------------------
|
|
191
|
+
# FIXED64
|
|
192
|
+
# -------------------------------------------------------
|
|
193
|
+
elif type_ == TYPES.FIXED64:
|
|
194
|
+
value = reader.read_buffer(8)
|
|
195
|
+
|
|
196
|
+
# -------------------------------------------------------
|
|
197
|
+
# LENGTH DELIMITED
|
|
198
|
+
# -------------------------------------------------------
|
|
199
|
+
elif type_ == TYPES.LENDELIM:
|
|
200
|
+
length = reader.read_varint()
|
|
201
|
+
|
|
202
|
+
if length < 0 or length > reader.left_bytes():
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"Invalid length-delimited size: {length}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
value = reader.read_buffer(length)
|
|
208
|
+
|
|
209
|
+
# -------------------------------------------------------
|
|
210
|
+
# START GROUP
|
|
211
|
+
# -------------------------------------------------------
|
|
212
|
+
elif type_ == 3:
|
|
213
|
+
group_start = reader.offset
|
|
214
|
+
depth = 1
|
|
215
|
+
|
|
216
|
+
while reader.left_bytes() > 0 and depth > 0:
|
|
217
|
+
tag = reader.read_varint()
|
|
218
|
+
wire = tag & 0x07
|
|
219
|
+
|
|
220
|
+
if wire == 3:
|
|
221
|
+
depth += 1
|
|
222
|
+
|
|
223
|
+
elif wire == 4:
|
|
224
|
+
depth -= 1
|
|
225
|
+
|
|
226
|
+
if depth == 0:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
elif wire == 0:
|
|
230
|
+
reader.read_varint()
|
|
231
|
+
|
|
232
|
+
elif wire == 1:
|
|
233
|
+
reader.read_buffer(8)
|
|
234
|
+
|
|
235
|
+
elif wire == 2:
|
|
236
|
+
length = reader.read_varint()
|
|
237
|
+
|
|
238
|
+
if length < 0 or length > reader.left_bytes():
|
|
239
|
+
raise ValueError(
|
|
240
|
+
"Invalid length inside group"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
reader.read_buffer(length)
|
|
244
|
+
|
|
245
|
+
elif wire == 5:
|
|
246
|
+
reader.read_buffer(4)
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Invalid wire type inside group: {wire}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if depth != 0:
|
|
254
|
+
raise ValueError("Unterminated protobuf group")
|
|
255
|
+
|
|
256
|
+
group_end = reader.offset
|
|
257
|
+
value = buffer[group_start:group_end]
|
|
258
|
+
|
|
259
|
+
# -------------------------------------------------------
|
|
260
|
+
# END GROUP
|
|
261
|
+
# -------------------------------------------------------
|
|
262
|
+
elif type_ == 4:
|
|
263
|
+
raise ValueError("Unexpected END_GROUP")
|
|
264
|
+
|
|
265
|
+
# -------------------------------------------------------
|
|
266
|
+
# FIXED32
|
|
267
|
+
# -------------------------------------------------------
|
|
268
|
+
elif type_ == TYPES.FIXED32:
|
|
269
|
+
value = reader.read_buffer(4)
|
|
270
|
+
|
|
271
|
+
else:
|
|
272
|
+
raise ValueError(f"Unknown wire type: {type_}")
|
|
273
|
+
|
|
274
|
+
parts.append({
|
|
275
|
+
"index": index,
|
|
276
|
+
"type": type_,
|
|
277
|
+
"value": value,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
except (ValueError, IndexError, struct.error):
|
|
281
|
+
reader.offset = field_start
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
leftover = reader.read_buffer(reader.left_bytes())
|
|
285
|
+
|
|
286
|
+
result = {
|
|
287
|
+
"parts": parts,
|
|
288
|
+
"leftOver": leftover,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------
|
|
292
|
+
# Embedded protobuf scan
|
|
293
|
+
# ---------------------------------------------------------------
|
|
294
|
+
parsed_bytes = len(buffer) - len(leftover)
|
|
295
|
+
|
|
296
|
+
has_bad_wire = any(
|
|
297
|
+
p.get("type") in (3, 4)
|
|
298
|
+
for p in parts
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
poor_parse = (
|
|
302
|
+
not parts
|
|
303
|
+
or has_bad_wire
|
|
304
|
+
or parsed_bytes < max(16, len(buffer) // 2)
|
|
305
|
+
or len(leftover) > max(16, len(buffer) // 2)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if scan_embedded and poor_parse and len(buffer) > 1:
|
|
309
|
+
|
|
310
|
+
best = None
|
|
311
|
+
|
|
312
|
+
# -----------------------------------------------------------
|
|
313
|
+
# Search every byte offset for a coherent protobuf message
|
|
314
|
+
# -----------------------------------------------------------
|
|
315
|
+
for start in range(1, len(buffer)):
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
tag_info = decode_varint(buffer, start)
|
|
319
|
+
|
|
320
|
+
first_tag = tag_info["value"]
|
|
321
|
+
first_wire = first_tag & 0x07
|
|
322
|
+
first_field = first_tag >> 3
|
|
323
|
+
|
|
324
|
+
if first_field <= 0:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Avoid false START_GROUP / END_GROUP candidates
|
|
328
|
+
if first_wire in (3, 4):
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
except (ValueError, IndexError, struct.error):
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
# -------------------------------------------------------
|
|
335
|
+
# Parse candidate
|
|
336
|
+
# -------------------------------------------------------
|
|
337
|
+
candidate = decode_proto(
|
|
338
|
+
buffer[start:],
|
|
339
|
+
parse_delimited=False,
|
|
340
|
+
skip_grpc_header=False,
|
|
341
|
+
scan_embedded=False,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
candidate_parts = candidate.get("parts", [])
|
|
345
|
+
candidate_left = candidate.get("leftOver", b"")
|
|
346
|
+
|
|
347
|
+
if not candidate_parts:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Reject candidates containing group wire types
|
|
351
|
+
if any(
|
|
352
|
+
p.get("type") in (3, 4)
|
|
353
|
+
for p in candidate_parts
|
|
354
|
+
):
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
candidate_buffer_length = len(buffer) - start
|
|
358
|
+
|
|
359
|
+
consumed = (
|
|
360
|
+
candidate_buffer_length
|
|
361
|
+
- len(candidate_left)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if consumed <= 0:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Require a meaningful protobuf sequence
|
|
368
|
+
if consumed < 32:
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
if len(candidate_parts) < 3:
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
# -------------------------------------------------------
|
|
375
|
+
# Candidate score
|
|
376
|
+
#
|
|
377
|
+
# Priority:
|
|
378
|
+
# 1. No leftover
|
|
379
|
+
# 2. Most consumed bytes
|
|
380
|
+
# 3. Most fields
|
|
381
|
+
# 4. Earliest offset
|
|
382
|
+
# -------------------------------------------------------
|
|
383
|
+
no_leftover = len(candidate_left) == 0
|
|
384
|
+
|
|
385
|
+
score = (
|
|
386
|
+
1 if no_leftover else 0,
|
|
387
|
+
consumed,
|
|
388
|
+
len(candidate_parts),
|
|
389
|
+
-start,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if best is None or score > best["score"]:
|
|
393
|
+
best = {
|
|
394
|
+
"score": score,
|
|
395
|
+
"offset": start,
|
|
396
|
+
"decoded": candidate,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
# -----------------------------------------------------------
|
|
400
|
+
# Use best embedded protobuf candidate
|
|
401
|
+
# -----------------------------------------------------------
|
|
402
|
+
if best is not None:
|
|
403
|
+
result = best["decoded"]
|
|
404
|
+
|
|
405
|
+
result["embeddedOffset"] = best["offset"]
|
|
406
|
+
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
# Part decoders (protobufPartDecoder.jsx)
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
def decode_fixed32(value):
|
|
413
|
+
int_value = struct.unpack_from("<i", value)[0]
|
|
414
|
+
uint_value = struct.unpack_from("<I", value)[0]
|
|
415
|
+
float_value = struct.unpack_from("<f", value)[0]
|
|
416
|
+
|
|
417
|
+
result = [{"type": "int", "value": int_value}]
|
|
418
|
+
if int_value != uint_value:
|
|
419
|
+
result.append({"type": "uint", "value": uint_value})
|
|
420
|
+
result.append({"type": "float", "value": float_value})
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def decode_fixed64(value):
|
|
425
|
+
double_value = struct.unpack_from("<d", value)[0]
|
|
426
|
+
uint_value = int(buffer_le_to_be_hex(value), 16)
|
|
427
|
+
int_value = interpret_as_twos_complement(uint_value, 64)
|
|
428
|
+
|
|
429
|
+
result = [{"type": "int", "value": str(int_value)}]
|
|
430
|
+
if int_value != uint_value:
|
|
431
|
+
result.append({"type": "uint", "value": str(uint_value)})
|
|
432
|
+
result.append({"type": "double", "value": double_value})
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def decode_varint_parts(value):
|
|
437
|
+
result = []
|
|
438
|
+
uint_val = int(value)
|
|
439
|
+
result.append({"type": "uint", "value": str(uint_val)})
|
|
440
|
+
|
|
441
|
+
for bits in (8, 16, 32, 64):
|
|
442
|
+
int_val = interpret_as_twos_complement(uint_val, bits)
|
|
443
|
+
if int_val != uint_val:
|
|
444
|
+
result.append({"type": f"int{bits}", "value": str(int_val)})
|
|
445
|
+
|
|
446
|
+
signed_int_val = interpret_as_signed_type(uint_val)
|
|
447
|
+
if signed_int_val != uint_val:
|
|
448
|
+
result.append({"type": "sint", "value": str(signed_int_val)})
|
|
449
|
+
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def decode_string_or_bytes(value):
|
|
454
|
+
if not len(value):
|
|
455
|
+
return {"type": "string|bytes", "value": ""}
|
|
456
|
+
try:
|
|
457
|
+
decoded_str = value.decode("utf-8")
|
|
458
|
+
|
|
459
|
+
# 1. Agar string me valid JSON formatted text hai, toh use JSON object me parse karein
|
|
460
|
+
trimmed = decoded_str.strip()
|
|
461
|
+
if (trimmed.startswith("{") and trimmed.endswith("}")) or (trimmed.startswith("[") and trimmed.endswith("]")):
|
|
462
|
+
try:
|
|
463
|
+
parsed_json = json.loads(trimmed)
|
|
464
|
+
return {"type": "json", "value": parsed_json}
|
|
465
|
+
except json.JSONDecodeError:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
# 2. Check karein ki string me sirf printable characters hain ya garbage control characters bhi hain
|
|
469
|
+
is_printable = True
|
|
470
|
+
for char in decoded_str:
|
|
471
|
+
o = ord(char)
|
|
472
|
+
# Control characters (ASCII < 32) ko exclude karein (except tab, newline, carriage return)
|
|
473
|
+
if o < 32 and o not in (9, 10, 13):
|
|
474
|
+
is_printable = False
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
if is_printable:
|
|
478
|
+
return {"type": "string", "value": decoded_str}
|
|
479
|
+
else:
|
|
480
|
+
# Agar control characters hain toh garbage string ke bajay binary hex return karein
|
|
481
|
+
return {"type": "bytes", "value": buffer_to_pretty_hex(value)}
|
|
482
|
+
|
|
483
|
+
except UnicodeDecodeError:
|
|
484
|
+
return {"type": "bytes", "value": buffer_to_pretty_hex(value)}
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# JSON Formatting Transformer
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
def process_decoded_to_dict(decoded):
|
|
490
|
+
"""Recursively converts the raw parsed decoded dict into a clean JSON-serializable dictionary."""
|
|
491
|
+
out_parts = []
|
|
492
|
+
|
|
493
|
+
for part in decoded["parts"]:
|
|
494
|
+
t = part["type"]
|
|
495
|
+
out_part = {
|
|
496
|
+
"field": part["index"] if part["index"] != -1 else None
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Remove field key if it is None (e.g., MSG_LEN_DELIMITER)
|
|
500
|
+
if out_part["field"] is None:
|
|
501
|
+
del out_part["field"]
|
|
502
|
+
|
|
503
|
+
if t == TYPES.VARINT:
|
|
504
|
+
out_part["type"] = "varint"
|
|
505
|
+
out_part["content"] = {d["type"]: d["value"] for d in decode_varint_parts(part["value"])}
|
|
506
|
+
|
|
507
|
+
elif t == TYPES.FIXED64:
|
|
508
|
+
out_part["type"] = "fixed64"
|
|
509
|
+
out_part["content"] = {d["type"]: d["value"] for d in decode_fixed64(part["value"])}
|
|
510
|
+
|
|
511
|
+
elif t == TYPES.FIXED32:
|
|
512
|
+
out_part["type"] = "fixed32"
|
|
513
|
+
out_part["content"] = {d["type"]: d["value"] for d in decode_fixed32(part["value"])}
|
|
514
|
+
|
|
515
|
+
elif t == TYPES.LENDELIM:
|
|
516
|
+
raw = part["value"]
|
|
517
|
+
nested = decode_proto(raw)
|
|
518
|
+
# Exactly same nested check condition
|
|
519
|
+
if len(raw) > 0 and len(nested["leftOver"]) == 0 and len(nested["parts"]) > 0:
|
|
520
|
+
out_part["type"] = "protobuf"
|
|
521
|
+
out_part["content"] = process_decoded_to_dict(nested)
|
|
522
|
+
else:
|
|
523
|
+
dec_str = decode_string_or_bytes(raw)
|
|
524
|
+
out_part["type"] = dec_str["type"]
|
|
525
|
+
out_part["content"] = dec_str["value"]
|
|
526
|
+
|
|
527
|
+
elif t == TYPES.MSG_LEN_DELIMITER:
|
|
528
|
+
out_part["type"] = "message_delimiter"
|
|
529
|
+
out_part["content"] = {"length": part["value"]}
|
|
530
|
+
|
|
531
|
+
else:
|
|
532
|
+
out_part["type"] = "unknown"
|
|
533
|
+
out_part["content"] = buffer_to_pretty_hex(part.get("value", b""))
|
|
534
|
+
|
|
535
|
+
out_parts.append(out_part)
|
|
536
|
+
|
|
537
|
+
result = {"parts": out_parts}
|
|
538
|
+
|
|
539
|
+
if len(decoded["leftOver"]) > 0:
|
|
540
|
+
result["leftOver"] = buffer_to_pretty_hex(decoded["leftOver"])
|
|
541
|
+
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# AES Decryption Helper
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
def try_aes_decrypt(ciphertext):
|
|
548
|
+
key = b'Yg&tc%DEuh6%Zc^8'
|
|
549
|
+
iv = b'6oyZDr22E3ychjM%'
|
|
550
|
+
try:
|
|
551
|
+
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
552
|
+
# Decrypt karega aur PKCS7 padding remove karega
|
|
553
|
+
decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
|
554
|
+
return decrypted
|
|
555
|
+
except Exception:
|
|
556
|
+
# Agar decrypt/unpad fail ho jaye to None return karega
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
# ---------------------------------------------------------------------------
|
|
560
|
+
# CLI
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
def main():
|
|
563
|
+
parser = argparse.ArgumentParser(
|
|
564
|
+
description="Decode a raw protobuf message (hex or base64) and print it as JSON."
|
|
565
|
+
)
|
|
566
|
+
parser.add_argument(
|
|
567
|
+
"data", nargs="?", help="Hex or base64 encoded protobuf bytes. Reads stdin if omitted."
|
|
568
|
+
)
|
|
569
|
+
parser.add_argument(
|
|
570
|
+
"--base64", action="store_true", help="Force interpreting input as base64."
|
|
571
|
+
)
|
|
572
|
+
parser.add_argument(
|
|
573
|
+
"--delimited", action="store_true",
|
|
574
|
+
help="Parse input as a stream of length-delimited messages (e.g. gRPC streaming)."
|
|
575
|
+
)
|
|
576
|
+
parser.add_argument(
|
|
577
|
+
"--grpc", action="store_true",
|
|
578
|
+
help="Try to skip a leading gRPC framing header (1 flag byte + 4 byte length)."
|
|
579
|
+
)
|
|
580
|
+
args = parser.parse_args()
|
|
581
|
+
|
|
582
|
+
raw_input = args.data if args.data is not None else sys.stdin.read()
|
|
583
|
+
if raw_input is None or raw_input.strip() == "":
|
|
584
|
+
print(json.dumps({"error": "No input provided."}, indent=2))
|
|
585
|
+
sys.exit(1)
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
buffer = parse_input(raw_input, force_base64=args.base64)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
print(json.dumps({"error": f"Failed to parse input as hex or base64: {e}"}, indent=2))
|
|
591
|
+
sys.exit(1)
|
|
592
|
+
|
|
593
|
+
# Pehle normal try karega
|
|
594
|
+
decoded = decode_proto(buffer, parse_delimited=args.delimited, skip_grpc_header=args.grpc)
|
|
595
|
+
|
|
596
|
+
# Firr KEY (AES) se try karega
|
|
597
|
+
decrypted_buffer = try_aes_decrypt(buffer)
|
|
598
|
+
if decrypted_buffer is not None:
|
|
599
|
+
decoded_aes = decode_proto(decrypted_buffer, parse_delimited=args.delimited, skip_grpc_header=args.grpc)
|
|
600
|
+
# Agar AES wali decoding zyada valid lagti hai (leftOver kam hai ya normal parts zero the)
|
|
601
|
+
if len(decoded_aes["leftOver"]) <= len(decoded["leftOver"]) or not decoded["parts"]:
|
|
602
|
+
decoded = decoded_aes
|
|
603
|
+
decoded["is_aes_decrypted"] = True
|
|
604
|
+
|
|
605
|
+
# Convert exactly parsed logic into a neat JSON dict and print it
|
|
606
|
+
json_data = process_decoded_to_dict(decoded)
|
|
607
|
+
|
|
608
|
+
# Agar AES se decode hua hai toh JSON me indicator add kar dega
|
|
609
|
+
if decoded.get("is_aes_decrypted"):
|
|
610
|
+
json_data["is_aes_decrypted"] = True
|
|
611
|
+
|
|
612
|
+
print(json.dumps(json_data, indent=2, ensure_ascii=False))
|
|
613
|
+
|
|
614
|
+
# Auto-save decoded JSON sequentially to /sdcard/#DECODED/
|
|
615
|
+
try:
|
|
616
|
+
dir_path = "/sdcard/#DECODED"
|
|
617
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
618
|
+
date_str = datetime.now().strftime("%d-%m-%Y")
|
|
619
|
+
|
|
620
|
+
index = 1
|
|
621
|
+
while True:
|
|
622
|
+
file_name = f"decoded{index}_{date_str}.json"
|
|
623
|
+
full_path = os.path.join(dir_path, file_name)
|
|
624
|
+
if not os.path.exists(full_path):
|
|
625
|
+
break
|
|
626
|
+
index += 1
|
|
627
|
+
|
|
628
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
629
|
+
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
630
|
+
f.write(f"\n\ninput = {raw_input.strip()}\n")
|
|
631
|
+
print(f"\n[+] Saved to: {full_path}")
|
|
632
|
+
except Exception as e:
|
|
633
|
+
print(f"\n[-] Failed to auto-save file to /sdcard: {e}")
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
if __name__ == "__main__":
|
|
637
|
+
main()
|
|
638
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crownx-decoder
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Protobuf raw and AES decoder CLI tool
|
|
5
|
+
Author: CrownX
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pycryptodome
|
|
9
|
+
Dynamic: author
|
|
10
|
+
Dynamic: description
|
|
11
|
+
Dynamic: description-content-type
|
|
12
|
+
Dynamic: requires-dist
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
Dynamic: summary
|
|
15
|
+
|
|
16
|
+
# Crownx Decoder
|
|
17
|
+
|
|
18
|
+
Protobuf & AES Decoder CLI Tool.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
```bash
|
|
22
|
+
pip install crownx-decoder
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
crownx_decoder/__init__.py
|
|
4
|
+
crownx_decoder/decoder.py
|
|
5
|
+
crownx_decoder.egg-info/PKG-INFO
|
|
6
|
+
crownx_decoder.egg-info/SOURCES.txt
|
|
7
|
+
crownx_decoder.egg-info/dependency_links.txt
|
|
8
|
+
crownx_decoder.egg-info/entry_points.txt
|
|
9
|
+
crownx_decoder.egg-info/requires.txt
|
|
10
|
+
crownx_decoder.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pycryptodome
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crownx_decoder
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="crownx-decoder",
|
|
5
|
+
version="1.0.0",
|
|
6
|
+
author="CrownX",
|
|
7
|
+
description="Protobuf raw and AES decoder CLI tool",
|
|
8
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
9
|
+
long_description_content_type="text/markdown",
|
|
10
|
+
packages=find_packages(),
|
|
11
|
+
install_requires=[
|
|
12
|
+
"pycryptodome",
|
|
13
|
+
],
|
|
14
|
+
entry_points={
|
|
15
|
+
"console_scripts": [
|
|
16
|
+
"dec = crownx_decoder.decoder:main", # Isse terminal me 'dec' command register hoga
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
python_requires=">=3.7",
|
|
20
|
+
)
|