telegram-mtproto-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +188 -0
- data/Rakefile +8 -0
- data/examples/complete_demo.rb +211 -0
- data/lib/telegram/auth.rb +438 -0
- data/lib/telegram/binary_reader.rb +156 -0
- data/lib/telegram/connection/tcp_full_connection.rb +248 -0
- data/lib/telegram/crypto.rb +323 -0
- data/lib/telegram/crypto_rsa_keys.rb +86 -0
- data/lib/telegram/senders/mtproto_encrypted_sender.rb +234 -0
- data/lib/telegram/senders/mtproto_plain_sender.rb +116 -0
- data/lib/telegram/serialization.rb +106 -0
- data/lib/telegram/tl/api.tl +2750 -0
- data/lib/telegram/tl/mtproto.tl +116 -0
- data/lib/telegram/tl_object.rb +132 -0
- data/lib/telegram/tl_reader.rb +120 -0
- data/lib/telegram/tl_schema.rb +113 -0
- data/lib/telegram/tl_writer.rb +103 -0
- data/lib/telegram_m_t_proto_clean.rb +1456 -0
- data/lib/telegram_mtproto/ruby/version.rb +9 -0
- data/lib/telegram_mtproto/ruby.rb +12 -0
- data/lib/telegram_mtproto/version.rb +5 -0
- data/lib/telegram_mtproto.rb +20 -0
- data/lib/telegram_plain_tcp.rb +92 -0
- data/sig/telegram/mtproto/ruby.rbs +8 -0
- metadata +69 -0
@@ -0,0 +1,438 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'crypto'
|
4
|
+
require_relative 'serialization'
|
5
|
+
require_relative 'binary_reader'
|
6
|
+
require_relative 'tl_object'
|
7
|
+
|
8
|
+
module Telegram
|
9
|
+
module Auth
|
10
|
+
include Serialization
|
11
|
+
include Crypto
|
12
|
+
|
13
|
+
# Complete DH handshake EXACTLY like Telethon authenticator.py
|
14
|
+
def self.perform_dh_handshake(plain_sender)
|
15
|
+
Rails.logger.info '🔐 Starting REAL DH handshake EXACTLY like Telethon authenticator.do_authentication()'
|
16
|
+
|
17
|
+
# Step 1: Generate nonce and send req_pq_multi
|
18
|
+
nonce_bytes = SecureRandom.random_bytes(16)
|
19
|
+
nonce = Serialization.bytes_to_int(nonce_bytes, 'big', signed: true)
|
20
|
+
|
21
|
+
Rails.logger.info '📤 Generating and sending nonce:'
|
22
|
+
Rails.logger.info " random bytes: #{nonce_bytes.unpack1('H*')}"
|
23
|
+
Rails.logger.info " parsed as big endian signed: #{nonce}"
|
24
|
+
|
25
|
+
res_pq_data = send_req_pq_multi(plain_sender, nonce)
|
26
|
+
return nil unless res_pq_data
|
27
|
+
|
28
|
+
# Step 2: Factor PQ and send req_DH_params
|
29
|
+
pq = res_pq_data[:pq]
|
30
|
+
p_int, q_int = Crypto.factorize_pq(pq)
|
31
|
+
|
32
|
+
# Convert to byte arrays EXACTLY like Telethon rsa.get_byte_array()
|
33
|
+
p_bytes = Serialization.integer_to_byte_array(p_int, signed: false)
|
34
|
+
q_bytes = Serialization.integer_to_byte_array(q_int, signed: false)
|
35
|
+
pq_bytes = Serialization.integer_to_byte_array(pq, signed: false)
|
36
|
+
|
37
|
+
# Generate new_nonce for DH exchange
|
38
|
+
new_nonce_bytes = SecureRandom.random_bytes(32)
|
39
|
+
new_nonce = Serialization.bytes_to_int(new_nonce_bytes, 'little', signed: true)
|
40
|
+
|
41
|
+
# Build and encrypt PQInnerData using byte arrays
|
42
|
+
# Add longer delay to avoid rate limiting (like Telethon retry logic)
|
43
|
+
Rails.logger.debug "⏱️ Adding 2s delay before req_DH_params to avoid rate limiting"
|
44
|
+
sleep(2)
|
45
|
+
|
46
|
+
pq_inner_data = build_pq_inner_data_bytes(res_pq_data, pq_bytes, p_bytes, q_bytes, new_nonce)
|
47
|
+
encryption_result = Crypto.rsa_encrypt_pq_inner_data(pq_inner_data, res_pq_data[:server_public_key_fingerprints])
|
48
|
+
|
49
|
+
dh_params = send_req_dh_params(plain_sender, res_pq_data, p_bytes, q_bytes,
|
50
|
+
encryption_result[:encrypted_data], encryption_result[:fingerprint])
|
51
|
+
return nil unless dh_params && dh_params[:success]
|
52
|
+
|
53
|
+
# Step 3: Complete DH exchange - add delay to avoid rate limiting
|
54
|
+
Rails.logger.debug "⏱️ Adding 1s delay before SetClientDHParams to avoid rate limiting"
|
55
|
+
sleep(1)
|
56
|
+
|
57
|
+
auth_key = complete_dh_exchange(plain_sender, res_pq_data, new_nonce, dh_params)
|
58
|
+
|
59
|
+
Rails.logger.info '🔑 DH handshake completed successfully'
|
60
|
+
auth_key
|
61
|
+
end
|
62
|
+
|
63
|
+
# Send req_pq_multi request EXACTLY like Telethon
|
64
|
+
def self.send_req_pq_multi(plain_sender, nonce)
|
65
|
+
# req_pq_multi#be7e8ef1 nonce:int128
|
66
|
+
request_data = TLObject.req_pq_multi(nonce: nonce)
|
67
|
+
|
68
|
+
Rails.logger.info "📤 Sending req_pq_multi with nonce=#{nonce} (0x#{nonce.to_s(16)})"
|
69
|
+
|
70
|
+
response = plain_sender.send(request_data)
|
71
|
+
return nil unless response && response.length >= 20
|
72
|
+
|
73
|
+
parse_res_pq_response(response, nonce)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Parse ResPQ response EXACTLY like Telethon
|
77
|
+
def self.parse_res_pq_response(data, expected_nonce)
|
78
|
+
offset = 0
|
79
|
+
|
80
|
+
# Constructor
|
81
|
+
constructor = data[offset, 4].unpack1('L<')
|
82
|
+
offset += 4
|
83
|
+
|
84
|
+
unless constructor == 0x05162463 # resPQ
|
85
|
+
Rails.logger.error "❌ Invalid response constructor: 0x#{constructor.to_s(16)}"
|
86
|
+
return nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Parse nonce (16 bytes) - TL int128 is little-endian signed
|
90
|
+
nonce_bytes = data[offset, 16]
|
91
|
+
nonce = Serialization.bytes_to_int(nonce_bytes, 'little', signed: true)
|
92
|
+
offset += 16
|
93
|
+
|
94
|
+
if nonce != expected_nonce
|
95
|
+
Rails.logger.error "❌ Nonce mismatch! Expected: #{expected_nonce}, got: #{nonce}"
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# Parse server_nonce (16 bytes) - TL int128 is little-endian signed
|
100
|
+
server_nonce_bytes = data[offset, 16]
|
101
|
+
server_nonce = Serialization.bytes_to_int(server_nonce_bytes, 'little', signed: true)
|
102
|
+
offset += 16
|
103
|
+
|
104
|
+
# Parse pq (TL string)
|
105
|
+
pq_length = data[offset, 1].unpack1('C')
|
106
|
+
offset += 1
|
107
|
+
pq_bytes = data[offset, pq_length]
|
108
|
+
pq = Serialization.bytes_to_int(pq_bytes, 'big', signed: false)
|
109
|
+
offset += pq_length
|
110
|
+
|
111
|
+
# Skip padding
|
112
|
+
padding = (4 - ((pq_length + 1) % 4)) % 4
|
113
|
+
offset += padding
|
114
|
+
|
115
|
+
# Parse fingerprints vector
|
116
|
+
vector_constructor = data[offset, 4].unpack1('L<')
|
117
|
+
offset += 4
|
118
|
+
|
119
|
+
unless vector_constructor == 0x1cb5c415 # Vector constructor
|
120
|
+
Rails.logger.error "❌ Invalid vector constructor: 0x#{vector_constructor.to_s(16)}"
|
121
|
+
return nil
|
122
|
+
end
|
123
|
+
|
124
|
+
fingerprint_count = data[offset, 4].unpack1('L<')
|
125
|
+
offset += 4
|
126
|
+
|
127
|
+
fingerprints = []
|
128
|
+
fingerprint_count.times do
|
129
|
+
fingerprint = data[offset, 8].unpack1('q<')
|
130
|
+
fingerprints << fingerprint
|
131
|
+
offset += 8
|
132
|
+
end
|
133
|
+
|
134
|
+
Rails.logger.info "✅ Got ResPQ: pq=#{pq}, fingerprints=#{fingerprints.map { |f| "0x#{f.to_s(16)}" }}"
|
135
|
+
|
136
|
+
{
|
137
|
+
nonce: nonce,
|
138
|
+
server_nonce: server_nonce,
|
139
|
+
pq: pq,
|
140
|
+
server_public_key_fingerprints: fingerprints
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
# Build PQInnerData using byte arrays EXACTLY like Telethon
|
145
|
+
def self.build_pq_inner_data_bytes(res_pq_data, pq_bytes, p_bytes, q_bytes, new_nonce)
|
146
|
+
# p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256
|
147
|
+
inner_data = ''
|
148
|
+
|
149
|
+
# Constructor
|
150
|
+
inner_data += [0x83c95aec].pack('L<')
|
151
|
+
|
152
|
+
# pq, p, q as TL strings (already byte arrays)
|
153
|
+
inner_data += Serialization.pack_tl_string(pq_bytes)
|
154
|
+
inner_data += Serialization.pack_tl_string(p_bytes)
|
155
|
+
inner_data += Serialization.pack_tl_string(q_bytes)
|
156
|
+
|
157
|
+
# nonce:int128
|
158
|
+
inner_data += Serialization.serialize_int128(res_pq_data[:nonce])
|
159
|
+
|
160
|
+
# server_nonce:int128
|
161
|
+
inner_data += Serialization.serialize_int128(res_pq_data[:server_nonce])
|
162
|
+
|
163
|
+
# new_nonce:int256
|
164
|
+
inner_data += Serialization.serialize_int256(new_nonce)
|
165
|
+
|
166
|
+
Rails.logger.info '🔍 PQInnerData structure:'
|
167
|
+
Rails.logger.info ' constructor: 83c95aec'
|
168
|
+
Rails.logger.info " pq_bytes: #{pq_bytes.unpack1('H*')} (#{pq_bytes.length} bytes)"
|
169
|
+
Rails.logger.info " p_bytes: #{p_bytes.unpack1('H*')} (#{p_bytes.length} bytes)"
|
170
|
+
Rails.logger.info " q_bytes: #{q_bytes.unpack1('H*')} (#{q_bytes.length} bytes)"
|
171
|
+
Rails.logger.info " nonce: #{res_pq_data[:nonce]}"
|
172
|
+
Rails.logger.info " server_nonce: #{res_pq_data[:server_nonce]}"
|
173
|
+
Rails.logger.info " new_nonce: #{new_nonce}"
|
174
|
+
Rails.logger.info " total inner_data: #{inner_data.length} bytes"
|
175
|
+
Rails.logger.info " inner_data HEX: #{inner_data.unpack1('H*')}"
|
176
|
+
|
177
|
+
inner_data
|
178
|
+
end
|
179
|
+
|
180
|
+
# Build PQInnerData using TL Schema (like Telethon)
|
181
|
+
def self.build_pq_inner_data(res_pq_data, p, q, new_nonce)
|
182
|
+
# Convert integers to byte arrays like Telethon
|
183
|
+
pq_bytes = Serialization.integer_to_byte_array(res_pq_data[:pq], signed: false)
|
184
|
+
p_bytes = Serialization.integer_to_byte_array(p, signed: false)
|
185
|
+
q_bytes = Serialization.integer_to_byte_array(q, signed: false)
|
186
|
+
|
187
|
+
result = TLObject.p_q_inner_data(
|
188
|
+
pq: pq_bytes, p: p_bytes, q: q_bytes,
|
189
|
+
nonce: res_pq_data[:nonce],
|
190
|
+
server_nonce: res_pq_data[:server_nonce],
|
191
|
+
new_nonce: new_nonce
|
192
|
+
)
|
193
|
+
|
194
|
+
Rails.logger.debug "🔍 TL Schema PQInnerData HEX: #{result.unpack1('H*')}"
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
198
|
+
# Send req_DH_params EXACTLY like Telethon
|
199
|
+
def self.send_req_dh_params(plain_sender, res_pq_data, p, q, encrypted_data, used_fingerprint)
|
200
|
+
|
201
|
+
# req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string
|
202
|
+
Rails.logger.debug "🔍 req_DH_params encrypted_data HEX: #{encrypted_data.unpack1('H*')}"
|
203
|
+
# Use TL Schema for proper serialization
|
204
|
+
request_data = TLObject.req_dh_params(
|
205
|
+
nonce: res_pq_data[:nonce],
|
206
|
+
server_nonce: res_pq_data[:server_nonce],
|
207
|
+
p: p, q: q,
|
208
|
+
public_key_fingerprint: used_fingerprint,
|
209
|
+
encrypted_data: encrypted_data
|
210
|
+
)
|
211
|
+
|
212
|
+
Rails.logger.info "📤 Sending req_DH_params: #{request_data.length} bytes"
|
213
|
+
Rails.logger.info '🔍 Debug nonce serialization:'
|
214
|
+
Rails.logger.info " nonce value: #{res_pq_data[:nonce]}"
|
215
|
+
Rails.logger.info " nonce hex: 0x#{res_pq_data[:nonce].to_s(16)}"
|
216
|
+
nonce_serialized = Serialization.serialize_int128(res_pq_data[:nonce])
|
217
|
+
Rails.logger.info " nonce serialized: #{nonce_serialized.unpack1('H*')}"
|
218
|
+
|
219
|
+
# Test round-trip conversion
|
220
|
+
nonce_back = Serialization.bytes_to_int(nonce_serialized, 'little', signed: true)
|
221
|
+
Rails.logger.info " nonce round-trip: #{nonce_back} (matches: #{nonce_back == res_pq_data[:nonce]})"
|
222
|
+
Rails.logger.info "🔍 req_DH_params HEX: #{request_data.unpack1('H*')}"
|
223
|
+
Rails.logger.info '🔍 req_DH_params structure breakdown:'
|
224
|
+
offset = 0
|
225
|
+
Rails.logger.info " constructor: #{request_data[offset, 4].unpack1('H*')} (#{request_data[offset, 4].unpack1('L<').to_s(16)})"
|
226
|
+
offset += 4
|
227
|
+
Rails.logger.info " nonce: #{request_data[offset, 16].unpack1('H*')}"
|
228
|
+
offset += 16
|
229
|
+
Rails.logger.info " server_nonce: #{request_data[offset, 16].unpack1('H*')}"
|
230
|
+
offset += 16
|
231
|
+
p_len = request_data[offset, 1].unpack1('C')
|
232
|
+
Rails.logger.info " p_len: #{p_len}"
|
233
|
+
offset += 1
|
234
|
+
Rails.logger.info " p_data: #{request_data[offset, p_len].unpack1('H*')}"
|
235
|
+
offset += p_len + ((4 - ((p_len + 1) % 4)) % 4) # padding
|
236
|
+
q_len = request_data[offset, 1].unpack1('C')
|
237
|
+
Rails.logger.info " q_len: #{q_len}"
|
238
|
+
offset += 1
|
239
|
+
Rails.logger.info " q_data: #{request_data[offset, q_len].unpack1('H*')}"
|
240
|
+
offset += q_len + ((4 - ((q_len + 1) % 4)) % 4) # padding
|
241
|
+
Rails.logger.info " fingerprint: #{request_data[offset, 8].unpack1('q<').to_s(16)}"
|
242
|
+
offset += 8
|
243
|
+
enc_len_marker = request_data[offset, 1].unpack1('C')
|
244
|
+
if enc_len_marker == 254
|
245
|
+
# Long form: 254 + 3-byte length (need to pad with 0 for unpack)
|
246
|
+
len_bytes = request_data[offset + 1, 3] + "\x00" # pad to 4 bytes
|
247
|
+
enc_len = len_bytes.unpack1('L<')
|
248
|
+
Rails.logger.info " encrypted_len: #{enc_len} (long form: 254 + 3-byte length)"
|
249
|
+
else
|
250
|
+
enc_len = enc_len_marker
|
251
|
+
Rails.logger.info " encrypted_len: #{enc_len} (short form)"
|
252
|
+
end
|
253
|
+
Rails.logger.info " total_size: #{request_data.length} bytes"
|
254
|
+
|
255
|
+
begin
|
256
|
+
response = plain_sender.send(request_data)
|
257
|
+
Rails.logger.info "📥 Got req_DH_params response: #{response&.length || 0} bytes"
|
258
|
+
|
259
|
+
return nil unless response && response.length >= 20
|
260
|
+
|
261
|
+
parse_server_dh_params_response(response, res_pq_data)
|
262
|
+
rescue StandardError => e
|
263
|
+
Rails.logger.error "❌ req_DH_params failed: #{e.message}"
|
264
|
+
Rails.logger.error " Backtrace: #{e.backtrace.first(5).join('\n ')}"
|
265
|
+
return nil
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Parse server_DH_params response
|
270
|
+
def self.parse_server_dh_params_response(response, res_pq_data)
|
271
|
+
constructor = response[0, 4].unpack1('L<')
|
272
|
+
|
273
|
+
case constructor
|
274
|
+
when 0xd0e8075c # server_DH_params_ok
|
275
|
+
Rails.logger.info '✅ Got server_DH_params_ok'
|
276
|
+
# server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string
|
277
|
+
offset = 4
|
278
|
+
|
279
|
+
# Skip nonce and server_nonce (32 bytes total)
|
280
|
+
offset += 32
|
281
|
+
|
282
|
+
# Read encrypted_answer
|
283
|
+
answer_length = response[offset, 4].unpack1('L<')
|
284
|
+
offset += 4
|
285
|
+
encrypted_answer = response[offset, answer_length]
|
286
|
+
|
287
|
+
{
|
288
|
+
success: true,
|
289
|
+
encrypted_answer: encrypted_answer,
|
290
|
+
nonce: res_pq_data[:nonce],
|
291
|
+
server_nonce: res_pq_data[:server_nonce]
|
292
|
+
}
|
293
|
+
when 0x79cb045d # server_DH_params_fail
|
294
|
+
Rails.logger.error '❌ Got server_DH_params_fail'
|
295
|
+
nil
|
296
|
+
else
|
297
|
+
Rails.logger.error "❌ Unknown response constructor: 0x#{constructor.to_s(16)}"
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Complete DH exchange EXACTLY like Telethon
|
303
|
+
def self.complete_dh_exchange(plain_sender, res_pq_data, new_nonce, dh_params)
|
304
|
+
Rails.logger.info '🔐 Completing DH exchange EXACTLY like Telethon...'
|
305
|
+
|
306
|
+
# Generate key and IV
|
307
|
+
server_nonce_bytes = Serialization.serialize_int128(res_pq_data[:server_nonce])
|
308
|
+
new_nonce_bytes_le = Serialization.serialize_int256(new_nonce)
|
309
|
+
|
310
|
+
key, iv = Crypto.generate_key_data_from_nonce(server_nonce_bytes, new_nonce_bytes_le)
|
311
|
+
|
312
|
+
Rails.logger.info "🔑 Generated AES key: #{key.length} bytes, IV: #{iv.length} bytes"
|
313
|
+
|
314
|
+
# Decrypt server_DH_inner_data
|
315
|
+
encrypted_answer = dh_params[:encrypted_answer]
|
316
|
+
raise Crypto::SecurityError.new('Step 3 AES block size mismatch') if encrypted_answer.length % 16 != 0
|
317
|
+
|
318
|
+
plain_text_answer = Crypto.decrypt_ige(encrypted_answer, key, iv)
|
319
|
+
|
320
|
+
# Skip hash sum (20 bytes) and parse server_dh_inner EXACTLY like Telethon
|
321
|
+
offset = 20
|
322
|
+
server_dh_data = plain_text_answer[offset..-1]
|
323
|
+
|
324
|
+
# Parse ServerDHInnerData like Telethon (authenticator.py line 118)
|
325
|
+
server_dh_inner = parse_server_dh_inner_data(server_dh_data)
|
326
|
+
|
327
|
+
# Calculate time_offset like Telethon (authenticator.py line 131)
|
328
|
+
server_time = server_dh_inner[:server_time]
|
329
|
+
time_offset = server_time - Time.now.to_i
|
330
|
+
Rails.logger.info "⏰ Server time: #{server_time}, local time: #{Time.now.to_i}, time_offset: #{time_offset}"
|
331
|
+
|
332
|
+
# Generate random b (256 bytes) like Telethon
|
333
|
+
b_bytes = SecureRandom.random_bytes(256)
|
334
|
+
b = Serialization.bytes_to_int(b_bytes, 'big', signed: false)
|
335
|
+
|
336
|
+
# Extract DH parameters from server like Telethon (authenticator.py lines 128-130)
|
337
|
+
g = server_dh_inner[:g]
|
338
|
+
dh_prime_bytes = server_dh_inner[:dh_prime]
|
339
|
+
dh_prime = Serialization.bytes_to_int(dh_prime_bytes, 'big', signed: false)
|
340
|
+
|
341
|
+
# Calculate g_b = g^b mod dh_prime
|
342
|
+
g_b = Crypto::ModularArithmetic.pow(g, b, dh_prime)
|
343
|
+
|
344
|
+
# Extract g_a from server DH inner data (already parsed)
|
345
|
+
g_a_bytes = server_dh_inner[:g_a]
|
346
|
+
g_a = Serialization.bytes_to_int(g_a_bytes, 'big', signed: false)
|
347
|
+
|
348
|
+
# Calculate auth_key = g_a^b mod dh_prime (EXACTLY like Telethon)
|
349
|
+
gab = Crypto::ModularArithmetic.pow(g_a, b, dh_prime)
|
350
|
+
|
351
|
+
# Prepare client DH Inner Data
|
352
|
+
client_dh_inner = ''
|
353
|
+
client_dh_inner += [0x6643b654].pack('L<') # ClientDHInnerData constructor
|
354
|
+
client_dh_inner += Serialization.serialize_int128(res_pq_data[:nonce]) # nonce
|
355
|
+
client_dh_inner += Serialization.serialize_int128(res_pq_data[:server_nonce]) # server_nonce
|
356
|
+
client_dh_inner += [0].pack('q<') # retry_id (always 0)
|
357
|
+
|
358
|
+
# g_b as big-endian byte array
|
359
|
+
g_b_bytes = Serialization.integer_to_byte_array(g_b, signed: false)
|
360
|
+
client_dh_inner += Serialization.pack_tl_string(g_b_bytes)
|
361
|
+
|
362
|
+
client_dh_inner_hashed = Digest::SHA1.digest(client_dh_inner) + client_dh_inner
|
363
|
+
client_dh_encrypted = Crypto.encrypt_ige(client_dh_inner_hashed, key, iv)
|
364
|
+
|
365
|
+
# Send SetClientDHParamsRequest
|
366
|
+
set_client_dh_params_data = ''
|
367
|
+
set_client_dh_params_data += [0xf5045f1f].pack('L<') # SetClientDHParams constructor
|
368
|
+
set_client_dh_params_data += Serialization.serialize_int128(res_pq_data[:nonce])
|
369
|
+
set_client_dh_params_data += Serialization.serialize_int128(res_pq_data[:server_nonce])
|
370
|
+
set_client_dh_params_data += Serialization.pack_tl_string(client_dh_encrypted)
|
371
|
+
|
372
|
+
Rails.logger.info '📤 Sending SetClientDHParams...'
|
373
|
+
dh_gen_response = plain_sender.send(set_client_dh_params_data)
|
374
|
+
|
375
|
+
return nil unless dh_gen_response && dh_gen_response.length >= 4
|
376
|
+
|
377
|
+
# Parse response
|
378
|
+
constructor = dh_gen_response[0, 4].unpack1('L<')
|
379
|
+
Rails.logger.info "🔍 DH gen response constructor: 0x#{constructor.to_s(16)}"
|
380
|
+
|
381
|
+
case constructor
|
382
|
+
when 0x3bcbf734 # dh_gen_ok
|
383
|
+
Rails.logger.info '✅ Got dh_gen_ok'
|
384
|
+
# Generate auth_key from gab
|
385
|
+
auth_key_bytes = Serialization.integer_to_byte_array(gab, signed: false)
|
386
|
+
# Pad or truncate to 256 bytes
|
387
|
+
if auth_key_bytes.length < 256
|
388
|
+
auth_key_bytes = ("\x00" * (256 - auth_key_bytes.length)) + auth_key_bytes
|
389
|
+
elsif auth_key_bytes.length > 256
|
390
|
+
auth_key_bytes = auth_key_bytes[-256..-1]
|
391
|
+
end
|
392
|
+
|
393
|
+
Rails.logger.info "🔑 Generated auth_key from DH exchange: #{auth_key_bytes.length} bytes"
|
394
|
+
|
395
|
+
# Return both auth_key and time_offset like Telethon (authenticator.py line 200)
|
396
|
+
{ auth_key: auth_key_bytes, time_offset: time_offset }
|
397
|
+
when 0x46dc1fb9 # dh_gen_retry
|
398
|
+
Rails.logger.error '❌ Got dh_gen_retry - need retry logic'
|
399
|
+
nil
|
400
|
+
when 0xa69dae02 # dh_gen_fail
|
401
|
+
Rails.logger.error '❌ Got dh_gen_fail'
|
402
|
+
nil
|
403
|
+
else
|
404
|
+
Rails.logger.error "❌ Unknown DH gen response: 0x#{constructor.to_s(16)}"
|
405
|
+
nil
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Parse ServerDHInnerData using Telethon BinaryReader
|
410
|
+
def self.parse_server_dh_inner_data(data)
|
411
|
+
Rails.logger.debug "🔍 Parsing ServerDHInnerData: #{data.length} bytes"
|
412
|
+
Rails.logger.debug "🔍 ServerDHInnerData HEX: #{data.unpack('H*')[0]}"
|
413
|
+
|
414
|
+
require_relative 'binary_reader'
|
415
|
+
|
416
|
+
reader = Telegram::BinaryReader.new(data)
|
417
|
+
server_dh_inner = reader.tgread_object
|
418
|
+
|
419
|
+
Rails.logger.info "✅ Parsed ServerDHInnerData successfully with Telethon BinaryReader"
|
420
|
+
Rails.logger.info "🔍 Constructor: 0x#{server_dh_inner[:constructor_id].to_s(16)}"
|
421
|
+
Rails.logger.info "🔍 g: #{server_dh_inner[:g]}"
|
422
|
+
Rails.logger.info "🔍 dh_prime length: #{server_dh_inner[:dh_prime].length} bytes"
|
423
|
+
Rails.logger.info "🔍 g_a length: #{server_dh_inner[:g_a].length} bytes"
|
424
|
+
Rails.logger.info "⏰ server_time: #{server_dh_inner[:server_time]} (#{Time.at(server_dh_inner[:server_time])})"
|
425
|
+
|
426
|
+
{
|
427
|
+
server_time: server_dh_inner[:server_time],
|
428
|
+
g: server_dh_inner[:g],
|
429
|
+
dh_prime: server_dh_inner[:dh_prime],
|
430
|
+
g_a: server_dh_inner[:g_a]
|
431
|
+
}
|
432
|
+
rescue => e
|
433
|
+
Rails.logger.error "❌ Error parsing ServerDHInnerData: #{e.message}"
|
434
|
+
Rails.logger.error "❌ Backtrace: #{e.backtrace.join("\n")}"
|
435
|
+
nil
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telegram
|
4
|
+
# Ruby port of Telethon's BinaryReader
|
5
|
+
# Based on telethon/extensions/binaryreader.py
|
6
|
+
class BinaryReader
|
7
|
+
def initialize(data)
|
8
|
+
@stream = data || ''
|
9
|
+
@position = 0
|
10
|
+
@last = nil # Should come in handy to spot -404 errors
|
11
|
+
end
|
12
|
+
|
13
|
+
# All numbers are written as little endian.
|
14
|
+
# https://core.telegram.org/mtproto
|
15
|
+
def read_byte
|
16
|
+
raise BufferError.new("No more data left to read (need 1, got 0)") if @position >= @stream.length
|
17
|
+
value = @stream[@position].unpack1('C')
|
18
|
+
@position += 1
|
19
|
+
value
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_int(signed: true)
|
23
|
+
length = 4
|
24
|
+
if @position + length > @stream.length
|
25
|
+
raise BufferError.new("No more data left to read (need #{length}, got #{@stream.length - @position})")
|
26
|
+
end
|
27
|
+
|
28
|
+
format = signed ? 'l<' : 'L<'
|
29
|
+
value = @stream[@position, length].unpack1(format)
|
30
|
+
@position += length
|
31
|
+
value
|
32
|
+
end
|
33
|
+
|
34
|
+
def read_long(signed: true)
|
35
|
+
length = 8
|
36
|
+
if @position + length > @stream.length
|
37
|
+
raise BufferError.new("No more data left to read (need #{length}, got #{@stream.length - @position})")
|
38
|
+
end
|
39
|
+
|
40
|
+
format = signed ? 'q<' : 'Q<'
|
41
|
+
value = @stream[@position, length].unpack1(format)
|
42
|
+
@position += length
|
43
|
+
value
|
44
|
+
end
|
45
|
+
|
46
|
+
def read(length)
|
47
|
+
if length >= 0
|
48
|
+
if @position + length > @stream.length
|
49
|
+
raise BufferError.new("No more data left to read (need #{length}, got #{@stream.length - @position})")
|
50
|
+
end
|
51
|
+
result = @stream[@position, length]
|
52
|
+
@position += length
|
53
|
+
else
|
54
|
+
result = @stream[@position..-1]
|
55
|
+
@position += result.length
|
56
|
+
end
|
57
|
+
|
58
|
+
@last = result
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
# Telegram-specific methods
|
63
|
+
|
64
|
+
def tgread_bytes
|
65
|
+
# Reads a Telegram-encoded byte array, without the need of specifying its length.
|
66
|
+
first_byte = read_byte
|
67
|
+
|
68
|
+
if first_byte == 254
|
69
|
+
# Long format: 254 + 3 bytes for length
|
70
|
+
length = read_byte | (read_byte << 8) | (read_byte << 16)
|
71
|
+
padding = length % 4
|
72
|
+
else
|
73
|
+
# Short format: first byte is length
|
74
|
+
length = first_byte
|
75
|
+
padding = (length + 1) % 4
|
76
|
+
end
|
77
|
+
|
78
|
+
data = read(length)
|
79
|
+
if padding > 0
|
80
|
+
padding = 4 - padding
|
81
|
+
read(padding) # Skip padding
|
82
|
+
end
|
83
|
+
|
84
|
+
data
|
85
|
+
end
|
86
|
+
|
87
|
+
def tgread_string
|
88
|
+
# Reads a Telegram-encoded string.
|
89
|
+
tgread_bytes.force_encoding('UTF-8')
|
90
|
+
end
|
91
|
+
|
92
|
+
def tgread_bool
|
93
|
+
# Reads a Telegram boolean value.
|
94
|
+
value = read_int(signed: false)
|
95
|
+
case value
|
96
|
+
when 0x997275b5 # boolTrue
|
97
|
+
true
|
98
|
+
when 0xbc799737 # boolFalse
|
99
|
+
false
|
100
|
+
else
|
101
|
+
raise "Invalid boolean code #{value.to_s(16)}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def tgread_object
|
106
|
+
# Reads a Telegram object.
|
107
|
+
constructor_id = read_int(signed: false)
|
108
|
+
|
109
|
+
case constructor_id
|
110
|
+
when 0xb5890dba # server_DH_inner_data
|
111
|
+
tgread_server_dh_inner_data
|
112
|
+
when 0x997275b5 # boolTrue
|
113
|
+
true
|
114
|
+
when 0xbc799737 # boolFalse
|
115
|
+
false
|
116
|
+
when 0x1cb5c415 # Vector
|
117
|
+
count = read_int(signed: false)
|
118
|
+
(0...count).map { tgread_object }
|
119
|
+
else
|
120
|
+
raise "Unknown TL constructor: 0x#{constructor_id.to_s(16)}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def tell_position
|
125
|
+
@position
|
126
|
+
end
|
127
|
+
|
128
|
+
def seek(offset)
|
129
|
+
@position = offset
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def tgread_server_dh_inner_data
|
135
|
+
# server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int
|
136
|
+
nonce = read(16)
|
137
|
+
server_nonce = read(16)
|
138
|
+
g = read_int(signed: false)
|
139
|
+
dh_prime = tgread_bytes
|
140
|
+
g_a = tgread_bytes
|
141
|
+
server_time = read_int(signed: true)
|
142
|
+
|
143
|
+
{
|
144
|
+
constructor_id: 0xb5890dba,
|
145
|
+
nonce: nonce,
|
146
|
+
server_nonce: server_nonce,
|
147
|
+
g: g,
|
148
|
+
dh_prime: dh_prime,
|
149
|
+
g_a: g_a,
|
150
|
+
server_time: server_time
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class BufferError < StandardError; end
|
156
|
+
end
|