mtproto 0.0.4 → 0.0.6

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +5 -0
  3. data/Rakefile +26 -1
  4. data/ext/aes_ige/Makefile +273 -0
  5. data/ext/aes_ige/aes_ige.c +103 -0
  6. data/ext/aes_ige/extconf.rb +25 -0
  7. data/ext/factorization/Makefile +273 -0
  8. data/ext/factorization/extconf.rb +3 -0
  9. data/ext/factorization/factorization.c +62 -0
  10. data/lib/mtproto/auth_key_generator.rb +241 -0
  11. data/lib/mtproto/client.rb +217 -20
  12. data/lib/mtproto/connection.rb +103 -0
  13. data/lib/mtproto/crypto/aes_ige.rb +23 -0
  14. data/lib/mtproto/crypto/auth_key_helper.rb +25 -0
  15. data/lib/mtproto/crypto/dh_key_exchange.rb +44 -0
  16. data/lib/mtproto/crypto/dh_validator.rb +80 -0
  17. data/lib/mtproto/crypto/factorization.rb +39 -0
  18. data/lib/mtproto/crypto/message_key.rb +32 -0
  19. data/lib/mtproto/crypto/rsa_key.rb +9 -15
  20. data/lib/mtproto/crypto/rsa_pad.rb +59 -0
  21. data/lib/mtproto/encrypted_message.rb +86 -0
  22. data/lib/mtproto/errors.rb +33 -0
  23. data/lib/mtproto/session.rb +20 -0
  24. data/lib/mtproto/tl/bad_msg_notification.rb +46 -0
  25. data/lib/mtproto/tl/client_dh_inner_data.rb +29 -0
  26. data/lib/mtproto/tl/code_settings.rb +25 -0
  27. data/lib/mtproto/tl/config.rb +124 -0
  28. data/lib/mtproto/tl/gzip_packed.rb +41 -0
  29. data/lib/mtproto/tl/message.rb +148 -2
  30. data/lib/mtproto/tl/msg_container.rb +40 -0
  31. data/lib/mtproto/tl/new_session_created.rb +30 -0
  32. data/lib/mtproto/tl/p_q_inner_data.rb +41 -0
  33. data/lib/mtproto/tl/rpc_error.rb +34 -0
  34. data/lib/mtproto/tl/sent_code.rb +128 -0
  35. data/lib/mtproto/tl/serializer.rb +55 -0
  36. data/lib/mtproto/tl/server_dh_inner_data.rb +85 -0
  37. data/lib/mtproto/transport/tcp_connection.rb +1 -1
  38. data/lib/mtproto/version.rb +1 -1
  39. data/lib/mtproto.rb +24 -0
  40. data/tmp/.keep +0 -0
  41. metadata +33 -1
@@ -1,36 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'digest'
4
5
  require_relative 'transport/tcp_connection'
5
6
  require_relative 'transport/abridged_packet_codec'
6
7
  require_relative 'tl/message'
7
8
 
8
9
  module MTProto
9
10
  class Client
10
- DC_ADDRESSES = {
11
- 1 => ['149.154.175.50', 443],
12
- 2 => ['149.154.167.51', 443],
13
- 3 => ['149.154.175.100', 443],
14
- 4 => ['149.154.167.91', 443],
15
- 5 => ['91.108.56.130', 443]
16
- }.freeze
11
+ attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session, :timeout
12
+ attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
17
13
 
18
- attr_reader :connection
14
+ CONSTRUCTOR_MSGS_ACK = 0x62d6b459
19
15
 
20
- def initialize(dc_id: 2)
21
- host, port = DC_ADDRESSES[dc_id]
22
- raise ArgumentError, "Unknown DC ID: #{dc_id}" unless host
16
+ def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key: nil, dc_number: nil, test_mode: false, timeout: 10)
17
+ raise ArgumentError, "host is required" if host.nil? || host.empty?
18
+ raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
23
19
 
24
20
  codec = Transport::AbridgedPacketCodec.new
25
21
  @connection = Transport::TCPConnection.new(host, port, codec)
