2017-06-09 17:13:39 +03:00
|
|
|
"""Various helpers not related to the Telegram API itself"""
|
2018-09-29 14:29:44 +03:00
|
|
|
import asyncio
|
2016-11-30 00:29:42 +03:00
|
|
|
import os
|
2018-06-29 12:04:42 +03:00
|
|
|
import struct
|
2018-01-06 03:55:11 +03:00
|
|
|
from hashlib import sha1, sha256
|
|
|
|
|
2016-08-30 18:40:49 +03:00
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# region Multiple utilities
|
2016-08-26 13:58:53 +03:00
|
|
|
|
|
|
|
|
|
|
|
def generate_random_long(signed=True):
|
2016-08-28 14:43:00 +03:00
|
|
|
"""Generates a random long integer (8 bytes), which is optionally signed"""
|
2016-09-03 11:54:58 +03:00
|
|
|
return int.from_bytes(os.urandom(8), signed=signed, byteorder='little')
|
2016-08-26 13:58:53 +03:00
|
|
|
|
|
|
|
|
2016-09-12 20:32:16 +03:00
|
|
|
def ensure_parent_dir_exists(file_path):
|
|
|
|
"""Ensures that the parent directory exists"""
|
|
|
|
parent = os.path.dirname(file_path)
|
|
|
|
if parent:
|
|
|
|
os.makedirs(parent, exist_ok=True)
|
|
|
|
|
2018-06-29 12:04:42 +03:00
|
|
|
|
|
|
|
def add_surrogate(text):
|
|
|
|
return ''.join(
|
|
|
|
# SMP -> Surrogate Pairs (Telegram offsets are calculated with these).
|
|
|
|
# See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more.
|
|
|
|
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
|
|
|
|
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def del_surrogate(text):
|
|
|
|
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
|
|
|
|
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# endregion
|
|
|
|
|
|
|
|
# region Cryptographic related utils
|
2016-08-30 18:40:49 +03:00
|
|
|
|
|
|
|
|
2017-05-21 14:59:16 +03:00
|
|
|
def generate_key_data_from_nonce(server_nonce, new_nonce):
|
|
|
|
"""Generates the key data corresponding to the given nonce"""
|
2017-09-28 12:36:51 +03:00
|
|
|
server_nonce = server_nonce.to_bytes(16, 'little', signed=True)
|
|
|
|
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
|
|
|
|
hash1 = sha1(new_nonce + server_nonce).digest()
|
|
|
|
hash2 = sha1(server_nonce + new_nonce).digest()
|
|
|
|
hash3 = sha1(new_nonce + new_nonce).digest()
|
2016-08-30 18:40:49 +03:00
|
|
|
|
2016-09-17 21:42:34 +03:00
|
|
|
key = hash1 + hash2[:12]
|
|
|
|
iv = hash2[12:20] + hash3 + new_nonce[:4]
|
|
|
|
return key, iv
|
2016-08-30 18:40:49 +03:00
|
|
|
|
|
|
|
|
2016-11-26 14:04:02 +03:00
|
|
|
def get_password_hash(pw, current_salt):
|
|
|
|
"""Gets the password hash for the two-step verification.
|
2017-09-04 18:10:04 +03:00
|
|
|
current_salt should be the byte array provided by
|
|
|
|
invoking GetPasswordRequest()
|
|
|
|
"""
|
2016-11-26 14:04:02 +03:00
|
|
|
|
|
|
|
# Passwords are encoded as UTF-8
|
2017-05-21 14:59:16 +03:00
|
|
|
# At https://github.com/DrKLO/Telegram/blob/e31388
|
|
|
|
# src/main/java/org/telegram/ui/LoginActivity.java#L2003
|
2016-11-26 14:04:02 +03:00
|
|
|
data = pw.encode('utf-8')
|
|
|
|
|
2016-11-30 00:29:42 +03:00
|
|
|
pw_hash = current_salt + data + current_salt
|
2017-06-02 17:49:03 +03:00
|
|
|
return sha256(pw_hash).digest()
|
2016-11-26 14:04:02 +03:00
|
|
|
|
2016-09-08 17:11:37 +03:00
|
|
|
# endregion
|
2018-08-03 00:00:10 +03:00
|
|
|
|
|
|
|
# region Custom Classes
|
|
|
|
|
|
|
|
class TotalList(list):
|
|
|
|
"""
|
|
|
|
A list with an extra `total` property, which may not match its `len`
|
|
|
|
since the total represents the total amount of items *available*
|
|
|
|
somewhere else, not the items *in this list*.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.total = 0
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '[{}, total={}]'.format(
|
|
|
|
', '.join(str(x) for x in self), self.total)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return '[{}, total={}]'.format(
|
|
|
|
', '.join(repr(x) for x in self), self.total)
|
|
|
|
|
2018-09-29 14:29:44 +03:00
|
|
|
|
|
|
|
class _ReadyQueue:
|
|
|
|
"""
|
|
|
|
A queue list that supports an arbitrary cancellation token for `get`.
|
|
|
|
"""
|
|
|
|
def __init__(self, loop):
|
|
|
|
self._list = []
|
|
|
|
self._loop = loop
|
|
|
|
self._ready = asyncio.Event(loop=loop)
|
|
|
|
|
|
|
|
def append(self, item):
|
|
|
|
self._list.append(item)
|
|
|
|
self._ready.set()
|
|
|
|
|
|
|
|
def extend(self, items):
|
|
|
|
self._list.extend(items)
|
|
|
|
self._ready.set()
|
|
|
|
|
|
|
|
async def get(self, cancellation):
|
|
|
|
"""
|
|
|
|
Returns a list of all the items added to the queue until now and
|
|
|
|
clears the list from the queue itself. Returns ``None`` if cancelled.
|
|
|
|
"""
|
2018-09-29 14:36:05 +03:00
|
|
|
ready = self._loop.create_task(self._ready.wait())
|
|
|
|
try:
|
|
|
|
done, pending = await asyncio.wait(
|
|
|
|
[ready, cancellation],
|
|
|
|
return_when=asyncio.FIRST_COMPLETED,
|
|
|
|
loop=self._loop
|
|
|
|
)
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
done = [cancellation]
|
|
|
|
|
2018-09-29 14:29:44 +03:00
|
|
|
if cancellation in done:
|
|
|
|
ready.cancel()
|
|
|
|
return None
|
|
|
|
|
|
|
|
result = self._list
|
|
|
|
self._list = []
|
|
|
|
self._ready.clear()
|
|
|
|
return result
|
|
|
|
|
2018-08-03 00:00:10 +03:00
|
|
|
# endregion
|