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.
- checksums.yaml +4 -4
- data/.env.example +5 -0
- data/Rakefile +26 -1
- data/ext/aes_ige/Makefile +273 -0
- data/ext/aes_ige/aes_ige.c +103 -0
- data/ext/aes_ige/extconf.rb +25 -0
- data/ext/factorization/Makefile +273 -0
- data/ext/factorization/extconf.rb +3 -0
- data/ext/factorization/factorization.c +62 -0
- data/lib/mtproto/auth_key_generator.rb +241 -0
- data/lib/mtproto/client.rb +217 -20
- data/lib/mtproto/connection.rb +103 -0
- data/lib/mtproto/crypto/aes_ige.rb +23 -0
- data/lib/mtproto/crypto/auth_key_helper.rb +25 -0
- data/lib/mtproto/crypto/dh_key_exchange.rb +44 -0
- data/lib/mtproto/crypto/dh_validator.rb +80 -0
- data/lib/mtproto/crypto/factorization.rb +39 -0
- data/lib/mtproto/crypto/message_key.rb +32 -0
- data/lib/mtproto/crypto/rsa_key.rb +9 -15
- data/lib/mtproto/crypto/rsa_pad.rb +59 -0
- data/lib/mtproto/encrypted_message.rb +86 -0
- data/lib/mtproto/errors.rb +33 -0
- data/lib/mtproto/session.rb +20 -0
- data/lib/mtproto/tl/bad_msg_notification.rb +46 -0
- data/lib/mtproto/tl/client_dh_inner_data.rb +29 -0
- data/lib/mtproto/tl/code_settings.rb +25 -0
- data/lib/mtproto/tl/config.rb +124 -0
- data/lib/mtproto/tl/gzip_packed.rb +41 -0
- data/lib/mtproto/tl/message.rb +148 -2
- data/lib/mtproto/tl/msg_container.rb +40 -0
- data/lib/mtproto/tl/new_session_created.rb +30 -0
- data/lib/mtproto/tl/p_q_inner_data.rb +41 -0
- data/lib/mtproto/tl/rpc_error.rb +34 -0
- data/lib/mtproto/tl/sent_code.rb +128 -0
- data/lib/mtproto/tl/serializer.rb +55 -0
- data/lib/mtproto/tl/server_dh_inner_data.rb +85 -0
- data/lib/mtproto/transport/tcp_connection.rb +1 -1
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +24 -0
- data/tmp/.keep +0 -0
- 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
|
data/lib/mtproto/tl/message.rb
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
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
|