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