mtproto 0.0.3 → 0.0.5

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +13 -1
  3. data/ext/aes_ige/Makefile +273 -0
  4. data/ext/aes_ige/aes_ige.bundle +0 -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.bundle +0 -0
  10. data/ext/factorization/factorization.c +62 -0
  11. data/lib/mtproto/auth_key_generator.rb +228 -0
  12. data/lib/mtproto/client.rb +235 -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 +66 -0
  20. data/lib/mtproto/crypto/rsa_pad.rb +59 -0
  21. data/lib/mtproto/encrypted_message.rb +86 -0
  22. data/lib/mtproto/session.rb +20 -0
  23. data/lib/mtproto/tl/bad_msg_notification.rb +46 -0
  24. data/lib/mtproto/tl/client_dh_inner_data.rb +29 -0
  25. data/lib/mtproto/tl/config.rb +122 -0
  26. data/lib/mtproto/tl/gzip_packed.rb +41 -0
  27. data/lib/mtproto/tl/message.rb +246 -0
  28. data/lib/mtproto/tl/msg_container.rb +40 -0
  29. data/lib/mtproto/tl/new_session_created.rb +30 -0
  30. data/lib/mtproto/tl/p_q_inner_data.rb +41 -0
  31. data/lib/mtproto/tl/rpc_error.rb +34 -0
  32. data/lib/mtproto/tl/serializer.rb +55 -0
  33. data/lib/mtproto/tl/server_dh_inner_data.rb +85 -0
  34. data/lib/mtproto/version.rb +1 -1
  35. data/lib/mtproto.rb +23 -0
  36. data/tmp/.keep +0 -0
  37. metadata +33 -1
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'digest'
5
+ require_relative 'transport/tcp_connection'
6
+ require_relative 'transport/abridged_packet_codec'
7
+ require_relative 'tl/message'
8
+
9
+ module MTProto
10
+ class Client
11
+ DC_ADDRESSES = {
12
+ 1 => ['149.154.175.50', 443],
13
+ 2 => ['149.154.167.51', 443],
14
+ 3 => ['149.154.175.100', 443],
15
+ 4 => ['149.154.167.91', 443],
16
+ 5 => ['91.108.56.130', 443]
17
+ }.freeze
18
+
19
+ attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session
20
+
21
+ def initialize(dc_id: 2)
22
+ host, port = DC_ADDRESSES[dc_id]
23
+ raise ArgumentError, "Unknown DC ID: #{dc_id}" unless host
24
+
25
+ codec = Transport::AbridgedPacketCodec.new
26
+ @connection = Transport::TCPConnection.new(host, port, codec)
27
+ @server_key = nil
28
+ @auth_key = nil
29
+ @server_salt = nil
30
+ @time_offset = 0
31
+ @session = nil
32
+ @connection_initialized = false
33
+ end
34
+
35
+ def connect
36
+ @connection.connect
37
+ end
38
+
39
+ def disconnect
40
+ @connection.close
41
+ end
42
+
43
+ def req_pq_multi
44
+ nonce = SecureRandom.random_bytes(16)
45
+
46
+ message = TL::Message.req_pq_multi(nonce)
47
+ payload = message.serialize
48
+
49
+ @connection.send(payload)
50
+
51
+ response_data = @connection.recv(timeout: 10)
52
+ response_message = TL::Message.deserialize(response_data)
53
+
54
+ res_pq = response_message.parse_res_pq
55
+
56
+ raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
57
+
58
+ res_pq
59
+ end
60
+
61
+ def make_auth_key
62
+ generator = AuthKeyGenerator.new(@connection)
63
+ result = generator.generate
64
+
65
+ @auth_key = generator.auth_key
66
+ @server_salt = generator.server_salt
67
+ @time_offset = generator.time_offset
68
+ @session = Session.new
69
+
70
+ result
71
+ end
72
+
73
+ def generate_msg_id
74
+ time = Time.now.to_f + @time_offset
75
+ msg_id = (time * (2**32)).to_i
76
+ (msg_id / 4) * 4
77
+ end
78
+
79
+ def rpc_call(body, content_related: true)
80
+ raise 'Auth key not generated' unless @auth_key
81
+ raise 'Session not initialized' unless @session
82
+
83
+ msg_id = generate_msg_id
84
+ seq_no = @session.next_seq_no(content_related: content_related)
85
+
86
+ encrypted_msg = EncryptedMessage.encrypt(
87
+ auth_key: @auth_key,
88
+ server_salt: @server_salt,
89
+ session_id: @session.session_id,
90
+ msg_id: msg_id,
91
+ seq_no: seq_no,
92
+ body: body
93
+ )
94
+
95
+ @connection.send(encrypted_msg.serialize)
96
+
97
+ response_data = @connection.recv(timeout: 10)
98
+
99
+ decrypted = EncryptedMessage.decrypt(
100
+ auth_key: @auth_key,
101
+ encrypted_message_data: response_data,
102
+ sender: :server
103
+ )
104
+
105
+ response_body = decrypted[:body]
106
+
107
+ constructor = response_body[0, 4].unpack1('L<')
108
+ puts " [RPC] First response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
109
+
110
+ if constructor == TL::NewSessionCreated::CONSTRUCTOR
111
+ puts " [RPC] Got new_session_created, waiting for actual response..."
112
+ session_info = TL::NewSessionCreated.deserialize(response_body)
113
+ @server_salt = session_info.server_salt
114
+ puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
115
+
116
+ response_data = @connection.recv(timeout: 10)
117
+ puts " [RPC] Second response received (#{response_data.bytesize} bytes encrypted)"
118
+ decrypted = EncryptedMessage.decrypt(
119
+ auth_key: @auth_key,
120
+ encrypted_message_data: response_data,
121
+ sender: :server
122
+ )
123
+ response_body = decrypted[:body]
124
+ constructor = response_body[0, 4].unpack1('L<')
125
+ puts " [RPC] Second response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
126
+ end
127
+
128
+ if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
129
+ puts " [RPC] Response is a container, unpacking..."
130
+ container = TL::MsgContainer.deserialize(response_body)
131
+ puts " [RPC] Container has #{container.messages.size} messages"
132
+
133
+ container.messages.each_with_index do |msg, i|
134
+ msg_constructor = msg[:body][0, 4].unpack1('L<')
135
+ puts " [RPC] Message #{i}: constructor=0x#{msg_constructor.to_s(16)}, size=#{msg[:body].bytesize}"
136
+ end
137
+
138
+ rpc_result = container.messages.find do |msg|
139
+ msg[:body][0, 4].unpack1('L<') == 0xf35c6d01
140
+ end
141
+
142
+ if rpc_result
143
+ puts " [RPC] Found rpc_result in container, extracting..."
144
+ return rpc_result[:body][12..]
145
+ end
146
+
147
+ new_session = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR }
148
+ if new_session
149
+ puts " [RPC] Container has new_session_created, updating salt and waiting for RPC result..."
150
+ session_info = TL::NewSessionCreated.deserialize(new_session[:body])
151
+ @server_salt = session_info.server_salt
152
+ puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
153
+
154
+ response_data = @connection.recv(timeout: 10)
155
+ puts " [RPC] Next response received (#{response_data.bytesize} bytes encrypted)"
156
+ decrypted = EncryptedMessage.decrypt(
157
+ auth_key: @auth_key,
158
+ encrypted_message_data: response_data,
159
+ sender: :server
160
+ )
161
+ response_body = decrypted[:body]
162
+ constructor = response_body[0, 4].unpack1('L<')
163
+ puts " [RPC] Next response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
164
+
165
+ if constructor == 0xf35c6d01
166
+ puts " [RPC] Next response is rpc_result, extracting payload..."
167
+ return response_body[12..]
168
+ end
169
+
170
+ if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
171
+ puts " [RPC] Next response is also a container, looking for rpc_result..."
172
+ container = TL::MsgContainer.deserialize(response_body)
173
+ rpc_result = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == 0xf35c6d01 }
174
+ return rpc_result[:body][12..] if rpc_result
175
+ end
176
+
177
+ return response_body
178
+ end
179
+
180
+ puts " [RPC] No rpc_result found, returning first message body"
181
+ return container.messages.first[:body]
182
+ end
183
+
184
+ if constructor == 0xf35c6d01
185
+ puts " [RPC] Response is rpc_result, extracting payload..."
186
+ return response_body[12..]
187
+ end
188
+
189
+ puts " [RPC] Returning raw response body (constructor: 0x#{constructor.to_s(16)})"
190
+ response_body
191
+ end
192
+
193
+ def invoke_with_layer(layer, query)
194
+ body = TL::Serializer.serialize_int(0xda9b0d0d)
195
+ body += TL::Serializer.serialize_int(layer)
196
+ body += query
197
+ body
198
+ end
199
+
200
+ def init_connection(api_id, device_model, system_version, app_version, system_lang_code, lang_code, query)
201
+ body = TL::Serializer.serialize_int(0xc1cd5ea9)
202
+ flags = 0
203
+ body += TL::Serializer.serialize_int(flags)
204
+ body += TL::Serializer.serialize_int(api_id)
205
+ body += TL::Serializer.serialize_string(device_model)
206
+ body += TL::Serializer.serialize_string(system_version)
207
+ body += TL::Serializer.serialize_string(app_version)
208
+ body += TL::Serializer.serialize_string(system_lang_code)
209
+ body += TL::Serializer.serialize_string('')
210
+ body += TL::Serializer.serialize_string(lang_code)
211
+ body += query
212
+ body
213
+ end
214
+
215
+ def help_get_config
216
+ query = [0xc4f9186b].pack('L<')
217
+
218
+ unless @connection_initialized
219
+ query = init_connection(
220
+ 123456,
221
+ 'Ruby MTProto',
222
+ 'Ruby 3.x',
223
+ '0.1.0',
224
+ 'en',
225
+ 'en',
226
+ query
227
+ )
228
+ query = invoke_with_layer(214, query)
229
+ @connection_initialized = true
230
+ end
231
+
232
+ rpc_call(query)
233
+ end
234
+ end
235
+ 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
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'digest'
5
+
6
+ module MTProto
7
+ module Crypto
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
+ attr_reader :key, :fingerprint
21
+
22
+ def initialize(pem_string)
23
+ @key = OpenSSL::PKey::RSA.new(pem_string)
24
+ @fingerprint = calculate_fingerprint
25
+ end
26
+
27
+ def self.telegram_key
28
+ @telegram_key ||= new(TELEGRAM_KEY)
29
+ end
30
+
31
+ def self.find_by_fingerprint(fingerprints)
32
+ telegram_key if fingerprints.include?(telegram_key.fingerprint)
33
+ end
34
+
35
+ private
36
+
37
+ def calculate_fingerprint
38
+ n_bytes = @key.n.to_s(2).unpack1('B*').scan(/.{8}/).map { |b| b.to_i(2) }.pack('C*')
39
+ e_bytes = @key.e.to_s(2).unpack1('B*').scan(/.{8}/).map { |b| b.to_i(2) }.pack('C*')
40
+
41
+ n_tl = serialize_bytes(n_bytes)
42
+ e_tl = serialize_bytes(e_bytes)
43
+
44
+ data = n_tl + e_tl
45
+ sha1 = Digest::SHA1.digest(data)
46
+
47
+ sha1[-8..].unpack1('Q<')
48
+ end
49
+
50
+ def serialize_bytes(bytes)
51
+ length = bytes.bytesize
52
+
53
+ if length <= 253
54
+ [length].pack('C') + bytes + padding(length + 1)
55
+ else
56
+ [254].pack('C') + [length].pack('L<')[0, 3] + bytes + padding(length + 4)
57
+ end
58
+ end
59
+
60
+ def padding(current_length)
61
+ pad_length = (4 - (current_length % 4)) % 4
62
+ "\x00" * pad_length
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'digest'
5
+ require 'openssl'
6
+
7
+ module MTProto
8
+ module Crypto
9
+ module RSA_PAD
10
+ module_function
11
+
12
+ def encrypt(data, rsa_key)
13
+ raise ArgumentError, 'Data too large' if data.bytesize > 144
14
+
15
+ loop do
16
+ result = perform_encryption(data)
17
+ result_bn = OpenSSL::BN.new(result, 2)
18
+
19
+ return rsa_key.key.public_encrypt(result, OpenSSL::PKey::RSA::NO_PADDING) if result_bn < rsa_key.key.n
20
+
21
+ # If result >= modulus, retry with new random data
22
+ end
23
+ end
24
+
25
+ def perform_encryption(data)
26
+ data_with_padding = pad_to_192(data)
27
+ data_reversed = data_with_padding.reverse
28
+ temp_key = SecureRandom.random_bytes(32)
29
+ data_hash = Digest::SHA256.digest(temp_key + data_with_padding)
30
+ to_encrypt = data_reversed + data_hash
31
+
32
+ aes_key = temp_key
33
+ aes_iv = "\x00" * 32
34
+ encrypted = AES_IGE.encrypt_ige(to_encrypt, aes_key, aes_iv)
35
+
36
+ encrypted_hash = Digest::SHA256.digest(encrypted)
37
+ temp_key_xor = xor_bytes(temp_key, encrypted_hash)
38
+
39
+ temp_key_xor + encrypted
40
+ end
41
+
42
+ def pad_to_192(data)
43
+ padding_length = 192 - data.bytesize
44
+ raise ArgumentError, 'Data too large for RSA_PAD' if padding_length < 0
45
+
46
+ data + SecureRandom.random_bytes(padding_length)
47
+ end
48
+
49
+ def xor_bytes(bytes1, bytes2)
50
+ length = [bytes1.bytesize, bytes2.bytesize].min
51
+ result = String.new(capacity: length)
52
+ length.times do |i|
53
+ result << (bytes1.getbyte(i) ^ bytes2.getbyte(i))
54
+ end
55
+ result
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'securerandom'
5
+
6
+ module MTProto
7
+ class EncryptedMessage
8
+ attr_reader :auth_key_id, :msg_key, :encrypted_data
9
+
10
+ def initialize(auth_key_id:, msg_key:, encrypted_data:)
11
+ @auth_key_id = auth_key_id
12
+ @msg_key = msg_key
13
+ @encrypted_data = encrypted_data
14
+ end
15
+
16
+ def self.encrypt(auth_key:, server_salt:, session_id:, msg_id:, seq_no:, body:)
17
+ salt_bytes = [server_salt].pack('Q<')
18
+ session_id_bytes = [session_id].pack('Q<')
19
+ msg_id_bytes = [msg_id].pack('Q<')
20
+ seq_no_bytes = [seq_no].pack('L<')
21
+ body_length_bytes = [body.bytesize].pack('L<')
22
+
23
+ plaintext = salt_bytes + session_id_bytes + msg_id_bytes + seq_no_bytes + body_length_bytes + body
24
+
25
+ padding_length = (16 - (plaintext.bytesize % 16)) % 16
26
+ padding_length += 16 if padding_length < 12
27
+ plaintext += SecureRandom.random_bytes(padding_length)
28
+
29
+ msg_key = Crypto::MessageKey.generate_msg_key(auth_key, plaintext, sender: :client)
30
+
31
+ keys = Crypto::MessageKey.derive_aes_key_iv(auth_key, msg_key, sender: :client)
32
+
33
+ encrypted_data = Crypto::AES_IGE.encrypt_ige(plaintext, keys[:aes_key], keys[:aes_iv])
34
+
35
+ auth_key_id = Digest::SHA1.digest(auth_key)[-8..].unpack1('Q<')
36
+
37
+ new(auth_key_id: auth_key_id, msg_key: msg_key, encrypted_data: encrypted_data)
38
+ end
39
+
40
+ def self.decrypt(auth_key:, encrypted_message_data:, sender: :server)
41
+ auth_key_id = encrypted_message_data[0, 8].unpack1('Q<')
42
+ msg_key = encrypted_message_data[8, 16]
43
+ encrypted_data = encrypted_message_data[24..]
44
+
45
+ keys = Crypto::MessageKey.derive_aes_key_iv(auth_key, msg_key, sender: sender)
46
+
47
+ plaintext = Crypto::AES_IGE.decrypt_ige(encrypted_data, keys[:aes_key], keys[:aes_iv])
48
+
49
+ expected_msg_key = Crypto::MessageKey.generate_msg_key(auth_key, plaintext, sender: sender)
50
+ raise 'msg_key mismatch!' unless msg_key == expected_msg_key
51
+
52
+ offset = 0
53
+ server_salt = plaintext[offset, 8].unpack1('Q<')
54
+ offset += 8
55
+
56
+ session_id = plaintext[offset, 8].unpack1('Q<')
57
+ offset += 8
58
+
59
+ msg_id = plaintext[offset, 8].unpack1('Q<')
60
+ offset += 8
61
+
62
+ seq_no = plaintext[offset, 4].unpack1('L<')
63
+ offset += 4
64
+
65
+ body_length = plaintext[offset, 4].unpack1('L<')
66
+ offset += 4
67
+
68
+ body = plaintext[offset, body_length]
69
+
70
+ {
71
+ auth_key_id: auth_key_id,
72
+ msg_key: msg_key,
73
+ server_salt: server_salt,
74
+ session_id: session_id,
75
+ msg_id: msg_id,
76
+ seq_no: seq_no,
77
+ body: body
78
+ }
79
+ end
80
+
81
+ def serialize
82
+ auth_key_id_bytes = [@auth_key_id].pack('Q<')
83
+ auth_key_id_bytes + @msg_key + @encrypted_data
84
+ end
85
+ end
86
+ end