22
+ @public_key = public_key
23
+ @dc_number = dc_number
24
+ @test_mode = test_mode
25
+ @timeout = timeout
26
+ @server_key = nil
27
+ @auth_key = nil
28
+ @server_salt = nil
29
+ @time_offset = 0
30
+ @session = nil
31
+ @connection_initialized = false
32
+
33
+ # Client configuration defaults
34
+ @api_id = api_id || 0
35
+ @api_hash = api_hash || ''
36
+ @device_model = 'Ruby MTProto'
37
+ @system_version = RUBY_DESCRIPTION
38
+ @app_version = '0.1.0'
39
+ @system_lang_code = 'en'
40
+ @lang_pack = ''
41
+ @lang_code = 'en'
26
42
  end
27
43
 
28
- def connect
29
- @connection.connect
44
+ def connect!
45
+ @connection.connect!
30
46
  end
31
47
 
32
- def disconnect
33
- @connection.close
48
+ def disconnect!
49
+ @connection.close if @connection
34
50
  end
35
51
 
36
52
  def req_pq_multi
@@ -41,18 +57,199 @@ module MTProto
41
57
 
42
58
  @connection.send(payload)
43
59
 
44
- response_data = @connection.recv(timeout: 10)
60
+ response_data = @connection.recv(timeout: @timeout)
45
61
  response_message = TL::Message.deserialize(response_data)
46
62
 
47
63
  res_pq = response_message.parse_res_pq
48
64
 
49
65
  raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
50
66
 
51
- rsa_key = Crypto::RSAKey.find_by_fingerprint(res_pq[:fingerprints])
52
- raise 'No matching RSA key found!' unless rsa_key
53
-
54
- res_pq[:rsa_key] = rsa_key
55
67
  res_pq
56
68
  end
