Skip to content

Commit a8cad5a

Browse files
committed
Add session package
1 parent afcd19a commit a8cad5a

File tree

8 files changed

+808
-0
lines changed

8 files changed

+808
-0
lines changed

pyrogram/session/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017 Dan Tès <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from .auth import Auth
20+
from .session import Session

pyrogram/session/auth.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017 Dan Tès <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
import logging
20+
import time
21+
from hashlib import sha1
22+
from io import BytesIO
23+
from os import urandom
24+
25+
from pyrogram.api import functions, types
26+
from pyrogram.api.core import Object, Long, Int
27+
from pyrogram.connection import Connection
28+
from pyrogram.crypto import IGE, RSA, Prime
29+
from .internals import MsgId, DataCenter
30+
31+
log = logging.getLogger(__name__)
32+
33+
34+
# TODO: When using TCP connection mode, the server may close it at any time, causing the Auth key creation to fail
35+
# The above is true when dealing with temporary keys, although for perm keys it didn't happened, yet.
36+
37+
class Auth:
38+
CURRENT_DH_PRIME = int(
39+
"C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F"
40+
"48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C37"
41+
"20FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F64"
42+
"2477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4"
43+
"A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754"
44+
"FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4"
45+
"E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F"
46+
"0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5B",
47+
16
48+
)
49+
50+
def __init__(self, dc_id: int, test_mode: bool):
51+
self.dc_id = dc_id
52+
self.test_mode = test_mode
53+
54+
self.connection = Connection(DataCenter(dc_id, test_mode))
55+
self.msg_id = MsgId()
56+
57+
def pack(self, data: Object) -> bytes:
58+
return (
59+
bytes(8)
60+
+ Long(self.msg_id())
61+
+ Int(len(data.write()))
62+
+ data.write()
63+
)
64+
65+
@staticmethod
66+
def unpack(b: BytesIO):
67+
b.seek(20) # Skip auth_key_id (8), message_id (8) and message_length (4)
68+
return Object.read(b)
69+
70+
def send(self, data: Object):
71+
data = self.pack(data)
72+
self.connection.send(data)
73+
response = BytesIO(self.connection.recv())
74+
75+
return self.unpack(response)
76+
77+
def create(self):
78+
"""
79+
https://core.telegram.org/mtproto/auth_key
80+
https://core.telegram.org/mtproto/samples-auth_key
81+
"""
82+
log.info("Start creating a new auth key on DC{}".format(self.dc_id))
83+
84+
self.connection.connect()
85+
86+
# Step 1; Step 2
87+
nonce = int.from_bytes(urandom(16), "little", signed=True)
88+
log.debug("Send req_pq: {}".format(nonce))
89+
res_pq = self.send(functions.ReqPq(nonce))
90+
log.debug("Got ResPq: {}".format(res_pq.server_nonce))
91+
92+
# Step 3
93+
pq = int.from_bytes(res_pq.pq, "big")
94+
log.debug("Start PQ factorization: {}".format(pq))
95+
start = time.time()
96+
g = Prime.decompose(pq)
97+
p, q = sorted((g, pq // g)) # p < q
98+
log.debug("Done PQ factorization ({}s): {} {}".format(round(time.time() - start, 3), p, q))
99+
100+
# Step 4
101+
server_nonce = res_pq.server_nonce
102+
new_nonce = int.from_bytes(urandom(32), "little", signed=True)
103+
104+
data = types.PQInnerData(
105+
res_pq.pq,
106+
int.to_bytes(p, 4, "big"),
107+
int.to_bytes(q, 4, "big"),
108+
nonce,
109+
server_nonce,
110+
new_nonce,
111+
).write()
112+
113+
sha = sha1(data).digest()
114+
padding = urandom(- (len(data) + len(sha)) % 255)
115+
data_with_hash = sha + data + padding
116+
encrypted_data = RSA.encrypt(data_with_hash, res_pq.server_public_key_fingerprints[0])
117+
118+
log.debug("Done encrypt data with RSA")
119+
120+
# Step 5. TODO: Handle "server_DH_params_fail". Code assumes response is ok
121+
log.debug("Send req_DH_params")
122+
server_dh_params = self.send(
123+
functions.ReqDhParams(
124+
nonce,
125+
server_nonce,
126+
int.to_bytes(p, 4, "big"),
127+
int.to_bytes(q, 4, "big"),
128+
res_pq.server_public_key_fingerprints[0],
129+
encrypted_data
130+
)
131+
)
132+
133+
encrypted_answer = server_dh_params.encrypted_answer
134+
135+
server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True)
136+
new_nonce = int.to_bytes(new_nonce, 32, "little", signed=True)
137+
138+
tmp_aes_key = (
139+
sha1(new_nonce + server_nonce).digest()
140+
+ sha1(server_nonce + new_nonce).digest()[:12]
141+
)
142+
143+
tmp_aes_iv = (
144+
sha1(server_nonce + new_nonce).digest()[12:]
145+
+ sha1(new_nonce + new_nonce).digest() + new_nonce[:4]
146+
)
147+
148+
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
149+
150+
answer_with_hash = IGE.decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv)
151+
answer = answer_with_hash[20:]
152+
153+
server_dh_inner_data = Object.read(BytesIO(answer))
154+
155+
log.debug("Done decrypting answer")
156+
157+
dh_prime = int.from_bytes(server_dh_inner_data.dh_prime, "big")
158+
delta_time = server_dh_inner_data.server_time - time.time()
159+
160+
log.debug("Delta time: {}".format(round(delta_time, 3)))
161+
162+
# Step 6
163+
g = server_dh_inner_data.g
164+
b = int.from_bytes(urandom(256), "big")
165+
g_b = int.to_bytes(pow(g, b, dh_prime), 256, "big")
166+
167+
retry_id = 0
168+
169+
data = types.ClientDhInnerData(
170+
nonce,
171+
server_nonce,
172+
retry_id,
173+
g_b
174+
).write()
175+
176+
sha = sha1(data).digest()
177+
padding = urandom(- (len(data) + len(sha)) % 16)
178+
data_with_hash = sha + data + padding
179+
encrypted_data = IGE.encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv)
180+
181+
log.debug("Send set_client_DH_params")
182+
set_client_dh_params_answer = self.send(
183+
functions.SetClientDhParams(
184+
nonce,
185+
server_nonce,
186+
encrypted_data
187+
)
188+
)
189+
190+
# TODO: Handle "auth_key_aux_hash" if the previous step fails
191+
192+
# Step 7; Step 8
193+
g_a = int.from_bytes(server_dh_inner_data.g_a, "big")
194+
auth_key = int.to_bytes(pow(g_a, b, dh_prime), 256, "big")
195+
server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True)
196+
197+
# TODO: Handle errors
198+
199+
#######################
200+
# Security checks
201+
#######################
202+
203+
assert dh_prime == self.CURRENT_DH_PRIME
204+
log.debug("DH parameters check: OK")
205+
206+
# https://core.telegram.org/mtproto/security_guidelines#g-a-and-g-b-validation
207+
g_b = int.from_bytes(g_b, "big")
208+
assert 1 < g < dh_prime - 1
209+
assert 1 < g_a < dh_prime - 1
210+
assert 1 < g_b < dh_prime - 1
211+
assert 2 ** (2048 - 64) < g_a < dh_prime - 2 ** (2048 - 64)
212+
assert 2 ** (2048 - 64) < g_b < dh_prime - 2 ** (2048 - 64)
213+
log.debug("g_a and g_b validation: OK")
214+
215+
# https://core.telegram.org/mtproto/security_guidelines#checking-sha1-hash-values
216+
answer = server_dh_inner_data.write() # Call .write() to remove padding
217+
assert answer_with_hash[:20] == sha1(answer).digest()
218+
log.debug("SHA1 hash values check: OK")
219+
220+
# https://core.telegram.org/mtproto/security_guidelines#checking-nonce-server-nonce-and-new-nonce-fields
221+
# 1st message
222+
assert nonce == res_pq.nonce
223+
# 2nd message
224+
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
225+
assert nonce == server_dh_params.nonce
226+
assert server_nonce == server_dh_params.server_nonce
227+
# 3rd message
228+
assert nonce == set_client_dh_params_answer.nonce
229+
assert server_nonce == set_client_dh_params_answer.server_nonce
230+
server_nonce = int.to_bytes(server_nonce, 16, "little", signed=True)
231+
log.debug("Nonce fields check: OK")
232+
233+
# Step 9
234+
server_salt = IGE.xor(new_nonce[:8], server_nonce[:8])
235+
236+
log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little")))
237+
238+
log.info(
239+
"Done auth key exchange: {}".format(
240+
set_client_dh_params_answer.__class__.__name__
241+
)
242+
)
243+
244+
self.connection.close()
245+
246+
return auth_key
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017 Dan Tès <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from .data_center import DataCenter
20+
from .msg_factory import MsgFactory
21+
from .msg_id import MsgId
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017 Dan Tès <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
20+
class DataCenter:
21+
TEST = {
22+
1: "149.154.175.10",
23+
2: "149.154.167.40",
24+
3: "149.154.175.117",
25+
}
26+
27+
PROD = {
28+
1: "149.154.175.50",
29+
2: "149.154.167.51",
30+
3: "149.154.175.100",
31+
4: "149.154.167.91",
32+
5: "91.108.56.149"
33+
}
34+
35+
def __new__(cls, dc_id: int, test_mode: bool):
36+
return cls.TEST[dc_id] if test_mode else cls.PROD[dc_id]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017 Dan Tès <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from pyrogram.api.core import Message, MsgContainer, Object
20+
from pyrogram.api.functions import Ping, HttpWait
21+
from pyrogram.api.types import MsgsAck
22+
from .msg_id import MsgId
23+
from .seq_no import SeqNo
24+
25+
not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer]
26+
27+
28+
class MsgFactory:
29+
def __init__(self, msg_id: MsgId):
30+
self.msg_id = msg_id
31+
self.seq_no = SeqNo()
32+
33+
def __call__(self, body: Object) -> Message:
34+
return Message(
35+
body,
36+
self.msg_id(),
37+
self.seq_no(type(body) not in not_content_related),
38+
len(body)
39+
)

0 commit comments

Comments
 (0)