Skip to content

Commit a464ea5

Browse files
committed
Add more tests and comments
1 parent 2be1c8f commit a464ea5

File tree

6 files changed

+85
-26
lines changed

6 files changed

+85
-26
lines changed

telegram/bot.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,10 @@
2626
import warnings
2727
from datetime import datetime
2828

29+
from cryptography.hazmat.backends import default_backend
30+
from cryptography.hazmat.primitives import serialization
2931
from future.utils import string_types
3032

31-
try:
32-
from cryptography.hazmat.backends import default_backend
33-
from cryptography.hazmat.primitives import serialization
34-
35-
CRYPTO = True
36-
except ImportError:
37-
CRYPTO = False
38-
3933
from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File,
4034
ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet,
4135
PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote,
@@ -133,8 +127,7 @@ def __init__(self, token, base_url=None, base_file_url=None, request=None, priva
133127
if private_key:
134128
self.private_key = serialization.load_pem_private_key(private_key,
135129
password=private_key_password,
136-
backend=default_backend()
137-
)
130+
backend=default_backend())
138131

139132
@property
140133
def request(self):

telegram/files/inputfile.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
from telegram import TelegramError
2929

3030
DEFAULT_MIME_TYPE = 'application/octet-stream'
31-
FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate',
32-
'video_note', 'png_sticker')
3331

3432

3533
class InputFile(object):

telegram/passport/credentials.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,23 @@
2020
import json
2121
from base64 import b64decode
2222

23+
from cryptography.hazmat.backends import default_backend
24+
from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
25+
from cryptography.hazmat.primitives.ciphers import Cipher
26+
from cryptography.hazmat.primitives.ciphers.algorithms import AES
27+
from cryptography.hazmat.primitives.ciphers.modes import CBC
28+
from cryptography.hazmat.primitives.hashes import SHA512, SHA256, Hash, SHA1
2329
from future.utils import bord
2430

25-
try:
26-
from cryptography.hazmat.backends import default_backend
27-
from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1
28-
from cryptography.hazmat.primitives.ciphers import Cipher
29-
from cryptography.hazmat.primitives.ciphers.algorithms import AES
30-
from cryptography.hazmat.primitives.ciphers.modes import CBC
31-
from cryptography.hazmat.primitives.hashes import SHA512, SHA256, Hash, SHA1
32-
33-
CRYPTO = True
34-
except ImportError:
35-
CRYPTO = False
36-
3731
from telegram import TelegramObject
3832

3933

4034
class _TelegramDecryptionError(Exception):
35+
"""
36+
Something went wrong with decryption. Never exposed to the user, gets turned into a
37+
warning inside PassportData.
38+
This is because if we raise an error during a update fetch, it might hang the Updater.
39+
"""
4140
pass
4241

4342