69
+
70
+ def make_auth_key
71
+ raise ArgumentError, "public_key is required for auth key generation" if @public_key.nil? || @public_key.empty?
72
+
73
+ generator = AuthKeyGenerator.new(@connection, @public_key, @dc_number, test_mode: @test_mode, timeout: @timeout)
74
+ result = generator.generate
75
+
76
+ @auth_key = generator.auth_key
77
+ @server_salt = generator.server_salt
78
+ @time_offset = generator.time_offset
79
+ @session = Session.new
80
+
81
+ result
82
+ end
83
+
84
+ def generate_msg_id
85
+ time = Time.now.to_f + @time_offset
86
+ msg_id = (time * (2**32)).to_i
87
+ (msg_id / 4) * 4
88
+ end
89
+
90
+ def rpc_call(body, content_related: true)
91
+ raise 'Auth key not generated' unless @auth_key
92
+ raise 'Session not initialized' unless @session
93
+
94
+ msg_id = generate_msg_id
95
+ seq_no = @session.next_seq_no(content_related: content_related)
96
+
97
+ encrypted_msg = EncryptedMessage.encrypt(
98
+ auth_key: @auth_key,
99
+ server_salt: @server_salt,
100
+ session_id: @session.session_id,
101
+ msg_id: msg_id,
102
+ seq_no: seq_no,
103
+ body: body
104
+ )
105
+
106
+ @connection.send(encrypted_msg.serialize)
107
+
108
+ response_data = @connection.recv(timeout: @timeout)
109
+
110
+ decrypted = EncryptedMessage.decrypt(
111
+ auth_key: @auth_key,
112
+ encrypted_message_data: response_data,
113
+ sender: :server
114
+ )
115
+
116
+ response_body = decrypted[:body]
117
+
118
+ constructor = response_body[0, 4].unpack1('L<')
119
+
120
+ if constructor == TL::NewSessionCreated::CONSTRUCTOR
121
+ session_info = TL::NewSessionCreated.deserialize(response_body)
122
+ @server_salt = session_info.server_salt
123
+
124
+ response_data = @connection.recv(timeout: @timeout)
125
+ decrypted = EncryptedMessage.decrypt(
126
+ auth_key: @auth_key,
127
+ encrypted_message_data: response_data,
128
+ sender: :server
129
+ )
130
+ response_body = decrypted[:body]
131
+ constructor = response_body[0, 4].unpack1('L<')
132
+ end
133
+
134
+ if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
135
+ container = TL::MsgContainer.deserialize(response_body)
136
+
137
+ rpc_result = container.messages.find do |msg|
138
+ msg[:body][0, 4].unpack1('L<') == 0xf35c6d01
139
+ end
140
+
141
+ return rpc_result[:body][12..] if rpc_result
142
+
143
+ new_session = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR }
144
+ if new_session
145
+ session_info = TL::NewSessionCreated.deserialize(new_session[:body])
146
+ @server_salt = session_info.server_salt
147
+
148
+ other_messages = container.messages.reject do |msg|
149
+ constructor = msg[:body][0, 4].unpack1('L<')
150
+ constructor == TL::NewSessionCreated::CONSTRUCTOR || constructor == CONSTRUCTOR_MSGS_ACK
151
+ end
152
+
153
+ if !other_messages.empty?
154
+ return other_messages.first[:body]
155
+ end
156
+
157
+ response_data = @connection.recv(timeout: @timeout)
158
+ decrypted = EncryptedMessage.decrypt(
159
+ auth_key: @auth_key,
160
+ encrypted_message_data: response_data,
161
+ sender: :server
162
+ )
163
+ response_body = decrypted[:body]
164
+ constructor = response_body[0, 4].unpack1('L<')
165
+
166
+ return response_body[12..] if constructor == 0xf35c6d01
167
+
168
+ if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
169
+ container = TL::MsgContainer.deserialize(response_body)
170
+ rpc_result = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == 0xf35c6d01 }
171
+ return rpc_result[:body][12..] if rpc_result
172
+ end
173
+
174
+ return response_body
175
+ end
176
+
177
+ return container.messages.first[:body]
178
+ end
179
+
180
+ return response_body[12..] if constructor == 0xf35c6d01
181
+
182
+ response_body
183
+ end
184
+
185
+ def invoke_with_layer(layer, query)
186
+ body = TL::Serializer.serialize_int(0xda9b0d0d)
187
+ body += TL::Serializer.serialize_int(layer)
188
+ body += query
189
+ body
190
+ end
191
+
192
+ def init_connection(api_id:, device_model:, system_version:, app_version:, system_lang_code:, lang_pack:, lang_code:, query:)
193
+ body = TL::Serializer.serialize_int(0xc1cd5ea9)
194
+ flags = 0
195
+ body += TL::Serializer.serialize_int(flags)
196
+ body += TL::Serializer.serialize_int(api_id)
197
+ body += TL::Serializer.serialize_string(device_model)
198
+ body += TL::Serializer.serialize_string(system_version)
199
+ body += TL::Serializer.serialize_string(app_version)
200
+ body += TL::Serializer.serialize_string(system_lang_code)
201
+ body += TL::Serializer.serialize_string(lang_pack)
202
+ body += TL::Serializer.serialize_string(lang_code)
203
+ body += query
204
+ body
205
+ end
206
+
207
+ def help_get_config
208
+ query = [0xc4f9186b].pack('L<')
209
+
210
+ unless @connection_initialized
211
+ query = init_connection(
212
+ api_id: @api_id,
213
+ device_model: @device_model,
214
+ system_version: @system_version,
215
+ app_version: @app_version,
216
+ system_lang_code: @system_lang_code,
217
+ lang_pack: @lang_pack,
218
+ lang_code: @lang_code,
219
+ query: query
220
+ )
221
+ query = invoke_with_layer(214, query)
222
+ @connection_initialized = true
223
+ end
224
+
225
+ rpc_call(query)
226
+ end
227
+
228
+ def auth_send_code(phone_number, code_settings: {})
229
+ raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
230
+
231
+ query = [0xa677244f].pack('L<')
232
+ query += TL::Serializer.serialize_string(phone_number)
233
+ query += TL::Serializer.serialize_int(@api_id)
234
+ query += TL::Serializer.serialize_string(@api_hash)
235
+ query += TL::CodeSettings.serialize(code_settings)
236
+
237
+ unless @connection_initialized
238
+ query = init_connection(
239
+ api_id: @api_id,
240
+ device_model: @device_model,
241
+ system_version: @system_version,
242
+ app_version: @app_version,
243
+ system_lang_code: @system_lang_code,
244
+ lang_pack: @lang_pack,
245
+ lang_code: @lang_code,
246
+ query: query
247
+ )
248
+ query = invoke_with_layer(214, query)
249
+ @connection_initialized = true
250
+ end
251
+
252
+ rpc_call(query)
253
+ end
57
254
  end
