mtproto 0.0.3 → 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/version.rb +1 -1
- data/lib/mtproto.rb +3 -0
- metadata +4 -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
|
data/lib/mtproto/version.rb
CHANGED
data/lib/mtproto.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative 'mtproto/version'
|
|
4
4
|
require_relative 'mtproto/transport/abridged_packet_codec'
|
|
5
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'
|
|
6
9
|
|
|
7
10
|
module MTProto
|
|
8
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,6 +20,9 @@ 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
|
|
24
27
|
- lib/mtproto/transport/tcp_connection.rb
|
|
25
28
|
- lib/mtproto/version.rb
|