mtproto 0.0.7 → 0.0.9
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 +4 -0
- data/ext/aes_ige/extconf.rb +3 -9
- data/ext/factorization/extconf.rb +2 -0
- data/lib/mtproto/auth_key_generator.rb +66 -103
- data/lib/mtproto/binary.rb +21 -0
- data/lib/mtproto/client/api/check_password.rb +41 -0
- data/lib/mtproto/client/api/export_login_token.rb +27 -0
- data/lib/mtproto/client/api/get_updates_difference.rb +21 -0
- data/lib/mtproto/client/api/get_updates_state.rb +14 -0
- data/lib/mtproto/client/api/get_users.rb +14 -0
- data/lib/mtproto/client/api/import_login_token.rb +23 -0
- data/lib/mtproto/client/api/send_code.rb +21 -0
- data/lib/mtproto/client/api/sign_in.rb +27 -0
- data/lib/mtproto/client/api.rb +28 -0
- data/lib/mtproto/client/rpc/response.rb +63 -0
- data/lib/mtproto/client/rpc.rb +67 -107
- data/lib/mtproto/client.rb +176 -31
- data/lib/mtproto/crypto/dh_key_exchange.rb +1 -2
- data/lib/mtproto/crypto/dh_validator.rb +17 -19
- data/lib/mtproto/crypto/factorization.rb +1 -1
- data/lib/mtproto/crypto/rsa_key.rb +2 -2
- data/lib/mtproto/crypto/srp.rb +117 -0
- data/lib/mtproto/delegate_methods.rb +11 -0
- data/lib/mtproto/message/message.rb +85 -0
- data/lib/mtproto/session.rb +1 -1
- data/lib/mtproto/tl/constructors.rb +2269 -0
- data/lib/mtproto/tl/object.rb +25 -0
- data/lib/mtproto/tl/objects/account_password.rb +72 -0
- data/lib/mtproto/tl/objects/authorization.rb +73 -0
- data/lib/mtproto/tl/objects/check_password.rb +46 -0
- data/lib/mtproto/tl/objects/client_dh_inner_data.rb +47 -0
- data/lib/mtproto/tl/objects/dh_gen_response.rb +50 -0
- data/lib/mtproto/tl/objects/export_login_token.rb +51 -0
- data/lib/mtproto/tl/objects/get_config.rb +15 -0
- data/lib/mtproto/tl/objects/get_difference.rb +36 -0
- data/lib/mtproto/tl/objects/get_password.rb +15 -0
- data/lib/mtproto/tl/objects/get_state.rb +15 -0
- data/lib/mtproto/tl/objects/get_users.rb +20 -0
- data/lib/mtproto/tl/objects/help_config.rb +77 -0
- data/lib/mtproto/tl/objects/import_login_token.rb +39 -0
- data/lib/mtproto/tl/objects/init_connection.rb +59 -0
- data/lib/mtproto/tl/objects/invoke_with_layer.rb +22 -0
- data/lib/mtproto/tl/objects/login_token.rb +82 -0
- data/lib/mtproto/tl/objects/pq_inner_data.rb +69 -0
- data/lib/mtproto/tl/objects/req_dh_params.rb +65 -0
- data/lib/mtproto/tl/objects/req_pq_multi.rb +23 -0
- data/lib/mtproto/tl/objects/res_pq.rb +75 -0
- data/lib/mtproto/tl/objects/send_code.rb +50 -0
- data/lib/mtproto/tl/objects/sent_code.rb +79 -0
- data/lib/mtproto/tl/objects/server_dh_inner_data.rb +74 -0
- data/lib/mtproto/tl/objects/server_dh_params.rb +53 -0
- data/lib/mtproto/tl/objects/set_client_dh_params.rb +48 -0
- data/lib/mtproto/tl/objects/sign_in.rb +47 -0
- data/lib/mtproto/tl/objects/update.rb +80 -0
- data/lib/mtproto/tl/objects/update_short.rb +22 -0
- data/lib/mtproto/tl/objects/update_short_message.rb +67 -0
- data/lib/mtproto/tl/objects/updates_difference.rb +157 -0
- data/lib/mtproto/tl/objects/updates_state.rb +37 -0
- data/lib/mtproto/tl/objects/users.rb +86 -0
- data/lib/mtproto/transport/abridged_packet_codec.rb +35 -12
- data/lib/mtproto/transport/connection.rb +23 -0
- data/lib/mtproto/transport/errors.rb +11 -0
- data/lib/mtproto/transport/packet.rb +19 -0
- data/lib/mtproto/transport/tcp_connection.rb +57 -46
- data/lib/mtproto/{tl → type}/bad_msg_notification.rb +11 -11
- data/lib/mtproto/{tl → type}/client_dh_inner_data.rb +1 -1
- data/lib/mtproto/{tl → type}/gzip_packed.rb +6 -4
- data/lib/mtproto/{tl → type}/message.rb +3 -3
- data/lib/mtproto/{tl → type}/msg_container.rb +1 -1
- data/lib/mtproto/{tl → type}/new_session_created.rb +1 -1
- data/lib/mtproto/{tl/p_q_inner_data.rb → type/pq_inner_data.rb} +1 -1
- data/lib/mtproto/{tl → type}/rpc_error.rb +1 -2
- data/lib/mtproto/{tl → type}/serializer.rb +1 -1
- data/lib/mtproto/{tl → type}/server_dh_inner_data.rb +1 -1
- data/lib/mtproto/updates_poller.rb +37 -33
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +21 -22
- data/scripts/generate_constructors.rb +65 -0
- metadata +80 -49
- data/lib/mtproto/rpc/get_config.rb +0 -37
- data/lib/mtproto/rpc/get_contacts.rb +0 -22
- data/lib/mtproto/rpc/get_updates_difference.rb +0 -33
- data/lib/mtproto/rpc/get_updates_state.rb +0 -22
- data/lib/mtproto/rpc/get_users.rb +0 -22
- data/lib/mtproto/rpc/ping.rb +0 -26
- data/lib/mtproto/rpc/send_code.rb +0 -44
- data/lib/mtproto/rpc/send_message.rb +0 -31
- data/lib/mtproto/rpc/sign_in.rb +0 -52
- data/lib/mtproto/tl/auth_key/dh_gen_response.rb +0 -37
- data/lib/mtproto/tl/auth_key/req_dh_params.rb +0 -31
- data/lib/mtproto/tl/auth_key/req_pq_multi.rb +0 -18
- data/lib/mtproto/tl/auth_key/res_pq.rb +0 -62
- data/lib/mtproto/tl/auth_key/server_dh_params.rb +0 -43
- data/lib/mtproto/tl/auth_key/set_client_dh_params.rb +0 -25
- data/lib/mtproto/tl/code_settings.rb +0 -25
- data/lib/mtproto/tl/config.rb +0 -124
- data/lib/mtproto/tl/method_builder.rb +0 -29
- data/lib/mtproto/tl/rpc/auth/authorization.rb +0 -107
- data/lib/mtproto/tl/rpc/auth/send_code.rb +0 -28
- data/lib/mtproto/tl/rpc/auth/sent_code.rb +0 -36
- data/lib/mtproto/tl/rpc/auth/sign_in.rb +0 -32
- data/lib/mtproto/tl/rpc/contacts/contacts.rb +0 -155
- data/lib/mtproto/tl/rpc/contacts/get_contacts.rb +0 -18
- data/lib/mtproto/tl/rpc/help/config.rb +0 -35
- data/lib/mtproto/tl/rpc/help/get_config.rb +0 -17
- data/lib/mtproto/tl/rpc/messages/send_message.rb +0 -43
- data/lib/mtproto/tl/rpc/messages/updates.rb +0 -87
- data/lib/mtproto/tl/rpc/ping.rb +0 -18
- data/lib/mtproto/tl/rpc/pong.rb +0 -46
- data/lib/mtproto/tl/rpc/updates/difference.rb +0 -332
- data/lib/mtproto/tl/rpc/updates/get_difference.rb +0 -42
- data/lib/mtproto/tl/rpc/updates/get_state.rb +0 -17
- data/lib/mtproto/tl/rpc/updates/state.rb +0 -59
- data/lib/mtproto/tl/rpc/users/get_users.rb +0 -25
- data/lib/mtproto/tl/rpc/users/users.rb +0 -99
- data/lib/mtproto/tl/sent_code.rb +0 -128
data/lib/mtproto/client/rpc.rb
CHANGED
|
@@ -1,140 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../message_id'
|
|
4
|
+
require_relative 'rpc/response'
|
|
4
5
|
|
|
5
6
|
module MTProto
|
|
6
7
|
class Client
|
|
7
8
|
class RPC
|
|
8
9
|
CONSTRUCTOR_RPC_RESULT = 0xf35c6d01
|
|
9
|
-
CONSTRUCTOR_MSGS_ACK = 0x62d6b459
|
|
10
10
|
CONSTRUCTOR_BAD_SERVER_SALT = 0xedab447b
|
|
11
11
|
|
|
12
|
+
attr_reader :pending_requests
|
|
13
|
+
|
|
12
14
|
def initialize(client)
|
|
13
15
|
@client = client
|
|
14
|
-
@
|
|
16
|
+
@pending_requests = {}
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
def call(
|
|
18
|
-
raise '
|
|
19
|
+
def call(request, response_class)
|
|
20
|
+
raise 'Mainloop not running' unless @client.mainloop_running?
|
|
21
|
+
raise 'Auth key not generated' unless @client.auth_key?
|
|
19
22
|
raise 'Session not initialized' unless @client.session
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
body = serialize_request(request)
|
|
25
|
+
msg_id = send_encrypted(body)
|
|
26
|
+
response = Response.new(msg_id, response_class, body)
|
|
27
|
+
@pending_requests[msg_id] = response
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
server_salt: @client.server_salt,
|
|
27
|
-
session_id: @client.session.session_id,
|
|
28
|
-
msg_id: msg_id,
|
|
29
|
-
seq_no: seq_no,
|
|
30
|
-
body: body
|
|
31
|
-
)
|
|
29
|
+
response
|
|
30
|
+
end
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
def handle_bad_server_salt(response_body, client)
|
|
33
|
+
bad_msg_id = response_body[4, 8].unpack1('Q<')
|
|
34
|
+
new_server_salt = response_body[20, 8].unpack1('Q<')
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
client.update_server_salt(new_server_salt)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
encrypted_message_data: response_data,
|
|
40
|
-
sender: :server
|
|
41
|
-
)
|
|
38
|
+
response = @pending_requests.delete(bad_msg_id)
|
|
39
|
+
return unless response
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Handle bad_server_salt
|
|
48
|
-
if constructor == CONSTRUCTOR_BAD_SERVER_SALT
|
|
49
|
-
# bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long
|
|
50
|
-
offset = 4
|
|
51
|
-
bad_msg_id = response_body[offset, 8].unpack1('Q<')
|
|
52
|
-
offset += 8
|
|
53
|
-
bad_msg_seqno = response_body[offset, 4].unpack1('L<')
|
|
54
|
-
offset += 4
|
|
55
|
-
error_code = response_body[offset, 4].unpack1('L<')
|
|
56
|
-
offset += 4
|
|
57
|
-
new_server_salt = response_body[offset, 8].unpack1('Q<')
|
|
58
|
-
|
|
59
|
-
# Update server salt and retry
|
|
60
|
-
update_server_salt(new_server_salt)
|
|
61
|
-
return call(body, content_related: content_related)
|
|
62
|
-
end
|
|
41
|
+
new_msg_id = send_encrypted(response.body_bytes)
|
|
42
|
+
response.instance_variable_set(:@msg_id, new_msg_id)
|
|
43
|
+
@pending_requests[new_msg_id] = response
|
|
44
|
+
end
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
response_data = @client.connection.recv(timeout: @client.timeout)
|
|
69
|
-
decrypted = EncryptedMessage.decrypt(
|
|
70
|
-
auth_key: @client.auth_key,
|
|
71
|
-
encrypted_message_data: response_data,
|
|
72
|
-
sender: :server
|
|
73
|
-
)
|
|
74
|
-
response_body = decrypted[:body]
|
|
75
|
-
constructor = response_body[0, 4].unpack1('L<')
|
|
76
|
-
end
|
|
46
|
+
def handle_rpc_result(response_body)
|
|
47
|
+
req_msg_id = response_body[4, 8].unpack1('Q<')
|
|
48
|
+
result = response_body[12..]
|
|
77
49
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
msg[:body][0, 4].unpack1('L<') == CONSTRUCTOR_RPC_RESULT
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
return rpc_result[:body][12..] if rpc_result
|
|
86
|
-
|
|
87
|
-
new_session = container.messages.find do |msg|
|
|
88
|
-
msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR
|
|
89
|
-
end
|
|
90
|
-
if new_session
|
|
91
|
-
session_info = TL::NewSessionCreated.deserialize(new_session[:body])
|
|
92
|
-
update_server_salt(session_info.server_salt)
|
|
93
|
-
|
|
94
|
-
other_messages = container.messages.reject do |msg|
|
|
95
|
-
constructor = msg[:body][0, 4].unpack1('L<')
|
|
96
|
-
(
|
|
97
|
-
constructor == TL::NewSessionCreated::CONSTRUCTOR ||
|
|
98
|
-
constructor == CONSTRUCTOR_MSGS_ACK
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
return other_messages.first[:body] unless other_messages.empty?
|
|
103
|
-
|
|
104
|
-
response_data = @client.connection.recv(timeout: @client.timeout)
|
|
105
|
-
decrypted = EncryptedMessage.decrypt(
|
|
106
|
-
auth_key: @client.auth_key,
|
|
107
|
-
encrypted_message_data: response_data,
|
|
108
|
-
sender: :server
|
|
109
|
-
)
|
|
110
|
-
response_body = decrypted[:body]
|
|
111
|
-
constructor = response_body[0, 4].unpack1('L<')
|
|
112
|
-
|
|
113
|
-
return response_body[12..] if constructor == CONSTRUCTOR_RPC_RESULT
|
|
114
|
-
|
|
115
|
-
if constructor == TL::MsgContainer::CONSTRUCTOR
|
|
116
|
-
container = TL::MsgContainer.deserialize(response_body)
|
|
117
|
-
rpc_result = container.messages.find do |msg|
|
|
118
|
-
msg[:body][0, 4].unpack1('L<') == CONSTRUCTOR_RPC_RESULT
|
|
119
|
-
end
|
|
120
|
-
return rpc_result[:body][12..] if rpc_result
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
return response_body
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
return container.messages.first[:body]
|
|
50
|
+
result_constructor = result[0, 4].unpack1('L<')
|
|
51
|
+
if result_constructor == Type::GzipPacked::CONSTRUCTOR
|
|
52
|
+
result = Type::GzipPacked.unpack(result)
|
|
53
|
+
result_constructor = result[0, 4].unpack1('L<')
|
|
127
54
|
end
|
|
128
55
|
|
|
129
|
-
|
|
56
|
+
response = @pending_requests.delete(req_msg_id)
|
|
57
|
+
return unless response
|
|
58
|
+
|
|
59
|
+
if result_constructor == Type::RpcError::CONSTRUCTOR
|
|
60
|
+
error = Type::RpcError.deserialize(result)
|
|
61
|
+
response.signal_error(RpcError.new(error.error_code, error.error_message))
|
|
62
|
+
else
|
|
63
|
+
response.signal(result)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
130
66
|
|
|
131
|
-
|
|
67
|
+
def signal_all_error(error)
|
|
68
|
+
@pending_requests.each_value { |response| response.signal_error(error) }
|
|
69
|
+
@pending_requests.clear
|
|
132
70
|
end
|
|
133
71
|
|
|
134
72
|
private
|
|
135
73
|
|
|
136
|
-
def
|
|
137
|
-
|
|
74
|
+
def serialize_request(request)
|
|
75
|
+
if request.respond_to?(:body)
|
|
76
|
+
request.body.pack('C*')
|
|
77
|
+
else
|
|
78
|
+
request
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def send_encrypted(body, content_related: true)
|
|
83
|
+
msg_id = MessageId.generate(time_offset: @client.time_offset)
|
|
84
|
+
seq_no = @client.session.next_seq_no(content_related: content_related)
|
|
85
|
+
|
|
86
|
+
encrypted_msg = EncryptedMessage.encrypt(
|
|
87
|
+
auth_key: @client.auth_key,
|
|
88
|
+
server_salt: @client.server_salt,
|
|
89
|
+
session_id: @client.session.session_id,
|
|
90
|
+
msg_id: msg_id,
|
|
91
|
+
seq_no: seq_no,
|
|
92
|
+
body: body
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@client.connection.send(Transport::Packet.new(encrypted_msg.serialize.bytes))
|
|
96
|
+
|
|
97
|
+
msg_id
|
|
138
98
|
end
|
|
139
99
|
end
|
|
140
100
|
end
|
data/lib/mtproto/client.rb
CHANGED
|
@@ -3,25 +3,59 @@
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'digest'
|
|
5
5
|
require 'base64'
|
|
6
|
+
require 'async'
|
|
7
|
+
require 'async/condition'
|
|
6
8
|
require_relative 'transport/tcp_connection'
|
|
9
|
+
require_relative 'transport/connection'
|
|
7
10
|
require_relative 'transport/abridged_packet_codec'
|
|
8
|
-
require_relative '
|
|
11
|
+
require_relative 'type/message'
|
|
9
12
|
require_relative 'client/rpc'
|
|
10
|
-
require_relative '
|
|
13
|
+
require_relative 'client/api'
|
|
14
|
+
require_relative 'tl/objects/invoke_with_layer'
|
|
15
|
+
require_relative 'tl/objects/init_connection'
|
|
16
|
+
require_relative 'tl/objects/get_config'
|
|
17
|
+
require_relative 'tl/objects/help_config'
|
|
11
18
|
|
|
12
19
|
module MTProto
|
|
13
20
|
class Client
|
|
14
|
-
|
|
21
|
+
extend DelegateMethods
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
|
|
23
|
+
API_LAYER = 214
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session,
|
|
26
|
+
:timeout, :user_id, :access_hash, :dc_number
|
|
27
|
+
attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version,
|
|
28
|
+
:system_lang_code, :lang_pack, :lang_code
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
delegate :connect!, :connected?, to: :connection
|
|
31
|
+
|
|
32
|
+
def auth_key?
|
|
33
|
+
!@auth_key.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mainloop_running?
|
|
37
|
+
@running
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(
|
|
41
|
+
host:,
|
|
42
|
+
api_id:,
|
|
43
|
+
api_hash:,
|
|
44
|
+
port: 443,
|
|
45
|
+
public_key: nil,
|
|
46
|
+
dc_number: nil,
|
|
47
|
+
test_mode: false,
|
|
48
|
+
timeout: 10
|
|
49
|
+
)
|
|
50
|
+
raise ArgumentError, 'host is required' if host.nil? || host.empty?
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, 'dc_number must be positive. Use test_mode: true for test DCs' if dc_number && dc_number < 0
|
|
53
|
+
|
|
54
|
+
@api_id = api_id
|
|
55
|
+
@api_hash = api_hash
|
|
56
|
+
|
|
57
|
+
transport = Transport::TCPConnection.new(host, port)
|
|
58
|
+
@connection = Transport::Connection.new(transport)
|
|
25
59
|
@public_key = public_key
|
|
26
60
|
@dc_number = dc_number
|
|
27
61
|
@test_mode = test_mode
|
|
@@ -35,29 +69,76 @@ module MTProto
|
|
|
35
69
|
@user_id = nil
|
|
36
70
|
@access_hash = nil
|
|
37
71
|
|
|
38
|
-
# Client configuration defaults
|
|
39
|
-
@api_id = api_id || 0
|
|
40
|
-
@api_hash = api_hash || ''
|
|
41
72
|
@device_model = 'Ruby MTProto'
|
|
42
73
|
@system_version = RUBY_DESCRIPTION
|
|
43
74
|
@app_version = '0.1.0'
|
|
44
75
|
@system_lang_code = 'en'
|
|
45
76
|
@lang_pack = ''
|
|
46
77
|
@lang_code = 'en'
|
|
78
|
+
|
|
79
|
+
@receiver_task = nil
|
|
80
|
+
@running = false
|
|
81
|
+
@on_update_callbacks = []
|
|
47
82
|
end
|
|
48
83
|
|
|
49
|
-
def
|
|
50
|
-
@
|
|
84
|
+
def on_update(&block)
|
|
85
|
+
@on_update_callbacks << block
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_mainloop
|
|
89
|
+
raise 'Auth key not set' unless auth_key?
|
|
90
|
+
raise 'Mainloop already running' if @running
|
|
91
|
+
raise ArgumentError, 'Block is required' unless block_given?
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
Async do
|
|
95
|
+
@running = true
|
|
96
|
+
@receiver_task = Async do
|
|
97
|
+
@connection.receive do |packet, error|
|
|
98
|
+
if error
|
|
99
|
+
warn "[MTProto] Packet read error: #{error.message}"
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
decrypted = EncryptedMessage.decrypt(
|
|
104
|
+
auth_key: @auth_key,
|
|
105
|
+
encrypted_message_data: packet.data.pack('C*'),
|
|
106
|
+
sender: :server
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
process_message(decrypted[:body])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
yield self
|
|
114
|
+
ensure
|
|
115
|
+
disconnect!
|
|
116
|
+
end
|
|
117
|
+
rescue Interrupt
|
|
118
|
+
# Ctrl+C
|
|
119
|
+
end
|
|
51
120
|
end
|
|
52
121
|
|
|
53
122
|
def disconnect!
|
|
54
|
-
@
|
|
123
|
+
@running = false
|
|
124
|
+
@receiver_task&.stop
|
|
125
|
+
@receiver_task = nil
|
|
126
|
+
|
|
127
|
+
rpc.signal_all_error(Transport::ConnectionError.new('Client shutting down'))
|
|
128
|
+
|
|
129
|
+
@connection.disconnect! if @connection&.connected?
|
|
55
130
|
end
|
|
56
131
|
|
|
57
132
|
def exchange_keys!
|
|
58
|
-
raise ArgumentError,
|
|
59
|
-
|
|
60
|
-
generator = AuthKeyGenerator.new(
|
|
133
|
+
raise ArgumentError, 'public_key is required for auth key generation' if @public_key.nil? || @public_key.empty?
|
|
134
|
+
|
|
135
|
+
generator = AuthKeyGenerator.new(
|
|
136
|
+
@connection,
|
|
137
|
+
@public_key,
|
|
138
|
+
@dc_number,
|
|
139
|
+
test_mode: @test_mode,
|
|
140
|
+
timeout: @timeout
|
|
141
|
+
)
|
|
61
142
|
result = generator.generate
|
|
62
143
|
|
|
63
144
|
@auth_key = generator.auth_key
|
|
@@ -72,8 +153,37 @@ module MTProto
|
|
|
72
153
|
@rpc ||= RPC.new(self)
|
|
73
154
|
end
|
|
74
155
|
|
|
75
|
-
def
|
|
76
|
-
|
|
156
|
+
def api
|
|
157
|
+
@api ||= API.new(self)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def init_connection!
|
|
161
|
+
return if @connection_initialized
|
|
162
|
+
|
|
163
|
+
query = TL::InvokeWithLayer.new(
|
|
164
|
+
layer: API_LAYER,
|
|
165
|
+
query: TL::InitConnection.new(
|
|
166
|
+
api_id: api_id,
|
|
167
|
+
device_model: device_model,
|
|
168
|
+
system_version: system_version,
|
|
169
|
+
app_version: app_version,
|
|
170
|
+
system_lang_code: system_lang_code,
|
|
171
|
+
lang_pack: lang_pack,
|
|
172
|
+
lang_code: lang_code,
|
|
173
|
+
query: TL::GetConfig.new
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
response = rpc.call(query, TL::HelpConfig)
|
|
178
|
+
response.wait!(timeout)
|
|
179
|
+
|
|
180
|
+
@connection_initialized = true
|
|
181
|
+
|
|
182
|
+
response.body
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def save_auth_data
|
|
186
|
+
raise 'Cannot save auth_data: auth_key not set' unless @auth_key
|
|
77
187
|
|
|
78
188
|
{
|
|
79
189
|
auth_key: Base64.strict_encode64(@auth_key),
|
|
@@ -85,18 +195,17 @@ module MTProto
|
|
|
85
195
|
}
|
|
86
196
|
end
|
|
87
197
|
|
|
88
|
-
def
|
|
89
|
-
raise ArgumentError, '
|
|
90
|
-
raise ArgumentError, 'auth_key is required' unless
|
|
91
|
-
raise ArgumentError, 'server_salt is required' unless
|
|
198
|
+
def load_auth_data(auth_data)
|
|
199
|
+
raise ArgumentError, 'auth_data must be a Hash' unless auth_data.is_a?(Hash)
|
|
200
|
+
raise ArgumentError, 'auth_key is required' unless auth_data[:auth_key]
|
|
201
|
+
raise ArgumentError, 'server_salt is required' unless auth_data[:server_salt]
|
|
92
202
|
|
|
93
|
-
@auth_key = Base64.strict_decode64(
|
|
94
|
-
@server_salt =
|
|
95
|
-
@time_offset =
|
|
96
|
-
@user_id =
|
|
97
|
-
@access_hash =
|
|
203
|
+
@auth_key = Base64.strict_decode64(auth_data[:auth_key])
|
|
204
|
+
@server_salt = auth_data[:server_salt]
|
|
205
|
+
@time_offset = auth_data[:time_offset] || 0
|
|
206
|
+
@user_id = auth_data[:user_id]
|
|
207
|
+
@access_hash = auth_data[:access_hash]
|
|
98
208
|
|
|
99
|
-
# Create a fresh session for the new connection
|
|
100
209
|
@session = Session.new
|
|
101
210
|
|
|
102
211
|
true
|
|
@@ -106,5 +215,41 @@ module MTProto
|
|
|
106
215
|
@user_id = user_id
|
|
107
216
|
@access_hash = access_hash
|
|
108
217
|
end
|
|
218
|
+
|
|
219
|
+
def update_server_salt(new_salt)
|
|
220
|
+
@server_salt = new_salt
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def process_message(response_body)
|
|
226
|
+
constructor = response_body[0, 4].unpack1('L<')
|
|
227
|
+
|
|
228
|
+
case constructor
|
|
229
|
+
when RPC::CONSTRUCTOR_BAD_SERVER_SALT
|
|
230
|
+
rpc.handle_bad_server_salt(response_body, self)
|
|
231
|
+
when Type::NewSessionCreated::CONSTRUCTOR
|
|
232
|
+
handle_new_session(response_body)
|
|
233
|
+
when Type::MsgContainer::CONSTRUCTOR
|
|
234
|
+
handle_container(response_body)
|
|
235
|
+
when RPC::CONSTRUCTOR_RPC_RESULT
|
|
236
|
+
rpc.handle_rpc_result(response_body)
|
|
237
|
+
else
|
|
238
|
+
@on_update_callbacks.each { |cb| cb.call(constructor, response_body) }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def handle_new_session(response_body)
|
|
243
|
+
session_info = Type::NewSessionCreated.deserialize(response_body)
|
|
244
|
+
@server_salt = session_info.server_salt
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def handle_container(response_body)
|
|
248
|
+
container = Type::MsgContainer.deserialize(response_body)
|
|
249
|
+
|
|
250
|
+
container.messages.each do |msg|
|
|
251
|
+
process_message(msg[:body])
|
|
252
|
+
end
|
|
253
|
+
end
|
|
109
254
|
end
|
|
110
255
|
end
|
|
@@ -22,22 +22,22 @@ module MTProto
|
|
|
22
22
|
raise 'Invalid g: must be 2, 3, 4, 5, 6, or 7' unless [2, 3, 4, 5, 6, 7].include?(g)
|
|
23
23
|
|
|
24
24
|
p_mod = case g
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
[1, 4].include?(mod5)
|
|
34
|
+
when 6
|
|
35
|
+
mod24 = dh_prime % 24
|
|
36
|
+
[19, 23].include?(mod24)
|
|
37
|
+
when 7
|
|
38
|
+
mod7 = dh_prime % 7
|
|
39
|
+
[3, 5, 6].include?(mod7)
|
|
40
|
+
end
|
|
41
41
|
|
|
42
42
|
raise "g=#{g} is not a valid generator for this prime" unless p_mod
|
|
43
43
|
|
|
@@ -52,9 +52,7 @@ module MTProto
|
|
|
52
52
|
min_prime = OpenSSL::BN.new(2)**2047
|
|
53
53
|
max_prime = OpenSSL::BN.new(2)**2048
|
|
54
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
|
|
55
|
+
raise 'dh_prime out of range (must be 2^2047 < p < 2^2048)' if dh_prime <= min_prime || dh_prime >= max_prime
|
|
58
56
|
|
|
59
57
|
true
|
|
60
58
|
end
|
|
@@ -9,7 +9,7 @@ module MTProto
|
|
|
9
9
|
attr_reader :key, :fingerprint
|
|
10
10
|
|
|
11
11
|
def initialize(pem_string)
|
|
12
|
-
raise ArgumentError,
|
|
12
|
+
raise ArgumentError, 'pem_string is required' if pem_string.nil? || pem_string.empty?
|
|
13
13
|
|
|
14
14
|
@key = OpenSSL::PKey::RSA.new(pem_string)
|
|
15
15
|
@fingerprint = calculate_fingerprint
|
|
@@ -20,7 +20,7 @@ module MTProto
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def self.find_by_fingerprint(fingerprints, public_key)
|
|
23
|
-
raise ArgumentError,
|
|
23
|
+
raise ArgumentError, 'public_key is required' if public_key.nil? || public_key.empty?
|
|
24
24
|
|
|
25
25
|
key = new(public_key)
|
|
26
26
|
key if fingerprints.include?(key.fingerprint)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
module Crypto
|
|
8
|
+
module SRP
|
|
9
|
+
SIZE_FOR_HASH = 256
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def compute_check(algo:, srp_b:, srp_id:, password:)
|
|
14
|
+
salt1 = algo[:salt1]
|
|
15
|
+
salt2 = algo[:salt2]
|
|
16
|
+
g = algo[:g]
|
|
17
|
+
p_bytes = algo[:p]
|
|
18
|
+
|
|
19
|
+
p_int = bytes_to_int(p_bytes)
|
|
20
|
+
g_int = g
|
|
21
|
+
b_int = bytes_to_int(srp_b)
|
|
22
|
+
|
|
23
|
+
pw_hash = compute_hash(salt1, salt2, password)
|
|
24
|
+
x = bytes_to_int(pw_hash)
|
|
25
|
+
|
|
26
|
+
p_for_hash = pad_to_256(p_bytes)
|
|
27
|
+
g_for_hash = int_to_256_bytes(g_int)
|
|
28
|
+
b_for_hash = pad_to_256(srp_b)
|
|
29
|
+
|
|
30
|
+
g_x = mod_pow(g_int, x, p_int)
|
|
31
|
+
k = bytes_to_int(sha256(p_for_hash, g_for_hash))
|
|
32
|
+
kg_x = (k * g_x) % p_int
|
|
33
|
+
|
|
34
|
+
a_int, a_for_hash, u = generate_and_check_random(g_int, p_int, b_for_hash)
|
|
35
|
+
|
|
36
|
+
g_b = (b_int - kg_x) % p_int
|
|
37
|
+
ux = u * x
|
|
38
|
+
a_ux = a_int + ux
|
|
39
|
+
s_int = mod_pow(g_b, a_ux, p_int)
|
|
40
|
+
k_key = sha256(int_to_256_bytes(s_int))
|
|
41
|
+
|
|
42
|
+
m1 = sha256(
|
|
43
|
+
xor(sha256(p_for_hash), sha256(g_for_hash)),
|
|
44
|
+
sha256(salt1),
|
|
45
|
+
sha256(salt2),
|
|
46
|
+
a_for_hash,
|
|
47
|
+
b_for_hash,
|
|
48
|
+
k_key
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
{ srp_id: srp_id, a: a_for_hash, m1: m1 }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def compute_hash(salt1, salt2, password)
|
|
55
|
+
hash1 = sha256(salt1, password.encode('utf-8'), salt1)
|
|
56
|
+
hash2 = sha256(salt2, hash1, salt2)
|
|
57
|
+
hash3 = pbkdf2_sha512(hash2, salt1, 100_000)
|
|
58
|
+
sha256(salt2, hash3, salt2)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sha256(*parts)
|
|
62
|
+
digest = OpenSSL::Digest.new('SHA256')
|
|
63
|
+
parts.each { |p| digest.update(p) }
|
|
64
|
+
digest.digest
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pbkdf2_sha512(password, salt, iterations)
|
|
68
|
+
OpenSSL::KDF.pbkdf2_hmac(
|
|
69
|
+
password,
|
|
70
|
+
salt: salt,
|
|
71
|
+
iterations: iterations,
|
|
72
|
+
length: 64,
|
|
73
|
+
hash: 'SHA512'
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def mod_pow(base, exp, mod)
|
|
78
|
+
base.to_bn.mod_exp(exp.to_bn, mod.to_bn).to_i
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_and_check_random(g, p, b_for_hash)
|
|
82
|
+
loop do
|
|
83
|
+
random = SecureRandom.random_bytes(256)
|
|
84
|
+
a_int = bytes_to_int(random)
|
|
85
|
+
a_big = mod_pow(g, a_int, p)
|
|
86
|
+
a_for_hash = int_to_256_bytes(a_big)
|
|
87
|
+
u = bytes_to_int(sha256(a_for_hash, b_for_hash))
|
|
88
|
+
next if u.zero?
|
|
89
|
+
|
|
90
|
+
return [a_int, a_for_hash, u]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pad_to_256(bytes)
|
|
95
|
+
bytes = bytes.b
|
|
96
|
+
return bytes if bytes.bytesize >= SIZE_FOR_HASH
|
|
97
|
+
|
|
98
|
+
("\x00" * (SIZE_FOR_HASH - bytes.bytesize)).b + bytes
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def int_to_256_bytes(num)
|
|
102
|
+
hex = num.to_s(16)
|
|
103
|
+
hex = "0#{hex}" if hex.length.odd?
|
|
104
|
+
raw = [hex].pack('H*')
|
|
105
|
+
pad_to_256(raw)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bytes_to_int(bytes)
|
|
109
|
+
bytes.unpack1('H*').to_i(16)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def xor(a, b)
|
|
113
|
+
a.bytes.zip(b.bytes).map { |x, y| x ^ y }.pack('C*')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DelegateMethods
|
|
4
|
+
def delegate(*methods, to:)
|
|
5
|
+
methods.each do |method|
|
|
6
|
+
define_method(method) do |*args, **kwargs, &block|
|
|
7
|
+
instance_variable_get(:"@#{to}").public_send(method, *args, **kwargs, &block)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|