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.
- checksums.yaml +4 -4
- data/Rakefile +13 -1
- data/ext/aes_ige/Makefile +273 -0
- data/ext/aes_ige/aes_ige.bundle +0 -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.bundle +0 -0
- data/ext/factorization/factorization.c +62 -0
- data/lib/mtproto/auth_key_generator.rb +228 -0
- data/lib/mtproto/client.rb +182 -5
- 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_pad.rb +59 -0
- data/lib/mtproto/encrypted_message.rb +86 -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/config.rb +122 -0
- data/lib/mtproto/tl/gzip_packed.rb +41 -0
- data/lib/mtproto/tl/message.rb +142 -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/serializer.rb +55 -0
- data/lib/mtproto/tl/server_dh_inner_data.rb +85 -0
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +20 -0
- data/tmp/.keep +0 -0
- metadata +30 -1
data/lib/mtproto/client.rb
CHANGED
|
@@ -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
|