@@ -61,6 +60,7 @@ def decrypt(secret, hash, data):
6160
:obj:`bytes`: The decrypted data as bytes
6261
6362
"""
63+
# First make sure that if secret, hash, or data was base64 encoded, to decode it into bytes
6464
try:
6565
secret = b64decode(secret)
6666
except (binascii.Error, TypeError):
@@ -73,18 +73,25 @@ def decrypt(secret, hash, data):
7373
data = b64decode(data)
7474
except (binascii.Error, TypeError):
7575
pass
76+
# Make a SHA512 hash of secret + update
7677
digest = Hash(SHA512(), backend=default_backend())
7778
digest.update(secret + hash)
7879
secret_hash_hash = digest.finalize()
80+
# First 32 chars is our key, next 16 is the initialisation vector
7981
key, iv = secret_hash_hash[:32], secret_hash_hash[32:32 + 16]
82+
# Init a AES-CBC cipher and decrypt the data
8083
cipher = Cipher(AES(key), CBC(iv), backend=default_backend())
8184
decryptor = cipher.decryptor()
8285
data = decryptor.update(data) + decryptor.finalize()
86+
# Calculate SHA256 hash of the decrypted data
8387
digest = Hash(SHA256(), backend=default_backend())
8488
digest.update(data)
8589
data_hash = digest.finalize()
90+
# If the newly calculated hash did not match the one telegram gave us
8691
if data_hash != hash:
92+
# Raise a error that is caught inside telegram.PassportData and transformed into a warning
8793
raise _TelegramDecryptionError("Hashes are not equal! {} != {}".format(data_hash, hash))
94+
# Return data without padding
8895
return data[bord(data[0]):]
8996

9097

@@ -140,19 +147,28 @@ def de_json(cls, data, bot):
140147
if not data:
141148
return None
142149

143-
# If already decrypted
150+
# If already decrypted just create the data object directly
144151
if isinstance(data['data'], dict):
145152
data['data'] = Credentials.de_json(data['data'], bot=bot)
146153
else:
147154
try:
155+
# Try decrypting according to step 1 at
156+
# https://core.telegram.org/passport#decrypting-data
157+
# We make sure to base64 decode the secret first.
158+
# Telegram says to use OAEP padding so we do that. The Mask Generation Function
159+
# is the default for OAEP, the algorithm is the default for PHP which is what
160+
# Telegram's backend servers run.
148161
data['secret'] = bot.private_key.decrypt(b64decode(data.get('secret')), OAEP(
149162
mgf=MGF1(algorithm=SHA1()),
150163
algorithm=SHA1(),
151164
label=None
152165
))
153166
except ValueError as e:
167+
# If decryption fails raise exception that is then caught inside PassportData
168+
# and turned into a warning
154169
raise _TelegramDecryptionError(e)
155170

171+
# Now that secret is decrypted, we can decrypt the data
156172
data['data'] = Credentials.de_json(decrypt_json(data.get('secret'),
157173
data.get('hash'),
158174
data.get('data')),

telegram/passport/passportdata.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,23 @@ def de_json(cls, data, bot):
6363
if not data:
6464
return None
6565

66+
# User did not configure any private_key, ignore PassportData (and therefore the entire
67+
# Update)
6668
if not hasattr(bot, 'private_key'):
6769
warnings.warn('Received update with PassportData but no private key is specified! '
6870
'See https://git.io/fAvYd for more info.')
6971
return None
7072

7173
try:
74+
# Try decrypting the credentials
7275
data = super(PassportData, cls).de_json(data, bot)
7376
data['credentials'] = EncryptedCredentials.de_json(data.get('credentials'), bot)
77+
# Passing them to where they are needed
7478
data['data'] = EncryptedPassportElement.de_list(data.get('data'), bot,
7579
credentials=data['credentials'])
7680
except _TelegramDecryptionError as e:
81+
# _TelegramDecryptionError is raised on a decryption error, we turn it into a
82+
# warning here, since if we allowed it to propagate it might hang the Updater.
7783
warnings.warn('Telegram passport decryption error: {} '
7884
'See https://git.io/fAvYd for more info.'.format(e))
7985
return None

tests/test_passport.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
import pytest
2323

24-
from telegram import PassportData, PassportFile, Bot, Update
24+
from telegram import (PassportData, PassportFile, Bot, Update, File, PassportElementErrorSelfie,
25+
PassportElementErrorDataField)
2526

2627
RAW_PASSPORT_DATA = {'data': [{'type': 'personal_details',
2728
'data': 'tj3pNwOpN+ZHsyb6F3aJcNmEyPxrOtGTbu3waBlCQDNaQ9oJlkbXpw+HI3y9faq/+TCeB/WsS/2TxRXTKZw4zXvGP2UsfdRkJ2SQq6x+Ffe/oTF9/q8sWp2BwU3hHUOz7ec1/QrdPBhPJjbwSykEBNggPweiBVDZ0x/DWJ0guCkGT9smYGqog1vqlqbIWG7AWcxVy2fpUy9w/zDXjxj5WQ3lRpHJmi46s9xIHobNGGBvWw6/bGBCInMoovgqRCEu1sgz2QXF3wNiUzGFycEzLz7o+1htLys5n8Pdi9MG4RY='},
@@ -99,6 +100,8 @@ class TestPassport(object):
99100
driver_license_reverse_side_file_id = 'DgADBAADNQQAAtoagFPf4wwmFZdmyQI'
100101
utility_bill_1_file_id = 'DgADBAADLAMAAhwfgVMyfGa5Nr0LvAI'
101102
utility_bill_2_file_id = 'DgADBAADaQQAAsFxgVNVfLZuT-_3ZQI'
103+
driver_license_selfie_credentials_file_hash = 'Cila/qLXSBH7DpZFbb5bRZIRxeFW2uv/ulL0u0JNsYI='
104+
driver_license_selfie_credentials_secret = 'tivdId6RNYNsvXYPppdzrbxOBuBOr9wXRPDcCvnXU7E='
102105

103106
def test_creation(self, passport_data):
104107
assert isinstance(passport_data, PassportData)
@@ -218,6 +221,44 @@ def test_wrong_key(self, bot):
218221
'Decryption failed.'):
219222
PassportData.de_json(data, bot=b)
220223

224+
def test_mocked_download_passport_file(self, passport_data, monkeypatch):
225+
# The files are not coming from our test bot, therefore the file id is invalid/wrong
226+
# when coming from this bot, so we monkeypatch the call, to make sure that Bot.get_file
227+
# at least gets called
228+
# TODO: Actually download a passport file in a test
229+
selfie = passport_data.data[1].selfie
230+
231+
def get_file(*args, **kwargs):
232+
return File(args[1])
233+
234+
monkeypatch.setattr('telegram.Bot.get_file', get_file)
235+
file = selfie.get_file()
236+
assert file.file_id == selfie.file_id
237+
assert file._credentials.file_hash == self.driver_license_selfie_credentials_file_hash
238+
assert file._credentials.secret == self.driver_license_selfie_credentials_secret
239+
240+
def test_mocked_set_passport_data_errors(self, monkeypatch, bot, chat_id, passport_data):
241+
def test(_, url, data, **kwargs):
242+
return (data['user_id'] == chat_id and
243+
data['errors'][0]['file_hash'] == (passport_data.credentials.data.secure_data
244+
.driver_license.selfie.file_hash) and
245+
data['errors'][1]['data_hash'] == (passport_data.credentials.data.secure_data
246+
.driver_license.data.data_hash))
247+
248+
monkeypatch.setattr('telegram.utils.request.Request.post', test)
249+
message = bot.set_passport_data_errors(chat_id, [
250+
PassportElementErrorSelfie('driver_license',
251+
(passport_data.credentials.data
252+
.secure_data.driver_license.selfie.file_hash),
253+
'You\'re not handsome enough to use this app!'),
254+
PassportElementErrorDataField('driver_license',
255+
'expiry_date',
256+
(passport_data.credentials.data
257+
.secure_data.driver_license.data.data_hash),
258+
'Your driver license is expired!')
259+
])
260+
assert message
261+
221262
def test_equality(self, passport_data):
222263
a = PassportData(passport_data.data, passport_data.credentials)
223264
b = PassportData(passport_data.data, passport_data.credentials)

tests/test_updater.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,8 @@ def test_mutual_exclude_token_bot(self):
351351
def test_no_token_or_bot(self):
352352
with pytest.raises(ValueError):
353353
Updater()
354+
355+
def test_mutual_exclude_bot_private_key(self):
356+
bot = Bot('123:zyxw')
357+
with pytest.raises(ValueError):
358+
Updater(bot=bot, private_key=b'key')

0 commit comments

Comments
 (0)