class ClientProtocol(asyncio.Protocol): """ Client that handles a single client """ def __init__(self, file_name, loop): """ Default constructor @param file_name: Name of the file to send @param loop: Asyncio Loop to use """ self.file_name = file_name self.loop = loop self.chunk_count = 0 self.last_pos = 0 self.symetric_ciphers = ['ChaCha20', 'AES', '3DES'] self.cipher_modes = ['CBC', 'ECB', 'GCM'] self.digest = ['SHA384', 'SHA256', 'SHA512', 'MD5', 'BLAKE2'] self.state = STATE_CONNECT # Initial State self.buffer = '' # Buffer to receive data chunks self.choosen_cipher = None self.choosen_mode = None self.choosen_digest = None self.host_name = "127.0.0.1" self.crypto = Crypto(self.choosen_cipher, self.choosen_mode, self.choosen_digest) self.encrypted_data = '' self.credentials = {} self.server_public_key = None self.nonce = os.urandom(16) self.server_nonce = None self.validation_type = "CHALLENGE" # CHALLENGE or CITIZEN_CARD self.rsa_public_key, self.rsa_private_key = self.crypto.key_pair_gen( 4096) self.recv_encrypted_data = '' self.recv_decrypted_data = [] def log_state(self, received): states = [ 'CONNECT', 'OPEN', 'DATA', 'CLOSE', 'KEY_ROTATION', 'NEGOTIATION', 'DIFFIE HELLMAN' ] logger.info("------------") #logger.info("State: {}".format(states[self.state])) logger.info("Received: {}".format(received)) def encrypt_payload(self, message: dict) -> None: """ Called when a secure message will be sent, in order to encrypt its payload. @param message: JSON message of type OPEN, DATA or CLOSE """ secure_message = {'type': 'SECURE_X', 'payload': None} payload = json.dumps(message).encode() if self.crypto.cipher_mode == 'GCM': criptogram = self.crypto.file_encryption(payload, b"HELLO") else: criptogram = self.crypto.file_encryption(payload) secure_message['payload'] = base64.b64encode(criptogram).decode() self.encrypted_data += secure_message['payload'] return secure_message def send_mac(self) -> None: """ Called when a secure message is sent and a MAC is necessary to check message authenticity. """ self.crypto.mac_gen(base64.b64decode(self.encrypted_data)) #logger.debug("My MAC: {}".format(self.crypto.mac)) if self.crypto.iv is None: iv = '' else: iv = base64.b64encode(self.crypto.iv).decode() if self.crypto.gcm_tag is None: tag = '' else: tag = base64.b64encode(self.crypto.gcm_tag).decode() if self.crypto.nonce is None: nonce = '' else: nonce = base64.b64encode(self.crypto.nonce).decode() message = { 'type': 'MAC', 'data': base64.b64encode(self.crypto.mac).decode(), 'iv': iv, 'tag': tag, 'nonce': nonce } self._send(message) self.encrypted_data = '' def process_mac(self, message: str) -> bool: """ Processes a MAC message from the client. It checks the authenticity/integrity of a previous received message. @param message: The message to process. """ logger.debug("Process MAC: {}".format(message)) client_mac = base64.b64decode(message['data']) # Generate server MAC self.crypto.mac_gen(base64.b64decode(self.recv_encrypted_data)) if client_mac == self.crypto.mac: logger.info("Integrity control: Success") return (True, None) else: return (False, 'Integrity control failed.') def connection_made(self, transport) -> None: """ Called when the client connects. @param transport: The transport stream to use for this client """ self.transport = transport logger.debug('Connected to Server') logger.debug('Sending cipher algorithms') logger.info('Connection to Server') logger.info('LOGIN_REQUEST') message = { 'type': 'NEGOTIATION', 'algorithms': { 'symetric_ciphers': self.symetric_ciphers, 'chiper_modes': self.cipher_modes, 'digest': self.digest } } #Generate a new NONCE self.crypto.auth_nonce = os.urandom(16) logger.debug(f"Nonce: {self.crypto.auth_nonce}") self._send(message) self.state = STATE_DH def data_received(self, data: str) -> None: """ Called when data is received from the server. Stores the data in the buffer @param data: The data that was received. This may not be a complete JSON message """ logger.debug('Received: {}'.format(data)) try: self.buffer += data.decode() except: logger.exception('Could not decode data from client') idx = self.buffer.find('\r\n') while idx >= 0: # While there are separators frame = self.buffer[:idx + 2].strip() # Extract the JSON object self.buffer = self.buffer[ idx + 2:] # Removes the JSON object from the buffer self.on_frame(frame) # Process the frame idx = self.buffer.find('\r\n') if len(self.buffer ) > 4096 * 1024 * 1024: # If buffer is larger than 4M logger.warning('Buffer to large') self.buffer = '' self.transport.close() def on_frame(self, frame: str) -> None: """ Processes a frame (JSON Object) @param frame: The JSON Object to process """ logger.debug("Frame: {}".format(frame)) try: message = json.loads(frame) except: logger.exception("Could not decode the JSON message") self.transport.close() return mtype = message.get('type', None) logger.info("Received: {}".format(mtype)) if mtype == 'SECURE_X': self.recv_encrypted_data += message['payload'] return elif mtype == 'MAC': (ret, error) = self.process_mac(message) if ret: iv = base64.b64decode(message['iv']) tag = base64.b64decode(message['tag']) nonce = base64.b64decode(message['nonce']) if iv == '': iv = None if tag == '': tag = None if nonce == '': nonce = None self.recv_decrypted_data.append( self.crypto.decryption( base64.b64decode(self.recv_encrypted_data.encode()), iv, tag, nonce)) # process secure message self.process_secure() return elif mtype == 'CHALLENGE_REQUEST': self.process_challenge(message) return elif mtype == 'CARD_LOGIN_RESPONSE': self.process_login_response(message) return elif mtype == 'SERVER_AUTH_RESPONSE': flag = self.process_server_auth(message) if not flag: message = {'type': 'SERVER_AUTH_FAILED'} secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() if flag: #Generate a new NONCE self.crypto.auth_nonce = os.urandom(16) self.state = STATE_CLIENT_AUTH if self.validation_type == "CHALLENGE": message = { 'type': 'LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode(), 'public_key': self.rsa_public_key } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() elif self.validation_type == "CITIZEN_CARD": message = { 'type': 'CARD_LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode() } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() return elif mtype == 'AUTH_RESPONSE': if message['status'] == 'SUCCESS': logger.info('User authentication with success.') self.process_authentication(message) elif message['status'] == 'DENIED': logger.info('User authentication denied.') else: logger.info('User authentication failed.') self.nonce = os.urandom(16) message = { 'type': 'LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode(), 'public_key': self.rsa_public_key } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() self.state = STATE_CLIENT_AUTH return elif mtype == 'FILE_REQUEST_RESPONSE': if message['status'] == 'PERMISSION_GRANTED': logger.info('Permission granted to transfer the file.') secure_message = self.encrypt_payload({ 'type': 'OPEN', 'file_name': self.file_name }) self._send(secure_message) self.send_mac() self.state = STATE_OPEN else: logger.error('Permission denied to transfer the file.') return elif mtype == 'OK': # Server replied OK. We can advance the state if self.state == STATE_OPEN: logger.info("Channel open") self.send_file(self.file_name) elif self.state == STATE_DATA: # Got an OK during a message transfer. # Reserved for future use pass else: logger.warning("Ignoring message from server") return elif mtype == 'ERROR': logger.warning("Got error from server: {}".format( message.get('data', None))) elif mtype == 'INTEGRITY_CONTROL': flag = message['data'] if flag == 'True': self._send(self.encrypt_payload({'type': 'CLOSE'})) self.send_mac() logger.info("File transferred. Closing transport") self.transport.close() elif mtype == 'DH_PARAMETERS_RESPONSE': logger.debug('DH_PARAMETERS_RESPONSE') public_key = bytes(message['parameters']['public_key'], 'ISO-8859-1') #Create shared key with the server public key self.crypto.create_shared_key(public_key) # Generate a symetric key self.crypto.symmetric_key_gen() logger.debug("Key: {}".format(self.crypto.symmetric_key)) if self.state == STATE_KEY_ROTATION: self.state = STATE_OPEN self.send_file(self.file_name) elif self.state == STATE_DH: self.crypto.auth_nonce = os.urandom(16) message = { 'type': 'SERVER_AUTH_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode() } secure_message = self.encrypt_payload(message) self.state = STATE_SERVER_AUTH self._send(secure_message) self.send_mac() return elif mtype == 'NEGOTIATION_RESPONSE': logger.debug("Negotiation response") # Receive the choosen algorithms by the server self.process_negotiation_response(message) # Generate Diffie Helman client private and public keys bytes_public_key, p, g, y = self.crypto.diffie_helman_client() message = { 'type': 'DH_PARAMETERS', 'parameters': { 'p': p, 'g': g, 'public_key': str(bytes_public_key, 'ISO-8859-1') } } self._send(message) self.state = STATE_DH return else: logger.warning("Invalid message type") logger.debug('Closing') self.transport.close() self.loop.stop() def process_secure(self): """ Processes a SECURE_X message from the client. It has an encrypted payload that should be decrypted. The payload has a JSON message that could be of type OPEN, DATA or CLOSE. """ message = json.loads(self.recv_decrypted_data[0]) mtype = message['type'] logger.info("Process SECURE_X: {}".format(mtype)) if mtype == 'CHALLENGE_REQUEST': self.process_challenge(message) elif mtype == 'CARD_LOGIN_RESPONSE': self.process_login_response(message) elif mtype == 'SERVER_AUTH_RESPONSE': flag = self.process_server_auth(message) if not flag: message = {'type': 'SERVER_AUTH_FAILED'} secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() if flag: #Generate a new NONCE self.crypto.auth_nonce = os.urandom(16) self.state = STATE_CLIENT_AUTH if self.validation_type == "CHALLENGE": message = { 'type': 'LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode(), 'public_key': self.rsa_public_key } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() elif self.validation_type == "CITIZEN_CARD": message = { 'type': 'CARD_LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode() } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() elif mtype == 'AUTH_RESPONSE': if message['status'] == 'SUCCESS': logger.info('User authentication with success.') self.process_authentication(message) elif message['status'] == 'DENIED': logger.info('User authentication denied.') else: logger.info('User authentication failed.') self.nonce = os.urandom(16) message = { 'type': 'LOGIN_REQUEST', 'nonce': base64.b64encode(self.crypto.auth_nonce).decode(), 'public_key': self.rsa_public_key } secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() self.state = STATE_CLIENT_AUTH elif mtype == 'FILE_REQUEST_RESPONSE': if message['status'] == 'PERMISSION_GRANTED': logger.info('Permission granted to transfer the file.') secure_message = self.encrypt_payload({ 'type': 'OPEN', 'file_name': self.file_name }) self._send(secure_message) self.send_mac() self.state = STATE_OPEN else: logger.error('Permission denied to transfer the file.') self.recv_encrypted_data = '' self.recv_decrypted_data = [] return def process_server_auth(self, message): """ Called when a client receives the server certificates and signature. After that the client performs the necessary validations. """ self.crypto.signature = base64.b64decode(message['signature'].encode()) server_cert_bytes = base64.b64decode(message['server_cert'].encode()) server_ca_cert_bytes = base64.b64decode( message['server_roots'].encode()) self.crypto.server_cert = self.crypto.load_cert_bytes( server_cert_bytes) self.crypto.server_public_key = self.crypto.server_cert.public_key() self.crypto.server_ca_cert = self.crypto.load_cert_bytes( server_ca_cert_bytes) # Validate server signature flag1 = self.crypto.rsa_signature_verification( self.crypto.signature, self.crypto.auth_nonce, self.crypto.server_public_key) logger.info(f'Server signature validation: {flag1}') #Validate common name flag2 = self.host_name == self.crypto.get_common_name( self.crypto.server_cert) logger.info(f'Server common_name validation: {flag2}') #Validate chain flag3 = self.crypto.validate_server_chain(self.crypto.server_cert, self.crypto.server_ca_cert) logger.info(f'Server chain validation: {flag3}') if flag1 and flag2 and flag3: logger.info("Server validated") return True else: return False def process_authentication(self, message): """ Called when a client is authenticated to perform access controll. """ secure_message = self.encrypt_payload({'type': 'FILE_REQUEST'}) self._send(secure_message) self.send_mac() self.state = STATE_OPEN def process_login_response(self, message): """ Called when a client is authenticating with citzent card. The client inserts it's username and creates a signature with it's card """ self.credentials['username'] = input("Username: "******""" Called when a client is authenticating with challenge. The client inserts it's username and password and creates a signature with it's private key """ self.credentials['username'] = input("Username: "******"Password: "******""" Called when a response of type NEGOTIATION is received. @param message: Received message """ logger.debug("Process Negotiation: {}".format(message)) self.crypto.symmetric_cipher = message['chosen_algorithms'][ 'symetric_cipher'] self.crypto.cipher_mode = message['chosen_algorithms']['chiper_mode'] self.crypto.digest = message['chosen_algorithms']['digest'] logger.info("Choosen algorithms: {} {} {}".format( self.crypto.symmetric_cipher, self.crypto.cipher_mode, self.crypto.digest)) def connection_lost(self, exc): """ Connection was lost for some reason. @param exc: """ logger.info('The server closed the connection') self.loop.stop() def send_file(self, file_name: str) -> None: """ Sends a file to the server. The file is read in chunks, encoded to Base64 and sent as part of a DATA JSON message @param file_name: File to send """ with open(file_name, 'rb') as f: message = {'type': 'DATA', 'data': None} file_ended = False read_size = 16 * 60 while True: if self.last_pos != 0: f.seek(self.last_pos) self.last_pos = 0 if self.chunk_count == 1000: self.state = STATE_KEY_ROTATION #Generate Diffie Helman client private and public keys bytes_public_key, p, g, y = self.crypto.diffie_helman_client( ) message = { 'type': 'DH_PARAMETERS', 'parameters': { 'p': p, 'g': g, 'public_key': str(bytes_public_key, 'ISO-8859-1') } } self.chunk_count = 0 self.last_pos = f.tell() self._send(message) break self.chunk_count += 1 data = f.read(16 * 60) message['data'] = base64.b64encode(data).decode() #logger.debug("Data: {} read size {}".format(data,f.tell())) secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() if len(data) != read_size: file_ended = True break # When it ends create MAC if file_ended: self._send(self.encrypt_payload({'type': 'CLOSE'})) self.send_mac() logger.info("File transferred. Closing transport") self.transport.close() def _send(self, message: str) -> None: """ Effectively encodes and sends a message :param message: :return: """ logger.info("Sent: {}".format(message['type'])) message_b = (json.dumps(message) + '\r\n').encode() self.transport.write(message_b)
class ClientProtocol(asyncio.Protocol): """ Client that handles a single client """ def __init__(self, file_name, loop): """ Default constructor @param file_name: Name of the file to send @param loop: Asyncio Loop to use """ self.file_name = file_name self.loop = loop self.chunk_count = 0 self.last_pos = 0 self.symetric_ciphers = ['ChaCha20', 'AES', '3DES'] self.cipher_modes = ['CBC', 'ECB', 'GCM'] self.digest = ['SHA384', 'SHA256', 'SHA512', 'MD5', 'BLAKE2'] self.state = STATE_CONNECT # Initial State self.buffer = '' # Buffer to receive data chunks self.choosen_cipher = None self.choosen_mode = None self.choosen_digest = None self.crypto = Crypto(self.choosen_cipher, self.choosen_mode, self.choosen_digest) self.encrypted_data = '' def log_state(self, received): states = [ 'CONNECT', 'OPEN', 'DATA', 'CLOSE', 'KEY_ROTATION', 'NEGOTIATION', 'DIFFIE HELLMAN' ] logger.info("------------") logger.info("State: {}".format(states[self.state])) logger.info("Received: {}".format(received)) def encrypt_payload(self, message: dict) -> None: """ Called when a secure message will be sent, in order to encrypt its payload. @param message: JSON message of type OPEN, DATA or CLOSE """ secure_message = {'type': 'SECURE_X', 'payload': None} payload = json.dumps(message).encode() if self.crypto.cipher_mode == 'GCM': criptogram = self.crypto.file_encryption(payload, b"HELLO") else: criptogram = self.crypto.file_encryption(payload) secure_message['payload'] = base64.b64encode(criptogram).decode() self.encrypted_data += secure_message['payload'] return secure_message def send_mac(self) -> None: """ Called when a secure message is sent and a MAC is necessary to check message authenticity. """ self.crypto.mac_gen(base64.b64decode(self.encrypted_data)) #logger.debug("My MAC: {}".format(self.crypto.mac)) if self.crypto.iv is None: iv = '' else: iv = base64.b64encode(self.crypto.iv).decode() if self.crypto.gcm_tag is None: tag = '' else: tag = base64.b64encode(self.crypto.gcm_tag).decode() if self.crypto.nonce is None: nonce = '' else: nonce = base64.b64encode(self.crypto.nonce).decode() message = { 'type': 'MAC', 'data': base64.b64encode(self.crypto.mac).decode(), 'iv': iv, 'tag': tag, 'nonce': nonce } self._send(message) self.encrypted_data = '' def connection_made(self, transport) -> None: """ Called when the client connects. @param transport: The transport stream to use for this client """ self.transport = transport logger.debug('Connected to Server') logger.debug('Sending cipher algorithms') message = { 'type': 'NEGOTIATION', 'algorithms': { 'symetric_ciphers': self.symetric_ciphers, 'chiper_modes': self.cipher_modes, 'digest': self.digest } } self._send(message) self.state = STATE_NEGOTIATION def data_received(self, data: str) -> None: """ Called when data is received from the server. Stores the data in the buffer @param data: The data that was received. This may not be a complete JSON message """ logger.debug('Received: {}'.format(data)) try: self.buffer += data.decode() except: logger.exception('Could not decode data from client') idx = self.buffer.find('\r\n') while idx >= 0: # While there are separators frame = self.buffer[:idx + 2].strip() # Extract the JSON object self.buffer = self.buffer[ idx + 2:] # Removes the JSON object from the buffer self.on_frame(frame) # Process the frame idx = self.buffer.find('\r\n') if len(self.buffer ) > 4096 * 1024 * 1024: # If buffer is larger than 4M logger.warning('Buffer to large') self.buffer = '' self.transport.close() def on_frame(self, frame: str) -> None: """ Processes a frame (JSON Object) @param frame: The JSON Object to process """ logger.debug("Frame: {}".format(frame)) try: message = json.loads(frame) except: logger.exception("Could not decode the JSON message") self.transport.close() return mtype = message.get('type', None) self.log_state(mtype) if mtype == 'OK': # Server replied OK. We can advance the state if self.state == STATE_OPEN: logger.info("Channel open") self.send_file(self.file_name) elif self.state == STATE_DATA: # Got an OK during a message transfer. # Reserved for future use pass else: logger.warning("Ignoring message from server") return elif mtype == 'ERROR': logger.warning("Got error from server: {}".format( message.get('data', None))) elif mtype == 'INTEGRITY_CONTROL': flag = message['data'] if flag == 'True': self._send(self.encrypt_payload({'type': 'CLOSE'})) self.send_mac() logger.info("File transferred. Closing transport") self.transport.close() elif mtype == 'DH_PARAMETERS_RESPONSE': logger.debug('DH_PARAMETERS_RESPONSE') public_key = bytes(message['parameters']['public_key'], 'ISO-8859-1') #Create shared key with the server public key self.crypto.create_shared_key(public_key) # Generate a symetric key self.crypto.symmetric_key_gen() logger.debug("Key: {}".format(self.crypto.symmetric_key)) if self.state == STATE_KEY_ROTATION: self.state = STATE_OPEN self.send_file(self.file_name) elif self.state == STATE_DH: secure_message = self.encrypt_payload({ 'type': 'OPEN', 'file_name': self.file_name }) self._send(secure_message) self.send_mac() self.state = STATE_OPEN return elif mtype == 'NEGOTIATION_RESPONSE': logger.debug("Negotiation response") # Receive the choosen algorithms by the server self.process_negotiation_response(message) # Generate Diffie Helman client private and public keys bytes_public_key, p, g, y = self.crypto.diffie_helman_client() message = { 'type': 'DH_PARAMETERS', 'parameters': { 'p': p, 'g': g, 'public_key': str(bytes_public_key, 'ISO-8859-1') } } self._send(message) self.state = STATE_DH return else: logger.warning("Invalid message type") logger.debug('CLosing') self.transport.close() self.loop.stop() def process_negotiation_response(self, message: str) -> bool: """ Called when a response of type NEGOTIATION is received. @param message: Received message """ logger.debug("Process Negotiation: {}".format(message)) self.crypto.symmetric_cipher = message['chosen_algorithms'][ 'symetric_cipher'] self.crypto.cipher_mode = message['chosen_algorithms']['chiper_mode'] self.crypto.digest = message['chosen_algorithms']['digest'] logger.info("Choosen algorithms: {} {} {}".format( self.crypto.symmetric_cipher, self.crypto.cipher_mode, self.crypto.digest)) def connection_lost(self, exc): """ Connection was lost for some reason. @param exc: """ logger.info('The server closed the connection') self.loop.stop() def send_file(self, file_name: str) -> None: """ Sends a file to the server. The file is read in chunks, encoded to Base64 and sent as part of a DATA JSON message @param file_name: File to send """ with open(file_name, 'rb') as f: message = {'type': 'DATA', 'data': None} file_ended = False read_size = 16 * 60 while True: if self.last_pos != 0: f.seek(self.last_pos) self.last_pos = 0 if self.chunk_count == 1000: self.state = STATE_KEY_ROTATION #Generate Diffie Helman client private and public keys bytes_public_key, p, g, y = self.crypto.diffie_helman_client( ) message = { 'type': 'DH_PARAMETERS', 'parameters': { 'p': p, 'g': g, 'public_key': str(bytes_public_key, 'ISO-8859-1') } } self.chunk_count = 0 self.last_pos = f.tell() self._send(message) break self.chunk_count += 1 data = f.read(16 * 60) message['data'] = base64.b64encode(data).decode() #logger.debug("Data: {} read size {}".format(data,f.tell())) secure_message = self.encrypt_payload(message) self._send(secure_message) self.send_mac() if len(data) != read_size: file_ended = True break # When it ends create MAC if file_ended: self._send(self.encrypt_payload({'type': 'CLOSE'})) self.send_mac() logger.info("File transferred. Closing transport") self.transport.close() def _send(self, message: str) -> None: """ Effectively encodes and sends a message :param message: :return: """ logger.info("Send: {}".format(message['type'])) logger.debug("Send: {}".format(message)) message_b = (json.dumps(message) + '\r\n').encode() self.transport.write(message_b)