mtproto 0.0.2 → 0.0.4
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/lib/mtproto/client.rb +58 -0
- data/lib/mtproto/crypto/rsa_key.rb +66 -0
- data/lib/mtproto/tl/message.rb +106 -0
- data/lib/mtproto/transport/tcp_connection.rb +91 -0
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +4 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 414d6f7ed0a5c1c20c5108844469daafff2af7d2c350c8370b2eb6cec73068fe
|
|
4
|
+
data.tar.gz: d7c8e3397861bdc4f9d3c695ba884a672698f39c051c06271e5e39dcdf0aa7bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ccbe16eaf853697c6267ebe6b84e8593c071a66d95de9b1a1be1cafb0e33317400fb52fe85ef7aec43da2aa69bbc3f4cbc55af25928116711518925f4ed7897
|
|
7
|
+
data.tar.gz: 9658e4c8bd82fed68fd43f40e35efb1a57742170d27aa5f6a641528628e6da0013dfa9ddb632f42cb6e5418cd82c3006b8a4624294ebc40567c08d636aeae96b
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'transport/tcp_connection'
|
|
5
|
+
require_relative 'transport/abridged_packet_codec'
|
|
6
|
+
require_relative 'tl/message'
|
|
7
|
+
|
|
8
|
+
module MTProto
|
|
9
|
+
class Client
|
|
10
|
+
DC_ADDRESSES = {
|
|
11
|
+
1 => ['149.154.175.50', 443],
|
|
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
|
|
17
|
+
|
|
18
|
+
attr_reader :connection
|
|
19
|
+
|
|
20
|
+
def initialize(dc_id: 2)
|
|
21
|
+
host, port = DC_ADDRESSES[dc_id]
|
|
22
|
+
raise ArgumentError, "Unknown DC ID: #{dc_id}" unless host
|
|
23
|
+
|
|
24
|
+
codec = Transport::AbridgedPacketCodec.new
|
|
25
|
+
@connection = Transport::TCPConnection.new(host, port, codec)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def connect
|
|
29
|
+
@connection.connect
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def disconnect
|
|
33
|
+
@connection.close
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def req_pq_multi
|
|
37
|
+
nonce = SecureRandom.random_bytes(16)
|
|
38
|
+
|
|
39
|
+
message = TL::Message.req_pq_multi(nonce)
|
|
40
|
+
payload = message.serialize
|
|
41
|
+
|
|
42
|
+
@connection.send(payload)
|
|
43
|
+
|
|
44
|
+
response_data = @connection.recv(timeout: 10)
|
|
45
|
+
response_message = TL::Message.deserialize(response_data)
|
|
46
|
+
|
|
47
|
+
res_pq = response_message.parse_res_pq
|
|
48
|
+
|
|
49
|
+
raise 'Nonce mismatch!' unless res_pq[:nonce] == nonce
|
|
50
|
+
|
|
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
|
+
res_pq
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'digest'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
module Crypto
|
|
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
|
+
attr_reader :key, :fingerprint
|
|
21
|
+
|
|
22
|
+
def initialize(pem_string)
|
|
23
|
+
@key = OpenSSL::PKey::RSA.new(pem_string)
|
|
24
|
+
@fingerprint = calculate_fingerprint
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.telegram_key
|
|
28
|
+
@telegram_key ||= new(TELEGRAM_KEY)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.find_by_fingerprint(fingerprints)
|
|
32
|
+
telegram_key if fingerprints.include?(telegram_key.fingerprint)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def calculate_fingerprint
|
|
38
|
+
n_bytes = @key.n.to_s(2).unpack1('B*').scan(/.{8}/).map { |b| b.to_i(2) }.pack('C*')
|
|
39
|
+
e_bytes = @key.e.to_s(2).unpack1('B*').scan(/.{8}/).map { |b| b.to_i(2) }.pack('C*')
|
|
40
|
+
|
|
41
|
+
n_tl = serialize_bytes(n_bytes)
|
|
42
|
+
e_tl = serialize_bytes(e_bytes)
|
|
43
|
+
|
|
44
|
+
data = n_tl + e_tl
|
|
45
|
+
sha1 = Digest::SHA1.digest(data)
|
|
46
|
+
|
|
47
|
+
sha1[-8..].unpack1('Q<')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serialize_bytes(bytes)
|
|
51
|
+
length = bytes.bytesize
|
|
52
|
+
|
|
53
|
+
if length <= 253
|
|
54
|
+
[length].pack('C') + bytes + padding(length + 1)
|
|
55
|
+
else
|
|
56
|
+
[254].pack('C') + [length].pack('L<')[0, 3] + bytes + padding(length + 4)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def padding(current_length)
|
|
61
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
62
|
+
"\x00" * pad_length
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class Message
|
|
6
|
+
CONSTRUCTOR_REQ_PQ_MULTI = 0xbe7e8ef1
|
|
7
|
+
CONSTRUCTOR_RES_PQ = 0x05162463
|
|
8
|
+
|
|
9
|
+
attr_reader :auth_key_id, :msg_id, :body
|
|
10
|
+
|
|
11
|
+
def initialize(auth_key_id: 0, msg_id: nil, body: '')
|
|
12
|
+
@auth_key_id = auth_key_id
|
|
13
|
+
@msg_id = msg_id || generate_msg_id
|
|
14
|
+
@body = body
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def serialize
|
|
18
|
+
auth_key_id_bytes = [@auth_key_id].pack('Q<')
|
|
19
|
+
msg_id_bytes = [@msg_id].pack('Q<')
|
|
20
|
+
body_length = [@body.bytesize].pack('L<')
|
|
21
|
+
|
|
22
|
+
auth_key_id_bytes + msg_id_bytes + body_length + @body
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.req_pq_multi(nonce)
|
|
26
|
+
raise ArgumentError, 'Nonce must be 16 bytes' unless nonce.bytesize == 16
|
|
27
|
+
|
|
28
|
+
constructor = [CONSTRUCTOR_REQ_PQ_MULTI].pack('L<')
|
|
29
|
+
body = constructor + nonce
|
|
30
|
+
|
|
31
|
+
new(auth_key_id: 0, body: body)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.deserialize(data)
|
|
35
|
+
auth_key_id = data[0, 8].unpack1('Q<')
|
|
36
|
+
msg_id = data[8, 8].unpack1('Q<')
|
|
37
|
+
body_length = data[16, 4].unpack1('L<')
|
|
38
|
+
body = data[20, body_length]
|
|
39
|
+
|
|
40
|
+
new(auth_key_id: auth_key_id, msg_id: msg_id, body: body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_res_pq
|
|
44
|
+
constructor = @body[0, 4].unpack1('L<')
|
|
45
|
+
raise "Unexpected constructor: 0x#{constructor.to_s(16)}" unless constructor == CONSTRUCTOR_RES_PQ
|
|
46
|
+
|
|
47
|
+
offset = 4
|
|
48
|
+
|
|
49
|
+
nonce = @body[offset, 16]
|
|
50
|
+
offset += 16
|
|
51
|
+
|
|
52
|
+
server_nonce = @body[offset, 16]
|
|
53
|
+
offset += 16
|
|
54
|
+
|
|
55
|
+
pq_length_byte = @body[offset].unpack1('C')
|
|
56
|
+
offset += 1
|
|
57
|
+
|
|
58
|
+
pq_length = if pq_length_byte == 254
|
|
59
|
+
@body[offset, 3].unpack1('L<') & 0xffffff
|
|
60
|
+
offset += 3
|
|
61
|
+
else
|
|
62
|
+
pq_length_byte
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
pq = @body[offset, pq_length]
|
|
66
|
+
offset += pq_length
|
|
67
|
+
offset += padding_length(pq_length + 1)
|
|
68
|
+
|
|
69
|
+
vector_constructor = @body[offset, 4].unpack1('L<')
|
|
70
|
+
offset += 4
|
|
71
|
+
raise 'Expected vector constructor' unless vector_constructor == 0x1cb5c415
|
|
72
|
+
|
|
73
|
+
fingerprints_count = @body[offset, 4].unpack1('L<')
|
|
74
|
+
offset += 4
|
|
75
|
+
|
|
76
|
+
fingerprints = []
|
|
77
|
+
fingerprints_count.times do
|
|
78
|
+
fingerprints << @body[offset, 8].unpack1('Q<')
|
|
79
|
+
offset += 8
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
nonce: nonce,
|
|
84
|
+
server_nonce: server_nonce,
|
|
85
|
+
pq: pq,
|
|
86
|
+
fingerprints: fingerprints
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def generate_msg_id
|
|
93
|
+
time_ns = (Time.now.to_f * 1_000_000_000).to_i
|
|
94
|
+
(time_ns / 4) * 4
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def padding_length(length)
|
|
98
|
+
(4 - (length % 4)) % 4
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.padding_length(length)
|
|
102
|
+
(4 - (length % 4)) % 4
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module MTProto
|
|
7
|
+
module Transport
|
|
8
|
+
class ConnectionError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class TCPConnection
|
|
11
|
+
attr_reader :host, :port, :codec
|
|
12
|
+
|
|
13
|
+
def initialize(host, port, codec)
|
|
14
|
+
@host = host
|
|
15
|
+
@port = port
|
|
16
|
+
@codec = codec
|
|
17
|
+
@socket = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connect
|
|
21
|
+
return if connected?
|
|
22
|
+
|
|
23
|
+
@socket = TCPSocket.new(@host, @port)
|
|
24
|
+
|
|
25
|
+
send_init_tag if @codec.class.const_defined?(:TAG)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def connected?
|
|
29
|
+
!@socket.nil? && !@socket.closed?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def send(data)
|
|
33
|
+
raise ConnectionError, 'Not connected' unless connected?
|
|
34
|
+
|
|
35
|
+
encoded = @codec.encode_packet(data)
|
|
36
|
+
@socket.write(encoded)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def recv(timeout: 60)
|
|
40
|
+
raise ConnectionError, 'Not connected' unless connected?
|
|
41
|
+
|
|
42
|
+
Timeout.timeout(timeout) do
|
|
43
|
+
read_packet
|
|
44
|
+
end
|
|
45
|
+
rescue Timeout::Error
|
|
46
|
+
raise ConnectionError, 'Receive timeout'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
return unless @socket
|
|
51
|
+
|
|
52
|
+
@socket.close
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
ensure
|
|
56
|
+
@socket = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def send_init_tag
|
|
62
|
+
tag = @codec.class.const_get(:TAG)
|
|
63
|
+
@socket.write(tag) if tag
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_packet
|
|
67
|
+
first_byte = read_exactly(1)
|
|
68
|
+
length = first_byte.unpack1('C')
|
|
69
|
+
|
|
70
|
+
if length >= 127
|
|
71
|
+
length_bytes = read_exactly(3)
|
|
72
|
+
length = (length_bytes + "\x00").unpack1('L<')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
actual_length = length << 2
|
|
76
|
+
read_exactly(actual_length)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def read_exactly(bytes_needed)
|
|
80
|
+
result = ''.b
|
|
81
|
+
while result.bytesize < bytes_needed
|
|
82
|
+
chunk = @socket.read(bytes_needed - result.bytesize)
|
|
83
|
+
raise ConnectionError, 'EOF while reading' if chunk.nil? || chunk.empty?
|
|
84
|
+
|
|
85
|
+
result += chunk
|
|
86
|
+
end
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/mtproto/version.rb
CHANGED
data/lib/mtproto.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'mtproto/version'
|
|
4
4
|
require_relative 'mtproto/transport/abridged_packet_codec'
|
|
5
|
+
require_relative 'mtproto/transport/tcp_connection'
|
|
6
|
+
require_relative 'mtproto/tl/message'
|
|
7
|
+
require_relative 'mtproto/crypto/rsa_key'
|
|
8
|
+
require_relative 'mtproto/client'
|
|
5
9
|
|
|
6
10
|
module MTProto
|
|
7
11
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mtproto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artem Levenkov
|
|
@@ -20,7 +20,11 @@ files:
|
|
|
20
20
|
- ".ruby-version"
|
|
21
21
|
- Rakefile
|
|
22
22
|
- lib/mtproto.rb
|
|
23
|
+
- lib/mtproto/client.rb
|
|
24
|
+
- lib/mtproto/crypto/rsa_key.rb
|
|
25
|
+
- lib/mtproto/tl/message.rb
|
|
23
26
|
- lib/mtproto/transport/abridged_packet_codec.rb
|
|
27
|
+
- lib/mtproto/transport/tcp_connection.rb
|
|
24
28
|
- lib/mtproto/version.rb
|
|
25
29
|
homepage: https://github.com/alev-pro/mtproto-ruby
|
|
26
30
|
licenses:
|