jetrauserapi 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.
- jetrauserapi/__init__.py +7 -0
- jetrauserapi/client.py +548 -0
- jetrauserapi/config.py +12 -0
- jetrauserapi/database.py +112 -0
- jetrauserapi/high_level.py +519 -0
- jetrauserapi/message.py +62 -0
- jetrauserapi/session.py +75 -0
- jetrauserapi/tl_builder.py +105 -0
- jetrauserapi-1.0.0.dist-info/METADATA +67 -0
- jetrauserapi-1.0.0.dist-info/RECORD +13 -0
- jetrauserapi-1.0.0.dist-info/WHEEL +5 -0
- jetrauserapi-1.0.0.dist-info/licenses/LICENSE +21 -0
- jetrauserapi-1.0.0.dist-info/top_level.txt +1 -0
jetrauserapi/__init__.py
ADDED
jetrauserapi/client.py
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import websockets
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import time
|
|
6
|
+
import math
|
|
7
|
+
import random
|
|
8
|
+
import hashlib
|
|
9
|
+
from Crypto.Cipher import AES
|
|
10
|
+
from Crypto.Util import Counter
|
|
11
|
+
|
|
12
|
+
from jetrauserapi.tl_builder import tl_string, tl_bytes
|
|
13
|
+
|
|
14
|
+
RSA_MODULUS_HEX = (
|
|
15
|
+
"a1d79eb1ffcc0edd282c52215e60491200cc4d5f907e6d5ceb759958282d389e9c"
|
|
16
|
+
"9443b0a8e4505a608bfe2f14a157c3e24b553017cb8d310d0c48fd8a63340cd556f"
|
|
17
|
+
"83bc4cd48400cd582dd881dc7c52b14d6b7f469f0e889444734f65736ad6ace97d5"
|
|
18
|
+
"7ef551a572414272760e2bffcca018e5c9951488cfbbcd0e87dc33a9242b2f71eca"
|
|
19
|
+
"049a4050df82f3e630a2725887f8b1498146fc9d0f253d0746b690655e6ac65485c"
|
|
20
|
+
"e8d257e22f7a95d75f24d8efa7a679deb794dc66bd888155ae5f3334a40861b4afa"
|
|
21
|
+
"c807843cc253df4595b7ef709c8395663b38f37db18778f7ae1ca4b7f30cbe714bc"
|
|
22
|
+
"e8de57ab54610273fd685c11dad77e3ad26031bda32f"
|
|
23
|
+
)
|
|
24
|
+
RSA_MODULUS = int(RSA_MODULUS_HEX, 16)
|
|
25
|
+
RSA_EXPONENT = 65537
|
|
26
|
+
|
|
27
|
+
JETRA_DH_PRIME = int("c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b", 16)
|
|
28
|
+
|
|
29
|
+
def tl_store_bytes(data):
|
|
30
|
+
n = len(data)
|
|
31
|
+
if n <= 253:
|
|
32
|
+
buf = bytes([n]) + data
|
|
33
|
+
else:
|
|
34
|
+
buf = b'\xfe' + n.to_bytes(3, 'little') + data
|
|
35
|
+
return buf + b'\x00' * ((4 - len(buf) % 4) % 4)
|
|
36
|
+
|
|
37
|
+
def aes_ige_encrypt(plaintext, key, iv):
|
|
38
|
+
cipher = AES.new(key, AES.MODE_ECB)
|
|
39
|
+
n = len(plaintext)
|
|
40
|
+
out = bytearray(n)
|
|
41
|
+
prev_c = iv[:16]
|
|
42
|
+
prev_p = iv[16:]
|
|
43
|
+
for i in range(0, n, 16):
|
|
44
|
+
block = plaintext[i:i+16]
|
|
45
|
+
x = bytes(a ^ b for a, b in zip(block, prev_c))
|
|
46
|
+
y = cipher.encrypt(x)
|
|
47
|
+
c = bytes(a ^ b for a, b in zip(y, prev_p))
|
|
48
|
+
out[i:i+16] = c
|
|
49
|
+
prev_c = c
|
|
50
|
+
prev_p = block
|
|
51
|
+
return bytes(out)
|
|
52
|
+
|
|
53
|
+
def aes_ige_decrypt(ciphertext, key, iv):
|
|
54
|
+
cipher = AES.new(key, AES.MODE_ECB)
|
|
55
|
+
n = len(ciphertext)
|
|
56
|
+
out = bytearray(n)
|
|
57
|
+
prev_c = iv[:16]
|
|
58
|
+
prev_p = iv[16:]
|
|
59
|
+
for i in range(0, n, 16):
|
|
60
|
+
block = ciphertext[i:i+16]
|
|
61
|
+
x = bytes(a ^ b for a, b in zip(block, prev_p))
|
|
62
|
+
y = cipher.decrypt(x)
|
|
63
|
+
c = bytes(a ^ b for a, b in zip(y, prev_c))
|
|
64
|
+
out[i:i+16] = c
|
|
65
|
+
prev_c = block
|
|
66
|
+
prev_p = c
|
|
67
|
+
return bytes(out)
|
|
68
|
+
|
|
69
|
+
def rsa_pad(inner_data):
|
|
70
|
+
n = RSA_MODULUS
|
|
71
|
+
for _ in range(100):
|
|
72
|
+
padded = inner_data + os.urandom(192 - len(inner_data))
|
|
73
|
+
rev = padded[::-1]
|
|
74
|
+
tk = os.urandom(32)
|
|
75
|
+
dwh = rev + hashlib.sha256(tk + padded).digest()
|
|
76
|
+
ige = aes_ige_encrypt(dwh, tk, b'\x00' * 32)
|
|
77
|
+
xored = bytes(a ^ b for a, b in zip(tk, hashlib.sha256(ige).digest()))
|
|
78
|
+
blob = xored + ige
|
|
79
|
+
if int.from_bytes(blob, 'big') < n:
|
|
80
|
+
c = pow(int.from_bytes(blob, 'big'), RSA_EXPONENT, n)
|
|
81
|
+
return c.to_bytes(256, 'big')
|
|
82
|
+
raise RuntimeError("RSA pad failed")
|
|
83
|
+
|
|
84
|
+
def wrap_init_connection(inner_query):
|
|
85
|
+
API_ID = 3893396
|
|
86
|
+
LAYER = 228
|
|
87
|
+
init_body = (
|
|
88
|
+
struct.pack("<I", 0xC1CD5EA9) +
|
|
89
|
+
struct.pack("<I", 0) +
|
|
90
|
+
struct.pack("<I", API_ID) +
|
|
91
|
+
tl_string("Samsung SM-G973F") +
|
|
92
|
+
tl_string("Android 12") +
|
|
93
|
+
tl_string("0.1.0 K voiceotp=1") +
|
|
94
|
+
tl_string("fa") +
|
|
95
|
+
tl_string("webk") +
|
|
96
|
+
tl_string("fa") +
|
|
97
|
+
inner_query
|
|
98
|
+
)
|
|
99
|
+
return (
|
|
100
|
+
struct.pack("<I", 0xDA9B0D0D) +
|
|
101
|
+
struct.pack("<i", LAYER) +
|
|
102
|
+
init_body
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
class JetraClient:
|
|
106
|
+
def __init__(self, ws_url="wss://web.jetra.ir/apiws", session=None):
|
|
107
|
+
self.ws_url = ws_url
|
|
108
|
+
self.ws = None
|
|
109
|
+
self.enc_cipher = None
|
|
110
|
+
self.dec_cipher = None
|
|
111
|
+
self.new_nonce = None
|
|
112
|
+
self.b = None
|
|
113
|
+
self.auth_key = None
|
|
114
|
+
self.auth_key_id = None
|
|
115
|
+
|
|
116
|
+
if session:
|
|
117
|
+
self.server_salt = session.server_salt
|
|
118
|
+
self.session_id = session.session_id
|
|
119
|
+
self.auth_key = session.auth_key
|
|
120
|
+
self.auth_key_id = session.auth_key_id
|
|
121
|
+
else:
|
|
122
|
+
self.server_salt = b'\x00' * 8
|
|
123
|
+
self.session_id = os.urandom(8)
|
|
124
|
+
|
|
125
|
+
self.seq_no = 0
|
|
126
|
+
|
|
127
|
+
def _pack_msg_id(self):
|
|
128
|
+
return struct.pack("<Q", (int(time.time()) << 32) | 4)
|
|
129
|
+
|
|
130
|
+
def _abridged_frame(self, data):
|
|
131
|
+
length = len(data)
|
|
132
|
+
if length < 127:
|
|
133
|
+
return struct.pack("<B", length >> 2) + data
|
|
134
|
+
else:
|
|
135
|
+
return b'\x7f' + struct.pack("<I", length >> 2)[:3] + data
|
|
136
|
+
|
|
137
|
+
async def connect(self):
|
|
138
|
+
print(f"Connecting to {self.ws_url}...")
|
|
139
|
+
self.ws = await websockets.connect(self.ws_url)
|
|
140
|
+
print("WebSocket connected!")
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
header = bytearray(os.urandom(64))
|
|
144
|
+
if header[0] == 0xEF:
|
|
145
|
+
continue
|
|
146
|
+
if struct.unpack("<I", header[0:4])[0] in (0x44414548, 0x54534F50, 0x20544745, 0x4954504F, 0xEEEEEEEE, 0xDDDDDDDD):
|
|
147
|
+
continue
|
|
148
|
+
if struct.unpack("<I", header[4:8])[0] == 0:
|
|
149
|
+
continue
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
header[56:60] = b'\xEF\xEF\xEF\xEF'
|
|
153
|
+
header_rev = header[::-1]
|
|
154
|
+
|
|
155
|
+
enc_key = bytes(header[8:40])
|
|
156
|
+
enc_iv = bytes(header[40:56])
|
|
157
|
+
dec_key = bytes(header_rev[8:40])
|
|
158
|
+
dec_iv = bytes(header_rev[40:56])
|
|
159
|
+
|
|
160
|
+
self.enc_cipher = AES.new(enc_key, AES.MODE_CTR, counter=Counter.new(128, initial_value=int.from_bytes(enc_iv, 'big')))
|
|
161
|
+
self.dec_cipher = AES.new(dec_key, AES.MODE_CTR, counter=Counter.new(128, initial_value=int.from_bytes(dec_iv, 'big')))
|
|
162
|
+
|
|
163
|
+
encrypted_tail = self.enc_cipher.encrypt(bytes(header))[56:64]
|
|
164
|
+
header[56:64] = encrypted_tail
|
|
165
|
+
|
|
166
|
+
print("Sending header...")
|
|
167
|
+
await self.ws.send(bytes(header))
|
|
168
|
+
|
|
169
|
+
async def send_req_pq(self):
|
|
170
|
+
self.new_nonce = os.urandom(32)
|
|
171
|
+
print(f"Generated new_nonce: {self.new_nonce.hex()}")
|
|
172
|
+
|
|
173
|
+
nonce = os.urandom(16)
|
|
174
|
+
msg_id = self._pack_msg_id()
|
|
175
|
+
method_buf = struct.pack("<I", 0xBE7E8EF1) + nonce
|
|
176
|
+
packet = b"\x00"*8 + msg_id + struct.pack("<I", len(method_buf)) + method_buf
|
|
177
|
+
|
|
178
|
+
framed = self._abridged_frame(packet)
|
|
179
|
+
encrypted = self.enc_cipher.encrypt(framed)
|
|
180
|
+
|
|
181
|
+
print(f"Sending req_pq_multi...")
|
|
182
|
+
await self.ws.send(encrypted)
|
|
183
|
+
return nonce
|
|
184
|
+
|
|
185
|
+
async def recv(self):
|
|
186
|
+
response = await self.ws.recv()
|
|
187
|
+
decrypted = self.dec_cipher.decrypt(response)
|
|
188
|
+
|
|
189
|
+
if len(decrypted) < 1:
|
|
190
|
+
return b''
|
|
191
|
+
|
|
192
|
+
first_byte = decrypted[0]
|
|
193
|
+
if first_byte < 0x7F:
|
|
194
|
+
frame_len = first_byte << 2
|
|
195
|
+
payload = decrypted[1:1+frame_len]
|
|
196
|
+
elif first_byte == 0x7F:
|
|
197
|
+
if len(decrypted) < 4:
|
|
198
|
+
return b''
|
|
199
|
+
frame_len = int.from_bytes(decrypted[1:4], 'little') << 2
|
|
200
|
+
payload = decrypted[4:4+frame_len]
|
|
201
|
+
else:
|
|
202
|
+
return decrypted
|
|
203
|
+
|
|
204
|
+
return payload
|
|
205
|
+
|
|
206
|
+
def parse_res_pq(self, data):
|
|
207
|
+
offset = 0
|
|
208
|
+
|
|
209
|
+
auth_key_id = data[offset:offset+8]
|
|
210
|
+
offset += 8
|
|
211
|
+
msg_id = data[offset:offset+8]
|
|
212
|
+
offset += 8
|
|
213
|
+
msg_len = struct.unpack("<I", data[offset:offset+4])[0]
|
|
214
|
+
offset += 4
|
|
215
|
+
res_pq_ctor = struct.unpack("<I", data[offset:offset+4])[0]
|
|
216
|
+
offset += 4
|
|
217
|
+
|
|
218
|
+
if res_pq_ctor != 0x05162463:
|
|
219
|
+
raise ValueError(f"Invalid resPQ constructor: {hex(res_pq_ctor)}")
|
|
220
|
+
|
|
221
|
+
nonce = data[offset:offset+16]
|
|
222
|
+
offset += 16
|
|
223
|
+
server_nonce = data[offset:offset+16]
|
|
224
|
+
offset += 16
|
|
225
|
+
pq_len = data[offset]
|
|
226
|
+
offset += 1
|
|
227
|
+
pq = data[offset:offset+pq_len]
|
|
228
|
+
offset += pq_len
|
|
229
|
+
offset = (offset + 3) & ~3
|
|
230
|
+
|
|
231
|
+
vector_ctor = struct.unpack("<I", data[offset:offset+4])[0]
|
|
232
|
+
offset += 4
|
|
233
|
+
|
|
234
|
+
fp_count = struct.unpack("<I", data[offset:offset+4])[0]
|
|
235
|
+
offset += 4
|
|
236
|
+
|
|
237
|
+
fingerprints = []
|
|
238
|
+
for _ in range(fp_count):
|
|
239
|
+
fp = struct.unpack("<Q", data[offset:offset+8])[0]
|
|
240
|
+
fingerprints.append(fp)
|
|
241
|
+
offset += 8
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
'nonce': nonce,
|
|
245
|
+
'server_nonce': server_nonce,
|
|
246
|
+
'pq': pq,
|
|
247
|
+
'fingerprints': fingerprints
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def factorize_pq(self, pq):
|
|
251
|
+
pq_int = int.from_bytes(pq, 'big')
|
|
252
|
+
if pq_int % 2 == 0:
|
|
253
|
+
return 2, pq_int // 2
|
|
254
|
+
|
|
255
|
+
def pollard_rho(n):
|
|
256
|
+
if n % 2 == 0:
|
|
257
|
+
return 2
|
|
258
|
+
x = random.randint(2, n-1)
|
|
259
|
+
y = x
|
|
260
|
+
c = random.randint(1, n-1)
|
|
261
|
+
d = 1
|
|
262
|
+
while d == 1:
|
|
263
|
+
x = (x*x + c) % n
|
|
264
|
+
y = (y*y + c) % n
|
|
265
|
+
y = (y*y + c) % n
|
|
266
|
+
d = math.gcd(abs(x-y), n)
|
|
267
|
+
return d
|
|
268
|
+
|
|
269
|
+
p = pollard_rho(pq_int)
|
|
270
|
+
q = pq_int // p
|
|
271
|
+
return p, q
|
|
272
|
+
|
|
273
|
+
async def send_req_dh_params(self, parsed_respq):
|
|
274
|
+
nonce = parsed_respq['nonce']
|
|
275
|
+
server_nonce = parsed_respq['server_nonce']
|
|
276
|
+
pq = parsed_respq['pq']
|
|
277
|
+
fingerprints = parsed_respq['fingerprints']
|
|
278
|
+
|
|
279
|
+
fingerprint = fingerprints[0]
|
|
280
|
+
|
|
281
|
+
p, q = self.factorize_pq(pq)
|
|
282
|
+
if p > q:
|
|
283
|
+
p, q = q, p
|
|
284
|
+
|
|
285
|
+
p_bytes = p.to_bytes((p.bit_length() + 7) // 8, 'big')
|
|
286
|
+
q_bytes = q.to_bytes((q.bit_length() + 7) // 8, 'big')
|
|
287
|
+
|
|
288
|
+
pq_tl = tl_store_bytes(pq)
|
|
289
|
+
p_tl = tl_store_bytes(p_bytes)
|
|
290
|
+
q_tl = tl_store_bytes(q_bytes)
|
|
291
|
+
|
|
292
|
+
inner = (
|
|
293
|
+
struct.pack("<I", 0xA9F55F95) +
|
|
294
|
+
pq_tl +
|
|
295
|
+
p_tl +
|
|
296
|
+
q_tl +
|
|
297
|
+
nonce +
|
|
298
|
+
server_nonce +
|
|
299
|
+
self.new_nonce +
|
|
300
|
+
struct.pack("<i", 2)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
encrypted_data = rsa_pad(inner)
|
|
304
|
+
|
|
305
|
+
req_body = (
|
|
306
|
+
struct.pack("<I", 0xD712E4BE) +
|
|
307
|
+
nonce +
|
|
308
|
+
server_nonce +
|
|
309
|
+
p_tl +
|
|
310
|
+
q_tl +
|
|
311
|
+
struct.pack("<Q", fingerprint) +
|
|
312
|
+
tl_store_bytes(encrypted_data)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
msg_id = self._pack_msg_id()
|
|
316
|
+
packet = b"\x00"*8 + msg_id + struct.pack("<I", len(req_body)) + req_body
|
|
317
|
+
|
|
318
|
+
framed = self._abridged_frame(packet)
|
|
319
|
+
encrypted = self.enc_cipher.encrypt(framed)
|
|
320
|
+
|
|
321
|
+
await self.ws.send(encrypted)
|
|
322
|
+
|
|
323
|
+
def parse_server_dh_params_ok(self, data):
|
|
324
|
+
if data[0] == 0x7f:
|
|
325
|
+
data = data[4:]
|
|
326
|
+
else:
|
|
327
|
+
data = data[1:]
|
|
328
|
+
|
|
329
|
+
offset = 24
|
|
330
|
+
|
|
331
|
+
nonce = data[offset:offset+16]
|
|
332
|
+
offset += 16
|
|
333
|
+
server_nonce = data[offset:offset+16]
|
|
334
|
+
offset += 16
|
|
335
|
+
|
|
336
|
+
enc_len_byte = data[offset]
|
|
337
|
+
if enc_len_byte < 0xFE:
|
|
338
|
+
enc_len = enc_len_byte
|
|
339
|
+
offset += 1
|
|
340
|
+
else:
|
|
341
|
+
enc_len = int.from_bytes(data[offset+1:offset+4], 'little')
|
|
342
|
+
offset += 4
|
|
343
|
+
|
|
344
|
+
encrypted_answer = data[offset:offset+enc_len]
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
'nonce': nonce,
|
|
348
|
+
'server_nonce': server_nonce,
|
|
349
|
+
'encrypted_answer': encrypted_answer
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async def complete_auth(self, parsed_respq, decrypted_answer):
|
|
353
|
+
nonce = parsed_respq['nonce']
|
|
354
|
+
server_nonce = parsed_respq['server_nonce']
|
|
355
|
+
|
|
356
|
+
inner_ctor = struct.pack("<I", 0xB5890DBA)
|
|
357
|
+
pos = decrypted_answer.find(inner_ctor)
|
|
358
|
+
if pos == -1:
|
|
359
|
+
raise ValueError("server_DH_inner_data not found")
|
|
360
|
+
|
|
361
|
+
server_dh_inner = decrypted_answer[pos:]
|
|
362
|
+
|
|
363
|
+
offset = 4 + 16 + 16 + 4
|
|
364
|
+
dh_prime_len = server_dh_inner[offset]
|
|
365
|
+
if dh_prime_len < 0xFE:
|
|
366
|
+
offset += 1 + dh_prime_len
|
|
367
|
+
else:
|
|
368
|
+
dh_prime_len = int.from_bytes(server_dh_inner[offset+1:offset+4], 'little')
|
|
369
|
+
offset += 4 + dh_prime_len
|
|
370
|
+
offset = (offset + 3) & ~3
|
|
371
|
+
|
|
372
|
+
g_a_len = server_dh_inner[offset]
|
|
373
|
+
if g_a_len < 0xFE:
|
|
374
|
+
g_a = server_dh_inner[offset+1:offset+1+g_a_len]
|
|
375
|
+
offset += 1 + g_a_len
|
|
376
|
+
else:
|
|
377
|
+
g_a_len = int.from_bytes(server_dh_inner[offset+1:offset+4], 'little')
|
|
378
|
+
g_a = server_dh_inner[offset+4:offset+4+g_a_len]
|
|
379
|
+
offset += 4 + g_a_len
|
|
380
|
+
|
|
381
|
+
self.server_salt = bytes(a ^ b for a, b in zip(self.new_nonce[:8], server_nonce[:8]))
|
|
382
|
+
|
|
383
|
+
self.b = os.urandom(256)
|
|
384
|
+
b_int = int.from_bytes(self.b, 'big')
|
|
385
|
+
|
|
386
|
+
g = 3
|
|
387
|
+
g_b_int = pow(g, b_int, JETRA_DH_PRIME)
|
|
388
|
+
g_b_bytes = g_b_int.to_bytes(256, 'big')
|
|
389
|
+
|
|
390
|
+
g_a_int = int.from_bytes(g_a, 'big')
|
|
391
|
+
auth_key_int = pow(g_a_int, b_int, JETRA_DH_PRIME)
|
|
392
|
+
self.auth_key = auth_key_int.to_bytes(256, 'big')
|
|
393
|
+
self.auth_key_id = hashlib.sha1(self.auth_key).digest()[-8:]
|
|
394
|
+
|
|
395
|
+
client_inner = (
|
|
396
|
+
struct.pack("<I", 0x6643B654) +
|
|
397
|
+
nonce +
|
|
398
|
+
server_nonce +
|
|
399
|
+
struct.pack("<Q", 0) +
|
|
400
|
+
tl_store_bytes(g_b_bytes)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
inner_hash = hashlib.sha1(client_inner).digest()
|
|
404
|
+
inner_with_hash = inner_hash + client_inner
|
|
405
|
+
|
|
406
|
+
sha1_a = hashlib.sha1(self.new_nonce + server_nonce).digest()
|
|
407
|
+
sha1_b = hashlib.sha1(server_nonce + self.new_nonce).digest()
|
|
408
|
+
sha1_c = hashlib.sha1(self.new_nonce + self.new_nonce).digest()
|
|
409
|
+
|
|
410
|
+
tmp_aes_key = sha1_a + sha1_b[:12]
|
|
411
|
+
tmp_aes_iv = sha1_b[12:] + sha1_c + self.new_nonce[:4]
|
|
412
|
+
|
|
413
|
+
padding_len = 16 - (len(inner_with_hash) % 16)
|
|
414
|
+
if padding_len < 16:
|
|
415
|
+
inner_with_hash = inner_with_hash + os.urandom(padding_len)
|
|
416
|
+
|
|
417
|
+
encrypted_inner = aes_ige_encrypt(inner_with_hash, tmp_aes_key, tmp_aes_iv)
|
|
418
|
+
|
|
419
|
+
req_body = (
|
|
420
|
+
struct.pack("<I", 0xF5045F1F) +
|
|
421
|
+
nonce +
|
|
422
|
+
server_nonce +
|
|
423
|
+
tl_store_bytes(encrypted_inner)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
msg_id = self._pack_msg_id()
|
|
427
|
+
packet = b"\x00"*8 + msg_id + struct.pack("<I", len(req_body)) + req_body
|
|
428
|
+
|
|
429
|
+
framed = self._abridged_frame(packet)
|
|
430
|
+
encrypted = self.enc_cipher.encrypt(framed)
|
|
431
|
+
|
|
432
|
+
await self.ws.send(encrypted)
|
|
433
|
+
|
|
434
|
+
def decrypt_response(self, response):
|
|
435
|
+
if len(response) < 24:
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
auth_key_id = response[0:8]
|
|
439
|
+
msg_key_received = response[8:24]
|
|
440
|
+
encrypted_data = response[24:]
|
|
441
|
+
|
|
442
|
+
if len(encrypted_data) % 16 != 0:
|
|
443
|
+
encrypted_data = encrypted_data[:len(encrypted_data) - (len(encrypted_data) % 16)]
|
|
444
|
+
|
|
445
|
+
if len(encrypted_data) == 0:
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
i = 8
|
|
449
|
+
|
|
450
|
+
a = bytearray(52)
|
|
451
|
+
a[0:16] = msg_key_received
|
|
452
|
+
a[16:52] = self.auth_key[i:i+36]
|
|
453
|
+
s0 = hashlib.sha256(bytes(a)).digest()
|
|
454
|
+
|
|
455
|
+
o = bytearray(52)
|
|
456
|
+
o[0:36] = self.auth_key[40+i:40+i+36]
|
|
457
|
+
o[36:52] = msg_key_received
|
|
458
|
+
s1 = hashlib.sha256(bytes(o)).digest()
|
|
459
|
+
|
|
460
|
+
aes_key = s0[0:8] + s1[8:24] + s0[24:32]
|
|
461
|
+
aes_iv = s1[0:8] + s0[8:24] + s1[24:32]
|
|
462
|
+
|
|
463
|
+
decrypted = aes_ige_decrypt(encrypted_data, aes_key, aes_iv)
|
|
464
|
+
|
|
465
|
+
offset = 0
|
|
466
|
+
salt = decrypted[offset:offset+8]; offset += 8
|
|
467
|
+
session_id = decrypted[offset:offset+8]; offset += 8
|
|
468
|
+
msg_id = struct.unpack("<Q", decrypted[offset:offset+8])[0]; offset += 8
|
|
469
|
+
seq_no = struct.unpack("<I", decrypted[offset:offset+4])[0]; offset += 4
|
|
470
|
+
msg_len = struct.unpack("<I", decrypted[offset:offset+4])[0]; offset += 4
|
|
471
|
+
msg_data = decrypted[offset:offset+msg_len]
|
|
472
|
+
|
|
473
|
+
return msg_data
|
|
474
|
+
|
|
475
|
+
async def send_request(self, method_bytes):
|
|
476
|
+
msg_id_bytes = self._pack_msg_id()
|
|
477
|
+
msg_id_int = struct.unpack("<Q", msg_id_bytes)[0]
|
|
478
|
+
|
|
479
|
+
self.seq_no += 1
|
|
480
|
+
seq_no = struct.pack("<I", self.seq_no * 2 - 1)
|
|
481
|
+
msg_len = struct.pack("<I", len(method_bytes))
|
|
482
|
+
|
|
483
|
+
buffer_without_padding = self.server_salt + self.session_id + msg_id_bytes + seq_no + msg_len + method_bytes
|
|
484
|
+
offset = len(buffer_without_padding)
|
|
485
|
+
padding_len = 16 - (offset % 16) + 16 * (1 + random.randint(0, 4))
|
|
486
|
+
padding = os.urandom(padding_len)
|
|
487
|
+
|
|
488
|
+
payload = buffer_without_padding + padding
|
|
489
|
+
|
|
490
|
+
msg_key = hashlib.sha256(self.auth_key[88:120] + payload).digest()[8:24]
|
|
491
|
+
|
|
492
|
+
a = bytearray(52)
|
|
493
|
+
a[0:16] = msg_key
|
|
494
|
+
a[16:52] = self.auth_key[0:36]
|
|
495
|
+
s0 = hashlib.sha256(bytes(a)).digest()
|
|
496
|
+
|
|
497
|
+
o = bytearray(52)
|
|
498
|
+
o[0:36] = self.auth_key[40:76]
|
|
499
|
+
o[36:52] = msg_key
|
|
500
|
+
s1 = hashlib.sha256(bytes(o)).digest()
|
|
501
|
+
|
|
502
|
+
aes_key = s0[0:8] + s1[8:24] + s0[24:32]
|
|
503
|
+
aes_iv = s1[0:8] + s0[8:24] + s1[24:32]
|
|
504
|
+
|
|
505
|
+
encrypted_data = aes_ige_encrypt(payload, aes_key, aes_iv)
|
|
506
|
+
|
|
507
|
+
packet = self.auth_key_id + msg_key + encrypted_data
|
|
508
|
+
framed = self._abridged_frame(packet)
|
|
509
|
+
encrypted = self.enc_cipher.encrypt(framed)
|
|
510
|
+
|
|
511
|
+
await self.ws.send(encrypted)
|
|
512
|
+
print(f"Request sent (msg_id: {msg_id_int})")
|
|
513
|
+
|
|
514
|
+
for _ in range(10):
|
|
515
|
+
response = await asyncio.wait_for(self.recv(), timeout=10.0)
|
|
516
|
+
msg_data = self.decrypt_response(response)
|
|
517
|
+
|
|
518
|
+
if msg_data and len(msg_data) >= 4:
|
|
519
|
+
ctor = struct.unpack("<I", msg_data[:4])[0]
|
|
520
|
+
if ctor == 0xF35C6D01:
|
|
521
|
+
req_msg_id = struct.unpack("<Q", msg_data[4:12])[0]
|
|
522
|
+
if req_msg_id == msg_id_int:
|
|
523
|
+
result_data = msg_data[12:]
|
|
524
|
+
return result_data
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
async def invoke(self, method_id: int, *args):
|
|
528
|
+
"""
|
|
529
|
+
Generic invoke method for any TL method.
|
|
530
|
+
Usage:
|
|
531
|
+
await client.invoke(0xFEF48F62, peer, message, random_id)
|
|
532
|
+
"""
|
|
533
|
+
body = struct.pack("<I", method_id)
|
|
534
|
+
for arg in args:
|
|
535
|
+
if isinstance(arg, bytes):
|
|
536
|
+
body += arg
|
|
537
|
+
elif isinstance(arg, int):
|
|
538
|
+
body += struct.pack("<i", arg)
|
|
539
|
+
elif isinstance(arg, str):
|
|
540
|
+
body += tl_string(arg)
|
|
541
|
+
else:
|
|
542
|
+
raise ValueError(f"Unsupported arg type: {type(arg)}")
|
|
543
|
+
|
|
544
|
+
return await self.send_request(wrap_init_connection(body))
|
|
545
|
+
|
|
546
|
+
async def close(self):
|
|
547
|
+
if self.ws:
|
|
548
|
+
await self.ws.close()
|
jetrauserapi/config.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
# Base directory of the project
|
|
4
|
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
5
|
+
|
|
6
|
+
# Data directory
|
|
7
|
+
DATA_DIR = os.path.join(BASE_DIR, "data")
|
|
8
|
+
|
|
9
|
+
# Default paths
|
|
10
|
+
DEFAULT_SESSION_FILE = os.path.join(DATA_DIR, "jetra_session.json")
|
|
11
|
+
DEFAULT_DB_FILE = os.path.join(DATA_DIR, "jetra_local.db")
|
|
12
|
+
DEFAULT_GROUPS_FILE = os.path.join(DATA_DIR, "groups_only.json")
|
jetrauserapi/database.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Database:
|
|
7
|
+
"""
|
|
8
|
+
Local SQLite database for storing messages, dialogs, and groups.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, db_path="jetra_local.db"):
|
|
12
|
+
self.db_path = db_path
|
|
13
|
+
self.conn = None
|
|
14
|
+
self._connect()
|
|
15
|
+
|
|
16
|
+
def _connect(self):
|
|
17
|
+
self.conn = sqlite3.connect(self.db_path)
|
|
18
|
+
self.conn.row_factory = sqlite3.Row
|
|
19
|
+
self._create_tables()
|
|
20
|
+
|
|
21
|
+
def _create_tables(self):
|
|
22
|
+
cursor = self.conn.cursor()
|
|
23
|
+
|
|
24
|
+
# Messages table
|
|
25
|
+
cursor.execute('''
|
|
26
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
27
|
+
id INTEGER NOT NULL,
|
|
28
|
+
peer_id INTEGER NOT NULL,
|
|
29
|
+
peer_type TEXT NOT NULL,
|
|
30
|
+
from_id INTEGER NOT NULL,
|
|
31
|
+
date INTEGER NOT NULL,
|
|
32
|
+
text TEXT,
|
|
33
|
+
PRIMARY KEY (id, peer_id, peer_type)
|
|
34
|
+
)
|
|
35
|
+
''')
|
|
36
|
+
|
|
37
|
+
# Dialogs table (users and groups)
|
|
38
|
+
cursor.execute('''
|
|
39
|
+
CREATE TABLE IF NOT EXISTS dialogs (
|
|
40
|
+
peer_id INTEGER PRIMARY KEY,
|
|
41
|
+
peer_type TEXT NOT NULL,
|
|
42
|
+
title TEXT,
|
|
43
|
+
access_hash INTEGER,
|
|
44
|
+
updated_at INTEGER
|
|
45
|
+
)
|
|
46
|
+
''')
|
|
47
|
+
|
|
48
|
+
self.conn.commit()
|
|
49
|
+
|
|
50
|
+
def save_message(self, message):
|
|
51
|
+
"""Save a message to the local database."""
|
|
52
|
+
cursor = self.conn.cursor()
|
|
53
|
+
try:
|
|
54
|
+
cursor.execute('''
|
|
55
|
+
INSERT OR REPLACE INTO messages
|
|
56
|
+
(id, peer_id, peer_type, from_id, date, text)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
58
|
+
''', (
|
|
59
|
+
message.id,
|
|
60
|
+
message.peer_id,
|
|
61
|
+
message.peer_type,
|
|
62
|
+
message.from_id,
|
|
63
|
+
message.date,
|
|
64
|
+
message.text,
|
|
65
|
+
))
|
|
66
|
+
self.conn.commit()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"Failed to save message: {e}")
|
|
69
|
+
|
|
70
|
+
def get_messages(self, peer_id=None, limit=100):
|
|
71
|
+
"""Get messages from a specific peer or all peers."""
|
|
72
|
+
cursor = self.conn.cursor()
|
|
73
|
+
if peer_id is not None:
|
|
74
|
+
cursor.execute('''
|
|
75
|
+
SELECT * FROM messages
|
|
76
|
+
WHERE peer_id = ?
|
|
77
|
+
ORDER BY date DESC
|
|
78
|
+
LIMIT ?
|
|
79
|
+
''', (peer_id, limit))
|
|
80
|
+
else:
|
|
81
|
+
cursor.execute('''
|
|
82
|
+
SELECT * FROM messages
|
|
83
|
+
ORDER BY date DESC
|
|
84
|
+
LIMIT ?
|
|
85
|
+
''', (limit,))
|
|
86
|
+
|
|
87
|
+
rows = cursor.fetchall()
|
|
88
|
+
return [dict(row) for row in rows]
|
|
89
|
+
|
|
90
|
+
def save_dialog(self, peer_id, peer_type, title, access_hash):
|
|
91
|
+
"""Save or update a dialog."""
|
|
92
|
+
cursor = self.conn.cursor()
|
|
93
|
+
import time
|
|
94
|
+
cursor.execute('''
|
|
95
|
+
INSERT OR REPLACE INTO dialogs
|
|
96
|
+
(peer_id, peer_type, title, access_hash, updated_at)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?)
|
|
98
|
+
''', (peer_id, peer_type, title, access_hash, int(time.time())))
|
|
99
|
+
self.conn.commit()
|
|
100
|
+
|
|
101
|
+
def get_dialogs(self, peer_type=None):
|
|
102
|
+
"""Get all dialogs, optionally filtered by type."""
|
|
103
|
+
cursor = self.conn.cursor()
|
|
104
|
+
if peer_type:
|
|
105
|
+
cursor.execute('SELECT * FROM dialogs WHERE peer_type = ? ORDER BY title', (peer_type,))
|
|
106
|
+
else:
|
|
107
|
+
cursor.execute('SELECT * FROM dialogs ORDER BY title')
|
|
108
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
109
|
+
|
|
110
|
+
def close(self):
|
|
111
|
+
if self.conn:
|
|
112
|
+
self.conn.close()
|