Skip to content

Commit 4e13aee

Browse files
committed
Handle credentials better
1 parent 9557b72 commit 4e13aee

File tree

6 files changed

+157
-128
lines changed

6 files changed

+157
-128
lines changed

examples/passportbot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def msg(bot, update):
2626
if passport_data:
2727
# If our payload doesn't match what we think, this Update did not originate from us
2828
# Ideally you would randomize the payload on the server
29-
if passport_data.decrypted_credentials.data.payload != 'thisisatest':
29+
if passport_data.decrypted_credentials.payload != 'thisisatest':
3030
return
3131

3232
# Print the decrypted credential data

telegram/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@
113113
from .passport.credentials import (Credentials,
114114
DataCredentials,
115115
SecureData,
116-
FileCredentials)
116+
FileCredentials,
117+
TelegramDecryptionError)
117118
from .version import __version__ # flake8: noqa
118119

119120
__author__ = 'devs@python-telegram-bot.org'
@@ -146,5 +147,5 @@
146147
'PassportElementErrorFiles', 'PassportElementErrorDataField', 'PassportElementErrorFile',
147148
'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData',
148149
'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation',
149-
'InputMediaAudio', 'InputMediaDocument'
150+
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError'
150151
]

telegram/passport/credentials.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class TelegramDecryptionError(TelegramError):
3737
"""
3838

3939
def __init__(self, message):
40-
super().__init__("TelegramDecryptionError" + message)
40+
super().__init__("TelegramDecryptionError: " + message)
4141

4242

4343
def decrypt(secret, hash, data, file=False):
@@ -109,21 +109,24 @@ class EncryptedCredentials(TelegramObject):
109109
authentication processes.
110110
111111
Attributes:
112-
data (:class:`telegram.Credentials`): Decrypted data with unique user's payload,
113-
data hashes and secrets used for EncryptedPassportElement decryption and
114-
authentication.
112+
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
113+
payload, data hashes and secrets used for EncryptedPassportElement decryption and
114+
authentication or base64 encrypted data.
115115
hash (:obj:`str`): Base64-encoded data hash for data authentication.
116-
secret (:obj:`str`): Decrypted secret used for decryption.
116+
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
117117
118118
Args:
119-
data (:obj:`str`): Base64-encoded encrypted JSON-serialized data with unique user's
120-
payload, data hashes and secrets required for EncryptedPassportElement decryption and
121-
authentication.
119+
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
120+
payload, data hashes and secrets used for EncryptedPassportElement decryption and
121+
authentication or base64 encrypted data.
122122
hash (:obj:`str`): Base64-encoded data hash for data authentication.
123-
secret (:obj:`str`): Base64-encoded secret, encrypted with the bot's public RSA key,
124-
required for data decryption.
123+
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
125124
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
126125
126+
Note:
127+
This object is decrypted only when originating from
128+
:obj:`telegram.PassportData.decrypted_credentials`.
129+
127130
"""
128131

129132
def __init__(self, data, hash, secret, bot=None, **kwargs):
@@ -135,7 +138,6 @@ def __init__(self, data, hash, secret, bot=None, **kwargs):
135138
self._id_attrs = (self.data, self.hash, self.secret)
136139

137140
self.bot = bot
138-
139141
self._decrypted_secret = None
140142
self._decrypted_data = None
141143

@@ -146,33 +148,54 @@ def de_json(cls, data, bot):
146148

147149
data = super(EncryptedCredentials, cls).de_json(data, bot)
148150

