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
@@ -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
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ class Error < StandardError; end
5
+
6
+ class NotConnectedError < Error
7
+ def initialize(msg = 'Not connected. Call connect first.')
8
+ super
9
+ end
10
+ end
11
+
12
+ class PingMismatchError < Error
13
+ def initialize(msg = 'Ping ID mismatch')
14
+ super
15
+ end
16
+ end
17
+
18
+ class RpcError < Error
19
+ attr_reader :error_code, :error_message
20
+
21
+ def initialize(error_code, error_message)
22
+ @error_code = error_code
23
+ @error_message = error_message
24
+ super("RPC Error #{error_code}: #{error_message}")
25
+ end
26
+ end
27
+
28
+ class UnexpectedConstructorError < Error
29
+ def initialize(constructor)
30
+ super("Unexpected constructor: 0x#{constructor.to_s(16)}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module MTProto
6
+ class Session
7
+ attr_reader :session_id, :seq_no
8
+
9
+ def initialize
10
+ @session_id = SecureRandom.random_bytes(8).unpack1('Q<')
11
+ @seq_no = 0
12
+ end
13
+
14
+ def next_seq_no(content_related: true)
15
+ current = @seq_no
16
+ @seq_no += content_related ? 1 : 0
17
+ current * 2 + (content_related ? 1 : 0)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class BadMsgNotification
6
+ CONSTRUCTOR = 0xa7eff811
7
+
8
+ attr_reader :bad_msg_id, :bad_msg_seqno, :error_code
9
+
10
+ def self.deserialize(data)
11
+ offset = 4
12
+ bad_msg_id = data[offset, 8].unpack1('Q<')
13
+ offset += 8
14
+
15
+ bad_msg_seqno = data[offset, 4].unpack1('L<')
16
+ offset += 4
17
+
18
+ error_code = data[offset, 4].unpack1('L<')
19
+
20
+ new(bad_msg_id: bad_msg_id, bad_msg_seqno: bad_msg_seqno, error_code: error_code)
21
+ end
22
+
23
+ def initialize(bad_msg_id:, bad_msg_seqno:, error_code:)
24
+ @bad_msg_id = bad_msg_id
25
+ @bad_msg_seqno = bad_msg_seqno
26
+ @error_code = error_code
27
+ end
28
+
29
+ def error_message
30
+ case @error_code
31
+ when 16 then "msg_id too low (client time is wrong)"
32
+ when 17 then "msg_id too high (client time needs sync)"
33
+ when 18 then "incorrect two lower order msg_id bits (must be divisible by 4)"
34
+ when 19 then "container msg_id is the same as previously received"
35
+ when 20 then "message too old"
36
+ when 32 then "msg_seqno too low"
37
+ when 33 then "msg_seqno too high"
38
+ when 34 then "even msg_seqno expected but odd received"
39
+ when 35 then "odd msg_seqno expected but even received"
40
+ when 48 then "incorrect server salt"
41
+ else "unknown error code #{@error_code}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'serializer'
4
+
5
+ module MTProto
6
+ module TL
7
+ class ClientDHInnerData
8
+ CONSTRUCTOR = 0x6643b654
9
+
10
+ attr_reader :nonce, :server_nonce, :retry_id, :g_b
11
+
12
+ def initialize(nonce:, server_nonce:, retry_id:, g_b:)
13
+ @nonce = nonce
14
+ @server_nonce = server_nonce
15
+ @retry_id = retry_id
16
+ @g_b = g_b
17
+ end
18
+
19
+ def serialize
20
+ data = Serializer.serialize_int(CONSTRUCTOR)
21
+ data += @nonce
22
+ data += @server_nonce
23
+ data += Serializer.serialize_long(@retry_id)
24
+ data += Serializer.serialize_bytes(@g_b)
25
+ data
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class CodeSettings
6
+ CONSTRUCTOR = 0xad253d78
7
+
8
+ def self.serialize(settings = {})
9
+ body = Serializer.serialize_int(CONSTRUCTOR)
10
+
11
+ flags = 0
12
+ flags |= (1 << 0) if settings[:allow_flashcall]
13
+ flags |= (1 << 1) if settings[:current_number]
14
+ flags |= (1 << 4) if settings[:allow_app_hash]
15
+ flags |= (1 << 5) if settings[:allow_missed_call]
16
+ flags |= (1 << 7) if settings[:allow_firebase]
17
+ flags |= (1 << 9) if settings[:unknown_number]
18
+
19
+ body += Serializer.serialize_int(flags)
20
+
21
+ body
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class Config
6
+ CONSTRUCTOR = 0xcc1a241e
7
+ CONSTRUCTOR_ALT = 0x3072cfa1
8
+
9
+ attr_reader :flags, :date, :expires, :test_mode, :this_dc, :dc_options
10
+
11
+ def self.deserialize(data)
12
+ offset = 4
13
+
14
+ flags = data[offset, 4].unpack1('L<')
15
+ offset += 4
16
+
17
+ date = data[offset, 4].unpack1('L<')
18
+ offset += 4
19
+
20
+ expires = data[offset, 4].unpack1('L<')
21
+ offset += 4
22
+
23
+ test_mode = (data[offset, 4].unpack1('L<') == 0x997275b5)
24
+ offset += 4
25
+
26
+ this_dc = data[offset, 4].unpack1('L<')
27
+ offset += 4
28
+
29
+ dc_options_constructor = data[offset, 4].unpack1('L<')
30
+ offset += 4
31
+
32
+ raise "Expected vector constructor 0x1cb5c415, got 0x#{dc_options_constructor.to_s(16)}" unless dc_options_constructor == 0x1cb5c415
33
+
34
+ dc_options_count = data[offset, 4].unpack1('L<')
35
+ offset += 4
36
+
37
+ dc_options = []
38
+ dc_options_count.times do
39
+ dc_option, bytes_read = DcOption.deserialize_from(data[offset..])
40
+ dc_options << dc_option
41
+ offset += bytes_read
42
+ end
43
+
44
+ new(
45
+ flags: flags,
46
+ date: date,
47
+ expires: expires,
48
+ test_mode: test_mode,
49
+ this_dc: this_dc,
50
+ dc_options: dc_options
51
+ )
52
+ end
53
+
54
+ def initialize(flags:, date:, expires:, test_mode:, this_dc:, dc_options:)
55
+ @flags = flags
56
+ @date = date
57
+ @expires = expires
58
+ @test_mode = test_mode
59
+ @this_dc = this_dc
60
+ @dc_options = dc_options
61
+ end
62
+ end
63
+
64
+ class DcOption
65
+ attr_reader :id, :ip_address, :port, :flags
66
+
67
+ def self.deserialize_from(data)
68
+ offset = 0
69
+
70
+ constructor = data[offset, 4].unpack1('L<')
71
+ offset += 4
72
+
73
+ raise "Expected dcOption constructor 0x18b7a10d, got 0x#{constructor.to_s(16)}" unless constructor == 0x18b7a10d
74
+
75
+ flags = data[offset, 4].unpack1('L<')
76
+ offset += 4
77
+
78
+ id = data[offset, 4].unpack1('L<')
79
+ offset += 4
80
+
81
+ ip_length = data[offset].ord
82
+ offset += 1
83
+
84
+ ip_address = data[offset, ip_length]
85
+ offset += ip_length
86
+
87
+ padding = (4 - ((ip_length + 1) % 4)) % 4
88
+ offset += padding
89
+
90
+ port = data[offset, 4].unpack1('L<')
91
+ offset += 4
92
+
93
+ [new(id: id, ip_address: ip_address, port: port, flags: flags), offset]
94
+ end
95
+
96
+ def initialize(id:, ip_address:, port:, flags:)
97
+ @id = id
98
+ @ip_address = ip_address
99
+ @port = port
100
+ @flags = flags
101
+ end
102
+
103
+ def ipv6?
104
+ (@flags & 1) != 0
105
+ end
106
+
107
+ def media_only?
108
+ (@flags & 2) != 0
109
+ end
110
+
111
+ def tcpo_only?
112
+ (@flags & 4) != 0
113
+ end
114
+
115
+ def cdn?
116
+ (@flags & 8) != 0
117
+ end
118
+
119
+ def static?
120
+ (@flags & 16) != 0
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+ require 'stringio'
5
+
6
+ module MTProto
7
+ module TL
8
+ module GzipPacked
9
+ CONSTRUCTOR = 0x3072cfa1
10
+
11
+ module_function
12
+
13
+ def unpack(data)
14
+ offset = 4
15
+
16
+ length_byte = data[offset].ord
17
+ offset += 1
18
+ puts " [GZIP] Length byte: #{length_byte} (0x#{length_byte.to_s(16)})" if $DEBUG
19
+
20
+ if length_byte == 254
21
+ length_bytes = data[offset, 3]
22
+ length = (length_bytes + "\x00").unpack1('L<')
23
+ offset += 3
24
+ puts " [GZIP] Extended length: #{length} bytes" if $DEBUG
25
+ elsif length_byte == 255
26
+ raise "Invalid TL string length: 255"
27
+ else
28
+ length = length_byte
29
+ puts " [GZIP] Short length: #{length} bytes" if $DEBUG
30
+ end
31
+
32
+ raise "Invalid length: #{length.inspect}" unless length.is_a?(Integer) && length > 0
33
+
34
+ compressed_data = data[offset, length]
35
+ raise "Not enough data: expected #{length}, got #{compressed_data&.bytesize}" if compressed_data.nil? || compressed_data.bytesize < length
36
+
37
+ Zlib::GzipReader.new(StringIO.new(compressed_data)).read.force_encoding(Encoding::BINARY)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,10 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'serializer'
4
+
3
5
  module MTProto
4
6
  module TL
5
7
  class Message
6
8
  CONSTRUCTOR_REQ_PQ_MULTI = 0xbe7e8ef1
7
9
  CONSTRUCTOR_RES_PQ = 0x05162463
10
+ CONSTRUCTOR_REQ_DH_PARAMS = 0xd712e4be
11
+ CONSTRUCTOR_SERVER_DH_PARAMS_OK = 0xd0e8075c
12
+ CONSTRUCTOR_SET_CLIENT_DH_PARAMS = 0xf5045f1f
13
+ CONSTRUCTOR_DH_GEN_OK = 0x3bcbf734
14
+ CONSTRUCTOR_DH_GEN_RETRY = 0x46dc1fb9
15
+ CONSTRUCTOR_DH_GEN_FAIL = 0xa69dae02
16
+ CONSTRUCTOR_PING = 0x7abe77ec
17
+ CONSTRUCTOR_PONG = 0x347773c5
18
+ CONSTRUCTOR_BAD_MSG_NOTIFICATION = 0xa7eff811
19
+ CONSTRUCTOR_MSG_CONTAINER = 0x73f1f8dc
8
20
 
9
21
  attr_reader :auth_key_id, :msg_id, :body
10
22
 
@@ -31,7 +43,43 @@ module MTProto
31
43
  new(auth_key_id: 0, body: body)
32
44
  end
33
45
 
46
+ def self.req_DH_params(nonce:, server_nonce:, p:, q:, public_key_fingerprint:, encrypted_data:)
47
+ raise ArgumentError, 'Nonce must be 16 bytes' unless nonce.bytesize == 16
48
+ raise ArgumentError, 'Server nonce must be 16 bytes' unless server_nonce.bytesize == 16
49
+
50
+ p_bytes = Serializer.integer_to_bytes(p)
51
+ q_bytes = Serializer.integer_to_bytes(q)
52
+
53
+ body = Serializer.serialize_int(CONSTRUCTOR_REQ_DH_PARAMS)
54
+ body += nonce
55
+ body += server_nonce
56
+ body += Serializer.serialize_bytes(p_bytes)
57
+ body += Serializer.serialize_bytes(q_bytes)
58
+ body += Serializer.serialize_long(public_key_fingerprint)
59
+ body += Serializer.serialize_bytes(encrypted_data)
60
+
61
+ new(auth_key_id: 0, body: body)
62
+ end
63
+
64
+ def self.set_client_DH_params(nonce:, server_nonce:, encrypted_data:)
65
+ raise ArgumentError, 'Nonce must be 16 bytes' unless nonce.bytesize == 16
66
+ raise ArgumentError, 'Server nonce must be 16 bytes' unless server_nonce.bytesize == 16
67
+
68
+ body = Serializer.serialize_int(CONSTRUCTOR_SET_CLIENT_DH_PARAMS)
69
+ body += nonce
70
+ body += server_nonce
71
+ body += Serializer.serialize_bytes(encrypted_data)
72
+
73
+ new(auth_key_id: 0, body: body)
74
+ end
75
+
34
76
  def self.deserialize(data)
77
+ if data.bytesize < 20
78
+ raise(ArgumentError,
79
+ "Invalid MTProto message: expected at least 20 bytes, got #{data.bytesize} bytes (hex: #{data.unpack1('H*')})",
80
+ )
81
+ end
82
+
35
83
  auth_key_id = data[0, 8].unpack1('Q<')
36
84
  msg_id = data[8, 8].unpack1('Q<')
37
85
  body_length = data[16, 4].unpack1('L<')
@@ -87,11 +135,109 @@ module MTProto
87
135
  }
