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
data/lib/mtproto/client.rb
CHANGED
|
@@ -1,36 +1,52 @@
|
|
|
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'
|
|
7
8
|
|
|
8
9
|
module MTProto
|
|
9
10
|
class Client
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
2 => ['149.154.167.51', 443],
|
|
13
|
-
3 => ['149.154.175.100', 443],
|
|
14
|
-
4 => ['149.154.167.91', 443],
|
|
15
|
-
5 => ['91.108.56.130', 443]
|
|
16
|
-
}.freeze
|
|
11
|
+
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session, :timeout
|
|
12
|
+
attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
CONSTRUCTOR_MSGS_ACK = 0x62d6b459
|
|
19
15
|
|
|
20
|
-
def initialize(
|
|
21
|
-
|
|
22
|
-
raise ArgumentError, "
|
|
16
|
+
def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key: nil, dc_number: nil, test_mode: false, timeout: 10)
|
|
17
|
+
raise ArgumentError, "host is required" if host.nil? || host.empty?
|
|
18
|
+
raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
|
|
23
19
|
|
|
24
20
|
codec = Transport::AbridgedPacketCodec.new
|
|
25
21
|
@connection = Transport::TCPConnection.new(host, port, codec)
|
|
22
|
+
@public_key = public_key
|
|
23
|
+
@dc_number = dc_number
|
|
24
|
+
@test_mode = test_mode
|
|
25
|
+
@timeout = timeout
|
|
26
|
+
@server_key = nil
|
|
27
|
+
@auth_key = nil
|
|
28
|
+
@server_salt = nil
|
|
29
|
+
@time_offset = 0
|
|
30
|
+
@session = nil
|
|
31
|
+
@connection_initialized = false
|
|
32
|
+
|
|
33
|
+
# Client configuration defaults
|
|
34
|
+
@api_id = api_id || 0
|
|
35
|
+
@api_hash = api_hash || ''
|
|
36
|
+
@device_model = 'Ruby MTProto'
|
|
37
|
+
@system_version = RUBY_DESCRIPTION
|
|
38
|
+
@app_version = '0.1.0'
|
|
39
|
+
@system_lang_code = 'en'
|
|
40
|
+
@lang_pack = ''
|
|
41
|
+
@lang_code = 'en'
|
|
26
42
|
end
|
|
27
43
|
|
|
28
|
-
def connect
|
|
29
|
-
@connection.connect
|
|
44
|
+
def connect!
|
|
45
|
+
@connection.connect!
|
|
30
46
|
end
|
|
31
47
|
|
|
32
|
-
def disconnect
|
|
33
|
-
@connection.close
|
|
48
|
+
def disconnect!
|
|
49
|
+
@connection.close if @connection
|
|
34
50
|
end
|
|
35
51
|
|
|
36
52
|
def req_pq_multi
|
|
@@ -41,18 +57,199 @@ module MTProto
|
|
|
41
57
|
|
|
42
58
|
@connection.send(payload)
|
|
43
59
|
|
|
44
|
-
response_data = @connection.recv(timeout:
|
|
60
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
45
61
|
response_message = TL::Message.deserialize(response_data)
|
|
46
62
|
|
|
47
63
|
res_pq = response_message.parse_res_pq
|
|
48
64
|
|
|
49
65
|
raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
|
|
50
66
|
|
|
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
67
|
res_pq
|
|
56
68
|
end
|
|
69
|
+
|
|
70
|
+
def make_auth_key
|
|
71
|
+
raise ArgumentError, "public_key is required for auth key generation" if @public_key.nil? || @public_key.empty?
|
|
72
|
+
|
|
73
|
+
generator = AuthKeyGenerator.new(@connection, @public_key, @dc_number, test_mode: @test_mode, timeout: @timeout)
|
|
74
|
+
result = generator.generate
|
|
75
|
+
|
|
76
|
+
@auth_key = generator.auth_key
|
|
77
|
+
@server_salt = generator.server_salt
|
|
78
|
+
@time_offset = generator.time_offset
|
|
79
|
+
@session = Session.new
|
|
80
|
+
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def generate_msg_id
|
|
85
|
+
time = Time.now.to_f + @time_offset
|
|
86
|
+
msg_id = (time * (2**32)).to_i
|
|
87
|
+
(msg_id / 4) * 4
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rpc_call(body, content_related: true)
|
|
91
|
+
raise 'Auth key not generated' unless @auth_key
|
|
92
|
+
raise 'Session not initialized' unless @session
|
|
93
|
+
|
|
94
|
+
msg_id = generate_msg_id
|
|
95
|
+
seq_no = @session.next_seq_no(content_related: content_related)
|
|
96
|
+
|
|
97
|
+
encrypted_msg = EncryptedMessage.encrypt(
|
|
98
|
+
auth_key: @auth_key,
|
|
99
|
+
server_salt: @server_salt,
|
|
100
|
+
session_id: @session.session_id,
|
|
101
|
+
msg_id: msg_id,
|
|
102
|
+
seq_no: seq_no,
|
|
103
|
+
body: body
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@connection.send(encrypted_msg.serialize)
|
|
107
|
+
|
|
108
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
109
|
+
|
|
110
|
+
decrypted = EncryptedMessage.decrypt(
|
|
111
|
+
auth_key: @auth_key,
|
|
112
|
+
encrypted_message_data: response_data,
|
|
113
|
+
sender: :server
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
response_body = decrypted[:body]
|
|
117
|
+
|
|
118
|
+
constructor = response_body[0, 4].unpack1('L<')
|
|
119
|
+
|
|
120
|
+
if constructor == TL::NewSessionCreated::CONSTRUCTOR
|
|
121
|
+
session_info = TL::NewSessionCreated.deserialize(response_body)
|
|
122
|
+
@server_salt = session_info.server_salt
|
|
123
|
+
|
|
124
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
125
|
+
decrypted = EncryptedMessage.decrypt(
|
|
126
|
+
auth_key: @auth_key,
|
|
127
|
+
encrypted_message_data: response_data,
|
|
128
|
+
sender: :server
|
|
129
|
+
)
|
|
130
|
+
response_body = decrypted[:body]
|
|
131
|
+
constructor = response_body[0, 4].unpack1('L<')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
|
|
135
|
+
container = TL::MsgContainer.deserialize(response_body)
|
|
136
|
+
|
|
137
|
+
rpc_result = container.messages.find do |msg|
|
|
138
|
+
msg[:body][0, 4].unpack1('L<') == 0xf35c6d01
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return rpc_result[:body][12..] if rpc_result
|
|
142
|
+
|
|
143
|
+
new_session = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR }
|
|
144
|
+
if new_session
|
|
145
|
+
session_info = TL::NewSessionCreated.deserialize(new_session[:body])
|
|
146
|
+
@server_salt = session_info.server_salt
|
|
147
|
+
|
|
148
|
+
other_messages = container.messages.reject do |msg|
|
|
149
|
+
constructor = msg[:body][0, 4].unpack1('L<')
|
|
150
|
+
constructor == TL::NewSessionCreated::CONSTRUCTOR || constructor == CONSTRUCTOR_MSGS_ACK
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if !other_messages.empty?
|
|
154
|
+
return other_messages.first[:body]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
158
|
+
decrypted = EncryptedMessage.decrypt(
|
|
159
|
+
auth_key: @auth_key,
|
|
160
|
+
encrypted_message_data: response_data,
|
|
161
|
+
sender: :server
|
|
162
|
+
)
|
|
163
|
+
response_body = decrypted[:body]
|
|
164
|
+
constructor = response_body[0, 4].unpack1('L<')
|
|
165
|
+
|
|
166
|
+
return response_body[12..] if constructor == 0xf35c6d01
|
|
167
|
+
|
|
168
|
+
if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
|
|
169
|
+
container = TL::MsgContainer.deserialize(response_body)
|
|
170
|
+
rpc_result = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == 0xf35c6d01 }
|
|
171
|
+
return rpc_result[:body][12..] if rpc_result
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
return response_body
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
return container.messages.first[:body]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
return response_body[12..] if constructor == 0xf35c6d01
|
|
181
|
+
|
|
182
|
+
response_body
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def invoke_with_layer(layer, query)
|
|
186
|
+
body = TL::Serializer.serialize_int(0xda9b0d0d)
|
|
187
|
+
body += TL::Serializer.serialize_int(layer)
|
|
188
|
+
body += query
|
|
189
|
+
body
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def init_connection(api_id:, device_model:, system_version:, app_version:, system_lang_code:, lang_pack:, lang_code:, query:)
|
|
193
|
+
body = TL::Serializer.serialize_int(0xc1cd5ea9)
|
|
194
|
+
flags = 0
|
|
195
|
+
body += TL::Serializer.serialize_int(flags)
|
|
196
|
+
body += TL::Serializer.serialize_int(api_id)
|
|
197
|
+
body += TL::Serializer.serialize_string(device_model)
|
|
198
|
+
body += TL::Serializer.serialize_string(system_version)
|
|
199
|
+
body += TL::Serializer.serialize_string(app_version)
|
|
200
|
+
body += TL::Serializer.serialize_string(system_lang_code)
|
|
201
|
+
body += TL::Serializer.serialize_string(lang_pack)
|
|
202
|
+
body += TL::Serializer.serialize_string(lang_code)
|
|
203
|
+
body += query
|
|
204
|
+
body
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def help_get_config
|
|
208
|
+
query = [0xc4f9186b].pack('L<')
|
|
209
|
+
|
|
210
|
+
unless @connection_initialized
|
|
211
|
+
query = init_connection(
|
|
212
|
+
api_id: @api_id,
|
|
213
|
+
device_model: @device_model,
|
|
214
|
+
system_version: @system_version,
|
|
215
|
+
app_version: @app_version,
|
|
216
|
+
system_lang_code: @system_lang_code,
|
|
217
|
+
lang_pack: @lang_pack,
|
|
218
|
+
lang_code: @lang_code,
|
|
219
|
+
query: query
|
|
220
|
+
)
|
|
221
|
+
query = invoke_with_layer(214, query)
|
|
222
|
+
@connection_initialized = true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
rpc_call(query)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def auth_send_code(phone_number, code_settings: {})
|
|
229
|
+
raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
|
|
230
|
+
|
|
231
|
+
query = [0xa677244f].pack('L<')
|
|
232
|
+
query += TL::Serializer.serialize_string(phone_number)
|
|
233
|
+
query += TL::Serializer.serialize_int(@api_id)
|
|
234
|
+
query += TL::Serializer.serialize_string(@api_hash)
|
|
235
|
+
query += TL::CodeSettings.serialize(code_settings)
|
|
236
|
+
|
|
237
|
+
unless @connection_initialized
|
|
238
|
+
query = init_connection(
|
|
239
|
+
api_id: @api_id,
|
|
240
|
+
device_model: @device_model,
|
|
241
|
+
system_version: @system_version,
|
|
242
|
+
app_version: @app_version,
|
|
243
|
+
system_lang_code: @system_lang_code,
|
|
244
|
+
lang_pack: @lang_pack,
|
|
245
|
+
lang_code: @lang_code,
|
|
246
|
+
query: query
|
|
247
|
+
)
|
|
248
|
+
query = invoke_with_layer(214, query)
|
|
249
|
+
@connection_initialized = true
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
rpc_call(query)
|
|
253
|
+
end
|
|
57
254
|
end
|
|
58
255
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
class Connection
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key:, dc_number:, test_mode: false, timeout: 10)
|
|
8
|
+
@client = Client.new(
|
|
9
|
+
api_id: api_id,
|
|
10
|
+
api_hash: api_hash,
|
|
11
|
+
host: host,
|
|
12
|
+
port: port,
|
|
13
|
+
public_key: public_key,
|
|
14
|
+
dc_number: dc_number,
|
|
15
|
+
test_mode: test_mode,
|
|
16
|
+
timeout: timeout
|
|
17
|
+
)
|
|
18
|
+
@connected = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def connect!
|
|
22
|
+
return if @connected
|
|
23
|
+
|
|
24
|
+
@client.connect!
|
|
25
|
+
@client.make_auth_key
|
|
26
|
+
@connected = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def disconnect!
|
|
30
|
+
@client.disconnect! if @connected
|
|
31
|
+
@connected = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ping(ping_id = nil)
|
|
35
|
+
raise NotConnectedError unless @connected
|
|
36
|
+
|
|
37
|
+
ping_id ||= rand(2**63)
|
|
38
|
+
body = TL::Message.ping(ping_id)
|
|
39
|
+
|
|
40
|
+
response_body = @client.rpc_call(body)
|
|
41
|
+
message = TL::Message.new(body: response_body)
|
|
42
|
+
pong = message.parse_pong
|
|
43
|
+
|
|
44
|
+
raise PingMismatchError unless pong[:ping_id] == ping_id
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_config
|
|
50
|
+
raise NotConnectedError unless @connected
|
|
51
|
+
|
|
52
|
+
response = @client.help_get_config
|
|
53
|
+
|
|
54
|
+
# Handle gzip compression
|
|
55
|
+
constructor = response[0, 4].unpack1('L<')
|
|
56
|
+
if constructor == TL::GzipPacked::CONSTRUCTOR
|
|
57
|
+
response = TL::GzipPacked.unpack(response)
|
|
58
|
+
constructor = response[0, 4].unpack1('L<')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handle RPC errors
|
|
62
|
+
if constructor == TL::RpcError::CONSTRUCTOR
|
|
63
|
+
error = TL::RpcError.deserialize(response)
|
|
64
|
+
raise MTProto::RpcError.new(error.error_code, error.error_message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parse and return config
|
|
68
|
+
if constructor == TL::Config::CONSTRUCTOR || constructor == TL::Config::CONSTRUCTOR_ALT
|
|
69
|
+
TL::Config.deserialize(response)
|
|
70
|
+
else
|
|
71
|
+
raise UnexpectedConstructorError.new(constructor)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def send_code(phone_number, code_settings: {})
|
|
76
|
+
raise NotConnectedError unless @connected
|
|
77
|
+
raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
|
|
78
|
+
|
|
79
|
+
response = @client.auth_send_code(phone_number, code_settings: code_settings)
|
|
80
|
+
|
|
81
|
+
# Handle gzip compression
|
|
82
|
+
constructor = response[0, 4].unpack1('L<')
|
|
83
|
+
if constructor == TL::GzipPacked::CONSTRUCTOR
|
|
84
|
+
response = TL::GzipPacked.unpack(response)
|
|
85
|
+
constructor = response[0, 4].unpack1('L<')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Handle RPC errors
|
|
89
|
+
if constructor == TL::RpcError::CONSTRUCTOR
|
|
90
|
+
error = TL::RpcError.deserialize(response)
|
|
91
|
+
raise MTProto::RpcError.new(error.error_code, error.error_message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse and return sent code
|
|
95
|
+
if constructor == TL::SentCode::CONSTRUCTOR
|
|
96
|
+
sent_code = TL::SentCode.deserialize(response)
|
|
97
|
+
sent_code.to_h
|
|
98
|
+
else
|
|
99
|
+
raise UnexpectedConstructorError.new(constructor)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
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
|
|
@@ -6,30 +6,24 @@ require 'digest'
|
|
|
6
6
|
module MTProto
|
|
7
7
|
module Crypto
|
|
8
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
9
|
attr_reader :key, :fingerprint
|
|
21
10
|
|
|
22
11
|
def initialize(pem_string)
|
|
12
|
+
raise ArgumentError, "pem_string is required" if pem_string.nil? || pem_string.empty?
|
|
13
|
+
|
|
23
14
|
@key = OpenSSL::PKey::RSA.new(pem_string)
|
|
24
15
|
@fingerprint = calculate_fingerprint
|
|
25
16
|
end
|
|
26
17
|
|
|
27
|
-
def self.
|
|
28
|
-
|
|
18
|
+
def self.from_pem(pem_string)
|
|
19
|
+
new(pem_string)
|
|
29
20
|
end
|
|
30
21
|
|
|
31
|
-
def self.find_by_fingerprint(fingerprints)
|
|
32
|
-
|
|
22
|
+
def self.find_by_fingerprint(fingerprints, public_key)
|
|
23
|
+
raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
|
|
24
|
+
|
|
25
|
+
key = new(public_key)
|
|
26
|
+
key if fingerprints.include?(key.fingerprint)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
private
|