ff-decoder 1.0.0__py3-none-any.whl

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.
ff_decoder/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ Free Fire payload decoder.
3
+ """
4
+
5
+ from .decoder import decode_payload, format_messages
6
+
7
+ __all__ = ["decode_payload", "format_messages"]
ff_decoder/cli.py ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Free Fire payload decoder – command line interface.
4
+ """
5
+
6
+ import sys
7
+ import argparse
8
+ from .decoder import decode_payload, format_messages
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(description="Decode Free Fire encrypted payloads.")
12
+ parser.add_argument("file", nargs="?", help="Path to a file containing the payload")
13
+ parser.add_argument("--json", action="store_true", help="Output as JSON (not yet implemented, but reserved)")
14
+ parser.add_argument("--no-decrypt", action="store_true", help="Do not attempt outer decryption (use raw data)")
15
+ args = parser.parse_args()
16
+
17
+ # Read input
18
+ if args.file:
19
+ try:
20
+ with open(args.file, 'r', encoding='utf-8') as f:
21
+ raw_input = f.read().strip()
22
+ except Exception as e:
23
+ print(f"Error reading file: {e}", file=sys.stderr)
24
+ sys.exit(1)
25
+ else:
26
+ # If no file, read from stdin (allowing interactive paste)
27
+ raw_input = sys.stdin.read().strip()
28
+ if not raw_input:
29
+ # No piped input, prompt user
30
+ raw_input = input("Paste hex or base64 payload: ").strip()
31
+
32
+ try:
33
+ messages = decode_payload(raw_input)
34
+ if not messages:
35
+ print("No valid messages decoded.")
36
+ else:
37
+ output = format_messages(messages)
38
+ print(output)
39
+ except Exception as e:
40
+ print(f"Decoding failed: {e}", file=sys.stderr)
41
+ sys.exit(1)
42
+
43
+ if __name__ == "__main__":
44
+ main()
ff_decoder/decoder.py ADDED
@@ -0,0 +1,326 @@
1
+ """
2
+ Free Fire payload decoder – core module.
3
+ """
4
+
5
+ import binascii
6
+ import base64
7
+ import re
8
+ import zlib
9
+ import gzip
10
+ from io import BytesIO
11
+ from Crypto.Cipher import AES
12
+ from Crypto.Util.Padding import unpad, pad
13
+
14
+ # ==================== FIXED KEY AND IV ====================
15
+ KEY = bytes([89, 103, 38, 116, 99, 37, 68, 69, 117, 104, 54, 37, 90, 99, 94, 56])
16
+ IV = bytes([54, 111, 121, 90, 68, 114, 50, 50, 69, 51, 121, 99, 104, 106, 77, 37])
17
+
18
+ # ==================== VARINT HELPERS ====================
19
+ def decode_varint(data, offset):
20
+ value = 0
21
+ shift = 0
22
+ while True:
23
+ if offset >= len(data):
24
+ raise ValueError("Truncated varint")
25
+ b = data[offset]
26
+ value |= (b & 0x7F) << shift
27
+ offset += 1
28
+ if not (b & 0x80):
29
+ break
30
+ shift += 7
31
+ return value, offset
32
+
33
+ # ==================== CUSTOM ID DECRYPTION ====================
34
+ dec = ['80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f',
35
+ '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f',
36
+ 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af',
37
+ 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf',
38
+ 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf',
39
+ 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df',
40
+ 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef',
41
+ 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff']
42
+
43
+ x_list = ['1','01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f',
44
+ '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f',
45
+ '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f',
46
+ '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f',
47
+ '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f',
48
+ '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f',
49
+ '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f',
50
+ '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f']
51
+
52
+ def decrypt_id(da):
53
+ """Decrypt custom ID format (6‑byte hex string like '30c1fab8b103')"""
54
+ if da is not None and len(da) == 10:
55
+ w = 128
56
+ xxx = len(da)/2 - 1
57
+ xxx = str(xxx)[:1]
58
+ for i in range(int(xxx)-1):
59
+ w = w * 128
60
+ x1 = da[:2]
61
+ x2 = da[2:4]
62
+ x3 = da[4:6]
63
+ x4 = da[6:8]
64
+ x5 = da[8:10]
65
+ return str(w * x_list.index(x5) + (dec.index(x2) * 128) + dec.index(x1) + (dec.index(x3) * 128 * 128) + (dec.index(x4) * 128 * 128 * 128))
66
+ if da is not None and len(da) == 8:
67
+ w = 128
68
+ xxx = len(da)/2 - 1
69
+ xxx = str(xxx)[:1]
70
+ for i in range(int(xxx)-1):
71
+ w = w * 128
72
+ x1 = da[:2]
73
+ x2 = da[2:4]
74
+ x3 = da[4:6]
75
+ x4 = da[6:8]
76
+ return str(w * x_list.index(x4) + (dec.index(x2) * 128) + dec.index(x1) + (dec.index(x3) * 128 * 128))
77
+ return None
78
+
79
+ # ==================== DECRYPTION / DECOMPRESSION HELPERS ====================
80
+ def decrypt_aes_cbc(data, key=KEY, iv=IV):
81
+ """Try to decrypt AES‑CBC; fallback to no‑padding if unpadding fails."""
82
+ cipher = AES.new(key, AES.MODE_CBC, iv)
83
+ try:
84
+ # Try with padding
85
+ return unpad(cipher.decrypt(data), AES.block_size)
86
+ except ValueError:
87
+ # If padding fails, assume raw decrypted data is correct
88
+ return cipher.decrypt(data)
89
+
90
+ def try_zlib(data):
91
+ """Try to decompress as zlib. Return (decompressed, True) on success."""
92
+ try:
93
+ return zlib.decompress(data), True
94
+ except:
95
+ return data, False
96
+
97
+ def try_gzip(data):
98
+ """Try to decompress as gzip. Return (decompressed, True) on success."""
99
+ try:
100
+ with gzip.GzipFile(fileobj=BytesIO(data)) as gz:
101
+ return gz.read(), True
102
+ except:
103
+ return data, False
104
+
105
+ # ==================== RECURSIVE PROTOBUF PARSER (SINGLE MESSAGE) ====================
106
+ def parse_one_message(data, start, depth=0, parent_path=""):
107
+ """
108
+ Parse exactly one protobuf message starting at 'start'.
109
+ Returns (fields, next_offset). fields is a list of field dicts.
110
+ """
111
+ fields = []
112
+ idx = start
113
+ while idx < len(data):
114
+ try:
115
+ key, idx = decode_varint(data, idx)
116
+ except ValueError:
117
+ # Not enough bytes for a varint – we've reached the end of this message
118
+ break
119
+ field_num = key >> 3
120
+ wire_type = key & 0x07
121
+ path = f"{parent_path}.{field_num}" if parent_path else f"Field {field_num}"
122
+
123
+ if wire_type == 0: # varint
124
+ value, idx = decode_varint(data, idx)
125
+ fields.append({
126
+ 'num': field_num,
127
+ 'type': 0,
128
+ 'value': value,
129
+ 'display': f"varint {value}",
130
+ 'path': path,
131
+ 'nested': None
132
+ })
133
+ elif wire_type == 1: # 64-bit
134
+ if idx + 8 > len(data):
135
+ raise ValueError("Truncated 64-bit")
136
+ value = int.from_bytes(data[idx:idx+8], 'little')
137
+ idx += 8
138
+ fields.append({
139
+ 'num': field_num,
140
+ 'type': 1,
141
+ 'value': value,
142
+ 'display': f"64-bit {value}",
143
+ 'path': path,
144
+ 'nested': None
145
+ })
146
+ elif wire_type == 2: # length-delimited
147
+ length, idx = decode_varint(data, idx)
148
+ if idx + length > len(data):
149
+ # This length would exceed remaining data – stop here (incomplete message)
150
+ return fields, idx
151
+ raw = data[idx:idx+length]
152
+ idx += length
153
+
154
+ display = f"bytes ({len(raw)} bytes : {raw})"
155
+ nested = None
156
+ processed_raw = raw
157
+
158
+ # --- Automatic decoding attempts ---
159
+ # 1) Custom ID (0x30 + 5 bytes)
160
+ if len(raw) == 6 and raw[0] == 0x30:
161
+ id_hex = raw[1:].hex()
162
+ decrypted_id = decrypt_id(id_hex)
163
+ if decrypted_id:
164
+ display = f"custom ID -> {decrypted_id}"
165
+
166
+ # 2) Zlib decompression
167
+ decompressed, ok = try_zlib(raw)
168
+ if ok and decompressed != raw:
169
+ processed_raw = decompressed
170
+ display = f"zlib decompressed ({len(decompressed)} bytes)"
171
+ # Try to parse as protobuf
172
+ try:
173
+ nested, _ = parse_one_message(decompressed, 0, depth+1, path)
174
+ if nested:
175
+ display += f" → protobuf ({len(nested)} fields)"
176
+ except Exception:
177
+ pass
178
+
179
+ # 3) Gzip decompression (only if not already handled)
180
+ if nested is None and not ok: # only try if previous attempts didn't succeed
181
+ decompressed, ok = try_gzip(raw)
182
+ if ok and decompressed != raw:
183
+ processed_raw = decompressed
184
+ display = f"gzip decompressed ({len(decompressed)} bytes)"
185
+ try:
186
+ nested, _ = parse_one_message(decompressed, 0, depth+1, path)
187
+ if nested:
188
+ display += f" → protobuf ({len(nested)} fields)"
189
+ except Exception:
190
+ pass
191
+
192
+ # 4) AES decryption (if length multiple of 16)
193
+ if nested is None and len(raw) % 16 == 0 and len(raw) > 0:
194
+ try:
195
+ decrypted = decrypt_aes_cbc(raw)
196
+ if decrypted != raw:
197
+ processed_raw = decrypted
198
+ display = f"AES decrypted ({len(decrypted)} bytes)"
199
+ # Try to parse as protobuf
200
+ try:
201
+ nested, _ = parse_one_message(decrypted, 0, depth+1, path)
202
+ if nested:
203
+ display += f" → protobuf ({len(nested)} fields)"
204
+ except Exception:
205
+ pass
206
+ except Exception:
207
+ pass
208
+
209
+ # 5) Try parsing raw (or decompressed/decrypted) as protobuf directly
210
+ if nested is None:
211
+ try:
212
+ nested, _ = parse_one_message(processed_raw, 0, depth+1, path)
213
+ if nested:
214
+ display = f"protobuf ({len(nested)} fields)"
215
+ except Exception:
216
+ pass
217
+
218
+ # 6) Try UTF-8 string
219
+ if nested is None:
220
+ try:
221
+ s = processed_raw.decode('utf-8')
222
+ if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in s):
223
+ display = f"string '{s}'"
224
+ except UnicodeDecodeError:
225
+ pass
226
+
227
+ # 7) For very short bytes, try to interpret as integer
228
+ if nested is None:
229
+ if len(processed_raw) == 4:
230
+ val = int.from_bytes(processed_raw, 'little')
231
+ display = f"uint32 {val} (bytes: {processed_raw.hex()})"
232
+ elif len(processed_raw) == 8:
233
+ val = int.from_bytes(processed_raw, 'little')
234
+ display = f"uint64 {val} (bytes: {processed_raw.hex()})"
235
+
236
+ fields.append({
237
+ 'num': field_num,
238
+ 'type': 2,
239
+ 'value': processed_raw,
240
+ 'display': display,
241
+ 'path': path,
242
+ 'nested': nested
243
+ })
244
+ elif wire_type == 5: # 32-bit
245
+ if idx + 4 > len(data):
246
+ raise ValueError("Truncated 32-bit")
247
+ value = int.from_bytes(data[idx:idx+4], 'little')
248
+ idx += 4
249
+ fields.append({
250
+ 'num': field_num,
251
+ 'type': 5,
252
+ 'value': value,
253
+ 'display': f"32-bit {value}",
254
+ 'path': path,
255
+ 'nested': None
256
+ })
257
+ else:
258
+ raise ValueError(f"Unsupported wire type {wire_type}")
259
+ return fields, idx
260
+
261
+ def decode_payload(data):
262
+ """
263
+ Decode a payload (hex string, base64 string, or bytes) and return a list of messages.
264
+ Each message is a list of field dicts.
265
+ """
266
+ if isinstance(data, str):
267
+ # Try hex first
268
+ hex_chars = re.sub(r'[^0-9a-fA-F]', '', data)
269
+ if len(hex_chars) % 2 == 0 and len(hex_chars) > 0:
270
+ try:
271
+ data_bytes = binascii.unhexlify(hex_chars)
272
+ used_format = "hex"
273
+ except binascii.Error:
274
+ data_bytes = None
275
+ else:
276
+ data_bytes = None
277
+
278
+ if data_bytes is None:
279
+ b64_chars = re.sub(r'[^A-Za-z0-9+/=]', '', data)
280
+ if len(b64_chars) > 0:
281
+ try:
282
+ data_bytes = base64.b64decode(b64_chars)
283
+ used_format = "base64"
284
+ except binascii.Error:
285
+ data_bytes = None
286
+ if data_bytes is None:
287
+ raise ValueError("Could not decode input as hex or base64")
288
+ else:
289
+ # Assume bytes
290
+ data_bytes = data
291
+
292
+ # Try outer AES decryption
293
+ try:
294
+ plain = decrypt_aes_cbc(data_bytes)
295
+ except Exception:
296
+ plain = data_bytes
297
+
298
+ # Parse all messages
299
+ messages = []
300
+ pos = 0
301
+ while pos < len(plain):
302
+ try:
303
+ fields, pos = parse_one_message(plain, pos)
304
+ if not fields:
305
+ break
306
+ messages.append(fields)
307
+ except Exception:
308
+ break
309
+ return messages
310
+
311
+ def format_messages(messages):
312
+ """Return a string representation of messages."""
313
+ import sys
314
+ out_lines = []
315
+ for i, msg in enumerate(messages, start=1):
316
+ out_lines.append(f"\n--- Message {i} ---")
317
+ out_lines.extend(_format_fields(msg))
318
+ return "\n".join(out_lines)
319
+
320
+ def _format_fields(fields, indent=""):
321
+ lines = []
322
+ for f in fields:
323
+ lines.append(f"{indent}{f['path']} : {f['display']}")
324
+ if f['nested']:
325
+ lines.extend(_format_fields(f['nested'], indent + " "))
326
+ return lines
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: ff-decoder
3
+ Version: 1.0.0
4
+ Summary: Decode Free Fire encrypted payloads (hex/base64) with automatic decryption and expansion.
5
+ Author: UNKNOWN666
6
+ Author-email: arbasbilal25@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: pycryptodome>=3.15.0
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # FF Decoder
23
+
24
+ A Python library and command-line tool to decode encrypted Free Fire payloads (hex or base64). It automatically decrypts the outer AES layer, decompresses zlib/gzip, and recursively expands nested protobufs.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install ff-decoder
@@ -0,0 +1,8 @@
1
+ ff_decoder/__init__.py,sha256=91kpUDNNX4UOzWiafUS5j3kuoYxUU5HLbuDiYiaDJLg,137
2
+ ff_decoder/cli.py,sha256=HGqdhFM7_hvORAWP_2_lHEVw_Z9p3Pai39MeGvHO7K4,1509
3
+ ff_decoder/decoder.py,sha256=rMJxk6BOjkNw1ct0FM55DOOP-E27RkqlDVqloeBS_6Q,12695
4
+ ff_decoder-1.0.0.dist-info/METADATA,sha256=SEcYlwUe394niGNqMUnCf_tDPJbrw_Se9UW1yjquaQg,904
5
+ ff_decoder-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ ff_decoder-1.0.0.dist-info/entry_points.txt,sha256=C8m5DuDw6stXcRxOMLrWeCefjJyypr9QGJRPJ9y175I,50
7
+ ff_decoder-1.0.0.dist-info/top_level.txt,sha256=4wKXP6TNciqTKnpT8tcisiiJ5LSYbmeBSUHBm3jamoI,11
8
+ ff_decoder-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ff-decode = ff_decoder.cli:main
@@ -0,0 +1 @@
1
+ ff_decoder