mtproto 0.0.5 → 0.0.7
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 +13 -0
- data/lib/mtproto/auth_key_generator.rb +36 -13
- data/lib/mtproto/client/rpc.rb +141 -0
- data/lib/mtproto/client.rb +60 -185
- data/lib/mtproto/crypto/aes_ige.rb +1 -1
- data/lib/mtproto/crypto/factorization.rb +1 -1
- data/lib/mtproto/crypto/rsa_key.rb +9 -15
- data/lib/mtproto/errors.rb +33 -0
- data/lib/mtproto/message_id.rb +13 -0
- data/lib/mtproto/rpc/get_config.rb +37 -0
- data/lib/mtproto/rpc/get_contacts.rb +22 -0
- data/lib/mtproto/rpc/get_updates_difference.rb +33 -0
- data/lib/mtproto/rpc/get_updates_state.rb +22 -0
- data/lib/mtproto/rpc/get_users.rb +22 -0
- data/lib/mtproto/rpc/ping.rb +26 -0
- data/lib/mtproto/rpc/send_code.rb +44 -0
- data/lib/mtproto/rpc/send_message.rb +31 -0
- data/lib/mtproto/rpc/sign_in.rb +52 -0
- data/lib/mtproto/tl/auth_key/dh_gen_response.rb +37 -0
- data/lib/mtproto/tl/auth_key/req_dh_params.rb +31 -0
- data/lib/mtproto/tl/auth_key/req_pq_multi.rb +18 -0
- data/lib/mtproto/tl/auth_key/res_pq.rb +62 -0
- data/lib/mtproto/tl/auth_key/server_dh_params.rb +43 -0
- data/lib/mtproto/tl/auth_key/set_client_dh_params.rb +25 -0
- data/lib/mtproto/tl/code_settings.rb +25 -0
- data/lib/mtproto/tl/config.rb +4 -2
- data/lib/mtproto/tl/gzip_packed.rb +1 -1
- data/lib/mtproto/tl/message.rb +8 -216
- data/lib/mtproto/tl/method_builder.rb +29 -0
- data/lib/mtproto/tl/rpc/auth/authorization.rb +107 -0
- data/lib/mtproto/tl/rpc/auth/send_code.rb +28 -0
- data/lib/mtproto/tl/rpc/auth/sent_code.rb +36 -0
- data/lib/mtproto/tl/rpc/auth/sign_in.rb +32 -0
- data/lib/mtproto/tl/rpc/contacts/contacts.rb +155 -0
- data/lib/mtproto/tl/rpc/contacts/get_contacts.rb +18 -0
- data/lib/mtproto/tl/rpc/help/config.rb +35 -0
- data/lib/mtproto/tl/rpc/help/get_config.rb +17 -0
- data/lib/mtproto/tl/rpc/messages/send_message.rb +43 -0
- data/lib/mtproto/tl/rpc/messages/updates.rb +87 -0
- data/lib/mtproto/tl/rpc/ping.rb +18 -0
- data/lib/mtproto/tl/rpc/pong.rb +46 -0
- data/lib/mtproto/tl/rpc/updates/difference.rb +332 -0
- data/lib/mtproto/tl/rpc/updates/get_difference.rb +42 -0
- data/lib/mtproto/tl/rpc/updates/get_state.rb +17 -0
- data/lib/mtproto/tl/rpc/updates/state.rb +59 -0
- data/lib/mtproto/tl/rpc/users/get_users.rb +25 -0
- data/lib/mtproto/tl/rpc/users/users.rb +99 -0
- data/lib/mtproto/tl/sent_code.rb +128 -0
- data/lib/mtproto/transport/tcp_connection.rb +1 -1
- data/lib/mtproto/updates_poller.rb +111 -0
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +13 -0
- metadata +57 -6
- data/ext/aes_ige/Makefile +0 -273
- data/ext/aes_ige/aes_ige.bundle +0 -0
- data/ext/factorization/Makefile +0 -273
- data/ext/factorization/factorization.bundle +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2744b720c59f90c3e9d18021ce6096a1c8e559dae949fbd6a9b53948c14f8b5c
|
|
4
|
+
data.tar.gz: 3b9000cabc247db305244b279bc58474466e1549cca3efa07ca1202eaa2d7874
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 874a17be8f75bb305057d0a0cb2c136441f9e8da883c44a99169ad0975c5b745b4323abc84ab6edb96b72f2c8f500ec40536a1a6724f585730501514a2e7475b
|
|
7
|
+
data.tar.gz: 6a693436a7443f789cb73a6fe6ecebf523ee288bb174b297fdfac4274340c9a5a00544299d08f0558028645e4cf80dbfb0c70c844ec47d2bb6e413589a45a5b2
|
data/.env.example
ADDED
data/Rakefile
CHANGED
|
@@ -14,7 +14,20 @@ end
|
|
|
14
14
|
|
|
15
15
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
16
16
|
t.rspec_opts = '--require spec_helper'
|
|
17
|
+
t.pattern = 'spec/**/*_spec.rb'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RSpec::Core::RakeTask.new('spec:unit') do |t|
|
|
21
|
+
t.rspec_opts = '--require spec_helper'
|
|
22
|
+
t.pattern = 'spec/lib/**/*_spec.rb'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RSpec::Core::RakeTask.new('spec:integration') do |t|
|
|
26
|
+
t.rspec_opts = '--require spec_helper'
|
|
27
|
+
t.pattern = 'spec/integration/**/*_spec.rb'
|
|
17
28
|
end
|
|
18
29
|
|
|
19
30
|
task spec: :compile
|
|
31
|
+
task 'spec:unit': :compile
|
|
32
|
+
task 'spec:integration': :compile
|
|
20
33
|
task default: :spec
|
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'digest'
|
|
5
|
+
require_relative 'message_id'
|
|
6
|
+
require_relative 'tl/auth_key/req_pq_multi'
|
|
7
|
+
require_relative 'tl/auth_key/res_pq'
|
|
8
|
+
require_relative 'tl/auth_key/req_dh_params'
|
|
9
|
+
require_relative 'tl/auth_key/server_dh_params'
|
|
10
|
+
require_relative 'tl/auth_key/set_client_dh_params'
|
|
11
|
+
require_relative 'tl/auth_key/dh_gen_response'
|
|
5
12
|
|
|
6
13
|
module MTProto
|
|
7
14
|
class AuthKeyGenerator
|
|
8
|
-
attr_reader :connection, :auth_key, :server_salt, :time_offset
|
|
15
|
+
attr_reader :connection, :auth_key, :server_salt, :time_offset, :timeout
|
|
16
|
+
|
|
17
|
+
def initialize(connection, public_key, dc_number = nil, test_mode: false, timeout: 10)
|
|
18
|
+
raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
|
|
19
|
+
raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
|
|
9
20
|
|
|
10
|
-
def initialize(connection)
|
|
11
21
|
@connection = connection
|
|
22
|
+
@public_key = public_key
|
|
23
|
+
@dc_number = dc_number
|
|
24
|
+
@test_mode = test_mode
|
|
25
|
+
@timeout = timeout
|
|
12
26
|
@auth_key = nil
|
|
13
27
|
@server_salt = nil
|
|
14
28
|
@time_offset = 0
|
|
@@ -73,12 +87,13 @@ module MTProto
|
|
|
73
87
|
|
|
74
88
|
def req_pq_multi
|
|
75
89
|
nonce = SecureRandom.random_bytes(16)
|
|
76
|
-
|
|
90
|
+
body = TL::AuthKey::ReqPqMulti.build(nonce)
|
|
91
|
+
message = TL::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
|
|
77
92
|
@connection.send(message.serialize)
|
|
78
93
|
|
|
79
|
-
response_data = @connection.recv(timeout:
|
|
94
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
80
95
|
response_message = TL::Message.deserialize(response_data)
|
|
81
|
-
res_pq = response_message.
|
|
96
|
+
res_pq = TL::AuthKey::ResPq.parse(response_message.body)
|
|
82
97
|
|
|
83
98
|
raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
|
|
84
99
|
|
|
@@ -86,13 +101,19 @@ module MTProto
|
|
|
86
101
|
end
|
|
87
102
|
|
|
88
103
|
def find_server_key(fingerprints)
|
|
89
|
-
server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints)
|
|
104
|
+
server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints, @public_key)
|
|
90
105
|
raise 'No matching RSA key found!' unless server_key
|
|
91
106
|
|
|
92
107
|
server_key
|
|
93
108
|
end
|
|
94
109
|
|
|
95
110
|
def encrypt_pq_inner_data(res_pq, p, q, server_key, new_nonce)
|
|
111
|
+
raise ArgumentError, "dc_number is required for auth key generation" if @dc_number.nil?
|
|
112
|
+
|
|
113
|
+
# For test DCs, add 10000 to the DC number per MTProto spec
|
|
114
|
+
# For production DCs, use the DC number as-is
|
|
115
|
+
dc_value = @test_mode ? (@dc_number + 10000) : @dc_number
|
|
116
|
+
|
|
96
117
|
inner_data = TL::PQInnerData.new(
|
|
97
118
|
pq: Crypto::Factorization.bytes_to_integer(res_pq[:pq]),
|
|
98
119
|
p: p,
|
|
@@ -100,14 +121,14 @@ module MTProto
|
|
|
100
121
|
nonce: res_pq[:nonce],
|
|
101
122
|
server_nonce: res_pq[:server_nonce],
|
|
102
123
|
new_nonce: new_nonce,
|
|
103
|
-
dc:
|
|
124
|
+
dc: dc_value
|
|
104
125
|
)
|
|
105
126
|
|
|
106
127
|
Crypto::RSA_PAD.encrypt(inner_data.serialize, server_key)
|
|
107
128
|
end
|
|
108
129
|
|
|
109
130
|
def send_req_dh_params(res_pq, p, q, server_key, encrypted_data)
|
|
110
|
-
|
|
131
|
+
body = TL::AuthKey::ReqDHParams.build(
|
|
111
132
|
nonce: res_pq[:nonce],
|
|
112
133
|
server_nonce: res_pq[:server_nonce],
|
|
113
134
|
p: p,
|
|
@@ -115,12 +136,13 @@ module MTProto
|
|
|
115
136
|
public_key_fingerprint: server_key.fingerprint,
|
|
116
137
|
encrypted_data: encrypted_data
|
|
117
138
|
)
|
|
139
|
+
message = TL::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
|
|
118
140
|
|
|
119
141
|
@connection.send(message.serialize)
|
|
120
142
|
|
|
121
|
-
response_data = @connection.recv(timeout:
|
|
143
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
122
144
|
response_message = TL::Message.deserialize(response_data)
|
|
123
|
-
server_dh_params = response_message.
|
|
145
|
+
server_dh_params = TL::AuthKey::ServerDHParams.parse(response_message.body)
|
|
124
146
|
|
|
125
147
|
raise 'Nonce mismatch!' unless server_dh_params[:nonce] == res_pq[:nonce]
|
|
126
148
|
raise 'Server nonce mismatch!' unless server_dh_params[:server_nonce] == res_pq[:server_nonce]
|
|
@@ -178,17 +200,18 @@ module MTProto
|
|
|
178
200
|
tmp_aes_iv
|
|
179
201
|
)
|
|
180
202
|
|
|
181
|
-
|
|
203
|
+
body = TL::AuthKey::SetClientDHParams.build(
|
|
182
204
|
nonce: res_pq[:nonce],
|
|
183
205
|
server_nonce: res_pq[:server_nonce],
|
|
184
206
|
encrypted_data: client_dh_encrypted
|
|
185
207
|
)
|
|
208
|
+
message = TL::Message.new(auth_key_id: 0, msg_id: MessageId.generate, body: body)
|
|
186
209
|
|
|
187
210
|
@connection.send(message.serialize)
|
|
188
211
|
|
|
189
|
-
response_data = @connection.recv(timeout:
|
|
212
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
190
213
|
response_message = TL::Message.deserialize(response_data)
|
|
191
|
-
response_message.
|
|
214
|
+
TL::AuthKey::DHGenResponse.parse(response_message.body)
|
|
192
215
|
end
|
|
193
216
|
|
|
194
217
|
def verify_dh_gen_response(dh_gen_response, res_pq, new_nonce, auth_key)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../message_id'
|
|
4
|
+
|
|
5
|
+
module MTProto
|
|
6
|
+
class Client
|
|
7
|
+
class RPC
|
|
8
|
+
CONSTRUCTOR_RPC_RESULT = 0xf35c6d01
|
|
9
|
+
CONSTRUCTOR_MSGS_ACK = 0x62d6b459
|
|
10
|
+
CONSTRUCTOR_BAD_SERVER_SALT = 0xedab447b
|
|
11
|
+
|
|
12
|
+
def initialize(client)
|
|
13
|
+
@client = client
|
|
14
|
+
@connection_initialized = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(body, content_related: true)
|
|
18
|
+
raise 'Auth key not generated' unless @client.auth_key
|
|
19
|
+
raise 'Session not initialized' unless @client.session
|
|
20
|
+
|
|
21
|
+
msg_id = MessageId.generate(time_offset: @client.time_offset)
|
|
22
|
+
seq_no = @client.session.next_seq_no(content_related: content_related)
|
|
23
|
+
|
|
24
|
+
encrypted_msg = EncryptedMessage.encrypt(
|
|
25
|
+
auth_key: @client.auth_key,
|
|
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
|
+
)
|
|
32
|
+
|
|
33
|
+
@client.connection.send(encrypted_msg.serialize)
|
|
34
|
+
|
|
35
|
+
response_data = @client.connection.recv(timeout: @client.timeout)
|
|
36
|
+
|
|
37
|
+
decrypted = EncryptedMessage.decrypt(
|
|
38
|
+
auth_key: @client.auth_key,
|
|
39
|
+
encrypted_message_data: response_data,
|
|
40
|
+
sender: :server
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
response_body = decrypted[:body]
|
|
44
|
+
|
|
45
|
+
constructor = response_body[0, 4].unpack1('L<')
|
|
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
|
|
63
|
+
|
|
64
|
+
if constructor == TL::NewSessionCreated::CONSTRUCTOR
|
|
65
|
+
session_info = TL::NewSessionCreated.deserialize(response_body)
|
|
66
|
+
update_server_salt(session_info.server_salt)
|
|
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
|
|
77
|
+
|
|
78
|
+
if constructor == TL::MsgContainer::CONSTRUCTOR
|
|
79
|
+
container = TL::MsgContainer.deserialize(response_body)
|
|
80
|
+
|
|
81
|
+
rpc_result = container.messages.find do |msg|
|
|
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]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return response_body[12..] if constructor == CONSTRUCTOR_RPC_RESULT
|
|
130
|
+
|
|
131
|
+
response_body
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def update_server_salt(server_salt)
|
|
137
|
+
@client.instance_variable_set(:@server_salt, server_salt)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/mtproto/client.rb
CHANGED
|
@@ -2,64 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'digest'
|
|
5
|
+
require 'base64'
|
|
5
6
|
require_relative 'transport/tcp_connection'
|
|
6
7
|
require_relative 'transport/abridged_packet_codec'
|
|
7
8
|
require_relative 'tl/message'
|
|
9
|
+
require_relative 'client/rpc'
|
|
10
|
+
require_relative 'tl/method_builder'
|
|
8
11
|
|
|
9
12
|
module MTProto
|
|
10
13
|
class Client
|
|
11
|
-
|
|
12
|
-
1 => ['149.154.175.50', 443],
|
|
13
|
-
2 => ['149.154.167.51', 443],
|
|
14
|
-
3 => ['149.154.175.100', 443],
|
|
15
|
-
4 => ['149.154.167.91', 443],
|
|
16
|
-
5 => ['91.108.56.130', 443]
|
|
17
|
-
}.freeze
|
|
14
|
+
include TL::MethodBuilder
|
|
18
15
|
|
|
19
|
-
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session
|
|
16
|
+
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session, :timeout, :user_id, :access_hash, :dc_number
|
|
17
|
+
attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
|
|
20
18
|
|
|
21
|
-
def initialize(
|
|
22
|
-
|
|
23
|
-
raise ArgumentError, "
|
|
19
|
+
def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key: nil, dc_number: nil, test_mode: false, timeout: 10)
|
|
20
|
+
raise ArgumentError, "host is required" if host.nil? || host.empty?
|
|
21
|
+
raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
|
|
24
22
|
|
|
25
23
|
codec = Transport::AbridgedPacketCodec.new
|
|
26
24
|
@connection = Transport::TCPConnection.new(host, port, codec)
|
|
25
|
+
@public_key = public_key
|
|
26
|
+
@dc_number = dc_number
|
|
27
|
+
@test_mode = test_mode
|
|
28
|
+
@timeout = timeout
|
|
27
29
|
@server_key = nil
|
|
28
30
|
@auth_key = nil
|
|
29
31
|
@server_salt = nil
|
|
30
32
|
@time_offset = 0
|
|
31
33
|
@session = nil
|
|
32
34
|
@connection_initialized = false
|
|
35
|
+
@user_id = nil
|
|
36
|
+
@access_hash = nil
|
|
37
|
+
|
|
38
|
+
# Client configuration defaults
|
|
39
|
+
@api_id = api_id || 0
|
|
40
|
+
@api_hash = api_hash || ''
|
|
41
|
+
@device_model = 'Ruby MTProto'
|
|
42
|
+
@system_version = RUBY_DESCRIPTION
|
|
43
|
+
@app_version = '0.1.0'
|
|
44
|
+
@system_lang_code = 'en'
|
|
45
|
+
@lang_pack = ''
|
|
46
|
+
@lang_code = 'en'
|
|
33
47
|
end
|
|
34
48
|
|
|
35
|
-
def connect
|
|
36
|
-
@connection.connect
|
|
49
|
+
def connect!
|
|
50
|
+
@connection.connect!
|
|
37
51
|
end
|
|
38
52
|
|
|
39
|
-
def disconnect
|
|
40
|
-
@connection.close
|
|
53
|
+
def disconnect!
|
|
54
|
+
@connection.close if @connection
|
|
41
55
|
end
|
|
42
56
|
|
|
43
|
-
def
|
|
44
|
-
|
|
57
|
+
def exchange_keys!
|
|
58
|
+
raise ArgumentError, "public_key is required for auth key generation" if @public_key.nil? || @public_key.empty?
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
payload = message.serialize
|
|
48
|
-
|
|
49
|
-
@connection.send(payload)
|
|
50
|
-
|
|
51
|
-
response_data = @connection.recv(timeout: 10)
|
|
52
|
-
response_message = TL::Message.deserialize(response_data)
|
|
53
|
-
|
|
54
|
-
res_pq = response_message.parse_res_pq
|
|
55
|
-
|
|
56
|
-
raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
|
|
57
|
-
|
|
58
|
-
res_pq
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def make_auth_key
|
|
62
|
-
generator = AuthKeyGenerator.new(@connection)
|
|
60
|
+
generator = AuthKeyGenerator.new(@connection, @public_key, @dc_number, test_mode: @test_mode, timeout: @timeout)
|
|
63
61
|
result = generator.generate
|
|
64
62
|
|
|
65
63
|
@auth_key = generator.auth_key
|
|
@@ -70,166 +68,43 @@ module MTProto
|
|
|
70
68
|
result
|
|
71
69
|
end
|
|
72
70
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
msg_id = (time * (2**32)).to_i
|
|
76
|
-
(msg_id / 4) * 4
|
|
71
|
+
def rpc
|
|
72
|
+
@rpc ||= RPC.new(self)
|
|
77
73
|
end
|
|
78
74
|
|
|
79
|
-
def
|
|
80
|
-
raise '
|
|
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)
|
|
75
|
+
def save_session
|
|
76
|
+
raise 'Cannot save session: auth_key not set' unless @auth_key
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
auth_key: @auth_key,
|
|
78
|
+
{
|
|
79
|
+
auth_key: Base64.strict_encode64(@auth_key),
|
|
88
80
|
server_salt: @server_salt,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
81
|
+
user_id: @user_id,
|
|
82
|
+
access_hash: @access_hash,
|
|
83
|
+
dc_number: @dc_number,
|
|
84
|
+
time_offset: @time_offset
|
|
85
|
+
}
|
|
191
86
|
end
|
|
192
87
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
88
|
+
def load_session(session_data)
|
|
89
|
+
raise ArgumentError, 'session_data must be a Hash' unless session_data.is_a?(Hash)
|
|
90
|
+
raise ArgumentError, 'auth_key is required' unless session_data[:auth_key]
|
|
91
|
+
raise ArgumentError, 'server_salt is required' unless session_data[:server_salt]
|
|
92
|
+
|
|
93
|
+
@auth_key = Base64.strict_decode64(session_data[:auth_key])
|
|
94
|
+
@server_salt = session_data[:server_salt]
|
|
95
|
+
@time_offset = session_data[:time_offset] || 0
|
|
96
|
+
@user_id = session_data[:user_id]
|
|
97
|
+
@access_hash = session_data[:access_hash]
|
|
98
|
+
|
|
99
|
+
# Create a fresh session for the new connection
|
|
100
|
+
@session = Session.new
|
|
199
101
|
|
|
200
|
-
|
|
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
|
|
102
|
+
true
|
|
213
103
|
end
|
|
214
104
|
|
|
215
|
-
def
|
|
216
|
-
|
|
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)
|
|
105
|
+
def update_user(user_id:, access_hash: nil)
|
|
106
|
+
@user_id = user_id
|
|
107
|
+
@access_hash = access_hash
|
|
233
108
|
end
|
|
234
109
|
end
|
|
235
110
|
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
|