mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-06-01 04:03:18 +03:00
Verify the files downloaded from CDNs and raise on sha256 mismatch
This commit is contained in:
parent
00b5b5021b
commit
b504ce14bc
|
@ -147,6 +147,7 @@ This class is further specialized into further errors:
|
||||||
* ``ForbiddenError`` (403), privacy violation error.
|
* ``ForbiddenError`` (403), privacy violation error.
|
||||||
* ``NotFoundError`` (404), make sure you're invoking ``Request``'s!
|
* ``NotFoundError`` (404), make sure you're invoking ``Request``'s!
|
||||||
* ``FloodError`` (420), the same request was repeated many times. Must wait ``.seconds``.
|
* ``FloodError`` (420), the same request was repeated many times. Must wait ``.seconds``.
|
||||||
|
* ``CdnFileTamperedError``, if the media you were trying to download has been altered.
|
||||||
|
|
||||||
Further specialization is also available, for instance, the ``SessionPasswordNeededError``
|
Further specialization is also available, for instance, the ``SessionPasswordNeededError``
|
||||||
when signing in means that a password must be provided to continue.
|
when signing in means that a password must be provided to continue.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
from .aes import AES
|
from .aes import AES
|
||||||
from .auth_key import AuthKey
|
from .auth_key import AuthKey
|
||||||
from .factorization import Factorization
|
from .factorization import Factorization
|
||||||
|
from .hash_checker import HashChecker
|
||||||
|
|
39
telethon/crypto/hash_checker.py
Normal file
39
telethon/crypto/hash_checker.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from hashlib import sha256
|
||||||
|
from ..errors import CdnFileTamperedError
|
||||||
|
|
||||||
|
|
||||||
|
class HashChecker:
|
||||||
|
def __init__(self, cdn_file_hashes):
|
||||||
|
self.cdn_file_hashes = cdn_file_hashes
|
||||||
|
self.shaes = [sha256() for _ in range(len(cdn_file_hashes))]
|
||||||
|
|
||||||
|
def check(self, offset, data):
|
||||||
|
for cdn_hash, sha in zip(self.cdn_file_hashes, self.shaes):
|
||||||
|
inter = self.intersect(
|
||||||
|
cdn_hash.offset, cdn_hash.offset + cdn_hash.limit,
|
||||||
|
offset, offset + len(data)
|
||||||
|
)
|
||||||
|
if inter:
|
||||||
|
x1, x2 = inter[0] - offset, inter[1] - offset
|
||||||
|
sha.update(data[x1:x2])
|
||||||
|
elif offset > cdn_hash.offset:
|
||||||
|
if cdn_hash.hash == sha.digest():
|
||||||
|
self.cdn_file_hashes.remove(cdn_hash)
|
||||||
|
self.shaes.remove(sha)
|
||||||
|
else:
|
||||||
|
raise CdnFileTamperedError()
|
||||||
|
|
||||||
|
def finish_check(self):
|
||||||
|
for cdn_hash, sha in zip(self.cdn_file_hashes, self.shaes):
|
||||||
|
if cdn_hash.hash != sha.digest():
|
||||||
|
raise CdnFileTamperedError()
|
||||||
|
|
||||||
|
self.cdn_file_hashes.clear()
|
||||||
|
self.shaes.clear()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def intersect(x1, x2, z1, z2):
|
||||||
|
if x1 > z1:
|
||||||
|
return None if x1 > z2 else (x1, min(x2, z2))
|
||||||
|
else:
|
||||||
|
return (z1, min(x2, z2)) if x2 > z1 else None
|
|
@ -3,7 +3,7 @@ import re
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
ReadCancelledError, InvalidParameterError, TypeNotFoundError,
|
ReadCancelledError, InvalidParameterError, TypeNotFoundError,
|
||||||
InvalidChecksumError
|
InvalidChecksumError, CdnFileTamperedError
|
||||||
)
|
)
|
||||||
|
|
||||||
from .rpc_errors import (
|
from .rpc_errors import (
|
||||||
|
|
|
@ -35,3 +35,11 @@ class InvalidChecksumError(Exception):
|
||||||
|
|
||||||
self.checksum = checksum
|
self.checksum = checksum
|
||||||
self.valid_checksum = valid_checksum
|
self.valid_checksum = valid_checksum
|
||||||
|
|
||||||
|
|
||||||
|
class CdnFileTamperedError(Exception):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
self,
|
||||||
|
'The CDN file has been altered and its download cancelled.'
|
||||||
|
)
|
||||||
|
|
|
@ -12,8 +12,7 @@ from .errors import (
|
||||||
)
|
)
|
||||||
from .network import authenticator, MtProtoSender, TcpTransport
|
from .network import authenticator, MtProtoSender, TcpTransport
|
||||||
from .utils import get_appropriated_part_size
|
from .utils import get_appropriated_part_size
|
||||||
from .crypto import AES
|
from .crypto import rsa, HashChecker
|
||||||
from .crypto import rsa
|
|
||||||
|
|
||||||
# For sending and receiving requests
|
# For sending and receiving requests
|
||||||
from .tl import TLObject, JsonSession
|
from .tl import TLObject, JsonSession
|
||||||
|
@ -487,6 +486,7 @@ class TelegramBareClient:
|
||||||
try:
|
try:
|
||||||
offset_index = 0
|
offset_index = 0
|
||||||
cdn_file_token = None
|
cdn_file_token = None
|
||||||
|
hash_checker = None
|
||||||
|
|
||||||
def encrypt_method(x):
|
def encrypt_method(x):
|
||||||
return x # Defaults to no-op
|
return x # Defaults to no-op
|
||||||
|
@ -505,7 +505,11 @@ class TelegramBareClient:
|
||||||
))
|
))
|
||||||
|
|
||||||
if isinstance(result, FileCdnRedirect):
|
if isinstance(result, FileCdnRedirect):
|
||||||
client, cdn_file_token, encrypt_method, result = \
|
cdn_file_token = result.file_token
|
||||||
|
hash_checker = HashChecker(
|
||||||
|
result.cdn_file_hashes
|
||||||
|
)
|
||||||
|
client, encrypt_method, result = \
|
||||||
self._prepare_cdn_redirect(
|
self._prepare_cdn_redirect(
|
||||||
result, offset, part_size
|
result, offset, part_size
|
||||||
)
|
)
|
||||||
|
@ -524,9 +528,13 @@ class TelegramBareClient:
|
||||||
# So there is nothing left to download and write
|
# So there is nothing left to download and write
|
||||||
if not result.bytes:
|
if not result.bytes:
|
||||||
# Return some extra information, unless it's a cdn file
|
# Return some extra information, unless it's a cdn file
|
||||||
|
hash_checker.finish_check()
|
||||||
return getattr(result, 'type', '')
|
return getattr(result, 'type', '')
|
||||||
|
|
||||||
f.write(encrypt_method(result.bytes))
|
result.bytes = encrypt_method(result.bytes)
|
||||||
|
hash_checker.check(offset, result.bytes)
|
||||||
|
|
||||||
|
f.write(result.bytes)
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(f.tell(), file_size)
|
progress_callback(f.tell(), file_size)
|
||||||
finally:
|
finally:
|
||||||
|
@ -534,7 +542,7 @@ class TelegramBareClient:
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def _prepare_cdn_redirect(self, cdn_redirect, offset, part_size):
|
def _prepare_cdn_redirect(self, cdn_redirect, offset, part_size):
|
||||||
"""Returns (client, cdn_file_token, encrypt_method, result)"""
|
"""Returns (client, encrypt_method, result)"""
|
||||||
# https://core.telegram.org/cdn
|
# https://core.telegram.org/cdn
|
||||||
# TODO Use libssl if available
|
# TODO Use libssl if available
|
||||||
cdn_aes = pyaes.AESModeOfOperationCTR(
|
cdn_aes = pyaes.AESModeOfOperationCTR(
|
||||||
|
@ -559,9 +567,9 @@ class TelegramBareClient:
|
||||||
file_token=cdn_redirect.file_token,
|
file_token=cdn_redirect.file_token,
|
||||||
request_token=cdn_file.request_token
|
request_token=cdn_file.request_token
|
||||||
))
|
))
|
||||||
return client, cdn_redirect.file_token, cdn_aes.encrypt, None
|
return client, cdn_aes.encrypt, None
|
||||||
else:
|
else:
|
||||||
# We have the first bytes for the file
|
# We have the first bytes for the file
|
||||||
return client, cdn_redirect.file_token, cdn_aes.encrypt, cdn_file
|
return client, cdn_aes.encrypt, cdn_file
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
Loading…
Reference in New Issue
Block a user