1#!/usr/bin/env python
2#
3# A library that provides a Python interface to the Telegram Bot API
4# Copyright (C) 2015-2020
5# Leandro Toledo de Souza <devs@python-telegram-bot.org>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Lesser Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU Lesser Public License for more details.
16#
17# You should have received a copy of the GNU Lesser Public License
18# along with this program.  If not, see [http://www.gnu.org/licenses/].
19# pylint: disable=C0114, E0401, W0622
20try:
21    import ujson as json
22except ImportError:
23    import json  # type: ignore[no-redef]
24
25from base64 import b64decode
26from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, no_type_check
27
28from cryptography.hazmat.backends import default_backend
29from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP
30from cryptography.hazmat.primitives.ciphers import Cipher
31from cryptography.hazmat.primitives.ciphers.algorithms import AES
32from cryptography.hazmat.primitives.ciphers.modes import CBC
33from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash
34
35from telegram import TelegramError, TelegramObject
36from telegram.utils.types import JSONDict
37
38if TYPE_CHECKING:
39    from telegram import Bot
40
41
42class TelegramDecryptionError(TelegramError):
43    """
44    Something went wrong with decryption.
45    """
46
47    def __init__(self, message: Union[str, Exception]):
48        super().__init__(f"TelegramDecryptionError: {message}")
49        self._msg = str(message)
50
51    def __reduce__(self) -> Tuple[type, Tuple[str]]:
52        return self.__class__, (self._msg,)
53
54
55@no_type_check
56def decrypt(secret, hash, data):
57    """
58    Decrypt per telegram docs at https://core.telegram.org/passport.
59
60    Args:
61        secret (:obj:`str` or :obj:`bytes`): The encryption secret, either as bytes or as a
62            base64 encoded string.
63        hash (:obj:`str` or :obj:`bytes`): The hash, either as bytes or as a
64            base64 encoded string.
65        data (:obj:`str` or :obj:`bytes`): The data to decrypt, either as bytes or as a
66            base64 encoded string.
67        file (:obj:`bool`): Force data to be treated as raw data, instead of trying to
68            b64decode it.
69
70    Raises:
71        :class:`TelegramDecryptionError`: Given hash does not match hash of decrypted data.
72
73    Returns:
74        :obj:`bytes`: The decrypted data as bytes.
75
76    """
77    # Make a SHA512 hash of secret + update
78    digest = Hash(SHA512(), backend=default_backend())
79    digest.update(secret + hash)
80    secret_hash_hash = digest.finalize()
81    # First 32 chars is our key, next 16 is the initialisation vector
82    key, init_vector = secret_hash_hash[:32], secret_hash_hash[32 : 32 + 16]
83    # Init a AES-CBC cipher and decrypt the data
84    cipher = Cipher(AES(key), CBC(init_vector), backend=default_backend())
85    decryptor = cipher.decryptor()
86    data = decryptor.update(data) + decryptor.finalize()
87    # Calculate SHA256 hash of the decrypted data
88    digest = Hash(SHA256(), backend=default_backend())
89    digest.update(data)
90    data_hash = digest.finalize()
91    # If the newly calculated hash did not match the one telegram gave us
92    if data_hash != hash:
93        # Raise a error that is caught inside telegram.PassportData and transformed into a warning
94        raise TelegramDecryptionError(f"Hashes are not equal! {data_hash} != {hash}")
95    # Return data without padding
96    return data[data[0] :]
97
98
99@no_type_check
100def decrypt_json(secret, hash, data):
101    """Decrypts data using secret and hash and then decodes utf-8 string and loads json"""
102    return json.loads(decrypt(secret, hash, data).decode('utf-8'))
103
104
105class EncryptedCredentials(TelegramObject):
106    """Contains data required for decrypting and authenticating EncryptedPassportElement. See the
107    Telegram Passport Documentation for a complete description of the data decryption and
108    authentication processes.
109
110    Objects of this class are comparable in terms of equality. Two objects of this class are
111    considered equal, if their :attr:`data`, :attr:`hash` and :attr:`secret` are equal.
112
113    Attributes:
114        data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
115            nonce, data hashes and secrets used for EncryptedPassportElement decryption and
116            authentication or base64 encrypted data.
117        hash (:obj:`str`): Base64-encoded data hash for data authentication.
118        secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
119
120    Args:
121        data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
122            nonce, data hashes and secrets used for EncryptedPassportElement decryption and
123            authentication or base64 encrypted data.
124        hash (:obj:`str`): Base64-encoded data hash for data authentication.
125        secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
126        **kwargs (:obj:`dict`): Arbitrary keyword arguments.
127
128    Note:
129        This object is decrypted only when originating from
130        :obj:`telegram.PassportData.decrypted_credentials`.
131
132    """
133
134    def __init__(self, data: str, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any):
135        # Required
136        self.data = data
137        self.hash = hash
138        self.secret = secret
139
140        self._id_attrs = (self.data, self.hash, self.secret)
141
142        self.bot = bot
143        self._decrypted_secret = None
144        self._decrypted_data: Optional['Credentials'] = None
145
146    @property
147    def decrypted_secret(self) -> str:
148        """
149        :obj:`str`: Lazily decrypt and return secret.
150
151        Raises:
152            telegram.TelegramDecryptionError: Decryption failed. Usually due to bad
153                private/public key but can also suggest malformed/tampered data.
154        """
155        if self._decrypted_secret is None:
156            # Try decrypting according to step 1 at
157            # https://core.telegram.org/passport#decrypting-data
158            # We make sure to base64 decode the secret first.
159            # Telegram says to use OAEP padding so we do that. The Mask Generation Function
160            # is the default for OAEP, the algorithm is the default for PHP which is what
161            # Telegram's backend servers run.
162            try:
163                self._decrypted_secret = self.bot.private_key.decrypt(
164                    b64decode(self.secret),
165                    OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None),
166                )
167            except ValueError as exception:
168                # If decryption fails raise exception
169                raise TelegramDecryptionError(exception) from exception
170        return self._decrypted_secret
171
172    @property
173    def decrypted_data(self) -> 'Credentials':
174        """
175        :class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object
176            also contains the user specified nonce as
177            `decrypted_data.nonce`.
178
179        Raises:
180            telegram.TelegramDecryptionError: Decryption failed. Usually due to bad
181                private/public key but can also suggest malformed/tampered data.
182        """
183        if self._decrypted_data is None:
184            self._decrypted_data = Credentials.de_json(
185                decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)),
186                self.bot,
187            )
188        return self._decrypted_data
189
190
191class Credentials(TelegramObject):
192    """
193    Attributes:
194        secure_data (:class:`telegram.SecureData`): Credentials for encrypted data
195        nonce (:obj:`str`): Bot-specified nonce
196    """
197
198    def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any):
199        # Required
200        self.secure_data = secure_data
201        self.nonce = nonce
202
203        self.bot = bot
204
205    @classmethod
206    def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Credentials']:
207        data = cls.parse_data(data)
208
209        if not data:
210            return None
211
212        data['secure_data'] = SecureData.de_json(data.get('secure_data'), bot=bot)
213
214        return cls(bot=bot, **data)
215
216
217class SecureData(TelegramObject):
218    """
219    This object represents the credentials that were used to decrypt the encrypted data.
220    All fields are optional and depend on fields that were requested.
221
222    Attributes:
223        personal_details (:class:`telegram.SecureValue`, optional): Credentials for encrypted
224            personal details.
225        passport (:class:`telegram.SecureValue`, optional): Credentials for encrypted passport.
226        internal_passport (:class:`telegram.SecureValue`, optional): Credentials for encrypted
227            internal passport.
228        driver_license (:class:`telegram.SecureValue`, optional): Credentials for encrypted
229            driver license.
230        identity_card (:class:`telegram.SecureValue`, optional): Credentials for encrypted ID card
231        address (:class:`telegram.SecureValue`, optional): Credentials for encrypted
232            residential address.
233        utility_bill (:class:`telegram.SecureValue`, optional): Credentials for encrypted
234            utility bill.
235        bank_statement (:class:`telegram.SecureValue`, optional): Credentials for encrypted
236            bank statement.
237        rental_agreement (:class:`telegram.SecureValue`, optional): Credentials for encrypted
238            rental agreement.
239        passport_registration (:class:`telegram.SecureValue`, optional): Credentials for encrypted
240            registration from internal passport.
241        temporary_registration (:class:`telegram.SecureValue`, optional): Credentials for encrypted
242            temporary registration.
243    """
244
245    def __init__(
246        self,
247        personal_details: 'SecureValue' = None,
248        passport: 'SecureValue' = None,
249        internal_passport: 'SecureValue' = None,
250        driver_license: 'SecureValue' = None,
251        identity_card: 'SecureValue' = None,
252        address: 'SecureValue' = None,
253        utility_bill: 'SecureValue' = None,
254        bank_statement: 'SecureValue' = None,
255        rental_agreement: 'SecureValue' = None,
256        passport_registration: 'SecureValue' = None,
257        temporary_registration: 'SecureValue' = None,
258        bot: 'Bot' = None,
259        **_kwargs: Any,
260    ):
261        # Optionals
262        self.temporary_registration = temporary_registration
263        self.passport_registration = passport_registration
264        self.rental_agreement = rental_agreement
265        self.bank_statement = bank_statement
266        self.utility_bill = utility_bill
267        self.address = address
268        self.identity_card = identity_card
269        self.driver_license = driver_license
270        self.internal_passport = internal_passport
271        self.passport = passport
272        self.personal_details = personal_details
273
274        self.bot = bot
275
276    @classmethod
277    def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureData']:
278        data = cls.parse_data(data)
279
280        if not data:
281            return None
282
283        data['temporary_registration'] = SecureValue.de_json(
284            data.get('temporary_registration'), bot=bot
285        )
286        data['passport_registration'] = SecureValue.de_json(
287            data.get('passport_registration'), bot=bot
288        )
289        data['rental_agreement'] = SecureValue.de_json(data.get('rental_agreement'), bot=bot)
290        data['bank_statement'] = SecureValue.de_json(data.get('bank_statement'), bot=bot)
291        data['utility_bill'] = SecureValue.de_json(data.get('utility_bill'), bot=bot)
292        data['address'] = SecureValue.de_json(data.get('address'), bot=bot)
293        data['identity_card'] = SecureValue.de_json(data.get('identity_card'), bot=bot)
294        data['driver_license'] = SecureValue.de_json(data.get('driver_license'), bot=bot)
295        data['internal_passport'] = SecureValue.de_json(data.get('internal_passport'), bot=bot)
296        data['passport'] = SecureValue.de_json(data.get('passport'), bot=bot)
297        data['personal_details'] = SecureValue.de_json(data.get('personal_details'), bot=bot)
298
299        return cls(bot=bot, **data)
300
301
302class SecureValue(TelegramObject):
303    """
304    This object represents the credentials that were used to decrypt the encrypted value.
305    All fields are optional and depend on the type of field.
306
307    Attributes:
308        data (:class:`telegram.DataCredentials`, optional): Credentials for encrypted Telegram
309            Passport data. Available for "personal_details", "passport", "driver_license",
310            "identity_card", "identity_passport" and "address" types.
311        front_side (:class:`telegram.FileCredentials`, optional): Credentials for encrypted
312            document's front side. Available for "passport", "driver_license", "identity_card"
313            and "internal_passport".
314        reverse_side (:class:`telegram.FileCredentials`, optional): Credentials for encrypted
315            document's reverse side. Available for "driver_license" and "identity_card".
316        selfie (:class:`telegram.FileCredentials`, optional): Credentials for encrypted selfie
317            of the user with a document. Can be available for "passport", "driver_license",
318            "identity_card" and "internal_passport".
319        translation (List[:class:`telegram.FileCredentials`], optional): Credentials for an
320            encrypted translation of the document. Available for "passport", "driver_license",
321            "identity_card", "internal_passport", "utility_bill", "bank_statement",
322            "rental_agreement", "passport_registration" and "temporary_registration".
323        files (List[:class:`telegram.FileCredentials`], optional): Credentials for encrypted
324            files. Available for "utility_bill", "bank_statement", "rental_agreement",
325            "passport_registration" and "temporary_registration" types.
326
327    """
328
329    def __init__(
330        self,
331        data: 'DataCredentials' = None,
332        front_side: 'FileCredentials' = None,
333        reverse_side: 'FileCredentials' = None,
334        selfie: 'FileCredentials' = None,
335        files: List['FileCredentials'] = None,
336        translation: List['FileCredentials'] = None,
337        bot: 'Bot' = None,
338        **_kwargs: Any,
339    ):
340        self.data = data
341        self.front_side = front_side
342        self.reverse_side = reverse_side
343        self.selfie = selfie
344        self.files = files
345        self.translation = translation
346
347        self.bot = bot
348
349    @classmethod
350    def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['SecureValue']:
351        data = cls.parse_data(data)
352
353        if not data:
354            return None
355
356        data['data'] = DataCredentials.de_json(data.get('data'), bot=bot)
357        data['front_side'] = FileCredentials.de_json(data.get('front_side'), bot=bot)
358        data['reverse_side'] = FileCredentials.de_json(data.get('reverse_side'), bot=bot)
359        data['selfie'] = FileCredentials.de_json(data.get('selfie'), bot=bot)
360        data['files'] = FileCredentials.de_list(data.get('files'), bot=bot)
361        data['translation'] = FileCredentials.de_list(data.get('translation'), bot=bot)
362
363        return cls(bot=bot, **data)
364
365    def to_dict(self) -> JSONDict:
366        data = super().to_dict()
367
368        data['files'] = [p.to_dict() for p in self.files]
369        data['translation'] = [p.to_dict() for p in self.translation]
370
371        return data
372
373
374class _CredentialsBase(TelegramObject):
375    """Base class for DataCredentials and FileCredentials."""
376
377    def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any):
378        self.hash = hash
379        self.secret = secret
380
381        # Aliases just be be sure
382        self.file_hash = self.hash
383        self.data_hash = self.hash
384
385        self.bot = bot
386
387
388class DataCredentials(_CredentialsBase):
389    """
390    These credentials can be used to decrypt encrypted data from the data field in
391    EncryptedPassportData.
392
393    Args:
394        data_hash (:obj:`str`): Checksum of encrypted data
395        secret (:obj:`str`): Secret of encrypted data
396
397    Attributes:
398        hash (:obj:`str`): Checksum of encrypted data
399        secret (:obj:`str`): Secret of encrypted data
400    """
401
402    def __init__(self, data_hash: str, secret: str, **_kwargs: Any):
403        super().__init__(data_hash, secret, **_kwargs)
404
405    def to_dict(self) -> JSONDict:
406        data = super().to_dict()
407
408        del data['file_hash']
409        del data['hash']
410
411        return data
412
413
414class FileCredentials(_CredentialsBase):
415    """
416    These credentials can be used to decrypt encrypted files from the front_side,
417    reverse_side, selfie and files fields in EncryptedPassportData.
418
419    Args:
420        file_hash (:obj:`str`): Checksum of encrypted file
421        secret (:obj:`str`): Secret of encrypted file
422
423    Attributes:
424        hash (:obj:`str`): Checksum of encrypted file
425        secret (:obj:`str`): Secret of encrypted file
426    """
427
428    def __init__(self, file_hash: str, secret: str, **_kwargs: Any):
429        super().__init__(file_hash, secret, **_kwargs)
430
431    def to_dict(self) -> JSONDict:
432        data = super().to_dict()
433
434        del data['data_hash']
435        del data['hash']
436
437        return data
438