58
255
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ class Connection
5
+ attr_reader :client
6
+
7
+ def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key:, dc_number:, test_mode: false, timeout: 10)
8
+ @client = Client.new(
9
+ api_id: api_id,
10
+ api_hash: api_hash,
11
+ host: host,
12
+ port: port,
13
+ public_key: public_key,
14
+ dc_number: dc_number,
15
+ test_mode: test_mode,
16
+ timeout: timeout
17
+ )
18
+ @connected = false
19
+ end
20
+
21
+ def connect!
22
+ return if @connected
23
+
24
+ @client.connect!
25
+ @client.make_auth_key
26
+ @connected = true
27
+ end
28
+
29
+ def disconnect!
30
+ @client.disconnect! if @connected
31
+ @connected = false
32
+ end
33
+
34
+ def ping(ping_id = nil)
35
+ raise NotConnectedError unless @connected
36
+
37
+ ping_id ||= rand(2**63)
38
+ body = TL::Message.ping(ping_id)
39
+
40
+ response_body = @client.rpc_call(body)
41
+ message = TL::Message.new(body: response_body)
42
+ pong = message.parse_pong
43
+
44
+ raise PingMismatchError unless pong[:ping_id] == ping_id
45
+
46
+ true
47
+ end
48
+
49
+ def get_config
50
+ raise NotConnectedError unless @connected
51
+
52
+ response = @client.help_get_config
53
+
54
+ # Handle gzip compression
55
+ constructor = response[0, 4].unpack1('L<')
56
+ if constructor == TL::GzipPacked::CONSTRUCTOR
57
+ response = TL::GzipPacked.unpack(response)
58
+ constructor = response[0, 4].unpack1('L<')
59
+ end
60
+
61
+ # Handle RPC errors
62
+ if constructor == TL::RpcError::CONSTRUCTOR
63
+ error = TL::RpcError.deserialize(response)
64
+ raise MTProto::RpcError.new(error.error_code, error.error_message)
65
+ end
66
+
67
+ # Parse and return config
68
+ if constructor == TL::Config::CONSTRUCTOR || constructor == TL::Config::CONSTRUCTOR_ALT
69
+ TL::Config.deserialize(response)
70
+ else
71
+ raise UnexpectedConstructorError.new(constructor)
72
+ end
73
+ end
74
+
75
+ def send_code(phone_number, code_settings: {})
76
+ raise NotConnectedError unless @connected
77
+ raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
78
+
79
+ response = @client.auth_send_code(phone_number, code_settings: code_settings)
80
+
81
+ # Handle gzip compression
82
+ constructor = response[0, 4].unpack1('L<')
83
+ if constructor == TL::GzipPacked::CONSTRUCTOR
84
+ response = TL::GzipPacked.unpack(response)
85
+ constructor = response[0, 4].unpack1('L<')
86
+ end
87
+
88
+ # Handle RPC errors
89
+ if constructor == TL::RpcError::CONSTRUCTOR
90
+ error = TL::RpcError.deserialize(response)
91
+ raise MTProto::RpcError.new(error.error_code, error.error_message)
92
+ end
93
+
94
+ # Parse and return sent code
95
+ if constructor == TL::SentCode::CONSTRUCTOR
96
+ sent_code = TL::SentCode.deserialize(response)
97
+ sent_code.to_h
98
+ else
99
+ raise UnexpectedConstructorError.new(constructor)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'aes_ige/aes_ige'
5
+ rescue LoadError => e
6
+ warn "Failed to load AES-IGE C extension: #{e.message}"
7
+ warn 'Run: cd ext/aes_ige && ruby extconf.rb && make'
8
+ warn 'AES-IGE encryption/decryption will not be available'
9
+
10
+ module MTProto
11
+ module Crypto
12
+ module AES_IGE
13
+ def self.encrypt_ige(_plaintext, _key, _iv)
14
+ raise NotImplementedError, 'AES-IGE not available (C extension not loaded)'
15
+ end
16
+
17
+ def self.decrypt_ige(_ciphertext, _key, _iv)
18
+ raise NotImplementedError, 'AES-IGE not available (C extension not loaded)'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module MTProto
6
+ module Crypto
7
+ module AuthKeyHelper
8
+ module_function
9
+
10
+ def derive_tmp_aes_key(new_nonce, server_nonce)
11
+ sha1_a = Digest::SHA1.digest(new_nonce + server_nonce)
12
+ sha1_b = Digest::SHA1.digest(server_nonce + new_nonce)
13
+
14
+ sha1_a + sha1_b[0, 12]
15
+ end
16
+
17
+ def derive_tmp_aes_iv(new_nonce, server_nonce)
18
+ sha1_b = Digest::SHA1.digest(server_nonce + new_nonce)
19
+ sha1_c = Digest::SHA1.digest(new_nonce + new_nonce)
20
+
21
+ sha1_b[12, 8] + sha1_c + new_nonce[0, 4]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module MTProto
7
+ module Crypto
8
+ module DHKeyExchange
9
+ module_function
10
+
11
+ def generate_client_dh_params(g, dh_prime_bytes)
12
+ dh_prime = OpenSSL::BN.new(dh_prime_bytes, 2)
13
+ g_bn = OpenSSL::BN.new(g)
14
+
15
+ b = generate_random_2048_bit_number
16
+ g_b = g_bn.mod_exp(b, dh_prime)
17
+
18
+ DHValidator.validate_g_a(g_b, dh_prime)
19
+
20
+ {
21
+ b: b,
22
+ g_b: g_b,
23
+ g_b_bytes: g_b.to_s(2)
24
+ }
25
+ end
26
+
27
+ def compute_auth_key(g_a_bytes, b, dh_prime_bytes)
28
+ g_a = OpenSSL::BN.new(g_a_bytes, 2)
29
+ dh_prime = OpenSSL::BN.new(dh_prime_bytes, 2)
30
+
31
+ auth_key_bn = g_a.mod_exp(b, dh_prime)
32
+ auth_key_bn.to_s(2)
33
+ end
34
+
35
+ def generate_random_2048_bit_number
36
+ random_bytes = SecureRandom.random_bytes(256)
37
+ random_bytes[0] = (random_bytes[0].ord | 0x80).chr
38
+
39
+ bn = OpenSSL::BN.new(random_bytes, 2)
40
+ bn
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module MTProto
6
+ module Crypto
7
+ module DHValidator
8
+ module_function
9
+
10
+ def validate_dh_params(g, dh_prime_bytes, g_a_bytes)
11
+ dh_prime = OpenSSL::BN.new(dh_prime_bytes, 2)
12
+ g_a = OpenSSL::BN.new(g_a_bytes, 2)
13
+
14
+ validate_g(g, dh_prime)
15
+ validate_dh_prime(dh_prime)
16
+ validate_g_a(g_a, dh_prime)
17
+
18
+ true
19
+ end
20
+
21
+ def validate_g(g, dh_prime)
22
+ raise 'Invalid g: must be 2, 3, 4, 5, 6, or 7' unless [2, 3, 4, 5, 6, 7].include?(g)
23
+
24
+ p_mod = case g
25
+ when 2
26
+ dh_prime % 8 == 7
27
+ when 3
28
+ dh_prime % 3 == 2
29
+ when 4
30
+ true
31
+ when 5
32
+ mod5 = dh_prime % 5
33
+ mod5 == 1 || mod5 == 4
34
+ when 6
35
+ mod24 = dh_prime % 24
36
+ mod24 == 19 || mod24 == 23
37
+ when 7
38
+ mod7 = dh_prime % 7
39
+ [3, 5, 6].include?(mod7)
40
+ end
41
+
42
+ raise "g=#{g} is not a valid generator for this prime" unless p_mod
43
+
44
+ true
45
+ end
46
+
47
+ def validate_dh_prime(dh_prime)
48
+ bit_length = dh_prime.num_bits
49
+
50
+ raise 'dh_prime must be 2048 bits' unless bit_length == 2048
51
+
52
+ min_prime = OpenSSL::BN.new(2)**2047
53
+ max_prime = OpenSSL::BN.new(2)**2048
54
+
55
+ if dh_prime <= min_prime || dh_prime >= max_prime
56
+ raise 'dh_prime out of range (must be 2^2047 < p < 2^2048)'
57
+ end
58
+
59
+ true
60
+ end
61
+
62
+ def validate_g_a(g_a, dh_prime)
63
+ one = OpenSSL::BN.new(1)
64
+ dh_prime_minus_one = dh_prime - one
65
+
66
+ raise 'g_a must be > 1' if g_a <= one
67
+ raise 'g_a must be < dh_prime - 1' if g_a >= dh_prime_minus_one
68
+
69
+ safety_range_min = OpenSSL::BN.new(2)**1984
70
+ safety_range_max = dh_prime - safety_range_min
71
+
72
+ if g_a < safety_range_min || g_a > safety_range_max
73
+ raise 'g_a outside safety range (2^1984 to dh_prime - 2^1984)'
74
+ end
75
+
76
+ true
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'factorization/factorization'
5
+ rescue LoadError => e
6
+ warn "Failed to load Factorization C extension: #{e.message}"
7
+ warn 'Run: cd ext/factorization && ruby extconf.rb && make'
8
+ raise
9
+ end
10
+
11
+ module MTProto
12
+ module Crypto
13
+ module Factorization
14
+ module_function
15
+
16
+ def factorize_pq(pq_bytes)
17
+ FactorizationExt.factorize_pq(pq_bytes)
18
+ end
19
+
20
+ def bytes_to_integer(bytes)
21
+ bytes.unpack('C*').inject(0) { |sum, byte| (sum << 8) | byte }
22
+ end
23
+
24
+ def integer_sqrt(n)
25
+ return n if n < 2
26
+
27
+ x = n
28
+ y = (x + 1) / 2
29
+
30
+ while y < x
31
+ x = y
32
+ y = (x + n / x) / 2
33
+ end
34
+
35
+ x
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module MTProto
6
+ module Crypto
7
+ module MessageKey
8
+ module_function
9
+
10
+ def generate_msg_key(auth_key, plaintext, sender: :client)
11
+ x = sender == :client ? 0 : 8
12
+ auth_key_part = auth_key[88 + x, 32]
13
+
14
+ sha256_a = Digest::SHA256.digest(auth_key_part + plaintext)
15
+
16
+ sha256_a[8, 16]
17
+ end
18
+
19
+ def derive_aes_key_iv(auth_key, msg_key, sender: :client)
20
+ x = sender == :client ? 0 : 8
21
+
22
+ sha256_a = Digest::SHA256.digest(msg_key + auth_key[x, 36])
23
+ sha256_b = Digest::SHA256.digest(auth_key[40 + x, 36] + msg_key)
24
+
25
+ aes_key = sha256_a[0, 8] + sha256_b[8, 16] + sha256_a[24, 8]
26
+ aes_iv = sha256_b[0, 8] + sha256_a[8, 16] + sha256_b[24, 8]
27
+
28
+ { aes_key: aes_key, aes_iv: aes_iv }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -6,30 +6,24 @@ require 'digest'
6
6
  module MTProto
