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.
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'zlib'
5
+
6
+ module Telegram
7
+ module Connection
8
+ # TCP connection handler EXACTLY like Telethon FullPacketCodec
9
+ class TcpFullConnection
10
+ class ConnectionError < StandardError; end
11
+
12
+ def initialize(ip, port)
13
+ @ip = ip
14
+ @port = port
15
+ @socket = nil
16
+ @send_counter = 0 # Important or Telegram won't reply
17
+ end
18
+
19
+ def send(data)
20
+ connect unless @socket
21
+
22
+ # EXACTLY like Telethon: struct.pack('<ii', length, self._send_counter) + data
23
+ length = data.length + 12 # total length, sequence number, packet and checksum (CRC32)
24
+ packet_header = [length, @send_counter].pack('i<i<') # signed int32, signed int32
25
+ packet_data = packet_header + data
26
+ crc32 = Zlib::crc32(packet_data)
27
+ full_message = packet_data + [crc32].pack('L<') # unsigned int32 for CRC
28
+
29
+ @send_counter += 1
30
+
31
+ Rails.logger.info "📤 TCP FULL: sending packet"
32
+ Rails.logger.info " length: #{length} (4 bytes)"
33
+ Rails.logger.info " counter: #{@send_counter-1} (4 bytes)"
34
+ Rails.logger.info " data: #{data.length} bytes"
35
+ Rails.logger.info " crc32: #{crc32} (4 bytes)"
36
+ Rails.logger.info " total: #{full_message.length} bytes"
37
+ Rails.logger.debug "📤 TCP packet HEX: #{full_message.unpack('H*')[0]}"
38
+
39
+ written = @socket.write(full_message)
40
+ Rails.logger.info "📤 TCP FULL: wrote #{written} bytes to socket"
41
+ Rails.logger.info "📡 OUR TCP SEND (#{full_message.length} bytes): #{full_message.unpack1('H*')}"
42
+ end
43
+
44
+ def recv(timeout: 30)
45
+ connect unless @socket
46
+
47
+ Rails.logger.info "📥 TCP FULL: waiting for response (#{timeout}s timeout)..."
48
+
49
+ ready = IO.select([@socket], nil, nil, timeout)
50
+ unless ready
51
+ Rails.logger.error "❌ TCP FULL: timeout after #{timeout}s"
52
+ raise ConnectionError.new("Timeout waiting for response")
53
+ end
54
+
55
+ Rails.logger.info "📥 TCP FULL: data available, socket status: open=#{!@socket.closed?}"
56
+
57
+ # Check if we can peek at data
58
+ begin
59
+ @socket.recv_nonblock(1, Socket::MSG_PEEK)
60
+ Rails.logger.info "📥 TCP FULL: confirmed data is readable, proceeding..."
61
+ rescue IO::WaitReadable
62
+ Rails.logger.warn "⚠️ TCP FULL: data not ready despite select, retrying..."
63
+ sleep(0.1)
64
+ rescue => e
65
+ Rails.logger.error "❌ TCP FULL: peek failed: #{e.message}"
66
+ raise ConnectionError.new("Peek failed: #{e.message}")
67
+ end
68
+
69
+ # Read packet length and sequence (8 bytes: 4+4)
70
+ begin
71
+ packet_len_seq = read_exactly(8)
72
+ packet_len, seq = packet_len_seq.unpack('i<i<') # signed int32
73
+
74
+ Rails.logger.info "📥 TCP FULL header: packet_len=#{packet_len}, seq=#{seq}"
75
+ Rails.logger.debug "📥 TCP FULL header HEX: #{packet_len_seq.unpack('H*')[0]}"
76
+ rescue ConnectionError => e
77
+ # Check if the error message contains our decoded Telegram error
78
+ if e.message.include?('AUTH_KEY_UNREGISTERED')
79
+ Rails.logger.error "❌ TCP FULL: Auth key not registered - need to redo DH handshake"
80
+ raise ConnectionError.new("AUTH_KEY_UNREGISTERED: The authorization key needs to be re-registered")
81
+ elsif e.message.include?('FLOOD_WAIT')
82
+ Rails.logger.error "❌ TCP FULL: Rate limited by Telegram server"
83
+ raise ConnectionError.new("FLOOD_WAIT: Too many requests, please wait before trying again")
84
+ else
85
+ raise e
86
+ end
87
+
88
+ # Handle negative packet length (Telegram error format)
89
+ if packet_len < 0
90
+ Rails.logger.error "❌ TCP FULL: got negative packet_len=#{packet_len} - this is an error response"
91
+ # For negative packet length, the actual data is the error code
92
+ # Read the error code (should be in the sequence field or following bytes)
93
+ error_code = seq # In Telegram error format, seq contains the error code
94
+ Rails.logger.error "❌ TCP FULL: Telegram error code: #{error_code}"
95
+ raise ConnectionError.new("Telegram server error: #{error_code}")
96
+ elsif packet_len < 8
97
+ Rails.logger.error "❌ TCP FULL: invalid packet_len=#{packet_len} (minimum 8)"
98
+ raise ConnectionError.new("Invalid packet length: #{packet_len}")
99
+ end
100
+ rescue ConnectionError => e
101
+ # If we got a partial read and it looks like an error, try to interpret it
102
+ if e.message.include?('Timeout reading') && e.message.include?('partial data received')
103
+ partial_hex = e.message.match(/partial data received \(4 bytes\): ([0-9a-f]+)/i)&.[](1)
104
+ if partial_hex
105
+ partial_bytes = [partial_hex].pack('H*')
106
+ error_code = partial_bytes.unpack('i<')[0]
107
+ Rails.logger.error "❌ TCP FULL: Interpreted partial data as error code: #{error_code}"
108
+ raise ConnectionError.new("Telegram server error: #{error_code}")
109
+ end
110
+ end
111
+ raise e # Re-raise original error if we can't interpret it
112
+ end
113
+
114
+ # Read remaining data (packet_len - 8 already read)
115
+ remaining_data = read_exactly(packet_len - 8)
116
+
117
+ # Extract body and checksum
118
+ body = remaining_data[0...-4] # All except last 4 bytes
119
+ received_checksum = remaining_data[-4..-1].unpack('L<')[0]
120
+
121
+ # Verify checksum
122
+ expected_checksum = Zlib::crc32(packet_len_seq + body)
123
+ unless received_checksum == expected_checksum
124
+ raise ConnectionError.new("Invalid checksum: got #{received_checksum}, expected #{expected_checksum}")
125
+ end
126
+
127
+ Rails.logger.debug "📥 TCP FULL body: #{body.length} bytes, checksum OK"
128
+ body
129
+ end
130
+
131
+ private
132
+
133
+ def connect
134
+ # Close existing socket if any
135
+ disconnect if @socket
136
+
137
+ Rails.logger.info "🔌 TCP FULL: connecting to #{@ip}:#{@port}..."
138
+
139
+ retries = 3
140
+ begin
141
+ @socket = TCPSocket.new(@ip, @port)
142
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
143
+ # Set socket timeouts
144
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [30, 0].pack("l_2"))
145
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, [30, 0].pack("l_2"))
146
+
147
+ Rails.logger.info "✅ TCP FULL: connected to #{@ip}:#{@port}"
148
+ rescue => e
149
+ retries -= 1
150
+ if retries > 0
151
+ Rails.logger.warn "⚠️ TCP FULL: connection failed, retrying... (#{retries} left): #{e.message}"
152
+ sleep(1)
153
+ retry
154
+ else
155
+ Rails.logger.error "❌ TCP FULL: connection failed after all retries: #{e.message}"
156
+ raise ConnectionError.new("Failed to connect to #{@ip}:#{@port}: #{e.message}")
157
+ end
158
+ end
159
+ end
160
+
161
+ def disconnect
162
+ if @socket && !@socket.closed?
163
+ Rails.logger.info "🔌 TCP FULL: closing connection"
164
+ @socket.close rescue nil
165
+ end
166
+ @socket = nil
167
+ end
168
+
169
+ def read_exactly(bytes)
170
+ Rails.logger.info "📥 TCP FULL: reading #{bytes} bytes..."
171
+
172
+ total_read = 0
173
+ result = ""
174
+
175
+ while total_read < bytes
176
+ remaining = bytes - total_read
177
+ Rails.logger.debug "📥 TCP FULL: need #{remaining} more bytes..."
178
+
179
+ # Use readpartial with timeout
180
+ begin
181
+ ready = IO.select([@socket], nil, nil, 30) # 30 second timeout per read
182
+ unless ready
183
+ Rails.logger.error "❌ TCP FULL: timeout waiting for #{remaining} bytes"
184
+ raise ConnectionError.new("Timeout reading #{remaining} bytes")
185
+ end
186
+
187
+ chunk = @socket.readpartial(remaining)
188
+ Rails.logger.debug "📥 TCP FULL: got #{chunk.length} bytes chunk: #{chunk.unpack('H*')[0]}"
189
+
190
+ if chunk.nil? || chunk.empty?
191
+ Rails.logger.error "❌ TCP FULL: socket closed or empty chunk"
192
+ raise ConnectionError.new("Socket closed while reading")
193
+ end
194
+
195
+ result += chunk
196
+ total_read += chunk.length
197
+ rescue EOFError => e
198
+ Rails.logger.error "❌ TCP FULL: EOF after #{total_read}/#{bytes} bytes"
199
+ raise ConnectionError.new("EOF while reading: #{e.message}")
200
+ rescue => e
201
+ Rails.logger.error "❌ TCP FULL: read error after #{total_read}/#{bytes} bytes: #{e.message}"
202
+ if total_read > 0
203
+ Rails.logger.info "📊 TCP FULL: partial data received (#{total_read} bytes): #{result.unpack('H*')[0]}"
204
+
205
+ # Check if partial data looks like a Telegram error response
206
+ if total_read == 4 && bytes == 8
207
+ # We got 4 bytes when expecting 8 - this might be an error packet
208
+ error_code = result.unpack('i<')[0]
209
+ Rails.logger.error "❌ TCP FULL: detected Telegram error response: #{error_code}"
210
+ if error_code < 0
211
+ case error_code
212
+ when -404
213
+ raise ConnectionError.new("Telegram server error: AUTH_KEY_UNREGISTERED (#{error_code})")
214
+ when -420
215
+ raise ConnectionError.new("Telegram server error: FLOOD_WAIT - rate limited (#{error_code})")
216
+ else
217
+ raise ConnectionError.new("Telegram server error: #{error_code}")
218
+ end
219
+ end
220
+ end
221
+ end
222
+ raise ConnectionError.new("Read error: #{e.message}")
223
+ end
224
+ end
225
+
226
+ Rails.logger.info "✅ TCP FULL: successfully read #{result.length} bytes: #{result.unpack('H*')[0]}"
227
+ Rails.logger.info "📡 OUR TCP RECV (#{result.length} bytes): #{result.unpack1('H*')}"
228
+ result
229
+ end
230
+
231
+ # Helper method for TL string encoding
232
+ def build_tl_string(str)
233
+ bytes = str.encode('UTF-8').force_encoding('ASCII-8BIT')
234
+ length = bytes.length
235
+
236
+ if length < 254
237
+ # Short string: length (1 byte) + data + padding
238
+ padding = (4 - ((length + 1) % 4)) % 4
239
+ [length].pack('C') + bytes + ("\x00" * padding)
240
+ else
241
+ # Long string: 254 (1 byte) + length (3 bytes) + data + padding
242
+ padding = (4 - (length % 4)) % 4
243
+ "\xfe" + [length].pack('L<')[0,3] + bytes + ("\x00" * padding)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'digest/sha1'
5
+ require_relative 'crypto_rsa_keys'
6
+
7
+ module Telegram
8
+ module Crypto
9
+ class SecurityError < StandardError; end
10
+
11
+ # Modular arithmetic helper
12
+ module ModularArithmetic
13
+ def self.pow(base, exponent, modulus)
14
+ base.to_bn.mod_exp(exponent, modulus).to_i
15
+ end
16
+ end
17
+
18
+ # AES IGE encryption/decryption EXACTLY like Telethon aes.py
19
+ def self.encrypt_ige(plain_text, key, iv)
20
+ # EXACT copy of Telethon lines 77-111
21
+ padding = plain_text.length % 16
22
+ plain_text += SecureRandom.random_bytes(16 - padding) if padding != 0
23
+
24
+ iv1 = iv[0, iv.length / 2]
25
+ iv2 = iv[iv.length / 2, iv.length / 2]
26
+
27
+ # Use OpenSSL AES ECB for block encryption
28
+ cipher = OpenSSL::Cipher.new('AES-256-ECB')
29
+ cipher.encrypt
30
+ cipher.key = key
31
+ cipher.padding = 0 # No padding, we handle blocks manually
32
+
33
+ cipher_text = []
34
+ blocks_count = plain_text.length / 16
35
+
36
+ (0...blocks_count).each do |block_index|
37
+ plain_text_block = plain_text[block_index * 16, 16].bytes
38
+
39
+ # XOR with iv1 (Telethon lines 98-99)
40
+ (0...16).each do |i|
41
+ plain_text_block[i] ^= iv1.bytes[i]
42
+ end
43
+
44
+ # Encrypt block
45
+ cipher_text_block = cipher.update(plain_text_block.pack('C*')).bytes
46
+
47
+ # XOR with iv2 (Telethon lines 103-104)
48
+ (0...16).each do |i|
49
+ cipher_text_block[i] ^= iv2.bytes[i]
50
+ end
51
+
52
+ # Update IVs (Telethon lines 106-107)
53
+ iv1 = cipher_text_block.pack('C*')
54
+ iv2 = plain_text[block_index * 16, 16]
55
+
56
+ cipher_text.concat(cipher_text_block)
57
+ end
58
+
59
+ cipher_text.pack('C*')
60
+ end
61
+
62
+ def self.decrypt_ige(cipher_text, key, iv)
63
+ # EXACT copy of Telethon lines 45-69
64
+ iv1 = iv[0, iv.length / 2]
65
+ iv2 = iv[iv.length / 2, iv.length / 2]
66
+
67
+ # Use OpenSSL AES ECB for block decryption
68
+ cipher = OpenSSL::Cipher.new('AES-256-ECB')
69
+ cipher.decrypt
70
+ cipher.key = key
71
+ cipher.padding = 0 # No padding, we handle blocks manually
72
+
73
+ plain_text = []
74
+ blocks_count = cipher_text.length / 16
75
+
76
+ (0...blocks_count).each do |block_index|
77
+ cipher_text_block = [0] * 16
78
+
79
+ # XOR with iv2 (Telethon lines 55-57)
80
+ (0...16).each do |i|
81
+ cipher_text_block[i] = cipher_text.bytes[(block_index * 16) + i] ^ iv2.bytes[i]
82
+ end
83
+
84
+ # Decrypt block
85
+ plain_text_block = cipher.update(cipher_text_block.pack('C*')).bytes
86
+
87
+ # XOR with iv1 (Telethon lines 61-62)
88
+ (0...16).each do |i|
89
+ plain_text_block[i] ^= iv1.bytes[i]
90
+ end
91
+
92
+ # Update IVs (Telethon lines 64-65)
93
+ iv1 = cipher_text[block_index * 16, 16]
94
+ iv2 = plain_text_block.pack('C*')
95
+
96
+ plain_text.concat(plain_text_block)
97
+ end
98
+
99
+ plain_text.pack('C*')
100
+ end
101
+
102
+ # Generate key and IV from nonces EXACTLY like Telethon helpers.generate_key_data_from_nonce
103
+ def self.generate_key_data_from_nonce(server_nonce_bytes, new_nonce_bytes_le)
104
+ hash1 = Digest::SHA1.digest(new_nonce_bytes_le + server_nonce_bytes)
105
+ hash2 = Digest::SHA1.digest(server_nonce_bytes + new_nonce_bytes_le)
106
+ hash3 = Digest::SHA1.digest(new_nonce_bytes_le + new_nonce_bytes_le)
107
+
108
+ key = hash1 + hash2[0, 12] # 32 bytes total
109
+ iv = hash2[12, 8] + hash3 + new_nonce_bytes_le[0, 4] # 32 bytes total
110
+
111
+ [key, iv]
112
+ end
113
+
114
+ # RSA encryption EXACTLY like Telethon
115
+ def self.rsa_encrypt_pq_inner_data(data, fingerprints)
116
+ # Load hardcoded Telegram RSA keys
117
+ telegram_rsa_keys = parse_telegram_rsa_keys
118
+
119
+ # Find key by fingerprint - try main keys first, then old keys
120
+ target_fingerprint = nil
121
+ rsa_key_data = nil
122
+
123
+ # First attempt: main keys (old=false)
124
+ telegram_rsa_keys.reject { |k| k[:old] }.each do |key_data|
125
+ next unless fingerprints.include?(key_data[:fingerprint])
126
+
127
+ target_fingerprint = key_data[:fingerprint]
128
+ rsa_key_data = key_data
129
+ Rails.logger.info "✅ Found matching MAIN RSA key for fingerprint: 0x#{target_fingerprint.to_s(16)}"
130
+ break
131
+ end
132
+
133
+ # Second attempt: old keys (old=true) like Telethon use_old=True
134
+ unless rsa_key_data
135
+ Rails.logger.info '🔄 No main key matched, trying old keys...'
136
+ telegram_rsa_keys.select { |k| k[:old] }.each do |key_data|
137
+ next unless fingerprints.include?(key_data[:fingerprint])
138
+
139
+ target_fingerprint = key_data[:fingerprint]
140
+ rsa_key_data = key_data
141
+ Rails.logger.info "✅ Found matching OLD RSA key for fingerprint: 0x#{target_fingerprint.to_s(16)}"
142
+ break
143
+ end
144
+ end
145
+
146
+ raise SecurityError.new('No matching RSA key found') unless rsa_key_data
147
+
148
+ # EXACT copy of Telethon RSA encryption format
149
+ # Format: sha1(data) + data + random_padding
150
+ sha1_hash = Digest::SHA1.digest(data)
151
+
152
+ # Calculate padding needed
153
+ total_needed = 255 # RSA modulus size - 1
154
+ current_length = sha1_hash.length + data.length # 20 + data_length
155
+ padding_needed = total_needed - current_length
156
+
157
+ raise SecurityError.new('Data too large for RSA encryption') if padding_needed < 0
158
+
159
+ random_padding = SecureRandom.random_bytes(padding_needed)
160
+ plaintext = sha1_hash + data + random_padding
161
+
162
+ Rails.logger.info "🔐 RSA encryption format: #{sha1_hash.length} (sha1) + #{data.length} (data) + #{random_padding.length} (padding) = #{plaintext.length} total"
163
+
164
+ # Manual RSA encryption EXACTLY like Telethon lines 78-82
165
+ # payload = int.from_bytes(to_encrypt, 'big')
166
+ plaintext_int = Serialization.bytes_to_int(plaintext, 'big', signed: false)
167
+ e = rsa_key_data[:e]
168
+ n = rsa_key_data[:n]
169
+
170
+ # encrypted = rsa.core.encrypt_int(payload, key.e, key.n)
171
+ encrypted_int = ModularArithmetic.pow(plaintext_int, e, n)
172
+
173
+ # block = encrypted.to_bytes(256, 'big')
174
+ encrypted_data = Serialization.integer_to_byte_array(encrypted_int, signed: false)
175
+ # Pad to exactly 256 bytes
176
+ if encrypted_data.length < 256
177
+ encrypted_data = ("\x00" * (256 - encrypted_data.length)) + encrypted_data
178
+ elsif encrypted_data.length > 256
179
+ encrypted_data = encrypted_data[-256..-1]
180
+ end
181
+
182
+ Rails.logger.info "🔒 Manual RSA encryption successful: #{encrypted_data.length} bytes"
183
+
184
+ {
185
+ encrypted_data: encrypted_data,
186
+ fingerprint: target_fingerprint
187
+ }
188
+ end
189
+
190
+ # Parse Telegram RSA keys EXACTLY like Telethon
191
+ def self.parse_telegram_rsa_keys
192
+ keys = []
193
+
194
+ # Сначала пробуем основные ключи
195
+ RSAKeys::ALL_KEYS.each_with_index do |pem, index|
196
+ # Parse PEM using OpenSSL
197
+ rsa_key = OpenSSL::PKey::RSA.new(pem)
198
+
199
+ # Extract n and e like Telethon
200
+ n = rsa_key.n.to_i
201
+ e = rsa_key.e.to_i
202
+
203
+ # Calculate fingerprint EXACTLY like Telethon
204
+ # Format: TL serialize n + TL serialize e (like Telethon line 44-45)
205
+ n_bytes = Telegram::Serialization.integer_to_byte_array(n, signed: false)
206
+ e_bytes = Telegram::Serialization.integer_to_byte_array(e, signed: false)
207
+
208
+ # TL serialize as bytes (like TLObject.serialize_bytes)
209
+ n_tl = Telegram::Serialization.pack_tl_string(n_bytes)
210
+ e_tl = Telegram::Serialization.pack_tl_string(e_bytes)
211
+
212
+ key_material = n_tl + e_tl
213
+ hash = Digest::SHA1.digest(key_material)
214
+
215
+ # Last 8 bytes as little-endian signed long
216
+ fingerprint = hash[-8..-1].unpack1('q<')
217
+
218
+ keys << {
219
+ index: index,
220
+ n: n,
221
+ e: e,
222
+ fingerprint: fingerprint,
223
+ old: index >= RSAKeys::MAIN_KEYS.length # Ключи после основных считаются старыми
224
+ }
225
+
226
+ Rails.logger.debug { "🔑 RSA key #{index}: fingerprint=0x#{fingerprint.to_s(16)}" }
227
+ rescue StandardError => e
228
+ Rails.logger.error "❌ Failed to parse RSA key #{index}: #{e.message}"
229
+ end
230
+
231
+ Rails.logger.info "🔑 Loaded #{keys.length} RSA keys total"
232
+ keys
233
+ end
234
+
235
+ def self.parse_telegram_rsa_keys_OLD
236
+ # ТОЧНЫЕ PEM ключи из Telethon rsa.py (поправлю формат)
237
+ telegram_rsa_pems = [
238
+ "-----BEGIN RSA PUBLIC KEY-----
239
+ MIIBCgKCAQEAruw2yP/BCcsJliRoW5eBVBVle9dtjJw+OYED160Wybum9SXtBBLX
240
+ riwt4rROd9csv0t0OHCaTmRqBcQ0J8fxhN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/
241
+ j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvdl84Kd9ORYjDEAyFnEA7dD556OptgLQQ2
242
+ e2kNqidcbrM7HFKqSBVOlEALkacJOBBkJcsRlG8D8Dg0yDVsZPP2oJWR8PgqHSKN
243
+ 9rBMAPDLuCIuHE7Qjeu1M4CwUF7oGI+nUMxjJG9mGdDg71d7B2C2Y2QpSvQEQHlG
244
+ U6bqlhYVLaIj3I+Z7/1h7WjJpLPdNhzOzwU2QkuKH0j+2C5Qu72mCwIDAQAB
245
+ -----END RSA PUBLIC KEY-----",
246
+ "-----BEGIN RSA PUBLIC KEY-----
247
+ MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
248
+ lyDONS789sVoD8ADPBK8E4PqOQ8b3lzZ1hMwVhS0v0U1pV3HQU9rwNJr2HFWGpNA
249
+ g7NaFxTBxR8FkK3KUKt4C4OfmNM+HbNrVdmBbmh9sQqM9dTZFdGdYJJOBSW8kFo8
250
+ 4F9O7qj2Y/z8N+DZbX30D8C0v1iw7qNP6LOhkSdqHBaOhQHgwVMrCB1dQV0Y2IYM
251
+ VzQr9uMgFdWbkV2zJ7GQ0M+qxFYGqUgKKOQa9qOSqUZ9nwWB3nrJ5wTkrWKvAAQF
252
+ kZgY7V+xQe8M7GQEd2XW2fzPcS+KqF5WAQIDAQAB
253
+ -----END RSA PUBLIC KEY-----"
254
+ ]
255
+
256
+ keys = []
257
+ telegram_rsa_pems.each_with_index do |pem, index|
258
+ # Parse PEM using OpenSSL
259
+ rsa_key = OpenSSL::PKey::RSA.new(pem)
260
+
261
+ # Extract n and e like Telethon
262
+ n = rsa_key.n.to_i
263
+ e = rsa_key.e.to_i
264
+
265
+ # Calculate fingerprint EXACTLY like Telethon
266
+ # Format: TL serialize n + TL serialize e (like Telethon line 44-45)
267
+ n_bytes = Telegram::Serialization.integer_to_byte_array(n, signed: false)
268
+ e_bytes = Telegram::Serialization.integer_to_byte_array(e, signed: false)
269
+
270
+ # TL serialize as bytes (like TLObject.serialize_bytes)
271
+ n_tl = Telegram::Serialization.pack_tl_string(n_bytes)
272
+ e_tl = Telegram::Serialization.pack_tl_string(e_bytes)
273
+
274
+ key_material = n_tl + e_tl
275
+ hash = Digest::SHA1.digest(key_material)
276
+
277
+ # Last 8 bytes as little-endian signed long
278
+ fingerprint = hash[-8..-1].unpack1('q<')
279
+
280
+ keys << {
281
+ index: index,
282
+ n: n,
283
+ e: e,
284
+ fingerprint: fingerprint
285
+ }
286
+ rescue StandardError => e
287
+ Rails.logger.error "❌ Failed to parse RSA key #{index}: #{e.message}"
288
+ end
289
+
290
+ keys
291
+ end
292
+
293
+ # Pollard's rho factorization EXACTLY like efficient algorithms
294
+ def self.factorize_pq(pq)
295
+ # Quick check for small factors
296
+ [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37].each do |p|
297
+ return [p, pq / p] if pq % p == 0
298
+ end
299
+
300
+ # Pollard's rho algorithm
301
+ x = 2
302
+ y = 2
303
+ d = 1
304
+
305
+ f = ->(n) { ((n * n) + 1) % pq }
306
+
307
+ while d == 1
308
+ x = f.call(x)
309
+ y = f.call(f.call(y))
310
+ d = gcd((x - y).abs, pq)
311
+ end
312
+
313
+ raise SecurityError.new('Factorization failed') if d == pq
314
+
315
+ [d, pq / d]
316
+ end
317
+
318
+ def self.gcd(a, b)
319
+ a, b = b, a % b while b != 0
320
+ a
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Актуальные RSA ключи Telegram ТОЧНО из Telethon rsa.py
4
+ module Telegram
5
+ module RSAKeys
6
+ # Основные ключи (old=False)
7
+ MAIN_KEYS = [
8
+ "-----BEGIN RSA PUBLIC KEY-----
9
+ MIIBCgKCAQEAruw2yP/BCcsJliRoW5eBVBVle9dtjJw+OYED160Wybum9SXtBBLX
10
+ riwt4rROd9csv0t0OHCaTmRqBcQ0J8fxhN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/
11
+ j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvdl84Kd9ORYjDEAyFnEA7dD556OptgLQQ2
12
+ e2iVNq8NZLYTzLp5YpOdO1doK+ttrltggTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnS
13
+ Lj16yE5HvJQn0CNpRdENvRUXe6tBP78O39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wF
14
+ XGF710w9lwCGNbmNxNYhtIkdqfsEcwR5JwIDAQAB
15
+ -----END RSA PUBLIC KEY-----",
16
+
17
+ "-----BEGIN RSA PUBLIC KEY-----
18
+ MIIBCgKCAQEAvfLHfYH2r9R70w8prHblWt/nDkh+XkgpflqQVcnAfSuTtO05lNPs
19
+ pQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOOKPi0OfJXoRVylFzAQG/j83u5K3kRLbae
20
+ 7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ3TDS2pQOCtovG4eDl9wacrXOJTG2990V
21
+ jgnIKNA0UMoP+KF03qzryqIt3oTvZq03DyWdGK+AZjgBLaDKSnC6qD2cFY81UryR
22
+ WOab8zKkWAnhw2kFpcqhI0jdV5QaSCExvnsjVaX0Y1N0870931/5Jb9ICe4nweZ9
23
+ kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV/wIDAQAB
24
+ -----END RSA PUBLIC KEY-----",
25
+
26
+ "-----BEGIN RSA PUBLIC KEY-----
27
+ MIIBCgKCAQEAs/ditzm+mPND6xkhzwFIz6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGr
28
+ zqTDHkO30R8VeRM/Kz2f4nR05GIFiITl4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+
29
+ th6knSU0yLtNKuQVP6voMrnt9MV1X92LGZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvS
30
+ Uwwc+yi1/gGaybwlzZwqXYoPOhwMebzKUk0xW14htcJrRrq+PXXQbRzTMynseCoP
31
+ Ioke0dtCodbA3qQxQovE16q9zz4Otv2k4j63cz53J+mhkVWAeWxVGI0lltJmWtEY
32
+ K6er8VqqWot3nqmWMXogrgRLggv/NbbooQIDAQAB
33
+ -----END RSA PUBLIC KEY-----",
34
+
35
+ "-----BEGIN RSA PUBLIC KEY-----
36
+ MIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q05shjg8/4p6047bn6/m8yPy1RBsvIyvuD
37
+ uGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xbnfxL5BXHplJhMtADXKM9bWB11PU1Eioc
38
+ 3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvi
39
+ fRLJbY08/Gp66KpQvy7g8w7VB8wlgePexW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqe
40
+ Pji9NP3tJUFQjcECqcm0yV7/2d0t/pbCm+ZH1sadZspQCEPPrtbkQBlvHb4OLiIW
41
+ PGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6MAQIDAQAB
42
+ -----END RSA PUBLIC KEY-----"
43
+ ].freeze
44
+
45
+ # Старые ключи (old=True) для fallback
46
+ OLD_KEYS = [
47
+ "-----BEGIN RSA PUBLIC KEY-----
48
+ MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
49
+ lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
50
+ an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
51
+ Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
52
+ 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
53
+ Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
54
+ -----END RSA PUBLIC KEY-----",
55
+
56
+ "-----BEGIN RSA PUBLIC KEY-----
57
+ MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt
58
+ ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru
59
+ vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L
60
+ xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi
61
+ XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp
62
+ NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB
63
+ -----END RSA PUBLIC KEY-----",
64
+
65
+ "-----BEGIN RSA PUBLIC KEY-----
66
+ MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+
67
+ DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB
68
+ 1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s
69
+ g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z
70
+ hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f
71
+ x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB
72
+ -----END RSA PUBLIC KEY-----",
73
+
74
+ "-----BEGIN RSA PUBLIC KEY-----
75
+ MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa
76
+ xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i
77
+ qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc
78
+ /n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks
79
+ WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t
80
+ UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB
81
+ -----END RSA PUBLIC KEY-----"
82
+ ].freeze
83
+
84
+ ALL_KEYS = (MAIN_KEYS + OLD_KEYS).freeze
85
+ end
86
+ end