88
136
  end
89
137
 
138
+ def parse_server_DH_params_ok
139
+ constructor = @body[0, 4].unpack1('L<')
140
+ raise "Unexpected constructor: 0x#{constructor.to_s(16)}" unless constructor == CONSTRUCTOR_SERVER_DH_PARAMS_OK
141
+
142
+ offset = 4
143
+
144
+ nonce = @body[offset, 16]
145
+ offset += 16
146
+
147
+ server_nonce = @body[offset, 16]
148
+ offset += 16
149
+
150
+ length_byte = @body[offset].ord
151
+ offset += 1
152
+
153
+ if length_byte == 254
154
+ length_bytes = @body[offset, 3].bytes
155
+ encrypted_answer_length = length_bytes[0] | (length_bytes[1] << 8) | (length_bytes[2] << 16)
156
+ offset += 3
157
+ else
158
+ encrypted_answer_length = length_byte
159
+ end
160
+
161
+ encrypted_answer = @body[offset, encrypted_answer_length]
162
+
163
+ {
164
+ nonce: nonce,
165
+ server_nonce: server_nonce,
166
+ encrypted_answer: encrypted_answer
167
+ }
168
+ end
169
+
170
+ def parse_dh_gen_response
171
+ constructor = @body[0, 4].unpack1('L<')
172
+
173
+ offset = 4
174
+ nonce = @body[offset, 16]
175
+ offset += 16
176
+
177
+ server_nonce = @body[offset, 16]
178
+ offset += 16
179
+
180
+ new_nonce_hash = @body[offset, 16]
181
+
182
+ case constructor
183
+ when CONSTRUCTOR_DH_GEN_OK
184
+ { status: :ok, nonce: nonce, server_nonce: server_nonce, new_nonce_hash: new_nonce_hash }
185
+ when CONSTRUCTOR_DH_GEN_RETRY
186
+ { status: :retry, nonce: nonce, server_nonce: server_nonce, new_nonce_hash: new_nonce_hash }
187
+ when CONSTRUCTOR_DH_GEN_FAIL
188
+ { status: :fail, nonce: nonce, server_nonce: server_nonce, new_nonce_hash: new_nonce_hash }
189
+ else
190
+ raise "Unexpected constructor: 0x#{constructor.to_s(16)}"
191
+ end
192
+ end
193
+
194
+ def self.ping(ping_id)
195
+ body = Serializer.serialize_int(CONSTRUCTOR_PING)
196
+ body += [ping_id].pack('Q<')
197
+
198
+ body
199
+ end
200
+
201
+ def parse_pong
202
+ constructor = @body[0, 4].unpack1('L<')
203
+
204
+ if constructor == CONSTRUCTOR_BAD_MSG_NOTIFICATION
205
+ bad_msg = TL::BadMsgNotification.deserialize(@body)
206
+ raise "Bad message notification: #{bad_msg.error_message} (code: #{bad_msg.error_code}, msg_id: #{bad_msg.bad_msg_id}, seqno: #{bad_msg.bad_msg_seqno})"
207
+ end
208
+
209
+ if constructor == CONSTRUCTOR_MSG_CONTAINER
210
+ container = TL::MsgContainer.deserialize(@body)
211
+ pong_message = container.messages.find do |msg|
212
+ msg[:body][0, 4].unpack1('L<') == CONSTRUCTOR_PONG
213
+ end
214
+
215
+ raise 'No pong message found in container' unless pong_message
216
+
217
+ offset = 4
218
+ msg_id = pong_message[:body][offset, 8].unpack1('Q<')
219
+ offset += 8
220
+ ping_id = pong_message[:body][offset, 8].unpack1('Q<')
221
+
222
+ return { msg_id: msg_id, ping_id: ping_id }
223
+ end
224
+
225
+ raise "Unexpected constructor: 0x#{constructor.to_s(16)}" unless constructor == CONSTRUCTOR_PONG
226
+
227
+ offset = 4
228
+ msg_id = @body[offset, 8].unpack1('Q<')
229
+ offset += 8
230
+ ping_id = @body[offset, 8].unpack1('Q<')
231
+
232
+ { msg_id: msg_id, ping_id: ping_id }
233
+ end
234
+
90
235
  private