7
7
  module Crypto
8
8
  class RSAKey
9
- TELEGRAM_KEY = <<~PEM
10
- -----BEGIN RSA PUBLIC KEY-----
11
- MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
12
- 5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
13
- 62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
14
- +aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
15
- t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
16
- 5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
17
- -----END RSA PUBLIC KEY-----
18
- PEM
19
-
20
9
  attr_reader :key, :fingerprint
21
10
 
22
11
  def initialize(pem_string)
12
+ raise ArgumentError, "pem_string is required" if pem_string.nil? || pem_string.empty?
13
+
23
14
  @key = OpenSSL::PKey::RSA.new(pem_string)
24
15
  @fingerprint = calculate_fingerprint
25
16
  end
26
17
 
27
- def self.telegram_key
28
- @telegram_key ||= new(TELEGRAM_KEY)
18
+ def self.from_pem(pem_string)
19
+ new(pem_string)
29
20
  end
30
21
 
31
- def self.find_by_fingerprint(fingerprints)
32
- telegram_key if fingerprints.include?(telegram_key.fingerprint)
22
+ def self.find_by_fingerprint(fingerprints, public_key)
23
+ raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
24
+
25
+ key = new(public_key)
26
+ key if fingerprints.include?(key.fingerprint)
33
27
  end
34
28
 
35
29
  private