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,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
|