91
236
 
92
237
  def generate_msg_id
93
- time_ns = (Time.now.to_f * 1_000_000_000).to_i
94
- (time_ns / 4) * 4
238
+ time = Time.now.to_f
239
+ msg_id = (time * (2**32)).to_i
240
+ (msg_id / 4) * 4
95
241
  end
96
242
 
97
243
  def padding_length(length)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class MsgContainer
6
+ CONSTRUCTOR = 0x73f1f8dc
7
+
8
+ attr_reader :messages
9
+
10
+ def self.deserialize(data)
11
+ offset = 4
12
+ message_count = data[offset, 4].unpack1('L<')
13
+ offset += 4
14
+
15
+ messages = []
16
+ message_count.times do
17
+ msg_id = data[offset, 8].unpack1('Q<')
18
+ offset += 8
19
+
20
+ seqno = data[offset, 4].unpack1('L<')
21
+ offset += 4
22
+
23
+ bytes = data[offset, 4].unpack1('L<')
24
+ offset += 4
25
+
26
+ body = data[offset, bytes]
27
+ offset += bytes
28
+
29
+ messages << { msg_id: msg_id, seqno: seqno, body: body }
30
+ end
31
+
32
+ new(messages: messages)
33
+ end
34
+
35
+ def initialize(messages:)
36
+ @messages = messages
37
+ end
38
+ end
39
+ end
40
+ end