149-
# If already decrypted just create the data object directly
150-
if isinstance(data['data'], dict):
151-
data['data'] = Credentials.de_json(data['data'], bot=bot)
152-
else:
151+
return cls(bot=bot, **data)
152+
153+
@property
154+
def decrypted_secret(self):
155+
"""
156+
:obj:`str`: The secret as decrypted using the bot's private key.
157+
158+
Raises:
159+
telegram.TelegramDecryptionError: If a decryption error happened while attempting
160+
to decrypt the data. This is most often a wrong/missing private_key, but will
161+
also happen in cases of tampered data.
162+
"""
163+
if not self._decrypted_secret:
164+
# Try decrypting according to step 1 at
165+
# https://core.telegram.org/passport#decrypting-data
166+
# We make sure to base64 decode the secret first.
167+
# Telegram says to use OAEP padding so we do that. The Mask Generation Function
168+
# is the default for OAEP, the algorithm is the default for PHP which is what
169+
# Telegram's backend servers run.
153170
try:
154-
# Try decrypting according to step 1 at
155-
# https://core.telegram.org/passport#decrypting-data
156-
# We make sure to base64 decode the secret first.
157-
# Telegram says to use OAEP padding so we do that. The Mask Generation Function
158-
# is the default for OAEP, the algorithm is the default for PHP which is what
159-
# Telegram's backend servers run.
160-
data['secret'] = bot.private_key.decrypt(b64decode(data.get('secret')), OAEP(
171+
self._decrypted_secret = self.bot.private_key.decrypt(b64decode(self.secret), OAEP(
161172
mgf=MGF1(algorithm=SHA1()),
162173
algorithm=SHA1(),
163174
label=None
164175
))
165176
except ValueError as e:
166177
# If decryption fails raise exception
167178
raise TelegramDecryptionError(e)
179+
return self._decrypted_secret
168180

169-
# Now that secret is decrypted, we can decrypt the data
170-
data['data'] = Credentials.de_json(decrypt_json(data.get('secret'),
171-
data.get('hash'),
172-
data.get('data')),
173-
bot=bot)
174-
175-
return cls(bot=bot, **data)
181+
@property
182+
def decrypted_data(self):
183+
"""
184+
:class:`telegram.Credentials`: Decrypted credentials that were used to decrypt
185+
the data. This object also contains the user specified payload as
186+
`decrypted_data.payload`.
187+
188+
Raises:
189+
telegram.TelegramDecryptionError: If a decryption error happened while attempting
190+
to decrypt the data. This is most often a wrong/missing private_key, but will
191+
also happen in cases of tampered data.
192+
"""
193+
if not self._decrypted_data:
194+
self._decrypted_data = Credentials.de_json(decrypt_json(self.decrypted_secret,
195+
self.hash,
196+
self.data),
197+
self.bot)
198+
return self._decrypted_data
176199

177200

178201
class Credentials(TelegramObject):

telegram/passport/encryptedpassportelement.py

Lines changed: 72 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
#
2+
# flake8: noqa: E501
33
# A library that provides a Python interface to the Telegram Bot API
44
# Copyright (C) 2015-2018
55
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
@@ -24,70 +24,66 @@
2424

2525

2626
class EncryptedPassportElement(TelegramObject):
27-
"""Contains information about documents or other Telegram Passport elements shared with the bot
28-
by the user. The data has been automatically decrypted by python-telegram-bot.
27+
"""
28+
Contains information about documents or other Telegram Passport elements shared with the bot
29+
by the user. The data has been automatically decrypted by python-telegram-bot.
2930
3031
Attributes:
3132
type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license",
3233
"identity_card", "internal_passport", "address", "utility_bill", "bank_statement",
3334
"rental_agreement", "passport_registration", "temporary_registration", "phone_number",
3435
"email".
35-
data (:obj:`str`): Optional. Base64-encoded encrypted Telegram Passport element data
36-
provided by the user, available for "personal_details", "passport", "driver_license",
37-
"identity_card", "identity_passport" and "address" types. Can be decrypted and verified
38-
using the accompanying EncryptedCredentials.
36+
data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`):
37+
Optional. Decrypted or encrypted data, available for "personal_details", "passport",
38+
"driver_license", "identity_card", "identity_passport" and "address" types.
3939
phone_number (:obj:`str`): Optional. User's verified phone number, available only for
40-
"phone_number" type
40+
"phone_number" type.
4141
email (:obj:`str`): Optional. User's verified email address, available only for "email"
42-
type
43-
files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted files with
44-
documents provided by the user, available for "utility_bill", "bank_statement",
42+
type.
43+
files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files
44+
with documents provided by the user, available for "utility_bill", "bank_statement",
4545
"rental_agreement", "passport_registration" and "temporary_registration" types.
46-
Files can be decrypted and verified using the accompanying EncryptedCredentials.
47-
front_side (:class:`PassportFile`): Optional. Encrypted file with the front side of the
48-
document, provided by the user. Available for "passport", "driver_license",
49-
"identity_card" and "internal_passport". The file can be decrypted and verified using
50-
the accompanying EncryptedCredentials.
51-
reverse_side (:class:`PassportFile`): Optional. Encrypted file with the reverse side of the
52-
document, provided by the user. Available for "driver_license" and "identity_card".
53-
The file can be decrypted and verified using the accompanying EncryptedCredentials.
54-
selfie (:class:`PassportFile`): Optional. Encrypted file with the selfie of the user
55-
holding a document, provided by the user; available for "passport", "driver_license",
56-
"identity_card" and "internal_passport". The file can be decrypted and verified using
57-
the accompanying EncryptedCredentials.
46+
front_side (:class:`PassportFile`): Optional. Encrypted/decrypted file with the front side
47+
of the document, provided by the user. Available for "passport", "driver_license",
48+
"identity_card" and "internal_passport".
49+
reverse_side (:class:`PassportFile`): Optional. Encrypted/decrypted file with the reverse
50+
side of the document, provided by the user. Available for "driver_license" and
51+
"identity_card".
52+
selfie (:class:`PassportFile`): Optional. Encrypted/decrypted file with the selfie of the
53+
user holding a document, provided by the user; available for "passport",
54+
"driver_license", "identity_card" and "internal_passport".
5855
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
5956
6057
Args:
6158
type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license",
6259
"identity_card", "internal_passport", "address", "utility_bill", "bank_statement",
6360
"rental_agreement", "passport_registration", "temporary_registration", "phone_number",
6461
"email".
65-
data (:obj:`str`, optional): Base64-encoded encrypted Telegram Passport element data
66-
provided by the user, available for "personal_details", "passport", "driver_license",
67-
"identity_card", "identity_passport" and "address" types. Can be decrypted and verified
68-
using the accompanying EncryptedCredentials.
62+
data (:class:`telegram.PersonalDetails` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :obj:`str`, optional):
63+
Decrypted or encrypted data, available for "personal_details", "passport",
64+
"driver_license", "identity_card", "identity_passport" and "address" types.
6965
phone_number (:obj:`str`, optional): User's verified phone number, available only for
70-
"phone_number" type
66+
"phone_number" type.
7167
email (:obj:`str`, optional): User's verified email address, available only for "email"
72-
type
73-
files (List[:class:`telegram.PassportFile`], optional): Array of encrypted files with
74-
documents provided by the user, available for "utility_bill", "bank_statement",
68+
type.
69+
files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files
70+
with documents provided by the user, available for "utility_bill", "bank_statement",
7571
"rental_agreement", "passport_registration" and "temporary_registration" types.
76-
Files can be decrypted and verified using the accompanying EncryptedCredentials.
77-
front_side (:class:`PassportFile`, optional): Encrypted file with the front side of the
78-
document, provided by the user. Available for "passport", "driver_license",
79-
"identity_card" and "internal_passport". The file can be decrypted and verified using
80-
the accompanying EncryptedCredentials.
81-
reverse_side (:class:`PassportFile`, optional): Encrypted file with the reverse side of the
82-
document, provided by the user. Available for "driver_license" and "identity_card".
83-
The file can be decrypted and verified using the accompanying EncryptedCredentials.
84-
selfie (:class:`PassportFile`, optional): Encrypted file with the selfie of the user
85-
holding a document, provided by the user; available for "passport", "driver_license",
86-
"identity_card" and "internal_passport". The file can be decrypted and verified using
87-
the accompanying EncryptedCredentials.
72+
front_side (:class:`PassportFile`, optional): Encrypted/decrypted file with the front side
73+
of the document, provided by the user. Available for "passport", "driver_license",
74+
"identity_card" and "internal_passport".
75+
reverse_side (:class:`PassportFile`, optional): Encrypted/decrypted file with the reverse
76+
side of the document, provided by the user. Available for "driver_license" and
77+
"identity_card".
78+
selfie (:class:`PassportFile`, optional): Encrypted/decrypted file with the selfie of the
79+
user holding a document, provided by the user; available for "passport",
80+
"driver_license", "identity_card" and "internal_passport".
8881
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
8982
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
9083
84+
Note:
85+
This object is decrypted only when originating from
86+
:obj:`telegram.PassportData.decrypted_data`.
9187
"""
9288

9389
def __init__(self,
@@ -117,53 +113,55 @@ def __init__(self,
117113
self.front_side, self.reverse_side, self.selfie)
118114

119115
self.bot = bot
120-
self._credentials = credentials
121116

122117
# noinspection PyMethodOverriding
123118
@classmethod
124-
def de_json(cls, data, bot, credentials):
119+
def de_json(cls, data, bot, decrypt=False, credentials=None):
125120
if not data:
126121
return None
127122

128123
data = super(EncryptedPassportElement, cls).de_json(data, bot)
129124

130125
secure_data = None
131-
if data['type'] not in ('phone_number', 'email'):
132-
secure_data = getattr(credentials.data.secure_data, data['type'])
133-
134-
if secure_data.data is not None:
135-
# If not already decrypted
136-
if not isinstance(data['data'], dict):
137-
data['data'] = decrypt_json(secure_data.data.secret,
138-
secure_data.data.hash,
139-
data['data'])
140-
if data['type'] == 'personal_details':
141-
data['data'] = PersonalDetails.de_json(data['data'], bot=bot)
142-
elif data['type'] in ('passport', 'internal_passport',
143-
'driver_license', 'identity_card'):
144-
data['data'] = IdDocumentData.de_json(data['data'], bot=bot)
145-
elif data['type'] == 'address':
146-
data['data'] = ResidentialAddress.de_json(data['data'], bot=bot)
147-
148-
if secure_data:
149-
data['files'] = PassportFile.de_list(data.get('files'), bot, secure_data)
150-
data['front_side'] = PassportFile.de_json(data.get('front_side'),
151-
bot, secure_data.front_side)
152-
data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'),
153-
bot, secure_data.reverse_side)
154-
data['selfie'] = PassportFile.de_json(data.get('selfie'),
155-
bot, secure_data.selfie)
156-
157-
return cls(bot=bot, credentials=secure_data, **data)
126+
if decrypt:
127+
if data['type'] not in ('phone_number', 'email'):
128+
secure_data = getattr(credentials.secure_data, data['type'])
129+
130+
if secure_data.data is not None:
131+
# If not already decrypted
132+
if not isinstance(data['data'], dict):
133+
data['data'] = decrypt_json(secure_data.data.secret,
134+
secure_data.data.hash,
135+
data['data'])
136+
if data['type'] == 'personal_details':
137+
data['data'] = PersonalDetails.de_json(data['data'], bot=bot)
138+
elif data['type'] in ('passport', 'internal_passport',
139+
'driver_license', 'identity_card'):
140+
data['data'] = IdDocumentData.de_json(data['data'], bot=bot)
141+
elif data['type'] == 'address':
142+
data['data'] = ResidentialAddress.de_json(data['data'], bot=bot)
143+
144+
data['files'] = PassportFile.de_list(data.get('files'), bot,
145+
secure_data if secure_data else None)
146+
data['front_side'] = PassportFile.de_json(data.get('front_side'), bot,
147+
secure_data.front_side
148+
if secure_data else None)
149+
data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'), bot,
150+
secure_data.reverse_side
151+
if secure_data else None)
152+
data['selfie'] = PassportFile.de_json(data.get('selfie'), bot,
153+
secure_data.selfie if secure_data else None)
154+
155+
return cls(bot=bot, **data)
158156

159157
@classmethod
160-
def de_list(cls, data, bot, credentials):
158+
def de_list(cls, data, bot):
161159
if not data:
162160
return []
163161

164162
encrypted_passport_elements = list()
165163
for element in data:
166-
encrypted_passport_elements.append(cls.de_json(element, bot, credentials))
164+
encrypted_passport_elements.append(cls.de_json(element, bot))
167165

168166
return encrypted_passport_elements
169167

0 commit comments

Comments
 (0)