Several fixes to authenticator, added more unit tests

Some fixes include, in more detail:
- Using little over big endianess in some parts
- Flagging all the constructor numbers as unsigned
- Fixed bugs with factorizer
- Implemented TLSharp's RSA
This commit is contained in:
Lonami
2016-09-03 10:54:58 +02:00
parent 12cb66ab2c
commit 75a648f438
28 changed files with 226 additions and 53 deletions

0
utils/__init__.py Normal file → Executable file
View File

0
utils/auth_key.py Normal file → Executable file
View File

20
utils/binary_reader.py Normal file → Executable file
View File

@@ -21,34 +21,38 @@ class BinaryReader:
# region Reading
# "All numbers are written as little endian." |> Source: https://core.telegram.org/mtproto
def read_byte(self):
"""Reads a single byte value"""
return self.reader.read(1)[0]
return self.read(1)[0]
def read_int(self, signed=True):
"""Reads an integer (4 bytes) value"""
return int.from_bytes(self.reader.read(4), byteorder='big', signed=signed)
return int.from_bytes(self.read(4), byteorder='little', signed=signed)
def read_long(self, signed=True):
"""Reads a long integer (8 bytes) value"""
return int.from_bytes(self.reader.read(8), byteorder='big', signed=signed)
return int.from_bytes(self.read(8), byteorder='little', signed=signed)
# Network is always big-endian, this is, '>'
def read_float(self):
"""Reads a real floating point (4 bytes) value"""
return unpack('>f', self.reader.read(4))[0]
return unpack('<f', self.read(4))[0]
def read_double(self):
"""Reads a real floating point (8 bytes) value"""
return unpack('>d', self.reader.read(8))[0]
return unpack('<d', self.read(8))[0]
def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value"""
return int.from_bytes(self.reader.read(bits // 8), byteorder='big', signed=signed)
return int.from_bytes(self.read(bits // 8), byteorder='little', signed=signed)
def read(self, length):
"""Read the given amount of bytes"""
return self.reader.read(length)
result = self.reader.read(length)
if len(result) != length:
raise BufferError('Trying to read outside the data bounds (no more data left to read)')
return result
def get_bytes(self):
"""Gets the byte array representing the current buffer as a whole"""

16
utils/binary_writer.py Normal file → Executable file
View File

@@ -17,34 +17,30 @@ class BinaryWriter:
# region Writing
# "All numbers are written as little endian." |> Source: https://core.telegram.org/mtproto
def write_byte(self, value):
"""Writes a single byte value"""
self.writer.write(pack('B', value))
def write_int(self, value, signed=True):
"""Writes an integer value (4 bytes), which can or cannot be signed"""
if not signed:
value &= 0xFFFFFFFF # Ensure it's unsigned (see http://stackoverflow.com/a/30092291/4759433)
self.writer.write(int.to_bytes(value, length=4, byteorder='big', signed=signed))
self.writer.write(int.to_bytes(value, length=4, byteorder='little', signed=signed))
def write_long(self, value, signed=True):
"""Writes a long integer value (8 bytes), which can or cannot be signed"""
if not signed:
value &= 0xFFFFFFFFFFFFFFFF
self.writer.write(int.to_bytes(value, length=8, byteorder='big', signed=signed))
self.writer.write(int.to_bytes(value, length=8, byteorder='little', signed=signed))
# Network is always big-endian, this is, '>' when packing
def write_float(self, value):
"""Writes a floating point value (4 bytes)"""
self.writer.write(pack('>f', value))
self.writer.write(pack('<f', value))
def write_double(self, value):
"""Writes a floating point value (8 bytes)"""
self.writer.write(pack('>d', value))
self.writer.write(pack('<d', value))
def write_large_int(self, value, bits, signed=True):
"""Writes a n-bits long integer value"""
self.writer.write(int.to_bytes(value, length=bits // 8, byteorder='big', signed=signed))
self.writer.write(int.to_bytes(value, length=bits // 8, byteorder='little', signed=signed))
def write(self, data):
"""Writes the given bytes array"""

21
utils/factorizator.py Normal file → Executable file
View File

@@ -1,7 +1,6 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/MTProto/Crypto/Factorizator.cs
from random import randint
from math import gcd
class Factorizator:
@@ -10,7 +9,7 @@ class Factorizator:
g = 0
for i in range(3):
q = (randint(0, 127) & 15) + 17
x = randint(1000000000) + 1
x = randint(0, 1000000000) + 1
y = x
lim = 1 << (i + 18)
for j in range(1, lim):
@@ -27,7 +26,7 @@ class Factorizator:
x = c
z = y - x if x < y else x - y
g = gcd(z, what)
g = Factorizator.gcd(z, what)
if g != 1:
break
@@ -40,6 +39,22 @@ class Factorizator:
p = what // g
return min(p, g)
@staticmethod
def gcd(a, b):
while a != 0 and b != 0:
while b & 1 == 0:
b >>= 1
while a & 1 == 0:
a >>= 1
if a > b:
a -= b
else:
b -= a
return a if b == 0 else b
@staticmethod
def factorize(pq):
divisor = Factorizator.find_small_multiplier_lopatin(pq)

5
utils/helpers.py Normal file → Executable file
View File

@@ -8,7 +8,7 @@ bits_per_byte = 8
def generate_random_long(signed=True):
"""Generates a random long integer (8 bytes), which is optionally signed"""
return int.from_bytes(os.urandom(8), signed=signed, byteorder='big')
return int.from_bytes(os.urandom(8), signed=signed, byteorder='little')
def generate_random_bytes(count):
@@ -19,6 +19,7 @@ def generate_random_bytes(count):
def get_byte_array(integer, signed):
bits = integer.bit_length()
byte_length = (bits + bits_per_byte - 1) // bits_per_byte
# For some strange reason, this has to be big!
return int.to_bytes(integer, length=byte_length, byteorder='big', signed=signed)
@@ -65,6 +66,6 @@ def generate_key_data_from_nonces(server_nonce, new_nonce):
def sha1(data):
sha = hashlib.sha1
sha = hashlib.sha1()
sha.update(data)
return sha.digest()

62
utils/rsa.py Executable file
View File

@@ -0,0 +1,62 @@
# This file is based on TLSharp
# https://github.com/sochix/TLSharp/blob/master/TLSharp.Core/Auth/Authenticator.cs
from utils.binary_writer import BinaryWriter
import utils.helpers as utils
class RSAServerKey:
def __init__(self, fingerprint, m, e):
self.fingerprint = fingerprint
self.m = m
self.e = e
def encrypt(self, data, offset=None, length=None):
if offset is None:
offset = 0
if length is None:
length = len(data)
with BinaryWriter() as writer:
# Write SHA
writer.write(utils.sha1(data[offset:offset+length]))
# Write data
writer.write(data[offset:offset+length])
# Add padding if required
if length < 235:
writer.write(utils.generate_random_bytes(235 - length))
cipher_text = utils.get_byte_array(
pow(int.from_bytes(writer.get_bytes(), byteorder='big'), self.e, self.m),
signed=False)
if len(cipher_text) == 256:
return cipher_text
else:
padding = bytes(256 - len(cipher_text))
return padding + cipher_text
class RSA:
_server_keys = {
'216be86c022bb4c3':
RSAServerKey('216be86c022bb4c3', int('C150023E2F70DB7985DED064759CFECF0AF328E69A41DAF4D6F01B538135A6F9'
'1F8F8B2A0EC9BA9720CE352EFCF6C5680FFC424BD634864902DE0B4BD6D49F4E'
'580230E3AE97D95C8B19442B3C0A10D8F5633FECEDD6926A7F6DAB0DDB7D457F'
'9EA81B8465FCD6FFFEED114011DF91C059CAEDAF97625F6C96ECC74725556934'
'EF781D866B34F011FCE4D835A090196E9A5F0E4449AF7EB697DDB9076494CA5F'
'81104A305B6DD27665722C46B60E5DF680FB16B210607EF217652E60236C255F'
'6A28315F4083A96791D7214BF64C1DF4FD0DB1944FB26A2A57031B32EEE64AD1'
'5A8BA68885CDE74A5BFC920F6ABF59BA5C75506373E7130F9042DA922179251F',
16), int('010001', 16))
}
@staticmethod
def encrypt(fingerprint, data, offset=None, length=None):
if fingerprint.lower() not in RSA._server_keys:
return None
key = RSA._server_keys[fingerprint.lower()]
return key.encrypt(data, offset, length)