mtproto 0.0.4 → 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.
@@ -1,6 +1,7 @@
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'
@@ -15,7 +16,7 @@ module MTProto
15
16
  5 => ['91.108.56.130', 443]
16
17
  }.freeze
17
18
 
18
- attr_reader :connection
19
+ attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session
19
20
 
20
21
  def initialize(dc_id: 2)
21
22
  host, port = DC_ADDRESSES[dc_id]
@@ -23,6 +24,12 @@ module MTProto
23
24
 
24
25
  codec = Transport::AbridgedPacketCodec.new
25
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
26
33
  end
27
34
 
28
35
  def connect
@@ -48,11 +55,181 @@ module MTProto
48
55
 
49
56
  raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
50
57
 
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
58
  res_pq
56
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
57
234
  end
58
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,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,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