├── .gitignore ├── README.md ├── TL.py ├── TL_schema.JSON ├── classes ├── __init__.py ├── chat.py ├── contact.py ├── file.py ├── message.py ├── shell.py ├── telepy.py └── user.py ├── crypt.py ├── mtproto.py ├── prime.py ├── telepy.py ├── testing.py └── tests ├── Serialization and SHA.py ├── encrypt_decrypt_ige_test.py ├── file.py ├── ige.py └── research_decrypt_mode.py /.gitignore: -------------------------------------------------------------------------------- 1 | # JetBrains IDE 2 | .idea/ 3 | *.iml 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # testing apps 61 | credentials 62 | rsa.pub 63 | 64 | # emacs auto-saving files 65 | \#*# 66 | .#*# -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Join the chat at https://gitter.im/griganton/telepy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/griganton/telepy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | # About this repo 4 | This is Telegram API for python. 5 | Main aim is to implement MTProto protocol Telegram API on pure Python (not wrapped CLI) 6 | 7 | ### Plan 8 | - [ ] Make it work on Python 2 as well as 3. 9 | - [ ] Follow up the same functionality of CLI API. 10 | - [x] Serialize/Deserialize 11 | - [x] Send and receive PQ authorization with server [[doc]](https://core.telegram.org/mtproto/samples-auth_key) 12 | - [ ] Send and receive service messages with server like logging in to server [[doc]](https://core.telegram.org/mtproto/service_messages) 13 | 14 | ### Useful start points to join 15 | Detailed description of API and protocol can be found here: 16 | * https://core.telegram.org/api 17 | * https://core.telegram.org/mtproto 18 | 19 | API registration is needed to be done by yourself at http://my.telegram.org 20 | Follow Structure - Credentials for provide it your API information. 21 | 22 | # Structure 23 | 24 | - tests 25 | - Serialization and SHA.py 26 | - mtproto.py 27 | - testing.py 28 | - prime.py 29 | - credentials 30 | - rsa.pub 31 | 32 | ## Credentials 33 | Repo doesn't contain any credentials to connect Telegram servers. 34 | You can get yours from http://my.telegram.org 35 | 36 | You should place 2 files in the root of your repo: 37 | - credentials 38 | - rsa.pub 39 | 40 | Config example for "credentials" file: 41 | 42 | ``` 43 | [App data] 44 | api_id: 12345 45 | api_hash: 1234567890abcdef1234567890abcdef 46 | ip_address: 112.134.156.178 47 | port: 443 48 | ``` 49 | rsa.pub contains your RSA key. 50 | 51 | ## mtproto.py 52 | 53 | Contains functions to work with MTproto protocol: 54 | - TL schema parsing 55 | - serializing and deserializing 56 | - manage connections to server 57 | - send and receive messages 58 | 59 | ## testing.py 60 | 61 | testing.py is used to test functionality of modules. 62 | Now it makes steps from https://core.telegram.org/mtproto/samples-auth_key: 63 | - sends PQ request to server 64 | - parses the result 65 | - factorizes PQ 66 | 67 | ## prime.py 68 | prime.py is used in PQ factorization. It has been copied from https://stackoverflow.com/questions/4643647/fast-prime-factorization-module 69 | 70 | ## TL schema 71 | We use JSON format TL Shema. TL Schema file contains information about objects and methods, it is located in TL_schema.JSON file in the root of repo. It is fully equivalent to JSON TL Schema from 72 | https://core.telegram.org/schema/mtproto-json 73 | -------------------------------------------------------------------------------- /TL.py: -------------------------------------------------------------------------------- 1 | __author__ = 'agrigoryev' 2 | import os 3 | import struct 4 | import json 5 | import io 6 | from numbers import Number 7 | 8 | class TlConstructor: 9 | def __init__(self, json_dict): 10 | self.id = int(json_dict['id']) 11 | self.type = json_dict['type'] 12 | self.predicate = json_dict['predicate'] 13 | self.params = [] 14 | # case of vector 15 | for param in json_dict['params']: 16 | if param['type'] == "Vector": 17 | param['type'] = "Vector t" 18 | param['subtype'] = "long" 19 | elif param['type'] == "vector<%Message>": 20 | param['type'] = "vector" 21 | param['subtype'] = "message" 22 | elif param['type'] == "vector": 23 | param['type'] = "vector" 24 | param['subtype'] = "future_salt" 25 | else: 26 | param['subtype'] = None 27 | self.params.append(param) 28 | 29 | class TlMethod: 30 | def __init__(self, json_dict): 31 | self.id = int(json_dict['id']) 32 | self.type = json_dict['type'] 33 | self.method = json_dict['method'] 34 | self.params = json_dict['params'] 35 | 36 | 37 | class TLObject(dict): 38 | def __init__(self, tl_elem): 39 | self.name = tl_elem.predicate 40 | 41 | class TL: 42 | def __init__(self, filename): 43 | with open(filename, 'r') as f: 44 | TL_dict = json.load(f) 45 | 46 | # Read constructors 47 | 48 | self.constructors = TL_dict['constructors'] 49 | self.constructor_id = {} 50 | self.constructor_type = {} 51 | for elem in self.constructors: 52 | z = TlConstructor(elem) 53 | self.constructor_id[z.id] = z 54 | self.constructor_type[z.predicate] = z 55 | 56 | self.methods = TL_dict['methods'] 57 | self.method_id = {} 58 | self.method_name = {} 59 | for elem in self.methods: 60 | z = TlMethod(elem) 61 | self.method_id[z.id] = z 62 | self.method_name[z.method] = z 63 | 64 | 65 | ## Loading TL_schema (should be placed in the same directory as mtproto.py 66 | tl = TL(os.path.join(os.path.dirname(__file__), "TL_schema.JSON")) 67 | 68 | 69 | def serialize_obj(type_, **kwargs): 70 | bytes_io = io.BytesIO() 71 | try: 72 | tl_constructor = tl.constructor_type[type_] 73 | except KeyError: 74 | raise Exception("Could not extract type: %s" % type_) 75 | bytes_io.write(struct.pack('"}],"type":"ResPQ"},{"id":"-2083955988","predicate":"p_q_inner_data","params":[{"name":"pq","type":"bytes"},{"name":"p","type":"bytes"},{"name":"q","type":"bytes"},{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce","type":"int256"}],"type":"P_Q_inner_data"},{"id":"1013613780","predicate":"p_q_inner_data_temp","params":[{"name":"pq","type":"bytes"},{"name":"p","type":"bytes"},{"name":"q","type":"bytes"},{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce","type":"int256"},{"name":"expires_in","type":"int"}],"type":"P_Q_inner_data"},{"id":"2043348061","predicate":"server_DH_params_fail","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce_hash","type":"int128"}],"type":"Server_DH_Params"},{"id":"-790100132","predicate":"server_DH_params_ok","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"encrypted_answer","type":"bytes"}],"type":"Server_DH_Params"},{"id":"-1249309254","predicate":"server_DH_inner_data","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"g","type":"int"},{"name":"dh_prime","type":"bytes"},{"name":"g_a","type":"bytes"},{"name":"server_time","type":"int"}],"type":"Server_DH_inner_data"},{"id":"1715713620","predicate":"client_DH_inner_data","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"retry_id","type":"long"},{"name":"g_b","type":"bytes"}],"type":"Client_DH_Inner_Data"},{"id":"1003222836","predicate":"dh_gen_ok","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce_hash1","type":"int128"}],"type":"Set_client_DH_params_answer"},{"id":"1188831161","predicate":"dh_gen_retry","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce_hash2","type":"int128"}],"type":"Set_client_DH_params_answer"},{"id":"-1499615742","predicate":"dh_gen_fail","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"new_nonce_hash3","type":"int128"}],"type":"Set_client_DH_params_answer"},{"id":"-212046591","predicate":"rpc_result","params":[{"name":"req_msg_id","type":"long"},{"name":"result","type":"Object"}],"type":"RpcResult"},{"id":"558156313","predicate":"rpc_error","params":[{"name":"error_code","type":"int"},{"name":"error_message","type":"string"}],"type":"RpcError"},{"id":"1579864942","predicate":"rpc_answer_unknown","params":[],"type":"RpcDropAnswer"},{"id":"-847714938","predicate":"rpc_answer_dropped_running","params":[],"type":"RpcDropAnswer"},{"id":"-1539647305","predicate":"rpc_answer_dropped","params":[{"name":"msg_id","type":"long"},{"name":"seq_no","type":"int"},{"name":"bytes","type":"int"}],"type":"RpcDropAnswer"},{"id":"155834844","predicate":"future_salt","params":[{"name":"valid_since","type":"int"},{"name":"valid_until","type":"int"},{"name":"salt","type":"long"}],"type":"FutureSalt"},{"id":"-1370486635","predicate":"future_salts","params":[{"name":"req_msg_id","type":"long"},{"name":"now","type":"int"},{"name":"salts","type":"vector"}],"type":"FutureSalts"},{"id":"880243653","predicate":"pong","params":[{"name":"msg_id","type":"long"},{"name":"ping_id","type":"long"}],"type":"Pong"},{"id":"-501201412","predicate":"destroy_session_ok","params":[{"name":"session_id","type":"long"}],"type":"DestroySessionRes"},{"id":"1658015945","predicate":"destroy_session_none","params":[{"name":"session_id","type":"long"}],"type":"DestroySessionRes"},{"id":"-1631450872","predicate":"new_session_created","params":[{"name":"first_msg_id","type":"long"},{"name":"unique_id","type":"long"},{"name":"server_salt","type":"long"}],"type":"NewSession"},{"id":"1945237724","predicate":"msg_container","params":[{"name":"messages","type":"vector<%Message>"}],"type":"MessageContainer"},{"id":"1538843921","predicate":"message","params":[{"name":"msg_id","type":"long"},{"name":"seqno","type":"int"},{"name":"bytes","type":"int"},{"name":"body","type":"Object"}],"type":"Message"},{"id":"-530561358","predicate":"msg_copy","params":[{"name":"orig_message","type":"Message"}],"type":"MessageCopy"},{"id":"812830625","predicate":"gzip_packed","params":[{"name":"packed_data","type":"bytes"}],"type":"Object"},{"id":"1658238041","predicate":"msgs_ack","params":[{"name":"msg_ids","type":"Vector"}],"type":"MsgsAck"},{"id":"-1477445615","predicate":"bad_msg_notification","params":[{"name":"bad_msg_id","type":"long"},{"name":"bad_msg_seqno","type":"int"},{"name":"error_code","type":"int"}],"type":"BadMsgNotification"},{"id":"-307542917","predicate":"bad_server_salt","params":[{"name":"bad_msg_id","type":"long"},{"name":"bad_msg_seqno","type":"int"},{"name":"error_code","type":"int"},{"name":"new_server_salt","type":"long"}],"type":"BadMsgNotification"},{"id":"2105940488","predicate":"msg_resend_req","params":[{"name":"msg_ids","type":"Vector"}],"type":"MsgResendReq"},{"id":"-630588590","predicate":"msgs_state_req","params":[{"name":"msg_ids","type":"Vector"}],"type":"MsgsStateReq"},{"id":"81704317","predicate":"msgs_state_info","params":[{"name":"req_msg_id","type":"long"},{"name":"info","type":"bytes"}],"type":"MsgsStateInfo"},{"id":"-1933520591","predicate":"msgs_all_info","params":[{"name":"msg_ids","type":"Vector"},{"name":"info","type":"bytes"}],"type":"MsgsAllInfo"},{"id":"661470918","predicate":"msg_detailed_info","params":[{"name":"msg_id","type":"long"},{"name":"answer_msg_id","type":"long"},{"name":"bytes","type":"int"},{"name":"status","type":"int"}],"type":"MsgDetailedInfo"},{"id":"-2137147681","predicate":"msg_new_detailed_info","params":[{"name":"answer_msg_id","type":"long"},{"name":"bytes","type":"int"},{"name":"status","type":"int"}],"type":"MsgDetailedInfo"},{"id":"1973679973","predicate":"bind_auth_key_inner","params":[{"name":"nonce","type":"long"},{"name":"temp_auth_key_id","type":"long"},{"name":"perm_auth_key_id","type":"long"},{"name":"temp_session_id","type":"long"},{"name":"expires_at","type":"int"}],"type":"BindAuthKeyInner"}],"methods":[{"id":"1615239032","method":"req_pq","params":[{"name":"nonce","type":"int128"}],"type":"ResPQ"},{"id":"-686627650","method":"req_DH_params","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"p","type":"bytes"},{"name":"q","type":"bytes"},{"name":"public_key_fingerprint","type":"long"},{"name":"encrypted_data","type":"bytes"}],"type":"Server_DH_Params"},{"id":"-184262881","method":"set_client_DH_params","params":[{"name":"nonce","type":"int128"},{"name":"server_nonce","type":"int128"},{"name":"encrypted_data","type":"bytes"}],"type":"Set_client_DH_params_answer"},{"id":"1491380032","method":"rpc_drop_answer","params":[{"name":"req_msg_id","type":"long"}],"type":"RpcDropAnswer"},{"id":"-1188971260","method":"get_future_salts","params":[{"name":"num","type":"int"}],"type":"FutureSalts"},{"id":"2059302892","method":"ping","params":[{"name":"ping_id","type":"long"}],"type":"Pong"},{"id":"-213746804","method":"ping_delay_disconnect","params":[{"name":"ping_id","type":"long"},{"name":"disconnect_delay","type":"int"}],"type":"Pong"},{"id":"-414113498","method":"destroy_session","params":[{"name":"session_id","type":"long"}],"type":"DestroySessionRes"},{"id":"-1835453025","method":"http_wait","params":[{"name":"max_delay","type":"int"},{"name":"wait_after","type":"int"},{"name":"max_wait","type":"int"}],"type":"HttpWait"}]} -------------------------------------------------------------------------------- /classes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from . import chat, user, message, contact 4 | 5 | from .chat import Chat 6 | from .user import User 7 | from .message import Message 8 | from .contact import Contact 9 | 10 | __all__ = ['Chat', 'User', 'Message', 'Contact'] 11 | 12 | -------------------------------------------------------------------------------- /classes/chat.py: -------------------------------------------------------------------------------- 1 | 2 | class Chat(): 3 | def __init__(self): 4 | self._users = [] # users in this chatroom 5 | def add_user(self, user): 6 | self._users += user -------------------------------------------------------------------------------- /classes/contact.py: -------------------------------------------------------------------------------- 1 | 2 | class Contact(): 3 | pass -------------------------------------------------------------------------------- /classes/file.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | import os 3 | from subprocess import call 4 | from os.path import exists 5 | 6 | class File(): 7 | def __init__(self, path): 8 | self._path = path 9 | 10 | def write_bytes(self, bytes): 11 | ''' truncates the file and create new with :param bytes. 12 | :return number of bytes written''' 13 | with open(self._path, 'w+b') as file: 14 | return file.write(bytes) 15 | 16 | def read_bytes(self): 17 | ''' read the file as bytes. :return b'' on file not exist ''' 18 | if not exists(self._path): return b'' 19 | # buf = b'' 20 | with open(self._path, 'r+b') as file: 21 | return file.read() 22 | # return buf 23 | 24 | def open(self): 25 | '''tries to open with os default viewer''' 26 | call(('cmd /c start "" "'+ self._path +'"')if os.name is 'nt' else ('open' if platform.startswith('darwin') else 'xdg-open', self._path)) 27 | 28 | def remove(self): 29 | ''' try to remove the file ''' 30 | try: 31 | os.remove(self._path) 32 | except FileNotFoundError: pass -------------------------------------------------------------------------------- /classes/message.py: -------------------------------------------------------------------------------- 1 | import crypt 2 | 3 | class Message(): 4 | pass -------------------------------------------------------------------------------- /classes/shell.py: -------------------------------------------------------------------------------- 1 | import os, cmd 2 | 3 | class TelepyShell(cmd.Cmd): 4 | intro='Welcome to telepy interactive shell. Type help or ? for help.\n' 5 | prompt='>' 6 | 7 | def preloop(self): 8 | from classes.telepy import Telepy 9 | self._telepy = Telepy() 10 | def precmd(self, line): 11 | # convert first word(command name) to lower and return it as line 12 | line = line.lstrip() 13 | blank_pos = line.find(' ') 14 | if blank_pos < 0: return line.lower() 15 | return line[:blank_pos].lower() + ' ' + line[blank_pos+1:] 16 | def completedefault(self, *ignored): 17 | print(ignored) 18 | def complete(self, text, state): 19 | self.super().complete(text, state) 20 | print('completing') 21 | 22 | def do_shell(self, line): 23 | ''' 24 | shell 25 | lets you use external shell. ! for short-hand. 26 | ''' 27 | print(os.popen(line).read()) 28 | #detailed commands 29 | def do_msg(self, arg): 30 | ''' 31 | msg 32 | sends message to this peer 33 | ''' 34 | pass 35 | def do_fwd(self, arg): 36 | ''' 37 | fwd 38 | forward message to user. You can see message numbers starting client with -N 39 | ''' 40 | pass 41 | def do_chat_with_peer(self, arg): 42 | ''' 43 | chat_with_peer 44 | starts one on one chat session with this peer. /exit or /quit to end this mode. 45 | ''' 46 | pass 47 | def do_add_contact(self, arg): 48 | ''' 49 | add_contact 50 | tries to add contact to contact-list by phone 51 | ''' 52 | pass 53 | def do_rename_contact(self, arg): 54 | ''' 55 | rename_contact 56 | tries to rename contact. If you have another device it will be a fight 57 | ''' 58 | pass 59 | def do_mark_read(self, arg): 60 | ''' 61 | mark_read 62 | mark read all received messages with peer 63 | ''' 64 | pass 65 | def do_delete_msg(self, arg): 66 | ''' 67 | delete_msg 68 | deletes message (not completly, though) 69 | ''' 70 | pass 71 | def do_restore_msg(self, arg): 72 | ''' 73 | restore_msg 74 | restores delete message. Impossible for secret chats. Only possible short time (one hour, I think) after deletion 75 | ''' 76 | pass 77 | 78 | def do_send_photo(self, arg): 79 | ''' 80 | send_photo 81 | sends photo to peer 82 | ''' 83 | pass 84 | 85 | def do_send_video(self, arg): 86 | ''' 87 | send_video 88 | sends video to peer 89 | ''' 90 | pass 91 | def do_send_text(self, arg): 92 | ''' 93 | send_text 94 | sends text file as plain messages 95 | ''' 96 | pass 97 | 98 | def do_load_photo(self, arg): 99 | ''' 100 | load_photo 101 | loads photo to download dir 102 | ''' 103 | pass 104 | 105 | def do_load_video(self, arg): 106 | ''' 107 | load_video 108 | loads video to download dir 109 | ''' 110 | pass 111 | def do_load_video_thumb(self, arg): 112 | ''' 113 | load_video_thumb 114 | loads video thumbnail to download dir 115 | ''' 116 | pass 117 | def do_load_audio(self, arg): 118 | ''' 119 | load_audio 120 | loads audio to download dir 121 | ''' 122 | pass 123 | def do_load_document(self, arg): 124 | ''' 125 | load_document 126 | loads document to download dir 127 | ''' 128 | pass 129 | def do_load_document_thumb(self, arg): 130 | ''' 131 | load_document_thumb 132 | loads document thumbnail to download dir 133 | ''' 134 | pass 135 | 136 | def do_view_photo(self, arg): 137 | ''' 138 | view_photo 139 | loads photo/video to download dir and starts system default viewer 140 | ''' 141 | pass 142 | 143 | def do_view_video(self, arg): 144 | ''' 145 | view_video 146 | ''' 147 | pass 148 | def do_view_video_thumb(self, arg): 149 | ''' 150 | view_video_thumb 151 | ''' 152 | pass 153 | def do_view_audio(self, arg): 154 | ''' 155 | view_audio 156 | ''' 157 | pass 158 | def do_view_document(self, arg): 159 | ''' 160 | view_document 161 | ''' 162 | pass 163 | def do_view_document_thumb(self, arg): 164 | ''' 165 | view_document_thumb 166 | ''' 167 | pass 168 | 169 | def do_fwd_media(self, arg): 170 | ''' 171 | fwd_media 172 | send media in your message. Use this to prevent sharing info about author of media (though, it is possible to determine user_id from media itself, it is not possible get access_hash of this user) 173 | ''' 174 | pass 175 | def do_set_profile_photo(self, arg): 176 | ''' 177 | set_profile_photo 178 | sets userpic. Photo should be square, or server will cut biggest central square part 179 | ''' 180 | pass 181 | 182 | def do_chat_info(self, arg): 183 | ''' 184 | chat_info 185 | prints info about chat 186 | ''' 187 | arg=arg.split() 188 | if len(arg) is 1: 189 | print ('chat_info called with ', arg[0]) 190 | def do_chat_add_user(self,arg): 191 | ''' 192 | chat_add_user 193 | add user to chat 194 | ''' 195 | print(arg) 196 | def do_chat_del_user(self,arg): 197 | ''' 198 | chat_del_user 199 | remove user from chat 200 | ''' 201 | pass 202 | def do_chat_rename(self,arg): 203 | ''' 204 | chat_rename 205 | rename chat room 206 | ''' 207 | arg=arg.split() 208 | 209 | def do_create_group_chat(self, chat_topic, user1, user2, user3): 210 | ''' 211 | create_group_chat ... 212 | creates a groupchat with users, use chat_add_user to add more users 213 | ''' 214 | print(chat_topic) 215 | print(user1,user2,user3) 216 | 217 | pass 218 | def do_chat_set_photo(self, chat, photo): 219 | ''' 220 | chat_set_photo 221 | sets group chat photo. Same limits as for profile photos. 222 | ''' 223 | pass 224 | 225 | def do_search(self, pattern): 226 | ''' 227 | search 228 | searches pattern in messages with peer 229 | ''' 230 | pass 231 | def do_global_search(self, pattern): 232 | ''' 233 | global_search 234 | searches pattern in all messages 235 | ''' 236 | pass 237 | 238 | def do_create_secret_chat(self, user): 239 | ''' 240 | create_secret_chat 241 | creates secret chat with this user 242 | ''' 243 | pass 244 | def do_visualize_key(self, secret_chat): 245 | ''' 246 | visualize_key 247 | prints visualization of encryption key. You should compare it to your partner's one 248 | ''' 249 | pass 250 | def do_set_ttl(self, secret_chat, ttl): 251 | ''' 252 | set_ttl 253 | sets ttl to secret chat. Though client does ignore it, client on other end can make use of it 254 | ''' 255 | pass 256 | def do_accept_secret_chat(self, secret_chat): 257 | ''' 258 | accept_secret_chat 259 | manually accept secret chat (only useful when starting with -E key) 260 | ''' 261 | pass 262 | 263 | def do_user_info(self, user): 264 | ''' 265 | user_info 266 | prints info about user 267 | ''' 268 | pass 269 | def do_history(self, peer, limit=40): 270 | ''' 271 | history [limit] 272 | prints history (and marks it as read). Default limit = 40 273 | ''' 274 | if peer is '': 275 | print('no peer have specified') 276 | return 277 | args = peer.split() 278 | if len(args) not in (1,2) : 279 | print('not appropriate number of arguments : ', peer) 280 | return 281 | if len(args) is 2: 282 | if not args[1].isdecimal() or int(args[1]) < 1: 283 | print('not a valid limit:', args[1]) 284 | limit = int(args[1]) 285 | print(peer) 286 | print(limit) 287 | def do_dialog_list(self, ignored): 288 | ''' 289 | dialog_list 290 | prints info about your dialogs 291 | ''' 292 | pass 293 | def do_contact_list(self, ignored): 294 | ''' 295 | contact_list 296 | prints info about users in your contact list 297 | ''' 298 | pass 299 | def do_suggested_contacts(self, ignored): 300 | ''' 301 | suggested_contacts 302 | print info about contacts, you have max common friends 303 | ''' 304 | pass 305 | def do_stats(self, ignored): 306 | ''' 307 | stats 308 | just for debugging 309 | ''' 310 | pass 311 | 312 | def do_export_card(self, card): 313 | ''' 314 | export_card 315 | print your 'card' that anyone can later use to import your contact 316 | ''' 317 | pass 318 | def do_import_card(self, card): 319 | ''' 320 | import_card 321 | gets user by card. You can write messages to him after that. 322 | ''' 323 | pass 324 | 325 | def do_quit_force(self, ignored): 326 | ''' 327 | quit_force 328 | quit without waiting for query ends 329 | ''' 330 | return True 331 | def do_quit(self, ignored): 332 | ''' 333 | quit 334 | wait for all queries to end then quit 335 | ''' 336 | #TODO:safely end queries 337 | return True -------------------------------------------------------------------------------- /classes/telepy.py: -------------------------------------------------------------------------------- 1 | 2 | class Telepy(): 3 | def __init__(self): 4 | # Deal with py2 and py3 differences 5 | try: # this only works in py2.7 6 | import configparser 7 | except ImportError: 8 | import ConfigParser as configparser 9 | import mtproto 10 | 11 | self._config = configparser.ConfigParser() 12 | # Check if credentials is correctly loaded (when it doesn't read anything it returns []) 13 | if not self._config.read('credentials'): 14 | print("File 'credentials' seems to not exist.") 15 | exit(-1) 16 | ip = self._config.get('App data', 'ip_address') 17 | port = self._config.getint('App data', 'port') 18 | 19 | self._session = mtproto.Session(ip, port) 20 | self._session.create_auth_key() 21 | 22 | self._salt = future_salts = self._session.method_call('get_future_salts', num=3) 23 | -------------------------------------------------------------------------------- /classes/user.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class User(): 4 | me = None 5 | ''' current connected user ''' 6 | 7 | friends = [] 8 | ''' current connected user's friends ''' 9 | 10 | def __init__(self, uid): 11 | self.uid = uid 12 | -------------------------------------------------------------------------------- /crypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Sammy Pfeiffer 3 | # Author: Anton Grigoryev 4 | # This file implements the AES 256 IGE cipher 5 | # working in Python 2.7 and Python 3.4 (other versions untested) 6 | # as it's needed for the implementation of Telegram API 7 | # It's based on PyCryto 8 | 9 | from __future__ import print_function 10 | from Crypto.Util.strxor import strxor 11 | from Crypto.Cipher import AES 12 | 13 | # AES 256 IGE part 14 | 15 | def ige_encrypt(message, key, iv): 16 | return _ige(message, key, iv, operation="encrypt") 17 | 18 | def ige_decrypt(message, key, iv): 19 | return _ige(message, key, iv, operation="decrypt") 20 | 21 | def _ige(message, key, iv, operation="decrypt"): 22 | """Given a key, given an iv, and message 23 | do whatever operation asked in the operation field. 24 | Operation will be checked for: "decrypt" and "encrypt" strings. 25 | Returns the message encrypted/decrypted. 26 | message must be a multiple by 16 bytes (for division in 16 byte blocks) 27 | key must be 32 byte 28 | iv must be 32 byte (it's not internally used in AES 256 ECB, but it's 29 | needed for IGE)""" 30 | message = bytes(message) 31 | if len(key) != 32: 32 | raise ValueError("key must be 32 bytes long (was " + 33 | str(len(key)) + " bytes)") 34 | if len(iv) != 32: 35 | raise ValueError("iv must be 32 bytes long (was " + 36 | str(len(iv)) + " bytes)") 37 | 38 | cipher = AES.new(key, AES.MODE_ECB, iv) 39 | blocksize = cipher.block_size 40 | 41 | if len(message) % blocksize != 0: 42 | raise ValueError("message must be a multiple of 16 bytes (try adding " + 43 | str(16 - len(message) % 16) + " bytes of padding)") 44 | 45 | ivp = iv[0:blocksize] 46 | ivp2 = iv[blocksize:] 47 | 48 | ciphered = bytes() 49 | 50 | for i in range(0, len(message), blocksize): 51 | indata = message[i:i+blocksize] 52 | if operation == "decrypt": 53 | xored = strxor(indata, ivp2) 54 | decrypt_xored = cipher.decrypt(xored) 55 | outdata = strxor(decrypt_xored, ivp) 56 | ivp = indata 57 | ivp2 = outdata 58 | elif operation == "encrypt": 59 | xored = strxor(indata, ivp) 60 | encrypt_xored = cipher.encrypt(xored) 61 | outdata = strxor(encrypt_xored, ivp2) 62 | ivp = outdata 63 | ivp2 = indata 64 | else: 65 | raise ValueError("operation must be either 'decrypt' or 'encrypt'") 66 | ciphered += outdata 67 | return ciphered -------------------------------------------------------------------------------- /mtproto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Tue Sep 2 19:26:15 2014 4 | 5 | @author: Anton Grigoryev 6 | @author: Sammy Pfeiffer 7 | """ 8 | from binascii import crc32 as originalcrc32 9 | from time import time 10 | import io 11 | import os.path 12 | import socket 13 | import struct 14 | 15 | # pycrypto module 16 | from Crypto.Hash import SHA 17 | from Crypto.PublicKey import RSA 18 | from Crypto.Util.strxor import strxor 19 | from Crypto.Util.number import long_to_bytes, bytes_to_long 20 | 21 | # local modules 22 | import crypt 23 | import prime 24 | import TL 25 | 26 | def crc32(data): 27 | return originalcrc32(data) & 0xffffffff 28 | 29 | def vis(bs): 30 | """ 31 | Function to visualize byte streams. Split into bytes, print to console. 32 | :param bs: BYTE STRING 33 | """ 34 | bs = bytearray(bs) 35 | symbols_in_one_line = 8 36 | n = len(bs) // symbols_in_one_line 37 | i = 0 38 | for i in range(n): 39 | print(str(i*symbols_in_one_line)+" | "+" ".join(["%02X" % b for b in bs[i*symbols_in_one_line:(i+1)*symbols_in_one_line]])) # for every 8 symbols line 40 | if not len(bs) % symbols_in_one_line == 0: 41 | print(str((i+1)*symbols_in_one_line)+" | "+" ".join(["%02X" % b for b in bs[(i+1)*symbols_in_one_line:]])+"\n") # for last line 42 | 43 | 44 | class Session: 45 | """ Manages TCP Transport. encryption and message frames """ 46 | def __init__(self, ip, port, auth_key=None, server_salt=None): 47 | # creating socket 48 | self.sock = socket.socket() 49 | self.sock.connect((ip, port)) 50 | self.number = 0 51 | self.timedelta = 0 52 | self.session_id = os.urandom(8) 53 | self.auth_key = auth_key 54 | self.auth_key_id = SHA.new(self.auth_key).digest()[-8:] if self.auth_key else None 55 | self.sock.settimeout(5.0) 56 | self.MAX_RETRY = 5; 57 | self.AUTH_MAX_RETRY = 5; 58 | 59 | def __del__(self): 60 | # closing socket when session object is deleted 61 | self.sock.close() 62 | 63 | def send_message(self, message_data): 64 | """ 65 | Forming the message frame and sending message to server 66 | :param message: byte string to send 67 | """ 68 | 69 | message_id = struct.pack(' q: (p, q) = (q, p) 158 | assert p*q == pq and p < q 159 | 160 | print("Factorization %d = %d * %d" % (pq, p, q)) 161 | p_bytes = long_to_bytes(p) 162 | q_bytes = long_to_bytes(q) 163 | f = open(os.path.join(os.path.dirname(__file__), "rsa.pub")) 164 | key = RSA.importKey(f.read()) 165 | 166 | new_nonce = os.urandom(32) 167 | data = TL.serialize_obj('p_q_inner_data', 168 | pq=pq_bytes, 169 | p=p_bytes, 170 | q=q_bytes, 171 | nonce=nonce, 172 | server_nonce=server_nonce, 173 | new_nonce=new_nonce) 174 | 175 | sha_digest = SHA.new(data).digest() 176 | random_bytes = os.urandom(255-len(data)-len(sha_digest)) 177 | to_encrypt = sha_digest + data + random_bytes 178 | encrypted_data = key.encrypt(to_encrypt, 0)[0] 179 | 180 | print("Starting Diffie Hellman key exchange") 181 | server_dh_params = self.method_call('req_DH_params', 182 | nonce=nonce, 183 | server_nonce=server_nonce, 184 | p=p_bytes, 185 | q=q_bytes, 186 | public_key_fingerprint=public_key_fingerprint, 187 | encrypted_data=encrypted_data) 188 | assert nonce == server_dh_params['nonce'] 189 | assert server_nonce == server_dh_params['server_nonce'] 190 | 191 | encrypted_answer = server_dh_params['encrypted_answer'] 192 | 193 | tmp_aes_key = SHA.new(new_nonce + server_nonce).digest() + SHA.new(server_nonce + new_nonce).digest()[0:12] 194 | tmp_aes_iv = SHA.new(server_nonce + new_nonce).digest()[12:20] + SHA.new(new_nonce + new_nonce).digest() + new_nonce[0:4] 195 | 196 | answer_with_hash = crypt.ige_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv) 197 | 198 | answer_hash = answer_with_hash[:20] 199 | answer = answer_with_hash[20:] 200 | # TODO: SHA hash assertion here 201 | 202 | server_DH_inner_data = TL.deserialize(io.BytesIO(answer)) 203 | assert nonce == server_DH_inner_data['nonce'] 204 | assert server_nonce == server_DH_inner_data['server_nonce'] 205 | dh_prime_str = server_DH_inner_data['dh_prime'] 206 | g = server_DH_inner_data['g'] 207 | g_a_str = server_DH_inner_data['g_a'] 208 | server_time = server_DH_inner_data['server_time'] 209 | self.timedelta = server_time - time() 210 | print("Server-client time delta = %.1f s" % self.timedelta) 211 | 212 | dh_prime = bytes_to_long(dh_prime_str) 213 | g_a = bytes_to_long(g_a_str) 214 | 215 | assert prime.isprime(dh_prime) 216 | retry_id = 0 217 | b_str = os.urandom(256) 218 | b = bytes_to_long(b_str) 219 | g_b = pow(g, b, dh_prime) 220 | 221 | g_b_str = long_to_bytes(g_b) 222 | 223 | data = TL.serialize_obj('client_DH_inner_data', 224 | nonce=nonce, 225 | server_nonce=server_nonce, 226 | retry_id=retry_id, 227 | g_b=g_b_str) 228 | data_with_sha = SHA.new(data).digest()+data 229 | data_with_sha_padded = data_with_sha + os.urandom(-len(data_with_sha) % 16) 230 | encrypted_data = crypt.ige_encrypt(data_with_sha_padded, tmp_aes_key, tmp_aes_iv) 231 | 232 | for i in range(1, self.AUTH_MAX_RETRY): # retry when dh_gen_retry or dh_gen_fail 233 | Set_client_DH_params_answer = self.method_call('set_client_DH_params', 234 | nonce=nonce, 235 | server_nonce=server_nonce, 236 | encrypted_data=encrypted_data) 237 | 238 | # print Set_client_DH_params_answer 239 | auth_key = pow(g_a, b, dh_prime) 240 | auth_key_str = long_to_bytes(auth_key) 241 | auth_key_sha = SHA.new(auth_key_str).digest() 242 | auth_key_aux_hash = auth_key_sha[:8] 243 | 244 | new_nonce_hash1 = SHA.new(new_nonce+b'\x01'+auth_key_aux_hash).digest()[-16:] 245 | new_nonce_hash2 = SHA.new(new_nonce+b'\x02'+auth_key_aux_hash).digest()[-16:] 246 | new_nonce_hash3 = SHA.new(new_nonce+b'\x03'+auth_key_aux_hash).digest()[-16:] 247 | 248 | assert Set_client_DH_params_answer['nonce'] == nonce 249 | assert Set_client_DH_params_answer['server_nonce'] == server_nonce 250 | 251 | if Set_client_DH_params_answer.name == 'dh_gen_ok': 252 | assert Set_client_DH_params_answer['new_nonce_hash1'] == new_nonce_hash1 253 | print("Diffie Hellman key exchange processed successfully") 254 | 255 | self.server_salt = strxor(new_nonce[0:8], server_nonce[0:8]) 256 | self.auth_key = auth_key_str 257 | self.auth_key_id = auth_key_sha[-8:] 258 | print("Auth key generated") 259 | return "Auth Ok" 260 | elif Set_client_DH_params_answer.name == 'dh_gen_retry': 261 | assert Set_client_DH_params_answer['new_nonce_hash2'] == new_nonce_hash2 262 | print ("Retry Auth") 263 | elif Set_client_DH_params_answer.name == 'dh_gen_fail': 264 | assert Set_client_DH_params_answer['new_nonce_hash3'] == new_nonce_hash3 265 | print("Auth Failed") 266 | raise Exception("Auth Failed") 267 | else: raise Exception("Response Error") 268 | 269 | def aes_calculate(self, msg_key, direction="to server"): 270 | x = 0 if direction == "to server" else 8 271 | sha1_a = SHA.new(msg_key + self.auth_key[x:x+32]).digest() 272 | sha1_b = SHA.new(self.auth_key[x+32:x+48] + msg_key + self.auth_key[48+x:64+x]).digest() 273 | sha1_c = SHA.new(self.auth_key[x+64:x+96] + msg_key).digest() 274 | sha1_d = SHA.new(msg_key + self.auth_key[x+96:x+128]).digest() 275 | aes_key = sha1_a[0:8] + sha1_b[8:20] + sha1_c[4:16] 276 | aes_iv = sha1_a[8:20] + sha1_b[0:8] + sha1_c[16:20] + sha1_d[0:8] 277 | return aes_key, aes_iv 278 | -------------------------------------------------------------------------------- /prime.py: -------------------------------------------------------------------------------- 1 | # NOTICE!!! This is copied from https://stackoverflow.com/questions/4643647/fast-prime-factorization-module 2 | 3 | import random 4 | 5 | def primesbelow(N): 6 | # http://stackoverflow.com/questions/2068372/fastest-way-to-list-all-primes-below-n-in-python/3035188#3035188 7 | #""" Input N>=6, Returns a list of primes, 2 <= p < N """ 8 | correction = N % 6 > 1 9 | N = {0:N, 1:N-1, 2:N+4, 3:N+3, 4:N+2, 5:N+1}[N%6] 10 | sieve = [True] * (N // 3) 11 | sieve[0] = False 12 | for i in range(int(N ** .5) // 3 + 1): 13 | if sieve[i]: 14 | k = (3 * i + 1) | 1 15 | sieve[k*k // 3::2*k] = [False] * ((N//6 - (k*k)//6 - 1)//k + 1) 16 | sieve[(k*k + 4*k - 2*k*(i%2)) // 3::2*k] = [False] * ((N // 6 - (k*k + 4*k - 2*k*(i%2))//6 - 1) // k + 1) 17 | return [2, 3] + [(3 * i + 1) | 1 for i in range(1, N//3 - correction) if sieve[i]] 18 | 19 | smallprimeset = set(primesbelow(100000)) 20 | _smallprimeset = 100000 21 | 22 | def isprime(n, precision=7): 23 | # http://en.wikipedia.org/wiki/Miller-Rabin_primality_test#Algorithm_and_running_time 24 | if n == 1 or n % 2 == 0: 25 | return False 26 | elif n < 1: 27 | raise ValueError("Out of bounds, first argument must be > 0") 28 | elif n < _smallprimeset: 29 | return n in smallprimeset 30 | 31 | 32 | d = n - 1 33 | s = 0 34 | while d % 2 == 0: 35 | d //= 2 36 | s += 1 37 | 38 | for repeat in range(precision): 39 | a = random.randrange(2, n - 2) 40 | x = pow(a, d, n) 41 | 42 | if x == 1 or x == n - 1: continue 43 | 44 | for r in range(s - 1): 45 | x = pow(x, 2, n) 46 | if x == 1: return False 47 | if x == n - 1: break 48 | else: return False 49 | 50 | return True 51 | 52 | # https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/ 53 | def pollard_brent(n): 54 | if n % 2 == 0: return 2 55 | if n % 3 == 0: return 3 56 | 57 | y, c, m = random.randint(1, n-1), random.randint(1, n-1), random.randint(1, n-1) 58 | g, r, q = 1, 1, 1 59 | while g == 1: 60 | x = y 61 | for i in range(r): 62 | y = (pow(y, 2, n) + c) % n 63 | 64 | k = 0 65 | while k < r and g==1: 66 | ys = y 67 | for i in range(min(m, r-k)): 68 | y = (pow(y, 2, n) + c) % n 69 | q = q * abs(x-y) % n 70 | g = gcd(q, n) 71 | k += m 72 | r *= 2 73 | if g == n: 74 | while True: 75 | ys = (pow(ys, 2, n) + c) % n 76 | g = gcd(abs(x - ys), n) 77 | if g > 1: 78 | break 79 | 80 | return g 81 | 82 | smallprimes = primesbelow(10000) # might seem low, but 1000*1000 = 1000000, so this will fully factor every composite < 1000000 83 | def primefactors(n, sort=False): 84 | factors = [] 85 | 86 | limit = int(n ** .5) + 1 87 | for checker in smallprimes: 88 | if checker > limit: break 89 | while n % checker == 0: 90 | factors.append(checker) 91 | n //= checker 92 | limit = int(n ** .5) + 1 93 | if checker > limit: break 94 | 95 | if n < 2: return factors 96 | 97 | while n > 1: 98 | if isprime(n): 99 | factors.append(n) 100 | break 101 | factor = pollard_brent(n) # trial division did not fully factor, switch to pollard-brent 102 | factors.extend(primefactors(factor)) # recurse to factor the not necessarily prime factor returned by pollard-brent 103 | n //= factor 104 | 105 | if sort: factors.sort() 106 | 107 | return factors 108 | 109 | def factorization(n): 110 | factors = {} 111 | for p1 in primefactors(n): 112 | try: 113 | factors[p1] += 1 114 | except KeyError: 115 | factors[p1] = 1 116 | return factors 117 | 118 | totients = {} 119 | def totient(n): 120 | if n == 0: return 1 121 | 122 | try: return totients[n] 123 | except KeyError: pass 124 | 125 | tot = 1 126 | for p, exp in factorization(n).items(): 127 | tot *= (p - 1) * p ** (exp - 1) 128 | 129 | totients[n] = tot 130 | return tot 131 | 132 | def gcd(a, b): 133 | if a == b: return a 134 | while b > 0: a, b = b, a % b 135 | return a 136 | 137 | def lcm(a, b): 138 | return abs(a * b) // gcd(a, b) 139 | 140 | -------------------------------------------------------------------------------- /telepy.py: -------------------------------------------------------------------------------- 1 | #CLI like interface 2 | 3 | import argparse, getopt, os, io, struct, mtproto 4 | from classes.shell import TelepyShell 5 | 6 | if __name__ == '__main__': 7 | parser = argparse.ArgumentParser('telepy',description='Python implementation of telegram API.') 8 | parser.add_argument('command', nargs='?', choices=['cmd', 'dialog_list', 'contact_list'] + ['chat_' + sub for sub in ['info', 'add_user', 'add_user_to_chat', 'del_user', 'set_photo', 'rename']]) 9 | parser.add_argument('args', nargs='*') 10 | 11 | #for command, args, help in (('info', 1, 'prints info about chat'), ('add_user', 2, 'add user to chat'), ('del_user', 2, 'remove user from chat'), ('set_photo', 1, 'sets group chat photo. Same limits as for profile photos.')): 12 | # parser.add_argument('chat_' + command, nargs=args, help=help) 13 | #parser.add_argument 14 | args = parser.parse_args() 15 | 16 | if args.command is None: 17 | TelepyShell().cmdloop() -------------------------------------------------------------------------------- /testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import io 4 | import struct 5 | # Deal with py2 and py3 differences 6 | try: # this only works in py2.7 7 | import configparser 8 | except ImportError: 9 | import ConfigParser as configparser 10 | import mtproto 11 | 12 | 13 | 14 | config = configparser.ConfigParser() 15 | # Check if credentials is correctly loaded (when it doesn't read anything it returns []) 16 | if not config.read('credentials'): 17 | print("File 'credentials' seems to not exist.") 18 | exit(-1) 19 | ip = config.get('App data', 'ip_address') 20 | port = config.getint('App data', 'port') 21 | 22 | Session = mtproto.Session(ip, port) 23 | 24 | Session.create_auth_key() 25 | 26 | future_salts = Session.method_call('get_future_salts', num=3) 27 | print(future_salts) 28 | -------------------------------------------------------------------------------- /tests/Serialization and SHA.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | MTProto data serialization and SHA hash test 4 | 5 | @author: Anton Grigoryev 6 | @author: Sammy Pfeiffer 7 | """ 8 | from Crypto.Hash import SHA 9 | import io 10 | import mtproto 11 | 12 | # byte strings got from 13 | # https://core.telegram.org/mtproto/samples-auth_key - step 4 14 | 15 | z = io.BytesIO() 16 | mtproto.serialize_obj(z, 'p_q_inner_data', 17 | pq=b"\x17\xED\x48\x94\x1A\x08\xF9\x81", 18 | p=b"\x49\x4C\x55\x3B", 19 | q=b"\x53\x91\x10\x73", 20 | nonce=b"\x3E\x05\x49\x82\x8C\xCA\x27\xE9\x66\xB3\x01\xA4\x8F\xEC\xE2\xFC", 21 | server_nonce=b"\xA5\xCF\x4D\x33\xF4\xA1\x1E\xA8\x77\xBA\x4A\xA5\x73\x90\x73\x30", 22 | new_nonce=b"\x31\x1C\x85\xDB\x23\x4A\xA2\x64\x0A\xFC\x4A\x76\xA7\x35\xCF\x5B\x1F\x0F\xD6\x8B\xD1\x7F\xA1\x81\xE1\x22\x9A\xD8\x67\xCC\x02\x4D") 23 | x = z.getvalue() 24 | if SHA.new(x).digest() == b'\xDB\x76\x1C\x27\x71\x8A\x23\x05\x04\x4F\x71\xF2\xAD\x95\x16\x29\xD7\x8B\x24\x49': 25 | print("Test passed successfully") 26 | else: 27 | print("Test not passed") 28 | 29 | -------------------------------------------------------------------------------- /tests/encrypt_decrypt_ige_test.py: -------------------------------------------------------------------------------- 1 | # Author: Sammy Pfeiffer 2 | # This file tests the AES 256 IGE cipher 3 | # working in Python 2.7 and Python 3.4 (other versions untested) 4 | # as it's needed for the implementation of Telegram API 5 | from crypt import ige 6 | 7 | # AES 256 IGE is using AES ECB internally, it implies (extract from PyCrypto.cipher.AES): 8 | # key : byte string 9 | # The secret key to use in the symmetric cipher. 10 | # It must be 16 (*AES-128*), 24 (*AES-192*), or 32 (*AES-256*) bytes long. 11 | # IV : byte string 12 | # The initialization vector to use for encryption or decryption. 13 | # 14 | # It is ignored for `MODE_ECB` and `MODE_CTR`. 15 | # 16 | # For all other modes, it must be `block_size` bytes longs. 17 | 18 | # message must be a multiple of 16 in size 19 | msg_to_encrypt = "This is a secret message" 20 | padding_needed = 16 - len(msg_to_encrypt) % 16 21 | msg_to_encrypt_padded = msg_to_encrypt + str(0) * padding_needed 22 | print("Encrypting: '" + str(msg_to_encrypt) + "'") 23 | print("With padding: '" + str(msg_to_encrypt_padded) + "'") 24 | # 32 bytes long key 25 | aes_key = b'12345678901234567890123456789012' 26 | print("With key for AES 256 ECB: '" + str(aes_key) + "'") 27 | # Initialization Vector must be 32 bytes 28 | aes_iv = b'01234567890123456789012345678901' 29 | print("And initialization vector: '" + str(aes_iv) + "'") 30 | encrypted_msg = _ige(msg_to_encrypt_padded, aes_key, aes_iv, operation="encrypt") 31 | print("\nEncrypted msg: '" + str(encrypted_msg) + "'") 32 | print("In hex: " + encrypted_msg.__repr__()) 33 | decrypted_msg = _ige(encrypted_msg, aes_key, aes_iv, operation="decrypt") 34 | print("\nDecrypted msg: '" + str(decrypted_msg) + "'") 35 | print("In hex: " + decrypted_msg.__repr__()) 36 | 37 | if msg_to_encrypt_padded == decrypted_msg: 38 | print("Encrypt + Decrypt process, completed succesfully.") 39 | 40 | # Let's test incorrect inputs 41 | print("\n\nTesting incorrect inputs:") 42 | # Message with length not multiple of 16 43 | msg_not_multiple_of_16 = "6bytes" 44 | print("Trying to encrypt: '" + msg_not_multiple_of_16 + 45 | "' of size: " + str(len(msg_not_multiple_of_16))) 46 | try: 47 | encrypted_msg = _ige(msg_not_multiple_of_16, aes_key, aes_iv, operation="encrypt") 48 | except ValueError as ve: 49 | print(" Correctly got ValueError: '" + str(ve) + "'") 50 | 51 | # key not being 32 bytes 52 | aes_key_not_32_bytes = b'0123456789' 53 | print("Trying to use key: '" + str(aes_key_not_32_bytes) + "'") 54 | try: 55 | encrypted_msg = _ige(msg_to_encrypt_padded, aes_key_not_32_bytes, aes_iv, operation="encrypt") 56 | except ValueError as ve: 57 | print(" Correctly got ValueError: '" + str(ve) + "'") 58 | 59 | # iv not being 32 bytes 60 | iv_key_not_32_bytes = b'0123456789' 61 | print("Trying to use iv: '" + str(iv_key_not_32_bytes) + "'") 62 | try: 63 | encrypted_msg = _ige(msg_to_encrypt_padded, aes_key, iv_key_not_32_bytes, operation="encrypt") 64 | except ValueError as ve: 65 | print(" Correctly got ValueError: '" + str(ve) + "'") 66 | 67 | -------------------------------------------------------------------------------- /tests/file.py: -------------------------------------------------------------------------------- 1 | from classes.file import File 2 | from os.path import exists 3 | 4 | f = File('text.txt') 5 | assert f.write_bytes(b'testing bytes i/o'), 17 6 | assert f.read_bytes(), b'testing bytes i/o' 7 | f.open() # does it open any text editor on your system? 8 | f.remove() 9 | assert exists('text.txt'), False -------------------------------------------------------------------------------- /tests/ige.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Sammy Pfeiffer 3 | # This file implements the AES 256 IGE cipher 4 | # working in Python 2.7 and Python 3.4 (other versions untested) 5 | # as it's needed for the implementation of Telegram API 6 | # It's based on PyCryto 7 | from __future__ import print_function 8 | from Crypto.Util import number 9 | from Crypto.Cipher import AES 10 | MIN_SUPPORTED_PY3_VERSION = (3, 2, 0) 11 | from sys import version_info 12 | if version_info >= MIN_SUPPORTED_PY3_VERSION: 13 | from binascii import hexlify 14 | long = int 15 | 16 | 17 | # Some color codes for printing 18 | ENDC = '\033[0m' # To end a color 19 | REDFAIL = '\033[91m' # RED 20 | GREENOK = '\033[92m' # GREEN 21 | 22 | def hex_string_to_str_bytes(val): 23 | """Given a String like 24 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 25 | Convert it to it's byte representation, stored in py2 in a str, like: 26 | tmp_aes_key_hex = '\xf0\x11(\x08\x87\xc7\xbb\x01\xdf\x0f\xc4\xe1x0\xe0\xb9\x1f\xbb\x8b\xe4\xb2&|\xb9\x85\xae%\xf3;RrS' 27 | """ 28 | return val.decode("hex") 29 | 30 | def str_bytes_to_hex_string(val): 31 | """Given a str_bytes (so str()) like 32 | tmp_aes_key_hex = '\xf0\x11(\x08\x87\xc7\xbb\x01\xdf\x0f\xc4\xe1x0\xe0\xb9\x1f\xbb\x8b\xe4\xb2&|\xb9\x85\xae%\xf3;RrS' 33 | Convert it back to it's uppercase string representation, like: 34 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" """ 35 | if version_info >= MIN_SUPPORTED_PY3_VERSION: 36 | return str(hexlify(val).upper()) 37 | return val.encode("hex").upper() 38 | 39 | def hex_string_to_long(val): 40 | """Given a String like 41 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 42 | Convert it to int, which is actually long""" 43 | return int(val, 16) 44 | 45 | def xor_stuff(a, b): 46 | """XOR applied to every element of a with every element of b. 47 | Depending on python version and depeding on input some arrangements need to be done.""" 48 | if version_info < MIN_SUPPORTED_PY3_VERSION: 49 | if len(a) > len(b): 50 | return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)]) 51 | else: 52 | return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])]) 53 | else: 54 | if type(a) == str and type(b) == bytes:# cipher.encrypt returns string 55 | return bytes(ord(x) ^ y for x, y in zip(a, b)) 56 | elif type(a) == bytes and type(b) == str: 57 | return bytes(x ^ ord(y) for x, y in zip(a, b)) 58 | else: 59 | return bytes(x ^ y for x, y in zip(a, b)) 60 | 61 | def ige(message, key, iv, operation="decrypt"): 62 | """Given a key, given an iv, and message 63 | do whatever operation asked in the operation field. 64 | Operation will be checked for: "decrypt" and "encrypt" strings. 65 | Returns the message encrypted/decrypted. 66 | message must be a multiple by 16 bytes (for division in 16 byte blocks) 67 | key must be 32 byte 68 | iv must be 32 byte (it's not internally used in AES 256 ECB, but it's 69 | needed for IGE)""" 70 | if type(message) == long: 71 | message = number.long_to_bytes(message) 72 | if type(key) == long: 73 | key = number.long_to_bytes(key) 74 | if type(iv) == long: 75 | iv = number.long_to_bytes(iv) 76 | 77 | if len(key) != 32: 78 | raise ValueError("key must be 32 bytes long (was " + 79 | str(len(key)) + " bytes)") 80 | if len(iv) != 32: 81 | raise ValueError("iv must be 32 bytes long (was " + 82 | str(len(iv)) + " bytes)") 83 | 84 | cipher = AES.new(key, AES.MODE_ECB, iv) 85 | blocksize = cipher.block_size 86 | if len(message) % blocksize != 0: 87 | raise ValueError("message must be a multiple of 16 bytes (try adding " + 88 | str(16 - len(message) % 16) + " bytes of padding)") 89 | 90 | ivp = iv[0:blocksize] 91 | ivp2 = iv[blocksize:] 92 | 93 | ciphered = None 94 | 95 | for i in range(0, len(message), blocksize): 96 | indata = message[i:i+blocksize] 97 | if operation == "decrypt": 98 | xored = xor_stuff(indata, ivp2) 99 | decrypt_xored = cipher.decrypt(xored) 100 | outdata = xor_stuff(decrypt_xored, ivp) 101 | ivp = indata 102 | ivp2 = outdata 103 | elif operation == "encrypt": 104 | xored = xor_stuff(indata, ivp) 105 | encrypt_xored = cipher.encrypt(xored) 106 | outdata = xor_stuff(encrypt_xored, ivp2) 107 | ivp = outdata 108 | ivp2 = indata 109 | else: 110 | raise ValueError("operation must be either 'decrypt' or 'encrypt'") 111 | 112 | if ciphered is None: 113 | ciphered = outdata 114 | else: 115 | ciphered_ba = bytearray(ciphered) 116 | ciphered_ba.extend(outdata) 117 | if version_info >= MIN_SUPPORTED_PY3_VERSION: 118 | ciphered = bytes(ciphered_ba) 119 | else: 120 | ciphered = str(ciphered_ba) 121 | 122 | return ciphered 123 | 124 | 125 | if __name__ == "__main__": 126 | # Example data from https://core.telegram.org/mtproto/samples-auth_key#conversion-of-encrypted-answer-into-answer 127 | encrypted_answer_str = "28A92FE20173B347A8BB324B5FAB2667C9A8BBCE6468D5B509A4CBDDC186240AC912CF7006AF8926DE606A2E74C0493CAA57741E6C82451F54D3E068F5CCC49B4444124B9666FFB405AAB564A3D01E67F6E912867C8D20D9882707DC330B17B4E0DD57CB53BFAAFA9EF5BE76AE6C1B9B6C51E2D6502A47C883095C46C81E3BE25F62427B585488BB3BF239213BF48EB8FE34C9A026CC8413934043974DB03556633038392CECB51F94824E140B98637730A4BE79A8F9DAFA39BAE81E1095849EA4C83467C92A3A17D997817C8A7AC61C3FF414DA37B7D66E949C0AEC858F048224210FCC61F11C3A910B431CCBD104CCCC8DC6D29D4A5D133BE639A4C32BBFF153E63ACA3AC52F2E4709B8AE01844B142C1EE89D075D64F69A399FEB04E656FE3675A6F8F412078F3D0B58DA15311C1A9F8E53B3CD6BB5572C294904B726D0BE337E2E21977DA26DD6E33270251C2CA29DFCC70227F0755F84CFDA9AC4B8DD5F84F1D1EB36BA45CDDC70444D8C213E4BD8F63B8AB95A2D0B4180DC91283DC063ACFB92D6A4E407CDE7C8C69689F77A007441D4A6A8384B666502D9B77FC68B5B43CC607E60A146223E110FCB43BC3C942EF981930CDC4A1D310C0B64D5E55D308D863251AB90502C3E46CC599E886A927CDA963B9EB16CE62603B68529EE98F9F5206419E03FB458EC4BD9454AA8F6BA777573CC54B328895B1DF25EAD9FB4CD5198EE022B2B81F388D281D5E5BC580107CA01A50665C32B552715F335FD76264FAD00DDD5AE45B94832AC79CE7C511D194BC42B70EFA850BB15C2012C5215CABFE97CE66B8D8734D0EE759A638AF013" 128 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 129 | tmp_aes_iv_str = "3212D579EE35452ED23E0D0C92841AA7D31B2E9BDEF2151E80D15860311C85DB" 130 | answer_str = "BA0D89B53E0549828CCA27E966B301A48FECE2FCA5CF4D33F4A11EA877BA4AA57390733002000000FE000100C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C3720FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F642477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5BFE000100262AABA621CC4DF587DC94CF8252258C0B9337DFB47545A49CDD5C9B8EAE7236C6CADC40B24E88590F1CC2CC762EBF1CF11DCC0B393CAAD6CEE4EE5848001C73ACBB1D127E4CB93072AA3D1C8151B6FB6AA6124B7CD782EAF981BDCFCE9D7A00E423BD9D194E8AF78EF6501F415522E44522281C79D906DDB79C72E9C63D83FB2A940FF779DFB5F2FD786FB4AD71C9F08CF48758E534E9815F634F1E3A80A5E1C2AF210C5AB762755AD4B2126DFA61A77FA9DA967D65DFD0AFB5CDF26C4D4E1A88B180F4E0D0B45BA1484F95CB2712B50BF3F5968D9D55C99C0FB9FB67BFF56D7D4481B634514FBA3488C4CDA2FC0659990E8E868B28632875A9AA703BCDCE8FCB7AE551" 131 | 132 | if version_info < MIN_SUPPORTED_PY3_VERSION: 133 | # Crypto.Cipher.AES needs it's parameters to be 32byte str 134 | # So we can either give 'str' type like this ONLY WORKS ON PYTHON2.7 135 | encrypted_answer_hex = encrypted_answer_str.decode("hex") 136 | tmp_aes_key_hex = tmp_aes_key_str.decode("hex") 137 | tmp_aes_iv_hex = tmp_aes_iv_str.decode("hex") 138 | answer_hex = answer_str.decode("hex") 139 | decrypted_answer_in_str = ige(encrypted_answer_hex, tmp_aes_key_hex, tmp_aes_iv_hex) 140 | print("decrypted_answer using string version of input: ") 141 | print(decrypted_answer_in_str) 142 | 143 | # Or give it long's representing the big numbers (ige will take care of the conversion) 144 | encrypted_answer = int(encrypted_answer_str, 16) 145 | tmp_aes_key = int(tmp_aes_key_str, 16) 146 | tmp_aes_iv = int(tmp_aes_iv_str, 16) 147 | answer = int(answer_str, 16) 148 | decrypted_answer_in_int = ige(encrypted_answer, tmp_aes_key, tmp_aes_iv) 149 | print("decrypted_answer using int version of input: ") 150 | print(decrypted_answer_in_int) 151 | 152 | if version_info < MIN_SUPPORTED_PY3_VERSION: 153 | if decrypted_answer_in_str == decrypted_answer_in_int: 154 | print("\nBoth str input and int input give the same result") 155 | else: 156 | print("\nDifferent result!!") 157 | 158 | 159 | decrypt_ans_hex_str = str_bytes_to_hex_string(decrypted_answer_in_int) 160 | print("Human friendly view of decrypted_answer:") 161 | print(decrypt_ans_hex_str) 162 | print("\nAnd we should expect inside of it:") 163 | print(answer_str) 164 | 165 | if answer_str in decrypt_ans_hex_str: 166 | print("\n\nanswer_str is in decrypt_ans_hex_str!") 167 | idx = decrypt_ans_hex_str.index(answer_str) 168 | print(decrypt_ans_hex_str[:idx], end="") 169 | print(GREENOK + decrypt_ans_hex_str[idx:idx+len(answer_str)] + ENDC, end="") 170 | print(decrypt_ans_hex_str[idx+len(answer_str):]) 171 | print("There are " + str(idx/2) + " bytes at the start that are not part of the answer") 172 | print("Plus " + str(len(decrypt_ans_hex_str[len(answer_str)+idx:]) / 2) + " at the end not forming part") 173 | print("answer_str is: " + str(len(answer_str) / 2) + " bytes") 174 | print("decrypt_ans_hex_str is: " + str(len(decrypt_ans_hex_str) / 2) + " bytes") 175 | print("In total: " + str( (len(decrypt_ans_hex_str) - len(answer_str)) / 2) + " bytes that do not pertain") 176 | else: 177 | print("answer_str is not in decrypt_ans_hex_str :(") 178 | 179 | 180 | print("This is because the header (SHA1(answer)) is included and is 20 bytes long.") 181 | print("And in the end there are 0 to 15 bytes random to fill up the gap.") 182 | print("This means that we can safely ignore the starting 20bytes and all the extra bytes in the end") 183 | # answer_with_hash := SHA1(answer) + answer + (0-15 random bytes); such that the length be divisible by 16; 184 | # This... divisible by 16 is because of the blocksize of AES-256-ECB (yay!) 185 | 186 | -------------------------------------------------------------------------------- /tests/research_decrypt_mode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # THIS MODULE HAS ALL THE CONVERSIONS WE NEED! (I think) 3 | from Crypto.Util import number 4 | def vis(bs): 5 | """ 6 | Function to visualize byte streams. Split into bytes, print to console. 7 | :param bs: BYTE STRING 8 | """ 9 | bs = bytearray(bs) 10 | symbols_in_one_line = 8 11 | n = len(bs) // symbols_in_one_line 12 | for i in range(n): 13 | print(str(i*symbols_in_one_line)+" | "+" ".join(["%02X" % b for b in bs[i*symbols_in_one_line:(i+1)*symbols_in_one_line]])) # for every 8 symbols line 14 | if not len(bs) % symbols_in_one_line == 0: 15 | print(str((i+1)*symbols_in_one_line)+" | "+" ".join(["%02X" % b for b in bs[(i+1)*symbols_in_one_line:]])+"\n") # for last line 16 | 17 | def hex_string_to_str_bytes(val): 18 | """Given a String like 19 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 20 | Convert it to it's byte representation, stored in py2 in a str, like: 21 | tmp_aes_key_hex = '\xf0\x11(\x08\x87\xc7\xbb\x01\xdf\x0f\xc4\xe1x0\xe0\xb9\x1f\xbb\x8b\xe4\xb2&|\xb9\x85\xae%\xf3;RrS' 22 | """ 23 | return val.decode("hex") 24 | 25 | def str_bytes_to_hex_string(val): 26 | """Given a str_bytes (so str()) like 27 | tmp_aes_key_hex = '\xf0\x11(\x08\x87\xc7\xbb\x01\xdf\x0f\xc4\xe1x0\xe0\xb9\x1f\xbb\x8b\xe4\xb2&|\xb9\x85\xae%\xf3;RrS' 28 | Convert it back to it's uppercase string representation, like: 29 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" """ 30 | return val.encode("hex").upper() 31 | 32 | def hex_string_to_long(val): 33 | """Given a String like 34 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 35 | Convert it to int, which is actually long""" 36 | return int(val, 16) 37 | 38 | def long_to_hex_string(val): 39 | """Given a long like: 40 | 41 | Convert it to hex_string like: 42 | 43 | from: http://stackoverflow.com/questions/4358285/is-there-a-faster-way-to-convert-an-arbitrary-large-integer-to-a-big-endian-seque/4358429#4358429 44 | """ 45 | # number.long_to_bytes(val) 46 | # number.bytes_to_long() 47 | # number.tobytes() 48 | # number.bstr() 49 | return 50 | 51 | 52 | # Got from https://core.telegram.org/mtproto/samples-auth_key#conversion-of-encrypted-answer-into-answer 53 | # They say they use AES 256 IGE 54 | # Infinite Garble Extension (IGE) is a block cipher mode. (http://www.links.org/files/openssl-ige.pdf) 55 | encrypted_answer_str = "28A92FE20173B347A8BB324B5FAB2667C9A8BBCE6468D5B509A4CBDDC186240AC912CF7006AF8926DE606A2E74C0493CAA57741E6C82451F54D3E068F5CCC49B4444124B9666FFB405AAB564A3D01E67F6E912867C8D20D9882707DC330B17B4E0DD57CB53BFAAFA9EF5BE76AE6C1B9B6C51E2D6502A47C883095C46C81E3BE25F62427B585488BB3BF239213BF48EB8FE34C9A026CC8413934043974DB03556633038392CECB51F94824E140B98637730A4BE79A8F9DAFA39BAE81E1095849EA4C83467C92A3A17D997817C8A7AC61C3FF414DA37B7D66E949C0AEC858F048224210FCC61F11C3A910B431CCBD104CCCC8DC6D29D4A5D133BE639A4C32BBFF153E63ACA3AC52F2E4709B8AE01844B142C1EE89D075D64F69A399FEB04E656FE3675A6F8F412078F3D0B58DA15311C1A9F8E53B3CD6BB5572C294904B726D0BE337E2E21977DA26DD6E33270251C2CA29DFCC70227F0755F84CFDA9AC4B8DD5F84F1D1EB36BA45CDDC70444D8C213E4BD8F63B8AB95A2D0B4180DC91283DC063ACFB92D6A4E407CDE7C8C69689F77A007441D4A6A8384B666502D9B77FC68B5B43CC607E60A146223E110FCB43BC3C942EF981930CDC4A1D310C0B64D5E55D308D863251AB90502C3E46CC599E886A927CDA963B9EB16CE62603B68529EE98F9F5206419E03FB458EC4BD9454AA8F6BA777573CC54B328895B1DF25EAD9FB4CD5198EE022B2B81F388D281D5E5BC580107CA01A50665C32B552715F335FD76264FAD00DDD5AE45B94832AC79CE7C511D194BC42B70EFA850BB15C2012C5215CABFE97CE66B8D8734D0EE759A638AF013" 56 | tmp_aes_key_str = "F011280887C7BB01DF0FC4E17830E0B91FBB8BE4B2267CB985AE25F33B527253" 57 | tmp_aes_iv_str = "3212D579EE35452ED23E0D0C92841AA7D31B2E9BDEF2151E80D15860311C85DB" 58 | answer_str = "BA0D89B53E0549828CCA27E966B301A48FECE2FCA5CF4D33F4A11EA877BA4AA57390733002000000FE000100C71CAEB9C6B1C9048E6C522F70F13F73980D40238E3E21C14934D037563D930F48198A0AA7C14058229493D22530F4DBFA336F6E0AC925139543AED44CCE7C3720FD51F69458705AC68CD4FE6B6B13ABDC9746512969328454F18FAF8C595F642477FE96BB2A941D5BCD1D4AC8CC49880708FA9B378E3C4F3A9060BEE67CF9A4A4A695811051907E162753B56B0F6B410DBA74D8A84B2A14B3144E0EF1284754FD17ED950D5965B4B9DD46582DB1178D169C6BC465B0D6FF9CA3928FEF5B9AE4E418FC15E83EBEA0F87FA9FF5EED70050DED2849F47BF959D956850CE929851F0D8115F635B105EE2E4E15D04B2454BF6F4FADF034B10403119CD8E3B92FCC5BFE000100262AABA621CC4DF587DC94CF8252258C0B9337DFB47545A49CDD5C9B8EAE7236C6CADC40B24E88590F1CC2CC762EBF1CF11DCC0B393CAAD6CEE4EE5848001C73ACBB1D127E4CB93072AA3D1C8151B6FB6AA6124B7CD782EAF981BDCFCE9D7A00E423BD9D194E8AF78EF6501F415522E44522281C79D906DDB79C72E9C63D83FB2A940FF779DFB5F2FD786FB4AD71C9F08CF48758E534E9815F634F1E3A80A5E1C2AF210C5AB762755AD4B2126DFA61A77FA9DA967D65DFD0AFB5CDF26C4D4E1A88B180F4E0D0B45BA1484F95CB2712B50BF3F5968D9D55C99C0FB9FB67BFF56D7D4481B634514FBA3488C4CDA2FC0659990E8E868B28632875A9AA703BCDCE8FCB7AE551" 59 | print("encrypted_answer_str:") 60 | print(encrypted_answer_str) 61 | print("tmp_aes_key_str:") 62 | print(tmp_aes_key_str) 63 | print("tmp_aes_iv_str:") 64 | print(tmp_aes_iv_str) 65 | print("answer_str:") 66 | print(answer_str) 67 | 68 | # Convert them to bytes (strings in py2 anyways) 69 | # http://stackoverflow.com/questions/5649407/hexadecimal-string-to-byte-array-in-python 70 | encrypted_answer_hex = encrypted_answer_str.decode("hex") # int(encrypted_answer_str, 16) for py3 71 | tmp_aes_key_hex = tmp_aes_key_str.decode("hex") 72 | tmp_aes_iv_hex = tmp_aes_iv_str.decode("hex") 73 | answer_hex = answer_str.decode("hex") 74 | print("encrypted_answer_hex:") 75 | print(encrypted_answer_hex.__repr__()) 76 | print("tmp_aes_key_hex:") 77 | print(tmp_aes_key_hex.__repr__()) 78 | print("tmp_aes_iv_hex:") 79 | print(tmp_aes_iv_hex.__repr__()) 80 | print("answer_hex:") 81 | print(answer_hex.__repr__()) 82 | # Re-convert them to string 83 | encrypted_answer_hex_to_str = encrypted_answer_hex.encode("hex").upper() # int(encrypted_answer_str, 16) for py3 84 | tmp_aes_key_hex_to_str = tmp_aes_key_hex.encode("hex").upper() 85 | tmp_aes_iv_hex_to_str = tmp_aes_iv_hex.encode("hex").upper() 86 | answer_hex_to_str = answer_hex.encode("hex").upper() 87 | print("encrypted_answer_hex_to_str:") 88 | print(encrypted_answer_hex_to_str) 89 | print("tmp_aes_key_hex_to_str:") 90 | print(tmp_aes_key_hex_to_str) 91 | print("tmp_aes_iv_hex_to_str:") 92 | print(tmp_aes_iv_hex_to_str) 93 | print("answer_hex_to_str:") 94 | print(answer_hex_to_str) 95 | 96 | 97 | # Check if they are the same 98 | if encrypted_answer_hex_to_str == encrypted_answer_str: 99 | print("encrypted_answer_hex_to_str == encrypted_answer_str") 100 | else: 101 | print("encrypted_answer_hex_to_str != encrypted_answer_str") 102 | 103 | encrypted_answer = int(encrypted_answer_str, 16) # int(encrypted_answer_str, 16) for py3 104 | tmp_aes_key = int(tmp_aes_key_str, 16) 105 | tmp_aes_iv = int(tmp_aes_iv_str, 16) 106 | answer = int(answer_str, 16) 107 | print("longtohexstring") 108 | print(long_to_hex_string(tmp_aes_key)) 109 | 110 | # print("len(encrypted_answer): " + str(len(encrypted_answer))) 111 | # print("len(answer): " + str(len(answer))) 112 | 113 | # Got from testing.py 114 | # encrypted_answer = 'L\xd7\xddI\x0b\xc3\xeay\xf1\x07]\x93\x7fY\x0cmVAX\x03\xeb\n}\x99\xd6\x99\xaa\xba\x05\x9d\xaaB\xe2\x97\xb3\xf2\xf8\xd8\x9f\xa6\x13\x177a\xb45A\x0f}\xb3\x99\xa3D?L\x94\xa3\xbcG\xe8\xf2\x14 \xb9.\x8b\xa0\xf1\xa5\xf1\x18\x9aZ2\x8f\xae\x05\xd9\x84H\xa3&\xad\x84\x82w\x9e\xe8\xba\x8a\x87QT\xdd\x12\x8c\x86\xde\xd8\x7fLM\xb9\x81H;JX\x85\x14\x1af!\xb20\xea)\xa8>(\xa9\xce5,\x96\x14\xd7P\x0c\xb3\x02\x9a\x16\xfc\x94\xacT\xa0\xd4\x82\xe5S1\xf4\xe1\x8cB\xad\x89\xc3C\xa6\rt)\xfa\x0b\xfe3\t\xdd\x02\xbe\xecP\xd6\xd7p\xf6\xf3\xb5\xdf6\xfc\x90l\xaa\x06\x8a\xc0XO\x96>\x85\x18\xebN\x08\x13x\xc0\x1ah\xbd\xedO\x99T\xfd\xed\x87C}\x89!\x99Oz\xfe\x927z~ &"\x0e/\x01N\x13\xfa\xd1\x96\x87\x0f\x83\x98d\x12X\xa7\x8c\xa8\x1c/\xbc\xab\xb2:\x07\xa6\x14\xfa\xe3\xd2\x8cG\xd6\x84\xe4[\x8f\x9e\x8f\xbb\x9c\x8a\x80\xbd\xcf\xaf!\xf7E\x1b\x1f\x91\x18\xc2\x8eBE\xfb\x84\xb6\xc5e5Q\xfd\xb8\xcb\xbc\xb4\x9f\xb7\x92\xfe\xae\xda,\xfaA\x94\x7fq\x1e\xd1\x05\xe8=\x9d#,\xe6\xb7y1\xe6\xc7!\xa0\x0bx\xd1\xb3\xad9\xc4\xdd\x99Y\xca\r\x07+\x903\x1e?\x1d_\x8b\xb0M\xff\x14\xc3:\x95\xa8\xee\xc1\xb5\xff\xfb1\x95\xe1\xcaT\xe4D\xcf\xd2%\x11\xeb@`Att\xbe\x11\xfc/\x05\x9a\xd2\x15\r\xb6\x9d\x88\xae\xa8\xd1q\xe5\x9b\x05A\x8d[\xbf\xaaN\x1b\xee\xbf#4\x1c\xd5\xa4\x1f\x0fo\xaf\xd0\x00g\xc1\x9a\x82\x00\x8c_\xd4\xac!{K\xca\x89x\xde\xf9\x8d\x19\xec\x12\x8epY\xdb\x9f\x98\xe6\x88\xe7\xc1\x92\x90\x17\x80\x03Ry\xf1n\x97e\xe2\x8c\xe9\x8c\xd6<\xba2:\x9f\x06g\x05\xaa#\xf4\xca1\x16o\xb5\x8b\xcd\xfe\x814h\xac\xcd\x0e\xd0\x1c\x0c\xc71\x11\xbe\xa5\xb3#\xcfh\x07)\x91\xc7\xc8iy!\x03\xc8\xf0\xb2\x02\xf3\xc7\xdf\xafXm\xf5\xaf\xdd\xc8\xeb\xb3n7\xe34\xa7R\x8c\xaf\xa3\xb7y\xe7\x12\x0f\x0c\xc2\xa8v\x12E\xc3u\xc8Y\x1fh.\xcf\x01\xae\x8c\x00"v\x99V\xad>\xaf\x08)\x83V*\x9b\xad\xc0\x9c\x94\xa5D[\x08s\x88\xd1\xcb\xf6\xf8j\xa1c\xc1yb\xda\x12\xa1~\xf6\xd1"\x14\x11a\x02\xc1\xd3\xf5' 115 | # tmp_aes_key = b'\x82\xeb\x12\x0e\xbeT\x80>!\xaa\x01\xac\xc8\xe1u#d\x1b\x08\xf5G\xc7\xe5g\xa9\xc3\x1d*BC;6' 116 | # tmp_aes_iv = b'r\xbb/\xe8\x0bb,T\x19\x17\xf20WsTf\x1d_C\x83|2h\xd3s\x82\xaeVW\x10v\xff' 117 | 118 | from Crypto.Cipher import AES 119 | # # try all modes 120 | # aes_modes = [AES.MODE_CBC, AES.MODE_CFB, AES.MODE_CTR, AES.MODE_ECB, AES.MODE_OFB, AES.MODE_OPENPGP, AES.MODE_PGP] 121 | # aes_modes_names = ["AES.MODE_CBC", "AES.MODE_CFB", "AES.MODE_CTR", "AES.MODE_ECB", "AES.MODE_OFB", "AES.MODE_OPENPGP", "AES.MODE_PGP"] 122 | # 123 | # working_modes = [] 124 | # working_modenames = [] 125 | # for mode, modename in zip(aes_modes, aes_modes_names): 126 | # print("\n\nTrying mode: " + modename + "(" + str(mode) + ")") 127 | # try: 128 | # crypting_object = AES.new(tmp_aes_key, mode, tmp_aes_iv) # encrypter thing 129 | # decrypted_answer = crypting_object.decrypt(encrypted_answer) 130 | # print("decrypted_answer: ") 131 | # print(decrypted_answer.__repr__()) 132 | # vis(decrypted_answer) 133 | # working_modes.append(mode) 134 | # working_modenames.append(modename) 135 | # print("Which should look the same than: ") 136 | # print(answer.__repr__()) 137 | # vis(answer) 138 | # if answer == decrypted_answer: 139 | # print("THEY ARE THE SAME!!") 140 | # else: 141 | # print("THEY ARE DIFFERENT :(((((") 142 | # except Exception as e: 143 | # print("Exception: " + str(e)) 144 | # 145 | # print("\n\nModes " + str(working_modenames) + " (" + str(working_modes) + ") succesfully unencrypted the answer!") 146 | 147 | 148 | # From http://stackoverflow.com/questions/17797582/java-aes-256-decrypt-with-ige 149 | # public static final byte[] ige(final byte[] key, final byte[] IV, 150 | # final byte[] Message) throws Exception { 151 | def ige(key, iv, message, blocksize=16):#32): 152 | """given a key, ive and message, decrypt it. blocksize is the default one used in the javascript implementation""" 153 | # print("len(key): " + str(len(key))) 154 | # print("len(iv): " + str(len(iv))) 155 | # print("len(message): " + str(len(message))) 156 | # key = bytearray(key) 157 | # iv = bytearray(iv) 158 | # message = bytearray(message) 159 | # 160 | # final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); 161 | cipher = AES.new(key, AES.MODE_ECB, iv) 162 | blocksize = cipher.block_size 163 | 164 | # cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES")); 165 | # 166 | # final int blocksize = cipher.getBlockSize(); 167 | # 168 | # byte[] xPrev = Arrays.copyOfRange(IV, 0, blocksize); 169 | xPrev = iv[0:blocksize] 170 | # byte[] yPrev = Arrays.copyOfRange(IV, blocksize, IV.length); 171 | yPrev = iv[blocksize:] 172 | # 173 | # byte[] decrypted = new byte[0]; 174 | decrypted = None 175 | # 176 | # byte[] y, x; 177 | # y = bytearray() 178 | # x = bytearray() 179 | # for (int i = 0; i < Message.length; i += blocksize) { 180 | 181 | 182 | def xor_strings(a, b): # xor two strings of different lengths 183 | if len(a) > len(b): 184 | return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)]) 185 | else: 186 | return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])]) 187 | def add_strings(a, b): 188 | if len(a) > len(b): 189 | return "".join([chr(ord(x) + ord(y)) for (x, y) in zip(a[:len(b)], b)]) 190 | else: 191 | return "".join([chr(ord(x) + ord(y)) for (x, y) in zip(a, b[:len(a)])]) 192 | #return sum([a, b]) & 0xFFFFFFFF 193 | 194 | for i in range(0, len(message), blocksize): 195 | #print("i: " + str(i)) 196 | # x = java.util.Arrays.copyOfRange(Message, i, i + blocksize); 197 | x = message[i:i+blocksize] 198 | #print(" x: " + x.__repr__()) 199 | # y = xor(cipher.doFinal(xor(x, yPrev)), xPrev); 200 | 201 | y = xor_strings(cipher.decrypt(xor_strings(x, yPrev)), xPrev) 202 | #print(" y: " + y.__repr__()) 203 | #y = xor(cipher.decrypt(xor(x, yPrev)), xPrev) 204 | # xPrev = x; 205 | xPrev = x 206 | # yPrev = y; 207 | yPrev = y 208 | # 209 | # decrypted = sumBytes(decrypted, y); 210 | if decrypted is None: 211 | decrypted = y 212 | else: 213 | decrypted_ba = bytearray(decrypted) 214 | decrypted_ba.extend(y) 215 | decrypted = str(decrypted_ba) 216 | # all this did not work 217 | #decrypted = int(decrypted, 16) + int(y, 16) 218 | #decrypted.append(y) 219 | #add_strings(decrypted, y) 220 | #decrypted = decrypted + y # this is wrong 221 | #print(" decrypted: " + decrypted.__repr__()) 222 | # } 223 | # 224 | # return decrypted; 225 | print("len(key): " + str(len(key))) 226 | print("len(iv): " + str(len(iv))) 227 | print("len(message): " + str(len(message))) 228 | print("!!!!!!!!!!!!!!!!!!!!!! ---> cipher.block_size: " + str(cipher.block_size)) 229 | return decrypted 230 | # } 231 | 232 | #result = ige(tmp_aes_key, tmp_aes_iv, encrypted_answer) 233 | result = ige(tmp_aes_key_hex, tmp_aes_iv_hex, encrypted_answer_hex) 234 | print("result:") 235 | print(result) 236 | vis(result) 237 | if answer == result: 238 | print("THEY ARE THE SAME!!") 239 | else: 240 | print("THEY ARE DIFFERENT :(((((") 241 | print("len(result): " + str(len(result))) 242 | print("len(answer):" + str(len(answer))) 243 | 244 | vis(encrypted_answer) 245 | 246 | # Inspiration: http://passingcuriosity.com/2009/aes-encryption-in-python-with-m2crypto/ 247 | # sudo apt-get install swig 248 | # sudo pip install M2Crypto 249 | # This DOES NOT work. openssl wrapper m2crypto does not have IGE available 250 | # from base64 import b64encode, b64decode 251 | # from M2Crypto.EVP import Cipher 252 | # ENC=1 253 | # DEC=0 254 | # 255 | # def build_cipher(key, iv, op=ENC): 256 | # """""""" 257 | # #return Cipher(alg='aes_256_ecb', key=key, iv=iv, op=op) 258 | # return Cipher(alg='aes_256 ige', key=key, iv=iv, op=op) 259 | # 260 | # def encryptor(key, iv=None): 261 | # """""" 262 | # # Decode the key and iv 263 | # key = b64decode(key) 264 | # if iv is None: 265 | # iv = '\0' * 16 266 | # else: 267 | # iv = b64decode(iv) 268 | # 269 | # # Return the encryption function 270 | # def encrypt(data): 271 | # cipher = build_cipher(key, iv, ENC) 272 | # v = cipher.update(data) 273 | # v = v + cipher.final() 274 | # del cipher 275 | # v = b64encode(v) 276 | # return v 277 | # return encrypt 278 | # 279 | # def decryptor(key, iv=None): 280 | # """""" 281 | # # Decode the key and iv 282 | # #key = b64decode(key) 283 | # if iv is None: 284 | # iv = '\0' * 16 285 | # else: 286 | # #iv = b64decode(iv) 287 | # pass 288 | # 289 | # # Return the decryption function 290 | # def decrypt(data): 291 | # #data = b64decode(data) 292 | # cipher = build_cipher(key, iv, DEC) 293 | # v = cipher.update(data) 294 | # v = v + cipher.final() 295 | # del cipher 296 | # return v 297 | # return decrypt 298 | # 299 | # print("Decrypting with m2...") 300 | # decryptor_m2= decryptor(tmp_aes_key, tmp_aes_iv) 301 | # m2_decrypt_ans =decryptor_m2(encrypted_answer) 302 | # vis(m2_decrypt_ans) 303 | 304 | # Output was: 305 | # Trying mode: AES.MODE_CBC(2) 306 | # Exception: IV must be 16 bytes long 307 | # 308 | # 309 | # Trying mode: AES.MODE_CFB(3) 310 | # Exception: IV must be 16 bytes long 311 | # 312 | # 313 | # Trying mode: AES.MODE_CTR(6) 314 | # Exception: 'counter' keyword parameter is required with CTR mode 315 | # 316 | # 317 | # Trying mode: AES.MODE_ECB(1) 318 | # decrypted_answer: 319 | # '\xb4\x87<\xc5&\xe2J\x0e\x96\x1a\x08\xeaSR\xc4!.\x17\x0b]ZI\xd6\xdc\xbd5\x87i\x11\x1b\x9d\x04\x93;\xc5\xd7C\xe1[\xb9\xaa\x95a)\x14\xfd\x8f\xe8\xf8\xc7\xb1~\xdf\xd3\xf7\xf8\xdc\xc4v\xae \xd7\xff\x91\x0c}:\xf45T~\xad\x00w\xde\x98\xe7\xd5b\x7f\xafZg\xd0\x01\x1e\xcaF\xc7\xe4,@kS\xc8|\xe4\xedv\xa3g\x121.\x00\x10X$\x00\xd0\xea\x12\xe0\xc0n\xdd\x9c&\xb3\xd9\x15\x97\xc67\xaeH\xef\xfb\xd9\x8eC\x8b\x99\xb6P\x9f&\x9d\x95a\x00\xc3\xac\xd7@\x95\\\x127\x97\xca:\x9b\xfbA\xad\xc5K \xc7\xe1erK\x16\xd3m\xd4\x1b\xd3\xbf\x9e\xc2\x15\xd8\xd6^\xa7r9\xffC\xb0(\xb5W\xa0\n=\x8a\x0b"\x18&\x95l\xc4oF\xd9\xff\x98\xb5\xaf\xbd\xb89\x80p\xffC\xa3\xcb\xff\x8a1p\xd0t0\x13\x89\x1dG\x1e+l\xab\xd9AN$L\xd8~\xedE\xf7\x8c\x93\x0c\xc1\xfd\xdb\xc9\x9d\xe9\x0c\x1d\xbb}8\xf6j\x95>\xae\xee-\x9e?k\xaf\xaer\xe4`\xf9\t\x11\xb4\xd8\x03q*\x83\x13\x02\x0e\xe8\x9d?\xef\x0f\xd3^\xd82o4U\xc4l\xde\x17iJ\xfe\xd4\x8c`\xc6{H\x97\x16\xb7g\xb9\xddeUc\xd4\xc8\xa782IeK\x81\xc3r=!\x92\x97\x0f\x92\xd4\xf6\x01\x93\xa5\xbacL\x8b\x1a\xd5\x1d4\x9a2\x87m6\xc9\xf1\xb0&K\xdfo\xee\xeaQ\xf9\xc3\xa6\xfd?\n\xad\xfc\x9e\xaf\x8c\xfdJd\x84\xa5\x8bBl\xb1\xa0g\x8c\xb47\xf4\xc0`\x88\xe8\x88\n\x85\x81r\xf4J\xe3}\x89]\x8b\xfb|\x10\x05-)\xe2\xba\x96BT:\x16F\xaf\xd8\xa9\xcfew>\xc4QE\x91M\xffc\x07d\x1c\xf2\xb0G\xe5\x04\x03\x9bZ\xa0w\xa4\xd42\x0ex\xc3@\xdd\x9c\x15X\x0ey\x0e+\x12\x13ro\xda\xc2a\xfbH\xd0\x7f\x96\xad\xa7b&\xe7\xca+h\x1b\x13!\xf2\xf0cUw\xd7\x0f\xcd\x10>\x91\xcb\x0e\xba\xc1\xdec\xe6\x11\xdb\xba=y\x97\xe9\xc5\xfcW\x9b\x91)\xf1\x19\x12\xc4L\x83\xee"\xc2S\x9at\xd4\x01({\x01\xdc2e\xe7K\x10C\xa8J\xa3a\x1c=#\x03\x9b\xb2\x8e\xe0\x95\x9a\xf4R\x8d\xcf\xef\x88\xef\xce|\xe7\x9a5\xfe>\x13\x9d\x13\xe9 \xfc[\x02\xe2QP\xd4\x93\xe3\x15uJ3\xe6\xe1B\x12\xbdy\x81G\x9a*\x93K' 320 | # 321 | # 322 | # Trying mode: AES.MODE_OFB(5) 323 | # Exception: IV must be 16 bytes long 324 | # 325 | # 326 | # Trying mode: AES.MODE_OPENPGP(7) 327 | # Exception: Length of IV must be 16 or 18 bytes for MODE_OPENPGP 328 | # 329 | # 330 | # Trying mode: AES.MODE_PGP(4) 331 | # Exception: MODE_PGP is not supported anymore 332 | # 333 | # Modes ['AES.MODE_ECB'] ([1]) succesfully unencrypted the answer! 334 | 335 | --------------------------------------------------------------------------------