Initial Commit
This commit is contained in:
7
Client/ClientTempMail.py
Normal file
7
Client/ClientTempMail.py
Normal file
@ -0,0 +1,7 @@
|
||||
# This is a class to help the client side with holding the mail structure that mimics the one used by the server
|
||||
class ClientTempMail:
|
||||
def __init__(self):
|
||||
self.from_address = None
|
||||
self.to_address_list = []
|
||||
self.subject = None
|
||||
self.body = None
|
546
Client/ClientThread.py
Normal file
546
Client/ClientThread.py
Normal file
@ -0,0 +1,546 @@
|
||||
import queue
|
||||
import socket
|
||||
import selectors
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import ClientTempMail
|
||||
import Crypto
|
||||
|
||||
|
||||
# Included the unix timestamp function from the DBConn file so we dont have to use the entire class just for this
|
||||
def get_unix_timestamp(seconds_offset=0) -> int:
|
||||
"""
|
||||
Function to get the current unix timestamp to sim the NOW() sql method, seconds offset will pull a
|
||||
past of future timestamp
|
||||
"""
|
||||
return int(time.time()) + seconds_offset
|
||||
|
||||
|
||||
class ClientThread(Thread):
|
||||
"""Class for dealing with the threaded actions of the client app"""
|
||||
def __init__(self, current_socket: socket, address: tuple):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.__address = address
|
||||
self.__socket: socket.socket = current_socket
|
||||
self.__selector = selectors.DefaultSelector()
|
||||
|
||||
self.__buffer_in = queue.Queue()
|
||||
self.__buffer_out = queue.Queue()
|
||||
|
||||
# flag for the current network status, -1 for awaiting request, 0 for awaiting response, 1 for ready
|
||||
self.current_status = -1
|
||||
self.state = "INIT"
|
||||
self.__init_timeout = get_unix_timestamp()
|
||||
self.last_response_content = []
|
||||
|
||||
# Login and User variables
|
||||
self.__session_ref = None
|
||||
self.login_username = None
|
||||
self.__user_full_name = None
|
||||
self.__user_email = None
|
||||
self.__user_role = None
|
||||
self.initialised = False
|
||||
|
||||
# Mail class to hold the mail data structure that mimics the server version
|
||||
self.client_mail_temp: ClientTempMail.ClientTempMail or None = None
|
||||
|
||||
self.__queued_messages = []
|
||||
|
||||
# Handshake and Encryption Variables
|
||||
self.__pending_handshake = False
|
||||
self.__hand_shook = False
|
||||
self.__crypto_manager_receive: Crypto.Receiver or None = None
|
||||
self.__crypto_manager_transmit: Crypto.Transmitter or None = None
|
||||
|
||||
# Active and Heartbeat Vars
|
||||
self.active = True
|
||||
self.__heartbeat_timer = None
|
||||
self.__heartbeat_tick = -120
|
||||
|
||||
self.__selector.register(self.__socket, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Method for checking if the connection is active, and sending the noop command for the heartbeat"""
|
||||
if not self.__socket:
|
||||
return False
|
||||
|
||||
# If the connection is initialised we can send commands
|
||||
if self.initialised:
|
||||
try:
|
||||
# Send heartbeat NOOP command, this is a background command as to not ruin the flow of the program
|
||||
# since actual SMTP servers work in a specific order
|
||||
self.queue_command("NOOP")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_users_role(self) -> str:
|
||||
"""Method for getting the current users server role (USER or ADMIN)"""
|
||||
return self.__user_role
|
||||
|
||||
def run(self):
|
||||
"""Main threads looped run method that listens and writes our network data"""
|
||||
pending_handshake_counter = 0
|
||||
try:
|
||||
while self.active:
|
||||
waiting_commands = self.__selector.select(timeout=1)
|
||||
for key, mask in waiting_commands:
|
||||
try:
|
||||
if mask & selectors.EVENT_READ:
|
||||
self.__read()
|
||||
elif mask & selectors.EVENT_WRITE and not self.__buffer_out.empty():
|
||||
self.__write()
|
||||
# Process any queued messages when an actual message is not being processed
|
||||
elif mask & selectors.EVENT_WRITE and len(self.__queued_messages) > 0:
|
||||
self.__write_queue()
|
||||
except Exception as ex:
|
||||
print(f"Exception hit: {repr(ex)}")
|
||||
self.__close()
|
||||
|
||||
# Init time out makes sure that a "valid" connection it makes will time out if the server
|
||||
# does not respond the way we want it to
|
||||
if self.state == "INIT":
|
||||
if self.__init_timeout < get_unix_timestamp(-40):
|
||||
print("No INIT processed, the connection is most likely at fault.")
|
||||
self.active = False
|
||||
|
||||
# Pending handshake so we can send the AUTH response then set the handshook flag after it has been sent
|
||||
if self.__pending_handshake:
|
||||
if pending_handshake_counter == 1:
|
||||
self.__pending_handshake = False
|
||||
self.__hand_shook = True
|
||||
pending_handshake_counter = 0
|
||||
else:
|
||||
pending_handshake_counter += 1
|
||||
|
||||
# If logged in we can do our Heartbeat NOOP request to faux keep our connection alive
|
||||
if self.__session_ref:
|
||||
if self.__heartbeat_timer < get_unix_timestamp(self.__heartbeat_tick):
|
||||
if not self.is_connected():
|
||||
print("Server did not respond")
|
||||
self.__close()
|
||||
|
||||
if not self.__selector.get_map():
|
||||
break
|
||||
finally:
|
||||
self.__selector.close()
|
||||
|
||||
def __read(self):
|
||||
"""Private method for reading the network data coming into the client"""
|
||||
try:
|
||||
data_in = self.__socket.recv(1024)
|
||||
except BlockingIOError:
|
||||
pass
|
||||
else:
|
||||
if data_in:
|
||||
if self.__hand_shook:
|
||||
# We decode the bytes of the message we receive, this should be a hex string
|
||||
data_in_decoded_string = data_in.decode()
|
||||
# If needed we split the response into chunks so that we can decrypt the multiple blocks that
|
||||
# were sent
|
||||
chunk_size = self.__crypto_manager_receive.get_expected_block_size()
|
||||
data_in_chunks = [self.__crypto_manager_receive.decrypt_string(
|
||||
data_in_decoded_string[i:i + chunk_size]
|
||||
) for i in range(0, len(data_in_decoded_string), chunk_size)]
|
||||
# Join all of the decrypted chunks back together
|
||||
full_data_in = "".join(data_in_chunks)
|
||||
self.__buffer_in.put(full_data_in)
|
||||
else:
|
||||
# Data is treated normally when we have not shaken hands
|
||||
self.__buffer_in.put(data_in.decode())
|
||||
else:
|
||||
raise RuntimeError("Connection Closed")
|
||||
|
||||
self.process_response()
|
||||
|
||||
def __write_queue(self):
|
||||
"""Private method for writing a queue of data with a simulated 'latency' to stop the messages being joined"""
|
||||
# Some SMTP response require the server to send multiple responses per request
|
||||
while len(self.__queued_messages) > 0:
|
||||
# Sleep to simulate some kind of latency to stop the queue to hopefully stop
|
||||
# the response from being added to the previous.
|
||||
sleep(1)
|
||||
try:
|
||||
self.process_silent_command(self.__queued_messages.pop(0))
|
||||
response = self.__buffer_out.get_nowait()
|
||||
except Exception:
|
||||
response = None
|
||||
|
||||
if response:
|
||||
# This would sometimes interrupt the users input because a queued write would happen during it
|
||||
# print(f"Sending Queued response to Client: {self.__address}")
|
||||
try:
|
||||
self.__do_write(response)
|
||||
except BlockingIOError as ex:
|
||||
print(f"IO Error: {repr(ex)}")
|
||||
pass
|
||||
|
||||
def __write(self):
|
||||
"""Private method for doing a single write to the socket"""
|
||||
try:
|
||||
response = self.__buffer_out.get_nowait()
|
||||
except Exception:
|
||||
response = None
|
||||
|
||||
if response:
|
||||
print(f"Sending data: {repr(response)} to {self.__address}")
|
||||
try:
|
||||
self.__do_write(response)
|
||||
except BlockingIOError as ex:
|
||||
print(f"Could not send data: {repr(ex)}")
|
||||
pass
|
||||
|
||||
def __do_write(self, response: bytes) -> None:
|
||||
"""Shared Private method to do the actual write to the socket"""
|
||||
# Check if we have shook hands
|
||||
if self.__hand_shook:
|
||||
# Get the max chunk size for the key we generated in case we need to split the request down
|
||||
chunk_size = self.__crypto_manager_transmit.get_usable_byte_length()
|
||||
response_chunks = [self.__crypto_manager_transmit.encrypt_string(response[i:i + chunk_size])
|
||||
for i in range(0, len(response), chunk_size)]
|
||||
full_response = b"".join(response_chunks)
|
||||
self.__socket.send(full_response)
|
||||
else:
|
||||
self.__socket.send(response)
|
||||
|
||||
# Set the heartbeat timer since we just sent a command and wont need to send a noop again
|
||||
self.__heartbeat_timer = get_unix_timestamp()
|
||||
|
||||
def process_command(self, command: str) -> None:
|
||||
"""Method to Process the command and add it into the buffer, we also set the client to read mode"""
|
||||
self.current_status = 0
|
||||
self.__buffer_out.put(command.encode())
|
||||
|
||||
def process_silent_command(self, command: str) -> None:
|
||||
"""Method to silently add a command to the buffer, this does not cause the client to enter read mode"""
|
||||
self.__buffer_out.put(command.encode())
|
||||
|
||||
def queue_command(self, command: str) -> None:
|
||||
"""Method to add a command to the queued message array for queued writing"""
|
||||
self.__queued_messages.append(command)
|
||||
|
||||
def process_response(self):
|
||||
"""Method to process the response from the server and to set the client to the required state"""
|
||||
current_data = self.__buffer_in.get()
|
||||
|
||||
# Split the response string by the first space, this should give us a response code and response data
|
||||
response_parts = current_data.split(" ", 1)
|
||||
if len(response_parts) >= 2:
|
||||
|
||||
response_code = int(response_parts[0])
|
||||
|
||||
# If the response is not from the heartbeat print it
|
||||
if response_code != 295:
|
||||
print(f"Response: {repr(current_data)}")
|
||||
|
||||
if len(response_parts) > 1:
|
||||
response_data = response_parts[1]
|
||||
else:
|
||||
response_data = None
|
||||
|
||||
# Check the current state and respond accordingly
|
||||
if self.state == "INIT":
|
||||
if response_code == 220:
|
||||
self.state = "HELO"
|
||||
# Initialised, now send the HELO command
|
||||
self.process_command("HELO " + self.__address[0])
|
||||
self.current_status = 0
|
||||
self.initialised = True
|
||||
else:
|
||||
print("No response from the server...")
|
||||
self.__close()
|
||||
|
||||
elif self.state == "HELO":
|
||||
if response_code == 250:
|
||||
# HELO responded, send the AUTH command
|
||||
self.state = "AUTH"
|
||||
self.current_status = 0
|
||||
|
||||
elif self.state == "AUTH":
|
||||
if not self.__hand_shook:
|
||||
if response_code == 570:
|
||||
# set up the RSA encryption variables for encrypting the connection
|
||||
self.__crypto_manager_transmit = Crypto.Transmitter(response_data)
|
||||
self.__crypto_manager_receive = Crypto.Receiver()
|
||||
self.__crypto_manager_receive.generate_keys()
|
||||
# Send our public key back to the server
|
||||
self.process_command("AUTH " + self.__crypto_manager_receive.get_public_key_pair())
|
||||
# Set the client to a pending handshake
|
||||
self.__pending_handshake = True
|
||||
else:
|
||||
if response_code == 250:
|
||||
# Auth key was sent successfully, start the confirmation handshake
|
||||
self.process_command("HSHK " + response_data)
|
||||
self.state = "HSHK"
|
||||
|
||||
elif self.state == "HSHK":
|
||||
if response_code == 250:
|
||||
# The validation handshake was current, start the login state and set the write
|
||||
self.state = "LOGI"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "LOGI":
|
||||
if response_code == 250:
|
||||
# Login was successful, wait for the secondary role response so we can tell
|
||||
# if the user is and ADMIN or USER
|
||||
self.current_status = 0
|
||||
elif response_code == 265:
|
||||
self.__session_ref = response_data
|
||||
self.current_status = 0
|
||||
# Send the VRFY command to the get logged in users name and email address
|
||||
self.state = "VRFY"
|
||||
self.process_command("VRFY " + self.login_username)
|
||||
elif response_code == 421:
|
||||
pass
|
||||
else:
|
||||
# Bad login, try again
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append(response_data)
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "VRFY":
|
||||
if response_code == 261:
|
||||
self.__user_role = response_data
|
||||
elif response_code == 250:
|
||||
# Process the email address that gets sent back with the VRFY response
|
||||
if '@' in response_data:
|
||||
if '<' in response_data:
|
||||
start_bracket_index = response_data.find('<')
|
||||
end_bracket_index = response_data.rfind('>')
|
||||
self.__user_full_name = response_data[0:start_bracket_index-1].strip()
|
||||
self.__user_email = response_data[start_bracket_index+1:end_bracket_index].strip()
|
||||
else:
|
||||
#
|
||||
self.__user_full_name = "N/A"
|
||||
self.__user_email = response_data.strip()
|
||||
# Send the user to the Home menu (HMEN) as they are not logged in
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
else:
|
||||
# VRFY failed and we send the user back to the login
|
||||
self.reset_session()
|
||||
self.state = "LOGI"
|
||||
self.current_status = -1
|
||||
elif response_code == 500:
|
||||
# VRFY failed and we send the user back to the login
|
||||
self.reset_session()
|
||||
self.state = "LOGI"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "MAIL":
|
||||
# Start an email input, we start by asking for the receipt addresses (RCPT)
|
||||
if response_code == 250:
|
||||
self.state = "RCPT"
|
||||
self.current_status = -1
|
||||
else:
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "RCPT":
|
||||
self.last_response_content.clear()
|
||||
if response_code == 250:
|
||||
# The address was local and added
|
||||
self.last_response_content.append("Address Added.")
|
||||
elif response_code == 251:
|
||||
# The address was valid but not local and added
|
||||
self.last_response_content.append("Address Added, but not local.")
|
||||
elif response_code == 503:
|
||||
# The server said we are out of sequence, reset the transaction and start over
|
||||
self.last_response_content.append("There was an issue with the order of commands sent, RSET sent.")
|
||||
self.state = "RSET"
|
||||
self.process_command("RSET")
|
||||
else:
|
||||
# The address was rejected, we remove it from the clients end
|
||||
last_address = self.client_mail_temp.to_address_list.pop(-1)
|
||||
self.last_response_content.append(f"Address {last_address} not added for reason: " + response_data)
|
||||
|
||||
if self.state != "RSET":
|
||||
self.state = "RCPT"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "SUBJ":
|
||||
# The receipts were fine and we move onto the subject, subject does not have an official "state"
|
||||
# we process it fully from the client side and send it along with all of the starting DATA requests
|
||||
if response_code == 354:
|
||||
string_date = datetime.now()
|
||||
date_formatted = string_date.strftime("%d %b %y %H:%M:%S")
|
||||
# send the email headers that get used when "forwarding" to another server, for completeness
|
||||
# DATA starts with 4 lines, Date, From, Subject and To as a comma seperated list
|
||||
self.queue_command("Date: " + date_formatted)
|
||||
self.queue_command("From: " + self.__user_full_name + " <" + self.__user_email + ">")
|
||||
self.queue_command("Subject: " + self.client_mail_temp.subject)
|
||||
self.queue_command("To: " + ", ".join(self.client_mail_temp.to_address_list))
|
||||
# Set the state to DATA and start accepting lines from the client until the >SEND command is used
|
||||
self.state = "DATA"
|
||||
self.current_status = -1
|
||||
else:
|
||||
# The server said we are out of sequence, reset the transaction and start over
|
||||
self.last_response_content.append("There was an issue with the order of commands sent, RSET sent.")
|
||||
self.state = "RSET"
|
||||
self.process_command("RSET")
|
||||
|
||||
elif self.state == "DATA":
|
||||
if response_code == 250:
|
||||
# The data was sent and the server responded with an OK response, return to the home menu
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("Email sent.")
|
||||
self.client_mail_temp = None
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "RSET":
|
||||
if response_code == 250:
|
||||
# The server responded OK to our request to reset the current transaction
|
||||
self.client_mail_temp = None
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "VIEW":
|
||||
if response_code == 211:
|
||||
# Server responded to the VIEW command
|
||||
self.current_status = 0
|
||||
self.last_response_content.clear()
|
||||
elif response_code == 214:
|
||||
# Process the VIEW response until we get the end of DATA string
|
||||
if response_data == "\n.\n":
|
||||
self.current_status = -1
|
||||
else:
|
||||
self.last_response_content.append(response_data)
|
||||
elif response_code == 504:
|
||||
# There was an error getting a mail, usually if they try to enter
|
||||
# a number that doesnt belong to them
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("There was an issue loading this mail")
|
||||
self.current_status = -1
|
||||
else:
|
||||
# There was an unspecified error so we send the user back to the home menu
|
||||
self.last_response_content.clear()
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "ADMN":
|
||||
if response_code == 250:
|
||||
# Server has left ADMIN mode
|
||||
if response_data == "OK QUIT":
|
||||
self.last_response_content.clear()
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
# Server has processed the command given
|
||||
elif response_data == "OK DONE":
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("Action Completed.")
|
||||
self.current_status = -1
|
||||
else:
|
||||
self.last_response_content.clear()
|
||||
self.current_status = -1
|
||||
# Server sent a multipart ADMIN response
|
||||
elif response_code == 211:
|
||||
self.current_status = 0
|
||||
self.last_response_content.clear()
|
||||
elif response_code == 214:
|
||||
if response_data == "\n.\n":
|
||||
self.current_status = -1
|
||||
else:
|
||||
self.last_response_content.append(response_data)
|
||||
# Server sent an ADMIN error response
|
||||
elif response_code == 500:
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("Action Failed.")
|
||||
self.current_status = -1
|
||||
# Permission was denied by the server
|
||||
else:
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("Permission Denied.")
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
elif self.state == "LOUT":
|
||||
# Server logged the client out
|
||||
if response_code == 250:
|
||||
# Logged out
|
||||
self.reset_session()
|
||||
self.state = "LOGI"
|
||||
else:
|
||||
# Couldn't log out?
|
||||
self.state = "HMEN"
|
||||
|
||||
self.current_status = -1
|
||||
elif self.state == "HELP":
|
||||
# Server responded to the HELP command and is sending back the multipart response
|
||||
if response_code == 211:
|
||||
self.current_status = 0
|
||||
self.last_response_content.clear()
|
||||
elif response_code == 214:
|
||||
if response_data == "\n.\n":
|
||||
self.current_status = -1
|
||||
else:
|
||||
self.last_response_content.append(response_data)
|
||||
# Server responded with a command not implemented error becuase the help section does not exist
|
||||
elif response_code == 504:
|
||||
self.last_response_content.clear()
|
||||
self.last_response_content.append("There was no help on file for your request.")
|
||||
self.current_status = -1
|
||||
# Unspecified error from the server, just sending the user to the home menu
|
||||
else:
|
||||
self.last_response_content.clear()
|
||||
self.state = "HMEN"
|
||||
self.current_status = -1
|
||||
|
||||
# Status 421 and 221 is only used when the connection has been terminated and we just exit at that point
|
||||
if response_code == 421:
|
||||
print("The connection was closed by the server with the following message:")
|
||||
for line in self.last_response_content:
|
||||
print(line)
|
||||
else:
|
||||
print(f"Reason: {response_data}")
|
||||
self.__close()
|
||||
self.current_status = -2
|
||||
|
||||
if response_code == 221:
|
||||
self.state = "QUIT"
|
||||
self.__close()
|
||||
self.current_status = -2
|
||||
|
||||
else:
|
||||
# The response did not have a valid response code
|
||||
print("Response too short.")
|
||||
|
||||
def __close(self):
|
||||
"""Private method to close the connection to the server and end the thread"""
|
||||
print("Attempting to end the Connection...")
|
||||
try:
|
||||
self.__selector.unregister(self.__socket)
|
||||
self.__socket.close()
|
||||
print(f"Connection ended with {repr(self.__address)}")
|
||||
except OSError as ex:
|
||||
print(f"Could not close the connection: {repr(ex)}")
|
||||
pass
|
||||
finally:
|
||||
self.__socket = None
|
||||
self.active = False
|
||||
self.current_status = -1
|
||||
|
||||
def get_session_ref(self) -> str:
|
||||
"""Method to return the current session reference from the server"""
|
||||
return self.__session_ref
|
||||
|
||||
def get_user_name(self) -> str:
|
||||
"""Method to return the current users full name"""
|
||||
return self.__user_full_name
|
||||
|
||||
def get_user_email(self) -> str:
|
||||
"""Method to return the current users email address"""
|
||||
return self.__user_email
|
||||
|
||||
def reset_session(self) -> None:
|
||||
"""Method to restart a clients session without closing the connection, used for the logout command"""
|
||||
self.__session_ref = None
|
||||
self.__user_email = None
|
||||
self.__user_full_name = None
|
||||
self.__user_role = None
|
||||
self.login_username = None
|
284
Client/Crypto.py
Normal file
284
Client/Crypto.py
Normal file
@ -0,0 +1,284 @@
|
||||
# System Imports
|
||||
import math
|
||||
import random
|
||||
|
||||
# Project Imports
|
||||
import SHA256Custom
|
||||
|
||||
|
||||
class Receiver:
|
||||
"""Class for dealing with the receiving end of the custom simple RSA encryption"""
|
||||
def __init__(self):
|
||||
# A list of small prime numbers we can use to calculate a larger one
|
||||
self.__small_primes_list = [211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
||||
307, 311, 313, 317, 331, 337, 347, 349]
|
||||
|
||||
self.__p = None
|
||||
self.__q = None
|
||||
self.__n = None
|
||||
self.__t = None
|
||||
|
||||
# This is apparently the most commonly used shared co-prime used for RSA
|
||||
self.__e = 65537
|
||||
self.__d = None
|
||||
|
||||
# The generated public and private keys
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.generated = False
|
||||
|
||||
# The bit size of the key
|
||||
self.__bit_size = 1024
|
||||
|
||||
def generate_keys(self) -> bool:
|
||||
"""Method to generate the RSA Public and Private keys for encryption"""
|
||||
try:
|
||||
# Only generate keys if we haven't already
|
||||
if not self.generated:
|
||||
self.__p = self.__generate_prime_number()
|
||||
self.__q = self.__generate_prime_number()
|
||||
|
||||
# Make sure we have 2 different prime numbers
|
||||
while self.__q == self.__p:
|
||||
self.__q = self.__generate_prime_number()
|
||||
|
||||
# Most of the following is just the pseudo code for RSA prime generation
|
||||
self.__n = self.__p * self.__q
|
||||
self.__t = (self.__p - 1) * (self.__q - 1)
|
||||
|
||||
while math.gcd(self.__t, self.__e) > 1:
|
||||
self.__e += 2
|
||||
|
||||
if self.__e < 1 or self.__e > self.__t:
|
||||
raise Exception("E must be > 1 and < T")
|
||||
|
||||
if self.__t % self.__e == 0:
|
||||
raise Exception("E is not a co-prime with T")
|
||||
|
||||
self.__d = Receiver.find_inverse(self.__e, self.__t)
|
||||
|
||||
# We have our keys, set up the tuples and flag the generated as true
|
||||
self.__public_key = (self.__e, self.__n)
|
||||
self.__private_key = (self.__d, self.__n)
|
||||
self.generated = True
|
||||
|
||||
return True
|
||||
else:
|
||||
raise Exception("Keys have already been generated.")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_expected_block_size(self) -> int:
|
||||
"""Method to return the expected block size for the encrypted data"""
|
||||
# the encrypted request blocksize seems to be half of the bit size
|
||||
return int(self.__bit_size / 2)
|
||||
|
||||
def get_public_key_pair(self, hex_result=True):
|
||||
"""Method for returning the public key as a hex string or tuple depending on the hex_results"""
|
||||
if hex_result:
|
||||
# Return both public key elements as a hex string
|
||||
return "{0:x} {1:x}".format(self.__public_key[0], self.__public_key[1])
|
||||
else:
|
||||
return self.__public_key
|
||||
|
||||
def get_private_key_pair(self, hex_result=True):
|
||||
"""Method for returning the private key as a hex string or tuple, not really used"""
|
||||
if hex_result:
|
||||
# Return both private key elements as a hex string
|
||||
return "{0:x} {1:x}".format(self.__private_key[0], self.__private_key[1])
|
||||
else:
|
||||
return self.__private_key
|
||||
|
||||
def decrypt_string(self, string_in: str) -> str:
|
||||
"""Method for decrypting our custom RSA encrypted blocks, takes the hex string in"""
|
||||
if self.generated:
|
||||
# Convert the hex string back into an int
|
||||
encrypted_string_as_int = int(string_in, 16)
|
||||
# Do the math on our encrypted int
|
||||
decrypted_string_as_int = pow(encrypted_string_as_int, self.__d, self.__n)
|
||||
# Convert the result back into a hex string
|
||||
decrypted_string_as_hex = "{0:x}".format(decrypted_string_as_int)
|
||||
|
||||
start_padding_included = False
|
||||
plain_text_read = False
|
||||
|
||||
plain_text_string = ''
|
||||
hashed_checksum = ''
|
||||
|
||||
# Loop for every 2 characters (hex)
|
||||
for i in range(0, len(decrypted_string_as_hex), 2):
|
||||
current_byte = decrypted_string_as_hex[i:i+2]
|
||||
# the encrypted string should start with a padding FF value
|
||||
if not start_padding_included and current_byte.upper() == "FF":
|
||||
start_padding_included = True
|
||||
# the Checksum hash should start after the 00 value
|
||||
elif not plain_text_read and start_padding_included and current_byte == "00":
|
||||
plain_text_read = True
|
||||
# Everything else should be the message or the checksum hash
|
||||
elif start_padding_included:
|
||||
if not plain_text_read:
|
||||
# The message is padded after the fact to fill up the bit size, these are ignored
|
||||
if current_byte.upper() != "FF":
|
||||
# Convert the hex value back into a utf character
|
||||
plain_text_string += chr(int(current_byte, 16))
|
||||
else:
|
||||
# Reconstruct the hash checksum
|
||||
hashed_checksum += current_byte
|
||||
|
||||
# If the text of hash is missing then something went wrong with encrypting or sending
|
||||
if len(plain_text_string) == 0 or len(hashed_checksum) == 0:
|
||||
raise Exception("No text was decrypted, something must be wrong...")
|
||||
|
||||
# Hash the plain text string and make sure it matches our checksum hash
|
||||
hash_test = SHA256Custom.SHA256Custom()
|
||||
hash_test.update(plain_text_string.encode())
|
||||
hashed_plain_text = hash_test.hexdigest()
|
||||
|
||||
if hashed_plain_text == hashed_checksum:
|
||||
# The message has not been altered, decrypted successfully
|
||||
return plain_text_string
|
||||
else:
|
||||
raise Exception("Plain text did not match the Hashed Checksum, something is wrong.")
|
||||
|
||||
else:
|
||||
raise Exception("No key has been generated")
|
||||
|
||||
def __generate_prime_number(self) -> int:
|
||||
"""Private method for generating prime numbers with the bit size set"""
|
||||
while True:
|
||||
current_prime_candidate = self.__generate_low_level_prime(self.__bit_size)
|
||||
if not self.miller_rabin_prime_check(current_prime_candidate):
|
||||
continue
|
||||
else:
|
||||
return current_prime_candidate
|
||||
|
||||
def __generate_low_level_prime(self, prime_candidate: int) -> int:
|
||||
"""Private method for generating the prime number using the candidate number and the small prime array"""
|
||||
while True:
|
||||
current_prime_candidate = self.generate_random_large_int(prime_candidate)
|
||||
|
||||
# We try to generate a large prime number using the candidate and looping through the small primes
|
||||
# array until we get one
|
||||
for divisor in self.__small_primes_list:
|
||||
if current_prime_candidate % divisor == 0 and divisor ** 2 <= current_prime_candidate:
|
||||
break
|
||||
else:
|
||||
return current_prime_candidate
|
||||
|
||||
@staticmethod
|
||||
def generate_random_large_int(n_bit_size: int) -> int:
|
||||
"""Static method to generate a large int value"""
|
||||
# Get a random number in a range of the bit size given
|
||||
return random.randrange(2 ** (n_bit_size - 1) + 1, 2 ** n_bit_size - 1)
|
||||
|
||||
# Method to test that the prime number we generate passes the miller rabin prime candidate test
|
||||
@staticmethod
|
||||
def miller_rabin_prime_check(prime_candidate: int) -> bool:
|
||||
"""Static method for the miller rabin prime number checking algorithm, to ensure that we have a valid prime"""
|
||||
max_divisions_by_two = 0
|
||||
prime_minus_one = prime_candidate - 1
|
||||
while prime_minus_one % 2 == 0:
|
||||
prime_minus_one >>= 1
|
||||
max_divisions_by_two += 1
|
||||
|
||||
assert 2 ** max_divisions_by_two * prime_minus_one == prime_candidate - 1
|
||||
|
||||
def trial_composite(round_tester_in):
|
||||
"""Sub method for doing the actual prime number factorisation check"""
|
||||
if pow(round_tester_in, prime_minus_one, prime_candidate) == 1:
|
||||
return False
|
||||
|
||||
for x in range(max_divisions_by_two):
|
||||
if pow(round_tester_in, 2 ** x * prime_minus_one, prime_candidate) == prime_candidate - 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for i in range(0, 20):
|
||||
round_tester = random.randrange(2, prime_candidate)
|
||||
if trial_composite(round_tester):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def extended_euclidean_algorithm(a: int, b: int) -> tuple:
|
||||
"""Static method for the EEA from the pseudo code"""
|
||||
if b == 0:
|
||||
return 1, 0
|
||||
|
||||
q = a // b
|
||||
r = a % b
|
||||
s, t = Receiver.extended_euclidean_algorithm(b, r)
|
||||
return t, s-(q*t)
|
||||
|
||||
@staticmethod
|
||||
def find_inverse(a: int, b: int) -> int:
|
||||
"""Static method for finding the inverse of the primes given"""
|
||||
inverse = Receiver.extended_euclidean_algorithm(a, b)[0]
|
||||
if inverse < 1:
|
||||
inverse += b
|
||||
|
||||
return inverse
|
||||
|
||||
|
||||
class Transmitter:
|
||||
"""Class for dealing with the transmitting end of the custom simple RSA encryption"""
|
||||
def __init__(self, public_key: str):
|
||||
# We should be getting the string hex values from the receiver and splitting them into the actual parts
|
||||
public_key_parts = public_key.split(" ", 1)
|
||||
|
||||
# Must match the Receiver bit size
|
||||
self.__bit_size = 1024
|
||||
|
||||
# Check if the public key has 2 parts to be valid
|
||||
if len(public_key_parts) == 2:
|
||||
self.__e = int(public_key_parts[0], 16)
|
||||
self.__n = int(public_key_parts[1], 16)
|
||||
self.__has_key = True
|
||||
else:
|
||||
self.__has_key = False
|
||||
raise Exception("Public key was incorrect")
|
||||
|
||||
def get_current_key_pair(self):
|
||||
"""Method for getting the key pair values e and n"""
|
||||
return self.__e, self.__n
|
||||
|
||||
def get_usable_byte_length(self) -> int:
|
||||
"""Method for getting the usable byte length incase we need to split a request into multiple parts"""
|
||||
# Im not entirely sure on this as it was calculated mostly by "brute force" but it seems to be the
|
||||
# key size divided by 4
|
||||
# minus 2 bytes for the start and hash break bytes added
|
||||
# minus 32 for the SHA256 hash size
|
||||
# minus 32 for the RSA byte overhead, i've tried different values but getting too close to 256 bytes will
|
||||
# sometimes case issues which i think is down to
|
||||
return int(self.__bit_size / 4) - 2 - 32 - 32
|
||||
|
||||
def encrypt_string(self, string_in: bytes) -> bytes:
|
||||
"""Method for encrypting a string using the public RSA key provided by the receiver"""
|
||||
if self.__has_key:
|
||||
# Create a hash of the message given for our checksum
|
||||
string_hashing = SHA256Custom.SHA256Custom()
|
||||
string_hashing.update(string_in)
|
||||
|
||||
string_hashed = string_hashing.hexdigest()
|
||||
|
||||
# Pad to make the string the full size of the custom RSA block
|
||||
padding = ""
|
||||
if len(string_in) < self.get_usable_byte_length():
|
||||
padding_required = self.get_usable_byte_length() - len(string_in) - 1
|
||||
padding = "ff" * padding_required
|
||||
|
||||
# Add an FF pad at the start of the string, since if the input hex starts with a 0X value the 0 is dropped
|
||||
# when it is converted to an int, im not entirely sure how they get around this
|
||||
hex_string = "ff" + string_in.hex() + padding + "00" + string_hashed
|
||||
string_as_int = int(hex_string, 16)
|
||||
|
||||
# Do the math to the int value
|
||||
encrypted_string_as_int = pow(string_as_int, self.__e, self.__n)
|
||||
# Convert the int value into a hex string
|
||||
encrypted_string_hex = "{0:x}".format(encrypted_string_as_int)
|
||||
|
||||
return encrypted_string_hex.encode()
|
||||
else:
|
||||
raise Exception("No key has been generated")
|
126
Client/SHA256Custom.py
Normal file
126
Client/SHA256Custom.py
Normal file
@ -0,0 +1,126 @@
|
||||
class SHA256Custom:
|
||||
"""
|
||||
Custom SHA256 function, its not really custom but rewritten using the pseudocode from wikipedia and
|
||||
should mimic the built in function
|
||||
"""
|
||||
def __init__(self, data=None):
|
||||
# Setting up the initial bit values, i wont pretend to understand the math involved but these seem hardcoded
|
||||
self.__ks = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||
|
||||
self.__hs = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||
|
||||
# Copy the initial bit values for use since they are changed
|
||||
self.__h = self.__hs[:]
|
||||
self.__k = self.__ks[:]
|
||||
|
||||
# Create variables we will need in the class
|
||||
self.__string_byte_len = 0
|
||||
self.__buffer = b''
|
||||
self.__string_hashed = False
|
||||
self.__digested_string = b''
|
||||
|
||||
if data is not None:
|
||||
self.update(data)
|
||||
|
||||
# Static methods because pycharm is moaning about them not needing to be inside of the class
|
||||
@staticmethod
|
||||
def pad_string(string_len: int) -> bytes:
|
||||
"""Static method to pad the string digest for the hash"""
|
||||
# Adding a "1" bit to the end, although it seems to use 63 as the actual value, im guessing its possibly to do
|
||||
# with chunk size
|
||||
string_di = string_len & 0x3f
|
||||
# Get the 64bit big endian length of the post processed hash length
|
||||
post_process_length = (string_len << 3).to_bytes(8, "big")
|
||||
padded_length = 55
|
||||
if string_di < 56:
|
||||
padded_length -= string_di
|
||||
else:
|
||||
padded_length -= 199 - string_di
|
||||
|
||||
return b'\x80' + b'\x00' * padded_length + post_process_length
|
||||
|
||||
@staticmethod
|
||||
def rotate_right(a, b):
|
||||
"""Static method to rotate a and b bits right """
|
||||
return ((a >> b) | (a << (32 - b))) & 0xffffffff
|
||||
|
||||
def update(self, string_in: bytes) -> None:
|
||||
"""Method to update the hash value"""
|
||||
if (string_in and len(string_in) > 0) and not self.__string_hashed:
|
||||
self.__string_byte_len += len(string_in)
|
||||
string_buffer = self.__buffer + string_in
|
||||
|
||||
# Process the string as 512 bit chunks (64 bytes)
|
||||
for i in range(0, len(string_buffer) // 64):
|
||||
self.calculate_hash(string_buffer[64 * i:64 * (i + 1)])
|
||||
|
||||
# Set the class buffer
|
||||
self.__buffer = string_buffer[len(string_buffer) - (len(string_buffer) % 64):]
|
||||
else:
|
||||
return
|
||||
|
||||
def digest(self) -> bytes:
|
||||
"""Method to get the bytes digest of our hash value"""
|
||||
if not self.__string_hashed:
|
||||
self.update(self.pad_string(self.__string_byte_len))
|
||||
self.__digested_string = b''.join(val.to_bytes(4, 'big') for val in self.__h[:8])
|
||||
self.__string_hashed = True
|
||||
return self.__digested_string
|
||||
|
||||
def hexdigest(self) -> str:
|
||||
"""Method to get the hex digest of our hash value"""
|
||||
# Hex characters to use when changing the hex value back into a string
|
||||
tab = '0123456789abcdef'
|
||||
return "".join(tab[byt >> 4] + tab[byt & 0xf] for byt in self.digest())
|
||||
|
||||
def calculate_hash(self, chunk) -> None:
|
||||
"""Method to calculate the actual hash value"""
|
||||
# Create an array of 32bit words
|
||||
words = [0] * 64
|
||||
|
||||
# Converted from the pseudo code, but i believe its filling the word array with first 16 words we add
|
||||
words[0:16] = [int.from_bytes(chunk[i:i + 4], "big") for i in range(0, len(chunk), 4)]
|
||||
|
||||
for i in range(16, 64):
|
||||
s0 = self.rotate_right(words[i - 15], 7) ^ self.rotate_right(words[i - 15], 18) ^ (words[i - 15] >> 3)
|
||||
s1 = self.rotate_right(words[i - 2], 17) ^ self.rotate_right(words[i - 2], 19) ^ (words[i - 2] >> 10)
|
||||
words[i] = (words[i - 16] + s0 + words[i - 7] + s1) & 0xffffffff
|
||||
|
||||
# Initialize the values with the default hash value
|
||||
a, b, c, d, e, f, g, h = self.__h
|
||||
|
||||
# Calculation main loop
|
||||
for i in range(64):
|
||||
s0 = self.rotate_right(a, 2) ^ self.rotate_right(a, 13) ^ self.rotate_right(a, 22)
|
||||
t2 = s0 + self.maj(a, b, c)
|
||||
s1 = self.rotate_right(e, 6) ^ self.rotate_right(e, 11) ^ self.rotate_right(e, 25)
|
||||
t1 = h + s1 + self.ch(e, f, g) + self.__k[i] + words[i]
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + t1) & 0xffffffff
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (t1 + t2) & 0xffffffff
|
||||
|
||||
for i, (x, y) in enumerate(zip(self.__h, [a, b, c, d, e, f, g, h])):
|
||||
self.__h[i] = (x + y) & 0xffffffff
|
||||
|
||||
@staticmethod
|
||||
def maj(a, b, c):
|
||||
"""Static method for the calculation of the maj variable in the pseudo code"""
|
||||
return (a & b) ^ (a & c) ^ (b & c)
|
||||
|
||||
@staticmethod
|
||||
def ch(a, b, c):
|
||||
"""Static method to calculate the ch variable in the pseudo code"""
|
||||
return (a & b) ^ ((~a) & c)
|
243
Client/SMTPClient.py
Normal file
243
Client/SMTPClient.py
Normal file
@ -0,0 +1,243 @@
|
||||
import socket
|
||||
|
||||
import ClientTempMail
|
||||
from ClientThread import ClientThread
|
||||
|
||||
|
||||
class SMTPClient:
|
||||
"""Class to hold the methods needed to process the client side of the script"""
|
||||
def __init__(self, host: str, port: int):
|
||||
self.__host = host
|
||||
self.__port = port
|
||||
|
||||
self.__client_thread: ClientThread or None = None
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Method to check if the child thread is still active"""
|
||||
return self.__client_thread.active
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Method to check if the child thread is still connected"""
|
||||
try:
|
||||
return self.__client_thread.is_connected()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_state(self) -> str:
|
||||
"""Method to return the child threads state"""
|
||||
return self.__client_thread.state
|
||||
|
||||
def get_current_response_content(self) -> any:
|
||||
"""Method to return the client threads last response array"""
|
||||
return self.__client_thread.last_response_content
|
||||
|
||||
def get_current_login_username(self) -> str:
|
||||
"""Method to return the current logged in users username"""
|
||||
return self.__client_thread.login_username
|
||||
|
||||
def get_current_users_name(self) -> str:
|
||||
"""Method to return the current users full name"""
|
||||
return self.__client_thread.get_user_name()
|
||||
|
||||
def get_current_users_email_address(self) -> str:
|
||||
"""Method to return the current users email address"""
|
||||
return self.__client_thread.get_user_email()
|
||||
|
||||
def get_current_temp_mail(self) -> ClientTempMail.ClientTempMail:
|
||||
"""Method to return the current temp mail class object from the child thread"""
|
||||
return self.__client_thread.client_mail_temp
|
||||
|
||||
def current_status(self) -> int:
|
||||
"""Method to return the child threads current status"""
|
||||
return self.__client_thread.current_status
|
||||
|
||||
def get_users_role(self) -> str:
|
||||
"""Method to return the current users role"""
|
||||
return self.__client_thread.get_users_role()
|
||||
|
||||
def start_connection(self) -> bool:
|
||||
"""Method to start the clients connection to the server"""
|
||||
server_address = (self.__host, self.__port)
|
||||
|
||||
current_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
current_socket.setblocking(False)
|
||||
current_socket.connect_ex(server_address)
|
||||
|
||||
result = False
|
||||
try:
|
||||
# Create the child thread for the connection
|
||||
self.__client_thread = ClientThread(current_socket, server_address)
|
||||
self.__client_thread.start()
|
||||
|
||||
self.__client_thread.current_status = 0
|
||||
result = True
|
||||
except OSError as ex:
|
||||
print(f"Error connecting to the server: {repr(ex)}")
|
||||
|
||||
return result
|
||||
|
||||
def close_connection(self) -> None:
|
||||
"""Method to close the client connection"""
|
||||
print("Attempting to end the Connection...")
|
||||
try:
|
||||
self.__client_thread.close_connection()
|
||||
print(f"Connection ended with {self.__host}:{self.__port}")
|
||||
except OSError as ex:
|
||||
print(f"Could not close the connection: {repr(ex)}")
|
||||
finally:
|
||||
self.__client_thread = None
|
||||
|
||||
@staticmethod
|
||||
def email_string_sanitize(input_str: str):
|
||||
"""Static method to sanitize an email address"""
|
||||
# Allowable Characters from !#$%&'*+-/=?^_`{|}~@.
|
||||
special_chars = [33, 35, 36, 37, 38, 39, 42, 43, 45, 46, 47, 61, 63, 64, 94, 95, 96, 123, 124, 125, 126]
|
||||
|
||||
output_str = ""
|
||||
for letter in input_str:
|
||||
if (letter in special_chars) or (48 <= ord(letter) < 58) \
|
||||
or (65 <= ord(letter) < 91) or (97 <= ord(letter) < 122):
|
||||
output_str += letter
|
||||
|
||||
return output_str
|
||||
|
||||
def run_command(self, command: str, options=None) -> bool:
|
||||
"""Method that processes the commands as they're sent"""
|
||||
valid_command = False
|
||||
if command is not None:
|
||||
valid_command = True
|
||||
|
||||
full_command = None
|
||||
# Login command processing
|
||||
if command == "LOGI":
|
||||
if options['username'] is not None:
|
||||
# Clean the username to only accept email safe characters
|
||||
cleaned_username = self.email_string_sanitize(options['username'])
|
||||
else:
|
||||
cleaned_username = ""
|
||||
valid_command = False
|
||||
|
||||
password = options['password']
|
||||
|
||||
# Check if the username and password have been set correctly
|
||||
if len(cleaned_username) > 0 and len(password) > 0:
|
||||
self.__client_thread.login_username = cleaned_username
|
||||
full_command = "LOGI " + cleaned_username + " " + password
|
||||
else:
|
||||
valid_command = False
|
||||
|
||||
# Start the mail command
|
||||
elif command == "MAIL":
|
||||
self.__client_thread.state = "MAIL"
|
||||
# Create a mail object to hold the temp values we send to the server
|
||||
self.__client_thread.client_mail_temp = ClientTempMail.ClientTempMail()
|
||||
self.__client_thread.client_mail_temp.from_address = self.__client_thread.get_user_email
|
||||
# Send the initial MAIL FROM command
|
||||
full_command = "MAIL FROM <" + self.__client_thread.get_user_email() + ">"
|
||||
|
||||
elif command == "RCPT":
|
||||
# Strip any whitespaces to stop it bypassing the len check
|
||||
to_address = options['to_address'].strip()
|
||||
if len(to_address) > 0:
|
||||
# Check if the address is already in the list
|
||||
if to_address not in self.__client_thread.client_mail_temp.to_address_list:
|
||||
# Add the to address to the list and try to process it on the server
|
||||
self.__client_thread.client_mail_temp.to_address_list.append(to_address)
|
||||
full_command = "RCPT TO <" + to_address + ">"
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
elif command == "SUBJ":
|
||||
# Check if the user has entered at least 1 address
|
||||
if len(self.__client_thread.client_mail_temp.to_address_list) > 0:
|
||||
self.__client_thread.state = "SUBJ"
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
elif command == "DATA":
|
||||
# Set the Subject locally because SUBJ is not an actual command in the mail sequence
|
||||
self.__client_thread.client_mail_temp.subject = options['subject']
|
||||
# Start the local data input
|
||||
full_command = "DATA"
|
||||
|
||||
elif command == "LINE":
|
||||
# Send the current data line to the server to append to the body
|
||||
self.__client_thread.queue_command(options['data_line'])
|
||||
return True
|
||||
|
||||
elif command == "SEND":
|
||||
# Finish composing the email and send the end line
|
||||
self.__client_thread.current_status = 0
|
||||
self.__client_thread.process_command(options['data_line'])
|
||||
return True
|
||||
|
||||
# Send the reset command
|
||||
elif command == "RSET":
|
||||
self.__client_thread.current_status = 0
|
||||
self.__client_thread.state = "RSET"
|
||||
full_command = "RSET"
|
||||
|
||||
# Send the view request
|
||||
elif command == "VIEW":
|
||||
if options and options['mail_id']:
|
||||
# Client is trying to quit the view loop
|
||||
if options['mail_id'].upper() == ">QUIT":
|
||||
full_command = "VIEW QUIT"
|
||||
# Client is trying to view the entire mailbox lis
|
||||
elif options['mail_id'].upper() == ">LIST":
|
||||
full_command = "VIEW LIST"
|
||||
else:
|
||||
# Client is trying to view a specific mail
|
||||
full_command = "VIEW MAIL " + options['mail_id']
|
||||
else:
|
||||
# Do the original view list when the view option has been chosen
|
||||
self.__client_thread.state = "VIEW"
|
||||
full_command = "VIEW LIST"
|
||||
|
||||
# Send the logout command
|
||||
elif command == "LOUT":
|
||||
self.__client_thread.state = "LOUT"
|
||||
full_command = "LOUT"
|
||||
|
||||
# send the help command
|
||||
elif command == "HELP":
|
||||
self.__client_thread.state = "HELP"
|
||||
full_command = "HELP"
|
||||
# Check if the user has entered a help section to search
|
||||
if options and options['help_section']:
|
||||
# The client wants to quit
|
||||
if options['help_section'].upper() == ">QUIT":
|
||||
full_command += " QUIT"
|
||||
else:
|
||||
full_command += " " + options['help_section'].upper()
|
||||
|
||||
# Send the ADMN request for admin users
|
||||
elif command == "ADMN":
|
||||
self.__client_thread.state = "ADMN"
|
||||
if options and options['admin_command']:
|
||||
# Do an unlock command to unlock a locked out user
|
||||
if options['admin_command'].upper().startswith(">UNLK"):
|
||||
unlock_command_parts = options['admin_command'].split(" ", 1)
|
||||
full_command = "ADMN UNLK " + unlock_command_parts[1]
|
||||
# Do a server log audit view
|
||||
elif options['admin_command'].upper().startswith(">AUDT"):
|
||||
full_command = "ADMN AUDT"
|
||||
# Quit the admin command menu
|
||||
elif options['admin_command'].upper().startswith(">QUIT"):
|
||||
full_command = "ADMN QUIT"
|
||||
else:
|
||||
# Get the admin home menu
|
||||
full_command = "ADMN HOME"
|
||||
|
||||
# Command does not need special processing
|
||||
else:
|
||||
full_command = command
|
||||
|
||||
if full_command and valid_command:
|
||||
self.__client_thread.process_command(full_command)
|
||||
return True
|
||||
else:
|
||||
return False
|
301
Client/client.py
Normal file
301
Client/client.py
Normal file
@ -0,0 +1,301 @@
|
||||
import SMTPClient
|
||||
import socket
|
||||
import time
|
||||
|
||||
default_host = "127.0.0.1"
|
||||
default_port = 50000
|
||||
host = default_host
|
||||
port = default_port
|
||||
quit_flag = False
|
||||
# Variable to setting to print the title to sections only once
|
||||
current_state_initialised = False
|
||||
|
||||
|
||||
# Handy function to draw the title block for each section when needed
|
||||
def print_title(title: str) -> None:
|
||||
title_bar_char = '-'
|
||||
title_bar = "".join(title_bar_char * (len(title) + 2))
|
||||
print(f"+{title_bar}+\n| {title} |\n+{title_bar}+")
|
||||
|
||||
|
||||
# Handy function for dealing with Quit commands for the script
|
||||
def do_a_quit(message: str) -> bool:
|
||||
print(message)
|
||||
selected_option = input("[Y]es/[N]o: ").strip().upper()
|
||||
|
||||
if selected_option == "Y" or selected_option == "YES":
|
||||
result = False
|
||||
else:
|
||||
# quit the program loop
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# start main program loop
|
||||
if __name__ == "__main__":
|
||||
while not quit_flag:
|
||||
# ask user for a hostname and port number, or leave blank to use the default values
|
||||
print(f"Please enter a valid host address and port: (default: {default_host}:{default_port})")
|
||||
host_input = input("host:port, blank for default:")
|
||||
|
||||
valid_host = True
|
||||
test_host = ""
|
||||
test_port = ""
|
||||
|
||||
# check if a value has been entered or to use the default values
|
||||
if len(host_input.strip()) > 0:
|
||||
# strip any trailing/leading whitespace and split by the colon
|
||||
host_input = host_input.strip()
|
||||
host_parts = host_input.split(':', 1)
|
||||
|
||||
# check if the host_parts array has 2 values, these should be a host ip and port
|
||||
if len(host_parts) == 2:
|
||||
try:
|
||||
# convert the first item in host_parts into a valid IP address if host name is given
|
||||
test_host = socket.gethostbyname(host_parts[0])
|
||||
# check if the second item in host_parts is numeric for the port number
|
||||
if host_parts[1].isnumeric():
|
||||
test_port = int(host_parts[1])
|
||||
else:
|
||||
valid_host = False
|
||||
except socket.gaierror:
|
||||
valid_host = False
|
||||
else:
|
||||
valid_host = False
|
||||
# if no value is given then use the default host and port given at the top
|
||||
else:
|
||||
# ensure the default host and port are assigned to the test values since they do not need to be
|
||||
# checked/sanitized
|
||||
test_host = default_host
|
||||
test_port = default_port
|
||||
# check if the host is still valid
|
||||
if valid_host:
|
||||
# set the actual host and port variables
|
||||
host = test_host
|
||||
port = test_port
|
||||
|
||||
print(f"Connecting to {host}:{port}...")
|
||||
|
||||
quit_input_flag = False
|
||||
# Start the client with the given host and port
|
||||
smtp_client = SMTPClient.SMTPClient(host, port)
|
||||
|
||||
# Attempt to connect 3 times before giving up
|
||||
for i in range(3):
|
||||
if smtp_client.start_connection():
|
||||
break
|
||||
else:
|
||||
print("Connection failed, attempting again in 5 seconds")
|
||||
time.sleep(5)
|
||||
|
||||
if smtp_client.is_connected():
|
||||
# The main client loop
|
||||
while smtp_client.is_active() and not quit_flag:
|
||||
|
||||
# We wait for the current status to be in "write" mode
|
||||
if smtp_client.current_status() == -1:
|
||||
# Print the main menu if we are in the HMEN (home menu)
|
||||
if smtp_client.get_state() == "HMEN":
|
||||
if not current_state_initialised:
|
||||
print_title("SMTP server home menu")
|
||||
print(f"Welcome {smtp_client.get_current_users_name()}.")
|
||||
print(f"Email: {smtp_client.get_current_users_email_address()}")
|
||||
print("[1]: [C]ompose an Email")
|
||||
print("[2]: [V]iew your Mailbox")
|
||||
print("[3]: [H]elp")
|
||||
print("[4]: [L]og Out")
|
||||
print("[5]: [Q]uit")
|
||||
# Secret admin menu option if you are on an Admin account
|
||||
if smtp_client.get_users_role() == "ADMIN":
|
||||
print("[6]: [A]dmin console")
|
||||
current_state_initialised = True
|
||||
|
||||
# If we have any response messages waiting, print them
|
||||
if len(smtp_client.get_current_response_content()) > 0:
|
||||
print("\nLast Status Message:")
|
||||
print("----------------------")
|
||||
for line in smtp_client.get_current_response_content():
|
||||
print(line)
|
||||
|
||||
current_command = input("Option: ")
|
||||
if current_command in ["1", "C", "c"]:
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("MAIL")
|
||||
elif current_command in ["2", "V", "v"]:
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("VIEW")
|
||||
elif current_command in ["3", "H", "h"]:
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("HELP")
|
||||
elif current_command in ["4", "L", "l"]:
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("LOUT")
|
||||
elif current_command in ["5", "Q", "q"]:
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("QUIT")
|
||||
elif current_command in ["6", "A", "a"] and smtp_client.get_users_role() == "ADMIN":
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("ADMN")
|
||||
else:
|
||||
print("Invalid command, try again.")
|
||||
|
||||
# The admin menu allows for some specific Admin commands
|
||||
elif smtp_client.get_state() == "ADMN":
|
||||
print_title("Secret Admin Menu")
|
||||
|
||||
admin_content = smtp_client.get_current_response_content()
|
||||
if len(admin_content) > 0:
|
||||
print("SERVER RESPONDED: ")
|
||||
for admin_line in admin_content:
|
||||
print(admin_line)
|
||||
print("END\n")
|
||||
|
||||
print("Admin Commands:")
|
||||
print(">UNLK 'username' - Unlock a User account.")
|
||||
print(">AUDT - View last 20 server actions")
|
||||
print(">QUIT - Back to the Home menu")
|
||||
|
||||
current_command = input("Option: ")
|
||||
if len(current_command) > 0:
|
||||
smtp_client.run_command("ADMN", {"admin_command": current_command})
|
||||
|
||||
# Login details entry
|
||||
elif smtp_client.get_state() == "LOGI":
|
||||
print_title("Login Details")
|
||||
print("Please enter your Login details:")
|
||||
username = input("Username: ")
|
||||
password = input("Password: ")
|
||||
login_details = {"username": username, "password": password}
|
||||
if not smtp_client.run_command("LOGI", login_details):
|
||||
if do_a_quit("You did not enter a valid username and/or password, try again?"):
|
||||
# send the quit command to the server
|
||||
smtp_client.run_command("QUIT")
|
||||
quit_flag = True
|
||||
|
||||
# Recipient email address entry, this continues til an empty value is entered
|
||||
elif smtp_client.get_state() == "RCPT":
|
||||
if not current_state_initialised:
|
||||
print_title("Composing an Email: Recipients")
|
||||
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||
print("Input a recipient Email address and press enter, "
|
||||
"when you are done press enter again.")
|
||||
current_state_initialised = True
|
||||
|
||||
current_recp_address = input("TO: ")
|
||||
if len(current_recp_address) > 0:
|
||||
# Quit the compose email loop and send the RSET command
|
||||
if current_recp_address.upper() == ">QUIT":
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("RSET")
|
||||
else:
|
||||
if not smtp_client.run_command("RCPT", {'to_address': current_recp_address}):
|
||||
print("You must enter a unique recipient address.")
|
||||
else:
|
||||
# Continue to the Subject entry state if blank
|
||||
if smtp_client.run_command("SUBJ"):
|
||||
current_state_initialised = False
|
||||
else:
|
||||
# If a false was returned then they did not enter at least 1 address
|
||||
print("You must have at least 1 recipient")
|
||||
|
||||
# Subject entery state, its not an official state since there is no real "SUBJ" command
|
||||
elif smtp_client.get_state() == "SUBJ":
|
||||
if not current_state_initialised:
|
||||
print_title("Composing an Email: Subject")
|
||||
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||
print("Enter a Subject for this Email")
|
||||
current_state_initialised = True
|
||||
|
||||
current_subject = input("SUBJECT: ")
|
||||
# Quit the compose email loop and send the RSET command
|
||||
if current_subject.upper() == ">QUIT":
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("RSET")
|
||||
else:
|
||||
if len(current_subject) == 0:
|
||||
current_subject = "No Subject"
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("DATA", {"subject": current_subject})
|
||||
|
||||
# Enter the actual email body data entry mode, this will not end until you >QUIT or >SEND
|
||||
elif smtp_client.get_state() == "DATA":
|
||||
if not current_state_initialised:
|
||||
print_title("Composing an Email: Body")
|
||||
print("Input \">QUIT\" at any point to quit back to the menu.")
|
||||
print("Input your email text, press enter to start a new line,")
|
||||
print("Input \">SEND\" when you have finished your Email.")
|
||||
current_state_initialised = True
|
||||
|
||||
current_mail_line = input("DATA: ")
|
||||
# Quit the compose email loop and send the RSET command
|
||||
if current_mail_line.upper() == ">QUIT":
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("RSET")
|
||||
# Sends the \n.\n line to let the server know our data entry has ended
|
||||
elif current_mail_line.upper() == ">SEND":
|
||||
current_state_initialised = False
|
||||
smtp_client.run_command("SEND", {'data_line': "\n.\n"})
|
||||
else:
|
||||
# Each line is sent individually when enter is pressed and stored on the server
|
||||
smtp_client.run_command("LINE", {'data_line': current_mail_line})
|
||||
|
||||
# Output the mailbox or actual mail response from the server when in the VIEW state
|
||||
elif smtp_client.get_state() == "VIEW":
|
||||
print_title("Viewing Mail")
|
||||
view_content = smtp_client.get_current_response_content()
|
||||
for view_line in view_content:
|
||||
print(view_line)
|
||||
print("End of request, Enter a mail id, >LIST to view latest emails"
|
||||
" or >QUIT to return to the menu.")
|
||||
|
||||
view_command = input("VIEW ")
|
||||
smtp_client.run_command("VIEW", {"mail_id": view_command})
|
||||
|
||||
# Output the help response when the client is in the HELP state
|
||||
elif smtp_client.get_state() == "HELP":
|
||||
print_title("SMTP Server Help")
|
||||
help_content = smtp_client.get_current_response_content()
|
||||
for help_line in help_content:
|
||||
print(help_line)
|
||||
print("End of file, Enter a help subsection or >QUIT to return to the menu.")
|
||||
|
||||
help_command = input("HELP ")
|
||||
smtp_client.run_command("HELP", {"help_section": help_command})
|
||||
|
||||
# QUIT should have already quit but if not we just pass
|
||||
elif smtp_client.get_state() == "QUIT":
|
||||
pass
|
||||
|
||||
# Catch all for any commands, if we end here we've done something wrong
|
||||
# Kept here to help debug if we do actually run into an issue
|
||||
else:
|
||||
current_command = input(f"{smtp_client.get_state()}: ")
|
||||
smtp_client.run_command(current_command)
|
||||
|
||||
# If the client loses its active flag then the connection is no longer connected
|
||||
print("Connection no longer active, exiting")
|
||||
quit_flag = True
|
||||
else:
|
||||
if not do_a_quit("The connection could not be made, would you like to try another host?"):
|
||||
# reset the host and port values to the defaults
|
||||
host = default_host
|
||||
port = default_port
|
||||
give_up_flag = ""
|
||||
else:
|
||||
# quit the program loop
|
||||
quit_flag = True
|
||||
|
||||
# the host given was invalid, ask if the user would like to try again
|
||||
else:
|
||||
if not do_a_quit("You did not enter a valid host, try again?"):
|
||||
# reset the host and port values to the defaults
|
||||
host = default_host
|
||||
port = default_port
|
||||
give_up_flag = ""
|
||||
else:
|
||||
# quit the program loop
|
||||
quit_flag = True
|
||||
|
||||
# exit the program and say goodbye
|
||||
print("Program ending, thank you")
|
284
Server/Crypto.py
Normal file
284
Server/Crypto.py
Normal file
@ -0,0 +1,284 @@
|
||||
# System Imports
|
||||
import math
|
||||
import random
|
||||
|
||||
# Project Imports
|
||||
import SHA256Custom
|
||||
|
||||
|
||||
class Receiver:
|
||||
"""Class for dealing with the receiving end of the custom simple RSA encryption"""
|
||||
def __init__(self):
|
||||
# A list of small prime numbers we can use to calculate a larger one
|
||||
self.__small_primes_list = [211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293,
|
||||
307, 311, 313, 317, 331, 337, 347, 349]
|
||||
|
||||
self.__p = None
|
||||
self.__q = None
|
||||
self.__n = None
|
||||
self.__t = None
|
||||
|
||||
# This is apparently the most commonly used shared co-prime used for RSA
|
||||
self.__e = 65537
|
||||
self.__d = None
|
||||
|
||||
# The generated public and private keys
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.generated = False
|
||||
|
||||
# The bit size of the key
|
||||
self.__bit_size = 1024
|
||||
|
||||
def generate_keys(self) -> bool:
|
||||
"""Method to generate the RSA Public and Private keys for encryption"""
|
||||
try:
|
||||
# Only generate keys if we haven't already
|
||||
if not self.generated:
|
||||
self.__p = self.__generate_prime_number()
|
||||
self.__q = self.__generate_prime_number()
|
||||
|
||||
# Make sure we have 2 different prime numbers
|
||||
while self.__q == self.__p:
|
||||
self.__q = self.__generate_prime_number()
|
||||
|
||||
# Most of the following is just the pseudo code for RSA prime generation
|
||||
self.__n = self.__p * self.__q
|
||||
self.__t = (self.__p - 1) * (self.__q - 1)
|
||||
|
||||
while math.gcd(self.__t, self.__e) > 1:
|
||||
self.__e += 2
|
||||
|
||||
if self.__e < 1 or self.__e > self.__t:
|
||||
raise Exception("E must be > 1 and < T")
|
||||
|
||||
if self.__t % self.__e == 0:
|
||||
raise Exception("E is not a co-prime with T")
|
||||
|
||||
self.__d = Receiver.find_inverse(self.__e, self.__t)
|
||||
|
||||
# We have our keys, set up the tuples and flag the generated as true
|
||||
self.__public_key = (self.__e, self.__n)
|
||||
self.__private_key = (self.__d, self.__n)
|
||||
self.generated = True
|
||||
|
||||
return True
|
||||
else:
|
||||
raise Exception("Keys have already been generated.")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_expected_block_size(self) -> int:
|
||||
"""Method to return the expected block size for the encrypted data"""
|
||||
# the encrypted request blocksize seems to be half of the bit size
|
||||
return int(self.__bit_size / 2)
|
||||
|
||||
def get_public_key_pair(self, hex_result=True):
|
||||
"""Method for returning the public key as a hex string or tuple depending on the hex_results"""
|
||||
if hex_result:
|
||||
# Return both public key elements as a hex string
|
||||
return "{0:x} {1:x}".format(self.__public_key[0], self.__public_key[1])
|
||||
else:
|
||||
return self.__public_key
|
||||
|
||||
def get_private_key_pair(self, hex_result=True):
|
||||
"""Method for returning the private key as a hex string or tuple, not really used"""
|
||||
if hex_result:
|
||||
# Return both private key elements as a hex string
|
||||
return "{0:x} {1:x}".format(self.__private_key[0], self.__private_key[1])
|
||||
else:
|
||||
return self.__private_key
|
||||
|
||||
def decrypt_string(self, string_in: str) -> str:
|
||||
"""Method for decrypting our custom RSA encrypted blocks, takes the hex string in"""
|
||||
if self.generated:
|
||||
# Convert the hex string back into an int
|
||||
encrypted_string_as_int = int(string_in, 16)
|
||||
# Do the math on our encrypted int
|
||||
decrypted_string_as_int = pow(encrypted_string_as_int, self.__d, self.__n)
|
||||
# Convert the result back into a hex string
|
||||
decrypted_string_as_hex = "{0:x}".format(decrypted_string_as_int)
|
||||
|
||||
start_padding_included = False
|
||||
plain_text_read = False
|
||||
|
||||
plain_text_string = ''
|
||||
hashed_checksum = ''
|
||||
|
||||
# Loop for every 2 characters (hex)
|
||||
for i in range(0, len(decrypted_string_as_hex), 2):
|
||||
current_byte = decrypted_string_as_hex[i:i+2]
|
||||
# the encrypted string should start with a padding FF value
|
||||
if not start_padding_included and current_byte.upper() == "FF":
|
||||
start_padding_included = True
|
||||
# the Checksum hash should start after the 00 value
|
||||
elif not plain_text_read and start_padding_included and current_byte == "00":
|
||||
plain_text_read = True
|
||||
# Everything else should be the message or the checksum hash
|
||||
elif start_padding_included:
|
||||
if not plain_text_read:
|
||||
# The message is padded after the fact to fill up the bit size, these are ignored
|
||||
if current_byte.upper() != "FF":
|
||||
# Convert the hex value back into a utf character
|
||||
plain_text_string += chr(int(current_byte, 16))
|
||||
else:
|
||||
# Reconstruct the hash checksum
|
||||
hashed_checksum += current_byte
|
||||
|
||||
# If the text of hash is missing then something went wrong with encrypting or sending
|
||||
if len(plain_text_string) == 0 or len(hashed_checksum) == 0:
|
||||
raise Exception("No text was decrypted, something must be wrong...")
|
||||
|
||||
# Hash the plain text string and make sure it matches our checksum hash
|
||||
hash_test = SHA256Custom.SHA256Custom()
|
||||
hash_test.update(plain_text_string.encode())
|
||||
hashed_plain_text = hash_test.hexdigest()
|
||||
|
||||
if hashed_plain_text == hashed_checksum:
|
||||
# The message has not been altered, decrypted successfully
|
||||
return plain_text_string
|
||||
else:
|
||||
raise Exception("Plain text did not match the Hashed Checksum, something is wrong.")
|
||||
|
||||
else:
|
||||
raise Exception("No key has been generated")
|
||||
|
||||
def __generate_prime_number(self) -> int:
|
||||
"""Private method for generating prime numbers with the bit size set"""
|
||||
while True:
|
||||
current_prime_candidate = self.__generate_low_level_prime(self.__bit_size)
|
||||
if not self.miller_rabin_prime_check(current_prime_candidate):
|
||||
continue
|
||||
else:
|
||||
return current_prime_candidate
|
||||
|
||||
def __generate_low_level_prime(self, prime_candidate: int) -> int:
|
||||
"""Private method for generating the prime number using the candidate number and the small prime array"""
|
||||
while True:
|
||||
current_prime_candidate = self.generate_random_large_int(prime_candidate)
|
||||
|
||||
# We try to generate a large prime number using the candidate and looping through the small primes
|
||||
# array until we get one
|
||||
for divisor in self.__small_primes_list:
|
||||
if current_prime_candidate % divisor == 0 and divisor ** 2 <= current_prime_candidate:
|
||||
break
|
||||
else:
|
||||
return current_prime_candidate
|
||||
|
||||
@staticmethod
|
||||
def generate_random_large_int(n_bit_size: int) -> int:
|
||||
"""Static method to generate a large int value"""
|
||||
# Get a random number in a range of the bit size given
|
||||
return random.randrange(2 ** (n_bit_size - 1) + 1, 2 ** n_bit_size - 1)
|
||||
|
||||
# Method to test that the prime number we generate passes the miller rabin prime candidate test
|
||||
@staticmethod
|
||||
def miller_rabin_prime_check(prime_candidate: int) -> bool:
|
||||
"""Static method for the miller rabin prime number checking algorithm, to ensure that we have a valid prime"""
|
||||
max_divisions_by_two = 0
|
||||
prime_minus_one = prime_candidate - 1
|
||||
while prime_minus_one % 2 == 0:
|
||||
prime_minus_one >>= 1
|
||||
max_divisions_by_two += 1
|
||||
|
||||
assert 2 ** max_divisions_by_two * prime_minus_one == prime_candidate - 1
|
||||
|
||||
def trial_composite(round_tester_in):
|
||||
"""Sub method for doing the actual prime number factorisation check"""
|
||||
if pow(round_tester_in, prime_minus_one, prime_candidate) == 1:
|
||||
return False
|
||||
|
||||
for x in range(max_divisions_by_two):
|
||||
if pow(round_tester_in, 2 ** x * prime_minus_one, prime_candidate) == prime_candidate - 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for i in range(0, 20):
|
||||
round_tester = random.randrange(2, prime_candidate)
|
||||
if trial_composite(round_tester):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def extended_euclidean_algorithm(a: int, b: int) -> tuple:
|
||||
"""Static method for the EEA from the pseudo code"""
|
||||
if b == 0:
|
||||
return 1, 0
|
||||
|
||||
q = a // b
|
||||
r = a % b
|
||||
s, t = Receiver.extended_euclidean_algorithm(b, r)
|
||||
return t, s-(q*t)
|
||||
|
||||
@staticmethod
|
||||
def find_inverse(a: int, b: int) -> int:
|
||||
"""Static method for finding the inverse of the primes given"""
|
||||
inverse = Receiver.extended_euclidean_algorithm(a, b)[0]
|
||||
if inverse < 1:
|
||||
inverse += b
|
||||
|
||||
return inverse
|
||||
|
||||
|
||||
class Transmitter:
|
||||
"""Class for dealing with the transmitting end of the custom simple RSA encryption"""
|
||||
def __init__(self, public_key: str):
|
||||
# We should be getting the string hex values from the receiver and splitting them into the actual parts
|
||||
public_key_parts = public_key.split(" ", 1)
|
||||
|
||||
# Must match the Receiver bit size
|
||||
self.__bit_size = 1024
|
||||
|
||||
# Check if the public key has 2 parts to be valid
|
||||
if len(public_key_parts) == 2:
|
||||
self.__e = int(public_key_parts[0], 16)
|
||||
self.__n = int(public_key_parts[1], 16)
|
||||
self.__has_key = True
|
||||
else:
|
||||
self.__has_key = False
|
||||
raise Exception("Public key was incorrect")
|
||||
|
||||
def get_current_key_pair(self):
|
||||
"""Method for getting the key pair values e and n"""
|
||||
return self.__e, self.__n
|
||||
|
||||
def get_usable_byte_length(self) -> int:
|
||||
"""Method for getting the usable byte length incase we need to split a request into multiple parts"""
|
||||
# Im not entirely sure on this as it was calculated mostly by "brute force" but it seems to be the
|
||||
# key size divided by 4
|
||||
# minus 2 bytes for the start and hash break bytes added
|
||||
# minus 32 for the SHA256 hash size
|
||||
# minus 32 for the RSA byte overhead, i've tried different values but getting too close to 256 bytes will
|
||||
# sometimes case issues which i think is down to
|
||||
return int(self.__bit_size / 4) - 2 - 32 - 32
|
||||
|
||||
def encrypt_string(self, string_in: bytes) -> bytes:
|
||||
"""Method for encrypting a string using the public RSA key provided by the receiver"""
|
||||
if self.__has_key:
|
||||
# Create a hash of the message given for our checksum
|
||||
string_hashing = SHA256Custom.SHA256Custom()
|
||||
string_hashing.update(string_in)
|
||||
|
||||
string_hashed = string_hashing.hexdigest()
|
||||
|
||||
# Pad to make the string the full size of the custom RSA block
|
||||
padding = ""
|
||||
if len(string_in) < self.get_usable_byte_length():
|
||||
padding_required = self.get_usable_byte_length() - len(string_in) - 1
|
||||
padding = "ff" * padding_required
|
||||
|
||||
# Add an FF pad at the start of the string, since if the input hex starts with a 0X value the 0 is dropped
|
||||
# when it is converted to an int, im not entirely sure how they get around this
|
||||
hex_string = "ff" + string_in.hex() + padding + "00" + string_hashed
|
||||
string_as_int = int(hex_string, 16)
|
||||
|
||||
# Do the math to the int value
|
||||
encrypted_string_as_int = pow(string_as_int, self.__e, self.__n)
|
||||
# Convert the int value into a hex string
|
||||
encrypted_string_hex = "{0:x}".format(encrypted_string_as_int)
|
||||
|
||||
return encrypted_string_hex.encode()
|
||||
else:
|
||||
raise Exception("No key has been generated")
|
188
Server/DBConn.py
Normal file
188
Server/DBConn.py
Normal file
@ -0,0 +1,188 @@
|
||||
import sqlite3
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
# added pairing type, mostly to stop it complaining about line length, only works on 3.9
|
||||
# ParamPair = list[dict[str, any]]
|
||||
|
||||
|
||||
def get_unix_timestamp(seconds_offset=0) -> int:
|
||||
"""
|
||||
Function to get the current unix timestamp to sim the NOW() sql method, seconds offset will pull a
|
||||
past of future timestamp
|
||||
"""
|
||||
return int(time.time()) + seconds_offset
|
||||
|
||||
|
||||
class DBConn:
|
||||
"""Class to hold our methods for dealing with the SQLite3 Database"""
|
||||
# start the database, which we would usually have a config file with the host, port, username etc
|
||||
# but since sqlite just uses a file i've hard coded it
|
||||
def __init__(self):
|
||||
# Apparently outside of the pycharm env we need to tell it what DIR the DB is in, it should be in the server
|
||||
# folder
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
db = os.path.join(path, 'server.db')
|
||||
|
||||
self.__db_conn = sqlite3.connect(db)
|
||||
# set the row factory, to simulate the fetch_assoc rows other DB's use
|
||||
self.__db_conn.row_factory = sqlite3.Row
|
||||
self.__db_cursor = self.__db_conn.cursor()
|
||||
|
||||
def do_generic(self, query: str, return_values=False) -> any:
|
||||
"""method for doing a generic SQL query, optionally return the result"""
|
||||
result = False
|
||||
if len(query) > 0:
|
||||
try:
|
||||
current_query = self.__db_cursor.execute(query)
|
||||
if return_values:
|
||||
result = current_query.fetchall()
|
||||
else:
|
||||
result = True
|
||||
except Exception as ex:
|
||||
print(f"Generic query Error: {repr(ex)}")
|
||||
|
||||
return result
|
||||
|
||||
def do_insert(self, table: str, insert_pairs, return_id=False):
|
||||
"""Method for doing a generic insert query given the value pairs and table, we can return the insert id too"""
|
||||
result = False
|
||||
# Check if the table and insert values are not empty
|
||||
if len(table) > 0 and len(insert_pairs) > 0:
|
||||
fields = []
|
||||
params = []
|
||||
parsed_values = {}
|
||||
# Using prepared statements so we dont get SQL injected
|
||||
for pair in insert_pairs:
|
||||
fields.append(pair["field"])
|
||||
params.append(':' + pair["field"])
|
||||
parsed_values[pair["field"]] = pair["value"]
|
||||
|
||||
# Construct the insert query with the input values
|
||||
query_fields = ','.join(fields)
|
||||
query_values = ','.join(params)
|
||||
insert_query = "INSERT INTO {} ({}) VALUES ({})".format(table, query_fields, query_values)
|
||||
|
||||
try:
|
||||
if self.__db_cursor.execute(insert_query, parsed_values):
|
||||
self.__db_conn.commit()
|
||||
if return_id:
|
||||
# Return the last row id if requested, otherwise return True
|
||||
result = self.__db_cursor.lastrowid
|
||||
else:
|
||||
result = True
|
||||
except Exception as ex:
|
||||
print(f"Insert query Error: {repr(ex)}")
|
||||
|
||||
return result
|
||||
|
||||
def do_update(self, table: str, update_pairs, where: str, params) -> bool:
|
||||
"""Method for doing a generic update query"""
|
||||
result = False
|
||||
if len(table) > 0 and len(update_pairs) > 0:
|
||||
field_params = []
|
||||
parsed_values = {}
|
||||
# Setup the Update field/value pairs
|
||||
for pair in update_pairs:
|
||||
field_params.append("{} = {}".format(pair["field"], ':' + pair["field"]))
|
||||
parsed_values[pair["field"]] = pair["value"]
|
||||
|
||||
# Add the values to the parameters
|
||||
for param in params:
|
||||
parsed_values[param["field"]] = param["value"]
|
||||
|
||||
# Construct the update query
|
||||
query_fields = ','.join(field_params)
|
||||
insert_query = "UPDATE {} SET {} WHERE {}".format(table, query_fields, where)
|
||||
|
||||
try:
|
||||
if self.__db_cursor.execute(insert_query, parsed_values):
|
||||
self.__db_conn.commit()
|
||||
result = True
|
||||
except Exception as ex:
|
||||
print(f"Update query Error: {repr(ex)}")
|
||||
|
||||
return result
|
||||
|
||||
def do_select(self, table: str, fetch_type="all", fields="", search="", params=None, query_extra=""):
|
||||
"""Method for doing a generic select query"""
|
||||
result = None
|
||||
if len(table) > 0:
|
||||
query_fields = "*"
|
||||
query_block = ""
|
||||
if len(fields) > 0:
|
||||
# Check if the fields variable is a string or a list array
|
||||
if isinstance(fields, str):
|
||||
query_fields = fields
|
||||
if isinstance(fields, list):
|
||||
query_fields = ','.join(fields)
|
||||
|
||||
# Check if we have actual search parameters
|
||||
if len(search) > 0:
|
||||
query_block = " WHERE " + search
|
||||
|
||||
select_query = "SELECT {} FROM {}{} {}".format(query_fields, table, query_block, query_extra)
|
||||
|
||||
parsed_params = {}
|
||||
for param in params:
|
||||
parsed_params[param["field"]] = param["value"]
|
||||
|
||||
try:
|
||||
db_query = self.__db_conn.execute(select_query, parsed_params)
|
||||
# Return All, the Row or Column depending on the fetchtype passed in
|
||||
if fetch_type == "all":
|
||||
result = db_query.fetchall()
|
||||
elif fetch_type == "row":
|
||||
result = db_query.fetchone()
|
||||
elif fetch_type == "col":
|
||||
result = db_query.fetchone()[0]
|
||||
except Exception as ex:
|
||||
print(f"Select query Error: {repr(ex)}")
|
||||
|
||||
return result
|
||||
|
||||
def get_system_setting(self, key: str):
|
||||
"""Method for getting a global system setting"""
|
||||
setting_value = self.do_select("system_settings", "col", "setting_value", "setting_name = :setting_name",
|
||||
[
|
||||
{
|
||||
"field": "setting_name",
|
||||
"value": key
|
||||
}
|
||||
])
|
||||
|
||||
return setting_value
|
||||
|
||||
def set_system_setting(self, key: str, value) -> bool:
|
||||
"""Method for setting a system setting"""
|
||||
result = False
|
||||
# Check if the setting exists, i would normally just use a INSERT ON DUPLICATE KEY UPDATE query, but it doesnt
|
||||
# look like SQLite supports it
|
||||
value_exists = self.do_select("system_settings", "col", "COUNT(*)", "setting_name = :setting_name",
|
||||
[
|
||||
{
|
||||
"field": "setting_name",
|
||||
"value": key
|
||||
}
|
||||
])
|
||||
|
||||
# If the key exists then update other wise insert it
|
||||
if value_exists and value_exists > 0:
|
||||
if self.do_update("system_settings",
|
||||
[{"field": "setting_value", "value": value}],
|
||||
"setting_name = :setting_name",
|
||||
[{"field": "setting_name", "value": key}]):
|
||||
result = True
|
||||
else:
|
||||
if self.do_insert("system_settings",
|
||||
[{"field": "setting_name", "value": key},
|
||||
{"field": "setting_value", "value": value}]):
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def __del__(self):
|
||||
"""Close the connection and cursor when the object is removed"""
|
||||
self.__db_cursor.close()
|
||||
self.__db_conn.close()
|
120
Server/Mail.py
Normal file
120
Server/Mail.py
Normal file
@ -0,0 +1,120 @@
|
||||
from datetime import datetime
|
||||
|
||||
import RawMail
|
||||
import User
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class Mail:
|
||||
"""Class for holding the data in the mail table of the database"""
|
||||
def __init__(self):
|
||||
self.__mail_id = None
|
||||
self.__raw_id = None
|
||||
self.__from_id = None
|
||||
self.__to_id = None
|
||||
self.__mail_subject = None
|
||||
self.__mail_body = None
|
||||
self.__mail_read = None
|
||||
self.__mail_date_sent = None
|
||||
|
||||
# Holding variables for the from, to and raw mail class objects
|
||||
self.__from_user: User.User or None = None
|
||||
self.__to_user: User.User or None = None
|
||||
self.__raw_mail: RawMail.RawMail or None = None
|
||||
|
||||
self.__is_valid = False
|
||||
|
||||
def load_mail(self, mail_id: int):
|
||||
"""Method to load the mail with the id given"""
|
||||
self.__mail_id = mail_id
|
||||
self.__load_mail()
|
||||
|
||||
def __load_mail(self) -> None:
|
||||
"""Private method to load the mail from the database using the mail_id"""
|
||||
db_connection = DBConn()
|
||||
mail_details = db_connection.do_select("mail_store", "row", "*", "mail_id = :mail_id",
|
||||
[
|
||||
{
|
||||
"field": "mail_id",
|
||||
"value": self.__mail_id
|
||||
}
|
||||
])
|
||||
|
||||
if mail_details is not None and len(mail_details) > 0:
|
||||
self.__mail_id = mail_details['mail_id']
|
||||
self.__raw_id = mail_details['raw_id']
|
||||
self.__from_id = mail_details['from_id']
|
||||
self.__to_id = mail_details['to_id']
|
||||
self.__mail_subject = mail_details['mail_subject']
|
||||
self.__mail_body = mail_details['mail_body']
|
||||
self.__mail_read = mail_details['mail_read']
|
||||
self.__mail_date_sent = mail_details['mail_date_sent']
|
||||
self.__is_valid = True
|
||||
|
||||
def mail_is_valid(self):
|
||||
"""Method to check if we have loaded a valid mail from the DB"""
|
||||
return self.__is_valid
|
||||
|
||||
def get_mail_id(self):
|
||||
"""Method to return the mail id"""
|
||||
return self.__mail_id
|
||||
|
||||
def get_subject(self, trim_len: int = 0):
|
||||
"""Method to return the subject line, if given a number it will truncate the string to that len"""
|
||||
if trim_len > 0:
|
||||
return self.__mail_subject[0:trim_len]
|
||||
else:
|
||||
return self.__mail_subject
|
||||
|
||||
def get_date_sent(self, format_string: str = "%d/%m/%y %H:%M"):
|
||||
"""Method to get the sent date with the given date format"""
|
||||
date_time_string = datetime.utcfromtimestamp(self.__mail_date_sent).strftime(format_string)
|
||||
return date_time_string
|
||||
|
||||
def get_from_user(self) -> User.User or None:
|
||||
"""Method to load a user object into the from user variable"""
|
||||
if self.__from_id:
|
||||
# Preload the user into the object if not loaded beforehand
|
||||
if not self.__from_user:
|
||||
self.__from_user = User.User(self.__from_id)
|
||||
return self.__from_user
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_to_user(self) -> User.User or None:
|
||||
"""Method to load a user object into the to user variable"""
|
||||
if self.__to_id:
|
||||
# Preload the user into the object if not loaded beforehand
|
||||
if not self.__to_user:
|
||||
self.__to_user = User.User(self.__to_id)
|
||||
return self.__to_user
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_raw_mail(self):
|
||||
"""Method to load a raw mail object into the variable"""
|
||||
if not self.__raw_mail:
|
||||
self.__raw_mail = RawMail.RawMail()
|
||||
self.__raw_mail.load_raw_mail(self.__raw_id)
|
||||
return self.__raw_mail
|
||||
|
||||
def create_mail(self, raw_id: int, from_id: int, to_id: int, subject: str, body: str) -> bool:
|
||||
"""Method to add the mail to the database"""
|
||||
result = False
|
||||
if not self.__is_valid:
|
||||
if raw_id and from_id and to_id and len(subject) > 0 or len(body) > 0:
|
||||
db_connection = DBConn()
|
||||
self.__mail_id = db_connection.do_insert("mail_store",
|
||||
[{"field": "raw_id", "value": raw_id},
|
||||
{"field": "from_id", "value": from_id},
|
||||
{"field": "to_id", "value": to_id},
|
||||
{"field": "mail_subject", "value": subject},
|
||||
{"field": "mail_body", "value": body},
|
||||
{"field": "mail_read", "value": 0},
|
||||
{"field": "mail_date_sent", "value": get_unix_timestamp()}],
|
||||
True)
|
||||
if self.__mail_id:
|
||||
self.__load_mail()
|
||||
result = True
|
||||
|
||||
return result
|
135
Server/RawMail.py
Normal file
135
Server/RawMail.py
Normal file
@ -0,0 +1,135 @@
|
||||
import User
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class RawMail:
|
||||
"""Class for holding the raw mail variables whilst they are being added by a client"""
|
||||
|
||||
def __init__(self):
|
||||
self.__raw_id = None
|
||||
self.from_id = None
|
||||
self.__raw_mail = None
|
||||
self.__raw_date_sent = None
|
||||
|
||||
# Temp fields for holding the actual mail content, these can be parsed back from the raw_mail variable
|
||||
self.from_address = None
|
||||
self.to_address_list = []
|
||||
self.subject = None
|
||||
self.text_body = None
|
||||
|
||||
self.__from_user = None
|
||||
|
||||
self.__is_valid = False
|
||||
|
||||
def get_raw_id(self):
|
||||
"""Method to return the raw id"""
|
||||
return self.__raw_id
|
||||
|
||||
def get_raw_mail_content(self):
|
||||
"""Method to get the raw mail body contents"""
|
||||
return self.__raw_mail
|
||||
|
||||
def load_raw_mail(self, raw_id: int):
|
||||
"""Method for loading a raw mail from its id"""
|
||||
self.__raw_id = raw_id
|
||||
self.__load_raw_mail()
|
||||
|
||||
def __load_raw_mail(self) -> None:
|
||||
"""Private method to load the raw mail from the database"""
|
||||
db_connection = DBConn()
|
||||
raw_mail_details = db_connection.do_select("raw_mail", "row", "*", "raw_id = :raw_id",
|
||||
[
|
||||
{
|
||||
"field": "raw_id",
|
||||
"value": self.__raw_id
|
||||
}
|
||||
])
|
||||
|
||||
if raw_mail_details is not None and len(raw_mail_details) > 0:
|
||||
self.__raw_id = raw_mail_details['raw_id']
|
||||
self.from_id = raw_mail_details['from_id']
|
||||
self.__raw_mail = raw_mail_details['raw_mail']
|
||||
self.__raw_date_sent = raw_mail_details['raw_date_sent']
|
||||
self.__is_valid = True
|
||||
|
||||
def raw_mail_is_valid(self):
|
||||
"""Method to check if a valid raw mail has been loaded from the database"""
|
||||
return self.__is_valid
|
||||
|
||||
def get_from_user(self) -> User.User or None:
|
||||
"""Method to fill the from user variable with a user object"""
|
||||
if self.from_id:
|
||||
# Preload the user into the object if not loaded beforehand
|
||||
if not self.__from_user:
|
||||
self.__from_user = User.User(self.from_id)
|
||||
return self.__from_user
|
||||
else:
|
||||
return None
|
||||
|
||||
def strip_headers_from_body(self) -> str:
|
||||
"""Method for stripping the header lines from the mail body for storing in the database mail_store"""
|
||||
body_lines = self.text_body.split("\n")
|
||||
body_text = ""
|
||||
|
||||
# There should be 4 "header" lines, but we dont just want to accept all lines that start with a header as
|
||||
# the user could have entered one into the body of the email. Its not ideal but text parsers rarely are...
|
||||
header_count = 0
|
||||
for line in body_lines:
|
||||
if (line.startswith("From: ") or line.startswith("Date: ") or line.startswith("To: ")) and header_count < 4:
|
||||
header_count += 1
|
||||
elif line.startswith("Subject: ") and header_count < 4:
|
||||
header_count += 1
|
||||
# Set the subject since that is only set in the data state with the body
|
||||
if not self.subject:
|
||||
self.subject = line.replace("Subject: ", "", 1)
|
||||
else:
|
||||
# Anything else should be the actual email content
|
||||
body_text += line + "\n"
|
||||
|
||||
return body_text
|
||||
|
||||
def parse_raw_mail(self):
|
||||
"""Method for parsing a raw mail header lines to extract the from, to, date and subject values"""
|
||||
email_lines = self.__raw_mail.split("\n")
|
||||
self.text_body = ""
|
||||
|
||||
# There should be 4 "header" lines, but we dont just want to accept all lines that start with a header as
|
||||
# the user could have entered one into the body of the email. Its not ideal but text parsers rarely are...
|
||||
header_count = 0
|
||||
for line in email_lines:
|
||||
if line.startswith("From: ") and header_count < 4:
|
||||
# From user id is already stored in the actual raw email record
|
||||
header_count += 1
|
||||
pass
|
||||
elif line.startswith("Date: ") and header_count < 4:
|
||||
# The date is stored in the actual table so no need to text parse it
|
||||
header_count += 1
|
||||
pass
|
||||
elif line.startswith("To: ") and header_count < 4:
|
||||
header_count += 1
|
||||
if len(self.to_address_list) == 0:
|
||||
addresses_only = line.replace("To: ", "", 1)
|
||||
self.to_address_list = addresses_only.split(",")
|
||||
elif line.startswith("Subject: ") and header_count < 4:
|
||||
header_count += 1
|
||||
if not self.subject:
|
||||
self.subject = line.replace("Subject: ", "", 1)
|
||||
else:
|
||||
self.text_body += line + "\n"
|
||||
|
||||
def create_raw_mail(self, from_id: int, raw: str) -> bool:
|
||||
"""Method for saving the raw mail into the database"""
|
||||
result = False
|
||||
if not self.__is_valid:
|
||||
if from_id and len(raw) > 0:
|
||||
db_connection = DBConn()
|
||||
self.__raw_id = db_connection.do_insert("raw_mail",
|
||||
[{"field": "from_id", "value": from_id},
|
||||
{"field": "raw_mail", "value": raw},
|
||||
{"field": "raw_date_sent", "value": get_unix_timestamp()}],
|
||||
True)
|
||||
if self.__raw_id:
|
||||
self.__load_raw_mail()
|
||||
result = True
|
||||
|
||||
return result
|
126
Server/SHA256Custom.py
Normal file
126
Server/SHA256Custom.py
Normal file
@ -0,0 +1,126 @@
|
||||
class SHA256Custom:
|
||||
"""
|
||||
Custom SHA256 function, its not really custom but rewritten using the pseudocode from wikipedia and
|
||||
should mimic the built in function
|
||||
"""
|
||||
def __init__(self, data=None):
|
||||
# Setting up the initial bit values, i wont pretend to understand the math involved but these seem hardcoded
|
||||
self.__ks = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||
|
||||
self.__hs = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||
|
||||
# Copy the initial bit values for use since they are changed
|
||||
self.__h = self.__hs[:]
|
||||
self.__k = self.__ks[:]
|
||||
|
||||
# Create variables we will need in the class
|
||||
self.__string_byte_len = 0
|
||||
self.__buffer = b''
|
||||
self.__string_hashed = False
|
||||
self.__digested_string = b''
|
||||
|
||||
if data is not None:
|
||||
self.update(data)
|
||||
|
||||
# Static methods because pycharm is moaning about them not needing to be inside of the class
|
||||
@staticmethod
|
||||
def pad_string(string_len: int) -> bytes:
|
||||
"""Static method to pad the string digest for the hash"""
|
||||
# Adding a "1" bit to the end, although it seems to use 63 as the actual value, im guessing its possibly to do
|
||||
# with chunk size
|
||||
string_di = string_len & 0x3f
|
||||
# Get the 64bit big endian length of the post processed hash length
|
||||
post_process_length = (string_len << 3).to_bytes(8, "big")
|
||||
padded_length = 55
|
||||
if string_di < 56:
|
||||
padded_length -= string_di
|
||||
else:
|
||||
padded_length -= 199 - string_di
|
||||
|
||||
return b'\x80' + b'\x00' * padded_length + post_process_length
|
||||
|
||||
@staticmethod
|
||||
def rotate_right(a, b):
|
||||
"""Static method to rotate a and b bits right """
|
||||
return ((a >> b) | (a << (32 - b))) & 0xffffffff
|
||||
|
||||
def update(self, string_in: bytes) -> None:
|
||||
"""Method to update the hash value"""
|
||||
if (string_in and len(string_in) > 0) and not self.__string_hashed:
|
||||
self.__string_byte_len += len(string_in)
|
||||
string_buffer = self.__buffer + string_in
|
||||
|
||||
# Process the string as 512 bit chunks (64 bytes)
|
||||
for i in range(0, len(string_buffer) // 64):
|
||||
self.calculate_hash(string_buffer[64 * i:64 * (i + 1)])
|
||||
|
||||
# Set the class buffer
|
||||
self.__buffer = string_buffer[len(string_buffer) - (len(string_buffer) % 64):]
|
||||
else:
|
||||
return
|
||||
|
||||
def digest(self) -> bytes:
|
||||
"""Method to get the bytes digest of our hash value"""
|
||||
if not self.__string_hashed:
|
||||
self.update(self.pad_string(self.__string_byte_len))
|
||||
self.__digested_string = b''.join(val.to_bytes(4, 'big') for val in self.__h[:8])
|
||||
self.__string_hashed = True
|
||||
return self.__digested_string
|
||||
|
||||
def hexdigest(self) -> str:
|
||||
"""Method to get the hex digest of our hash value"""
|
||||
# Hex characters to use when changing the hex value back into a string
|
||||
tab = '0123456789abcdef'
|
||||
return "".join(tab[byt >> 4] + tab[byt & 0xf] for byt in self.digest())
|
||||
|
||||
def calculate_hash(self, chunk) -> None:
|
||||
"""Method to calculate the actual hash value"""
|
||||
# Create an array of 32bit words
|
||||
words = [0] * 64
|
||||
|
||||
# Converted from the pseudo code, but i believe its filling the word array with first 16 words we add
|
||||
words[0:16] = [int.from_bytes(chunk[i:i + 4], "big") for i in range(0, len(chunk), 4)]
|
||||
|
||||
for i in range(16, 64):
|
||||
s0 = self.rotate_right(words[i - 15], 7) ^ self.rotate_right(words[i - 15], 18) ^ (words[i - 15] >> 3)
|
||||
s1 = self.rotate_right(words[i - 2], 17) ^ self.rotate_right(words[i - 2], 19) ^ (words[i - 2] >> 10)
|
||||
words[i] = (words[i - 16] + s0 + words[i - 7] + s1) & 0xffffffff
|
||||
|
||||
# Initialize the values with the default hash value
|
||||
a, b, c, d, e, f, g, h = self.__h
|
||||
|
||||
# Calculation main loop
|
||||
for i in range(64):
|
||||
s0 = self.rotate_right(a, 2) ^ self.rotate_right(a, 13) ^ self.rotate_right(a, 22)
|
||||
t2 = s0 + self.maj(a, b, c)
|
||||
s1 = self.rotate_right(e, 6) ^ self.rotate_right(e, 11) ^ self.rotate_right(e, 25)
|
||||
t1 = h + s1 + self.ch(e, f, g) + self.__k[i] + words[i]
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + t1) & 0xffffffff
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (t1 + t2) & 0xffffffff
|
||||
|
||||
for i, (x, y) in enumerate(zip(self.__h, [a, b, c, d, e, f, g, h])):
|
||||
self.__h[i] = (x + y) & 0xffffffff
|
||||
|
||||
@staticmethod
|
||||
def maj(a, b, c):
|
||||
"""Static method for the calculation of the maj variable in the pseudo code"""
|
||||
return (a & b) ^ (a & c) ^ (b & c)
|
||||
|
||||
@staticmethod
|
||||
def ch(a, b, c):
|
||||
"""Static method to calculate the ch variable in the pseudo code"""
|
||||
return (a & b) ^ ((~a) & c)
|
71
Server/SMTPServer.py
Normal file
71
Server/SMTPServer.py
Normal file
@ -0,0 +1,71 @@
|
||||
import selectors
|
||||
import socket
|
||||
import ServerThread
|
||||
|
||||
|
||||
class SMTPServer:
|
||||
def __init__(self, host: str, port: int):
|
||||
self.__host = host
|
||||
self.__port = port
|
||||
self.__socket = None
|
||||
self.__selector = selectors.DefaultSelector()
|
||||
|
||||
self.__server_threads = []
|
||||
self.active = False
|
||||
|
||||
def start_server(self) -> bool:
|
||||
server_address = (self.__host, self.__port)
|
||||
|
||||
try:
|
||||
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.__socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.__socket.bind(server_address)
|
||||
self.__socket.listen()
|
||||
self.__socket.setblocking(False)
|
||||
self.__selector.register(self.__socket, selectors.EVENT_READ, data=None)
|
||||
self.active = True
|
||||
|
||||
print(f"Server Listening on {self.__host}:{self.__port}")
|
||||
result = True
|
||||
except OSError as ex:
|
||||
self.__socket.close()
|
||||
self.__socket = None
|
||||
print(f"Error opening the listener: {repr(ex)}")
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
def accept_wrapper(self, socket_in: socket):
|
||||
connection, address = socket_in.accept()
|
||||
print("Received Connection from", address)
|
||||
|
||||
connection.setblocking(False)
|
||||
new_server_thread = ServerThread.ServerThread(connection, address)
|
||||
self.__server_threads.append(new_server_thread)
|
||||
new_server_thread.start()
|
||||
|
||||
def open_server(self):
|
||||
try:
|
||||
while self.active:
|
||||
events = self.__selector.select(timeout=None)
|
||||
|
||||
for key, mask in events:
|
||||
if key.data is None:
|
||||
self.accept_wrapper(key.fileobj)
|
||||
else:
|
||||
pass
|
||||
except KeyboardInterrupt as ex:
|
||||
print("Keyboard Exception")
|
||||
for child_thread in self.__server_threads:
|
||||
child_thread.active = False
|
||||
self.active = False
|
||||
finally:
|
||||
self.__selector.close()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
126
Server/ServerLog.py
Normal file
126
Server/ServerLog.py
Normal file
@ -0,0 +1,126 @@
|
||||
from datetime import datetime
|
||||
|
||||
import Session
|
||||
import User
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class ServerLog:
|
||||
"""Class for holding a server log entry from the database"""
|
||||
def __init__(self, log_id: int = None):
|
||||
self.__log_id = log_id
|
||||
self.__session_ref = None
|
||||
self.__log_action_dir = None
|
||||
self.__log_action = None
|
||||
self.__log_client_ip = None
|
||||
self.__log_date = None
|
||||
self.__is_valid = False
|
||||
|
||||
self.__session: Session.Session or None = None
|
||||
self.__user: User.User or None = None
|
||||
|
||||
# if a log id is provided load the record from the database
|
||||
if self.__log_id:
|
||||
self.__load_server_log()
|
||||
|
||||
def __load_server_log(self) -> None:
|
||||
"""Private method for loading a server log from the database with its id"""
|
||||
db_connection = DBConn()
|
||||
server_log_details = db_connection.do_select("server_logs", "row", "*", "log_id = :log_id",
|
||||
[
|
||||
{
|
||||
"field": "log_id",
|
||||
"value": self.__log_id
|
||||
}
|
||||
])
|
||||
if server_log_details is not None and len(server_log_details) > 0:
|
||||
self.__log_id = server_log_details['log_id']
|
||||
self.__session_ref = server_log_details['session_ref']
|
||||
self.__log_action_dir = server_log_details['log_action_dir']
|
||||
self.__log_action = server_log_details['log_action']
|
||||
self.__log_client_ip = server_log_details['log_client_ip']
|
||||
self.__log_date = server_log_details['log_date']
|
||||
self.__is_valid = True
|
||||
|
||||
def server_log_is_valid(self) -> bool:
|
||||
"""Method to check if a valid server log has been loaded from the database"""
|
||||
return self.__is_valid
|
||||
|
||||
def get_log_action_dir(self) -> str:
|
||||
"""Method to get the logs direction variable"""
|
||||
return self.__log_action_dir
|
||||
|
||||
def get_log_action(self) -> str:
|
||||
"""Method to get the log action variable"""
|
||||
return self.__log_action
|
||||
|
||||
def get_log_action_with_dir(self) -> str:
|
||||
"""Method to return both the log direction and action as a string"""
|
||||
return f"{self.get_log_action_dir()} {self.get_log_action()}"
|
||||
|
||||
def output_log_line(self, output_format_string: str, date_format_string: str = "%d/%m/%y %H:%M") -> str:
|
||||
"""Method for writing the log as a string in the format given"""
|
||||
date_time_string = datetime.utcfromtimestamp(self.__log_date).strftime(date_format_string)
|
||||
|
||||
# Check if the log has a user and get the user name, otherwise it was an out of session action
|
||||
if self.get_user():
|
||||
username = self.get_user().username
|
||||
else:
|
||||
username = "N/A"
|
||||
|
||||
log_string = output_format_string.format(
|
||||
date_time_string,
|
||||
username,
|
||||
self.__log_client_ip,
|
||||
self.__log_action_dir,
|
||||
self.__log_action[0: 40]
|
||||
)
|
||||
|
||||
return log_string
|
||||
|
||||
def get_session(self) -> Session.Session or None:
|
||||
"""Method for getting the session object to use in the session variable"""
|
||||
if self.__session_ref and self.__session_ref != "":
|
||||
if not self.__session:
|
||||
self.__session = Session.Session(self.__session_ref, None)
|
||||
self.__session.load_session()
|
||||
return self.__session
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_user(self) -> User.User or None:
|
||||
"""Method for getting the user object for the user variable"""
|
||||
if self.__session_ref and self.__session_ref != "":
|
||||
if self.get_session():
|
||||
# Preload the user into the object if not loaded beforehand
|
||||
if not self.__user:
|
||||
self.__user = self.__session.get_user()
|
||||
return self.__user
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def create_server_log(self, session_ref: str, direction: str, action: str or bytes, ip: str) -> bool:
|
||||
"""Method for saving the current server log action to the database"""
|
||||
result = False
|
||||
if not self.__is_valid:
|
||||
if len(direction) > 0 and len(action) > 0 and len(ip) > 0:
|
||||
db_connection = DBConn()
|
||||
|
||||
# Force any byte strings into normal strings before putting it into the DB
|
||||
try:
|
||||
action = action.decode()
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
pass
|
||||
|
||||
if db_connection.do_insert("server_logs",
|
||||
[{"field": "session_ref", "value": session_ref},
|
||||
{"field": "log_action_dir", "value": direction},
|
||||
{"field": "log_action", "value": action},
|
||||
{"field": "log_client_ip", "value": ip},
|
||||
{"field": "log_date", "value": get_unix_timestamp()}]):
|
||||
self.__load_server_log()
|
||||
result = True
|
||||
|
||||
return result
|
742
Server/ServerThread.py
Normal file
742
Server/ServerThread.py
Normal file
@ -0,0 +1,742 @@
|
||||
# System imports
|
||||
import random
|
||||
import string
|
||||
import socket
|
||||
import selectors
|
||||
import queue
|
||||
import uuid
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
# My class imports
|
||||
import Crypto
|
||||
import Mail
|
||||
import RawMail
|
||||
import ServerLog
|
||||
import Session
|
||||
import User
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class ServerThread(Thread):
|
||||
"""Class for holding the server thread specific methods"""
|
||||
def __init__(self, current_socket: socket, address: tuple):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.__address = address
|
||||
self.__socket = current_socket
|
||||
self.__selector = selectors.DefaultSelector()
|
||||
|
||||
self.__buffer_in = queue.Queue()
|
||||
self.__buffer_out = queue.Queue()
|
||||
|
||||
# flag for the current network status, -1 for awaiting request, 0 for awaiting response, 1 for ready
|
||||
self.current_status = -1
|
||||
self.active = True
|
||||
self.initialised = False
|
||||
self.__pending_close = False
|
||||
self.__main_state = "INIT"
|
||||
self.__sub_state = ""
|
||||
|
||||
# Holding objects for the current session and raw mail
|
||||
self.__current_session: Session.Session or None = None
|
||||
self.__current_mail: RawMail.RawMail or None = None
|
||||
|
||||
# Queue array for queued responses
|
||||
self.__queued_messages = []
|
||||
|
||||
# Variables needed for the custom RSA encryption
|
||||
self.__pending_handshake = False
|
||||
self.__hand_shook = False
|
||||
self.__crypt_test_phrase = None
|
||||
self.__crypto_manager_receive: Crypto.Receiver or None = None
|
||||
self.__crypto_manager_transmit: Crypto.Transmitter or None = None
|
||||
|
||||
self.__selector.register(self.__socket, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
|
||||
|
||||
def run(self):
|
||||
"""Method for doing the threads main loop"""
|
||||
pending_close_counter = 0
|
||||
pending_handshake_counter = 0
|
||||
timed_out = False
|
||||
try:
|
||||
while self.active:
|
||||
waiting_commands = self.__selector.select(timeout=None)
|
||||
for key, mask in waiting_commands:
|
||||
try:
|
||||
if mask & selectors.EVENT_READ:
|
||||
self.__read()
|
||||
elif mask & selectors.EVENT_WRITE and not self.__buffer_out.empty():
|
||||
# Write/Send the initial message
|
||||
self.__write()
|
||||
elif mask & selectors.EVENT_WRITE and len(self.__queued_messages) > 0:
|
||||
# Write/Send any other queued messages for multipart responses
|
||||
self.__write_queue()
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
self.__close()
|
||||
|
||||
# If the connection has not been initialised send the initial service ready response
|
||||
if not self.initialised:
|
||||
self.__process_smtp_response(220, self.__address[0] + " SMTP service ready")
|
||||
self.__main_state = "HELO"
|
||||
self.initialised = True
|
||||
|
||||
# If the user is logged in but the connection has not received a heartbeat or request in the time then
|
||||
# close te connection and send the timeout response
|
||||
if self.__current_session and not timed_out:
|
||||
if self.__current_session.get_session_last_action_timestamp() < get_unix_timestamp(-300):
|
||||
self.__process_smtp_response(421, "Timeout, closing connection")
|
||||
timed_out = True
|
||||
|
||||
# Check for a pending handshake, we dont want to flag it straight away as it will start encoding the
|
||||
# response before we are ready, this will allow the current request to be processed before flipping it
|
||||
if self.__pending_handshake:
|
||||
if pending_handshake_counter == 1:
|
||||
self.__pending_handshake = False
|
||||
self.__hand_shook = True
|
||||
pending_handshake_counter = 0
|
||||
else:
|
||||
pending_handshake_counter += 1
|
||||
|
||||
# Check if we have a pending close request, this allows us to send the good bye response before rudely
|
||||
# ending the connection
|
||||
if self.__pending_close:
|
||||
if pending_close_counter == 1:
|
||||
self.active = False
|
||||
self.__close()
|
||||
else:
|
||||
pending_close_counter += 1
|
||||
|
||||
if not self.__selector.get_map():
|
||||
break
|
||||
|
||||
finally:
|
||||
self.__selector.close()
|
||||
|
||||
def __read(self):
|
||||
"""Private method for reading the response from the client"""
|
||||
try:
|
||||
received = self.__socket.recv(4096)
|
||||
except BlockingIOError as ex:
|
||||
print(f"IO Error: {repr(ex)}")
|
||||
else:
|
||||
if received:
|
||||
# If we have received a response, and the hand is shook and its not a plain emergency quit response we
|
||||
# decrypt the data here
|
||||
if self.__hand_shook and received.decode() not in ["QUIT"]:
|
||||
received_decoded_string = received.decode()
|
||||
# The request is split into the chunks needed and decrypted
|
||||
chunk_size = self.__crypto_manager_receive.get_expected_block_size()
|
||||
received_chunks = [self.__crypto_manager_receive.decrypt_string(
|
||||
received_decoded_string[i:i + chunk_size]
|
||||
) for i in range(0, len(received_decoded_string), chunk_size)]
|
||||
# Once we have decrypted all of the request blocks we can join them back into 1 string
|
||||
full_received = "".join(received_chunks)
|
||||
print(f"Received request: {repr(full_received)} from Client: {self.__address}")
|
||||
self.__buffer_in.put(full_received)
|
||||
|
||||
# We log the previous action in the session object and the time of the last action
|
||||
if self.__current_session:
|
||||
state_string = self.__main_state
|
||||
if self.__sub_state:
|
||||
state_string += "." + self.__sub_state
|
||||
self.__current_session.update_session(state_string, full_received)
|
||||
|
||||
# and log the action in the server logs
|
||||
self.__write_log_entry("IN", full_received)
|
||||
else:
|
||||
# If we are not hand shook then we treat the request as normal and log the
|
||||
# action in the server logs
|
||||
self.__buffer_in.put(received.decode())
|
||||
print(f"Received request: {repr(received)} from Client: {self.__address}")
|
||||
self.__write_log_entry("IN", received)
|
||||
else:
|
||||
raise RuntimeError("Connection Read Error, peer disconnected?")
|
||||
|
||||
self.__process_smtp_request()
|
||||
|
||||
def __write_queue(self):
|
||||
"""Private method for processing the write queue"""
|
||||
# Some SMTP response require the server to send multiple responses per request
|
||||
while len(self.__queued_messages) > 0:
|
||||
# Sleep to simulate some kind of latency to stop the queue to hopefully stop
|
||||
# the response from being added to the previous.
|
||||
sleep(1)
|
||||
# Pop each item from the array until we have nothing
|
||||
item = self.__queued_messages.pop(0)
|
||||
self.__process_smtp_response(item['code'], item['message'])
|
||||
self.__write()
|
||||
|
||||
self.__queued_messages.clear()
|
||||
|
||||
def __write(self):
|
||||
"""Private method for doing a normal socket write"""
|
||||
try:
|
||||
response = self.__buffer_out.get_nowait()
|
||||
except Exception:
|
||||
response = None
|
||||
|
||||
if response:
|
||||
print(f"Sending Response: {response} to Client: {self.__address}")
|
||||
try:
|
||||
self.__do_write(response)
|
||||
except BlockingIOError as ex:
|
||||
print(f"IO Error: {repr(ex)}")
|
||||
pass
|
||||
|
||||
def __do_write(self, response: bytes) -> None:
|
||||
"""Private method shared between the write methods to do the actual socket write and encryption"""
|
||||
# If the hand is shook then do the encryption
|
||||
if self.__hand_shook:
|
||||
# Write the log item before encrypting so we know what it was
|
||||
self.__write_log_entry("OUT", response)
|
||||
# Split into the chunk sizes we need/if needed
|
||||
chunk_size = self.__crypto_manager_transmit.get_usable_byte_length()
|
||||
response_chunks = [self.__crypto_manager_transmit.encrypt_string(response[i:i + chunk_size])
|
||||
for i in range(0, len(response), chunk_size)]
|
||||
# Join the request back together and send
|
||||
full_response = b"".join(response_chunks)
|
||||
self.__socket.send(full_response)
|
||||
else:
|
||||
# Otherwise just send as normal and log the server action
|
||||
self.__socket.send(response)
|
||||
self.__write_log_entry("OUT", response)
|
||||
|
||||
def __write_log_entry(self, direction: str, action: str or bytes) -> None:
|
||||
"""Private method to write a log to the server logs table"""
|
||||
# Check if the client is logged in and has an active session to log
|
||||
if self.__current_session:
|
||||
session_ref = self.__current_session.session_unique_ref
|
||||
else:
|
||||
session_ref = ""
|
||||
|
||||
# Check if its the login request and remove the password since logging that would be pretty silly...
|
||||
try:
|
||||
if bytes(action).startswith(b"LOGI"):
|
||||
action_parts = action.split(" ")
|
||||
if len(action_parts) == 3:
|
||||
action = "LOGI " + action_parts[1]
|
||||
else:
|
||||
action = "LOGI Empty/Bad Request"
|
||||
# The auth requests are too long and storing the keys seems dumb so ignore those too
|
||||
elif bytes(action).startswith(b"AUTH"):
|
||||
action = "AUTH Auth sending"
|
||||
elif bytes(action).startswith(b"570"):
|
||||
action = "570 Auth sending"
|
||||
# Multipart requests also flood the table as well as self replicate when checking, ignore those as we log
|
||||
# the initial request anyway
|
||||
elif bytes(action).startswith(b"214"):
|
||||
return
|
||||
|
||||
ServerLog.ServerLog().create_server_log(session_ref, direction, action, self.__address[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __queue_smtp_response(self, code: int, message: str):
|
||||
"""Private method for queuing an SMTP response to the client"""
|
||||
self.__queued_messages.append({"code": code, "message": message})
|
||||
|
||||
def __process_smtp_response(self, code: int, message: str):
|
||||
"""Private method for sending an SMTP response to a client """
|
||||
data_out = str(code)
|
||||
if len(message) > 0:
|
||||
data_out += " " + message
|
||||
|
||||
encoded_data_out = data_out.encode()
|
||||
self.__buffer_out.put(encoded_data_out)
|
||||
|
||||
def __process_smtp_request(self):
|
||||
"""Private method for processing a request from the client and sending responses back"""
|
||||
current_command = self.__buffer_in.get()
|
||||
|
||||
# Split the request as it should be in a format like XXXX YYYYYYYYYYYY, where Y would be any optional data
|
||||
current_command_parts = current_command.split(" ", 1)
|
||||
|
||||
# Create a connection to the DB
|
||||
db_connection = DBConn()
|
||||
|
||||
# Check if we are in Data entry mode, otherwise process the command as normal
|
||||
if current_command_parts[0] not in ["RSET", "NOOP"] \
|
||||
and (self.__main_state == "MAIL" and self.__sub_state == "DATA"):
|
||||
# Check if we receive the end data input line
|
||||
if current_command != "\n.\n":
|
||||
self.__current_mail.text_body += current_command + "\n"
|
||||
else:
|
||||
# If it has been we process the mail and "send" it to the users
|
||||
self.__current_mail.create_raw_mail(self.__current_session.get_user().get_user_id(),
|
||||
self.__current_mail.text_body)
|
||||
email_body = self.__current_mail.strip_headers_from_body()
|
||||
|
||||
for recipient in self.__current_mail.to_address_list:
|
||||
# The local recipients are added as their user id when processing the RCPT requests
|
||||
if isinstance(recipient, int):
|
||||
temp_email = Mail.Mail()
|
||||
temp_email.create_mail(self.__current_mail.get_raw_id(),
|
||||
self.__current_mail.from_id,
|
||||
recipient,
|
||||
self.__current_mail.subject,
|
||||
email_body)
|
||||
else:
|
||||
# We dont really care about the non local recipients,
|
||||
# but they would be forwarded to their server
|
||||
pass
|
||||
# Mail has been saved, send the OK and set us back to the main menu state
|
||||
self.__process_smtp_response(250, "Ok")
|
||||
self.__main_state = "HMEN"
|
||||
self.__sub_state = None
|
||||
|
||||
else:
|
||||
if len(current_command_parts) > 0:
|
||||
# Check if the user is logged in when required
|
||||
if current_command_parts[0] not in ["HELO", "NOOP", "AUTH", "HSHK", "LOGI"]:
|
||||
if self.__current_session is not None:
|
||||
# User is not valid, boot them from the server
|
||||
if self.__current_session.get_user() and not self.__current_session.get_user().user_logged_in():
|
||||
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||
return False
|
||||
else:
|
||||
# Session is invalid boot the client
|
||||
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Respond to the HELO command
|
||||
if current_command_parts[0] == "HELO":
|
||||
if self.__main_state == "HELO":
|
||||
self.__process_smtp_response(250, "Greetings from " + self.__address[0])
|
||||
# Setup the custom RSA encryption variables and generate the keys to send
|
||||
self.__crypto_manager_receive = Crypto.Receiver()
|
||||
self.__crypto_manager_receive.generate_keys()
|
||||
# Queue the public key response to send after the previous response
|
||||
self.__queue_smtp_response(570, self.__crypto_manager_receive.get_public_key_pair())
|
||||
self.__main_state = "AUTH"
|
||||
else:
|
||||
# We arnt expecting a HELO command, most likely because we didnt initialise
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
# Response to the AUTH state requests
|
||||
elif current_command_parts[0] == "AUTH":
|
||||
if self.__main_state == "AUTH":
|
||||
if len(current_command_parts) == 2:
|
||||
# We should have received the public key from the client, we generate a random string
|
||||
# to send during the handshake to make sure both public keys are working correctly
|
||||
self.__crypto_manager_transmit = Crypto.Transmitter(current_command_parts[1])
|
||||
self.__crypt_test_phrase = "".join(
|
||||
random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||
self.__queue_smtp_response(250, self.__crypt_test_phrase)
|
||||
self.__main_state = "HSHK"
|
||||
self.__hand_shook = True
|
||||
else:
|
||||
# If we didnt get a valid request the handshake most likely errored, we may as well
|
||||
# close the connection
|
||||
print(f"Bad handshake from {self.__address}")
|
||||
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||
self.__pending_close = True
|
||||
|
||||
elif current_command_parts[0] == "HSHK":
|
||||
if self.__main_state == "HSHK":
|
||||
if len(current_command_parts) == 2:
|
||||
# Check if the client returned the correct test phrase back, if so the encryption
|
||||
# is working both ways and we should be safe to proceed
|
||||
if current_command_parts[1] == self.__crypt_test_phrase:
|
||||
self.__queue_smtp_response(250, "OK")
|
||||
self.__main_state = "LOGI"
|
||||
else:
|
||||
# If the phrase is wrong there is obviously an issue with the encryption keys and
|
||||
# we just close the connection so the user can try again
|
||||
print(f"Bad handshake response from {self.__address}")
|
||||
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||
self.__pending_close = True
|
||||
else:
|
||||
# If the response didnt have the right amount of elements then the handshake failed and we
|
||||
# close the connection so the user can try again
|
||||
print(f"Bad handshake response from {self.__address}")
|
||||
self.__process_smtp_response(421, "Bad handshake, bye")
|
||||
self.__pending_close = True
|
||||
|
||||
# Process the NOOP Keep alive/Heartbeat response request
|
||||
elif current_command_parts[0] == "NOOP":
|
||||
self.__process_smtp_response(295, "OK")
|
||||
|
||||
# Process the LOGI login request
|
||||
elif current_command_parts[0] == "LOGI":
|
||||
# If the client is already logged in or the server is not expecting the login command
|
||||
# send the bad sequence response
|
||||
if self.__main_state != "LOGI" \
|
||||
or (self.__current_session and self.__current_session.session_is_valid()):
|
||||
# if the user is already logged in then they should not be sending the LOGI command
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
elif len(current_command_parts) == 2 and len(current_command_parts[1]) > 0:
|
||||
# Login parts 0 should be the username and 1 the password
|
||||
login_parts = current_command_parts[1].split(" ", 1)
|
||||
# We should have 2 login parts
|
||||
if len(login_parts) == 2:
|
||||
user_login = User.User(login_parts[0])
|
||||
current_login = user_login.login(login_parts[1], self.__address[0])
|
||||
|
||||
# The user has successfully logged in
|
||||
if current_login['status_code'] == 1:
|
||||
# Generate a UUID to use as the sessions unique ID
|
||||
session_uuid = str(uuid.uuid4())
|
||||
self.__current_session = Session.Session(session_uuid, user_login)
|
||||
self.__current_session.create_session(
|
||||
user_login.get_user_id(),
|
||||
self.__address[0]
|
||||
)
|
||||
self.__process_smtp_response(250, "OK, Welcome " + user_login.username)
|
||||
# Send the session reference back so the client can use this to verify itself
|
||||
self.__queue_smtp_response(265, self.__current_session.session_unique_ref)
|
||||
self.__main_state = "HMEN"
|
||||
|
||||
elif current_login['status_code'] == -1:
|
||||
# Too many login attempts for this user so the connection is rejected
|
||||
self.__process_smtp_response(421, "Authentication failed, your connection has been "
|
||||
"terminated, please wait and try again later")
|
||||
self.__pending_close = True
|
||||
|
||||
else:
|
||||
# 535 is the only error code that relates to authentication
|
||||
self.__process_smtp_response(535, "Authentication failed")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(535, "Authentication failed")
|
||||
|
||||
else:
|
||||
# Syntax error because they did not send the command correctly
|
||||
self.__process_smtp_response(501, "Syntax Error")
|
||||
|
||||
# Respond to the VRFY request
|
||||
elif current_command_parts[0] == "VRFY":
|
||||
if len(current_command_parts) == 2:
|
||||
# Get the logged in users name and email address
|
||||
verify_user = User.User(current_command_parts[1])
|
||||
if self.__current_session.get_user().get_user_id() == verify_user.get_user_id():
|
||||
self.__queue_smtp_response(261, verify_user.role.upper())
|
||||
self.__queue_smtp_response(250, verify_user.get_user_fullname()
|
||||
+ " <" + verify_user.email_address + ">")
|
||||
else:
|
||||
self.__process_smtp_response(421, "Session invalid, Goodbye")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
# Respond to the Log out Command
|
||||
elif current_command_parts[0] == "LOUT":
|
||||
logged_in_user = self.__current_session.get_user()
|
||||
if logged_in_user and logged_in_user.user_logged_in():
|
||||
self.__current_session = None
|
||||
self.__process_smtp_response(250, "OK, Goodbye " + logged_in_user.username)
|
||||
self.__main_state = "LOGI"
|
||||
self.__sub_state = None
|
||||
else:
|
||||
# the user wasn't logged in so we shouldn't be getting this request
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
# Process the MAIL command
|
||||
elif current_command_parts[0] == "MAIL":
|
||||
if self.__main_state == "HMEN":
|
||||
if len(current_command_parts) == 2:
|
||||
# To start the mail process the client should be sending the FROM address to us
|
||||
mail_command_parts = current_command_parts[1].split(" ", 1)
|
||||
if len(mail_command_parts) == 2 and mail_command_parts[0] == "FROM" \
|
||||
and len(mail_command_parts[1].strip(" <>")) > 0:
|
||||
# Create a raw mail for us to add the temp fields in before creating the mail
|
||||
self.__current_mail = RawMail.RawMail()
|
||||
self.__current_mail.from_id = self.__current_session.get_user().get_user_id()
|
||||
self.__current_mail.from_address = mail_command_parts[1].strip(" <>")
|
||||
self.__main_state = "MAIL"
|
||||
self.__sub_state = "RCPT"
|
||||
self.__process_smtp_response(250, "OK")
|
||||
else:
|
||||
# Bad Email address sent
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
# Bad command sent
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
# Tried to mail from the wrong state
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
# Process the RCPT request
|
||||
elif current_command_parts[0] == "RCPT":
|
||||
if self.__sub_state == "RCPT":
|
||||
if len(current_command_parts) == 2:
|
||||
# Split the command so that we can extract the email address
|
||||
rcpt_command_parts = current_command_parts[1].split(" ", 1)
|
||||
if len(rcpt_command_parts) == 2 and rcpt_command_parts[0] == "TO" \
|
||||
and len(rcpt_command_parts[1].strip(" <>")) > 0:
|
||||
email_cleaned = rcpt_command_parts[1].strip(" <>")
|
||||
email_parts = email_cleaned.split("@", 1)
|
||||
if len(email_parts) == 2:
|
||||
# Check if the email address is from the local domain from the site settings
|
||||
if email_parts[1] == db_connection.get_system_setting("SERVER_DOMAIN"):
|
||||
local_user = db_connection.do_select("users", "col", "user_id",
|
||||
"user_email = :user_email",
|
||||
[
|
||||
{
|
||||
"field": "user_email",
|
||||
"value": email_cleaned
|
||||
}
|
||||
])
|
||||
if local_user and int(local_user) > 0:
|
||||
# Address is a valid local address
|
||||
if local_user not in self.__current_mail.to_address_list:
|
||||
self.__current_mail.to_address_list.append(local_user)
|
||||
self.__process_smtp_response(250, "Ok")
|
||||
else:
|
||||
# Address is already in the list...
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
# Address does not exist locally
|
||||
self.__process_smtp_response(550, "Mailbox unavailable")
|
||||
else:
|
||||
# The email uses a non local domain, so we pretend we can forward it
|
||||
if email_cleaned not in self.__current_mail.to_address_list:
|
||||
self.__current_mail.to_address_list.append(email_cleaned)
|
||||
self.__process_smtp_response(251,
|
||||
"User not local, will attempt to forward")
|
||||
else:
|
||||
# Address is already in the list...
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Not a valid email address")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
# Start the DATA request response
|
||||
elif current_command_parts[0] == "DATA":
|
||||
if self.__main_state == "MAIL" and self.__sub_state == "RCPT":
|
||||
if len(self.__current_mail.to_address_list) > 0:
|
||||
# Set the server into data entry mode, this will cause it to loop at the start
|
||||
# of this method instead of match commands until the end characters are sent
|
||||
self.__process_smtp_response(354, "Start mail input; end with <CRLF>.<CRLF>")
|
||||
self.__sub_state = "DATA"
|
||||
self.__current_mail.text_body = ""
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
|
||||
# Process the VIEW request
|
||||
elif current_command_parts[0] == "VIEW":
|
||||
if len(current_command_parts) == 2:
|
||||
view_command_parts = current_command_parts[1].split(" ", 1)
|
||||
# The client wants to quit the VIEW state
|
||||
if view_command_parts[0] == "QUIT":
|
||||
self.__process_smtp_response(250, "OK")
|
||||
self.__main_state = "HMEN"
|
||||
self.__sub_state = None
|
||||
# The client wants to view a mail
|
||||
elif view_command_parts[0] == "MAIL":
|
||||
self.__main_state = "VIEW"
|
||||
self.__sub_state = "MAIL"
|
||||
|
||||
user_id = self.__current_session.get_user().get_user_id()
|
||||
mail_id = view_command_parts[1]
|
||||
|
||||
mail_item = db_connection.do_select("mail_store", "row", "mail_id",
|
||||
"mail_id = :mail_id AND to_id = :to_id",
|
||||
[
|
||||
{
|
||||
"field": "mail_id",
|
||||
"value": mail_id
|
||||
},
|
||||
{
|
||||
"field": "to_id",
|
||||
"value": user_id
|
||||
}
|
||||
])
|
||||
|
||||
# Return the mail if the query returns it, it must be one of their emails and a valid
|
||||
# mail id
|
||||
if mail_item:
|
||||
current_mail = Mail.Mail()
|
||||
current_mail.load_mail(mail_item['mail_id'])
|
||||
|
||||
mail_body = current_mail.get_raw_mail().get_raw_mail_content()
|
||||
mail_body_lines = mail_body.split("\n")
|
||||
|
||||
# Send each line of the email then the output ending string
|
||||
self.__process_smtp_response(211, "OK")
|
||||
for line in mail_body_lines:
|
||||
if len(line) > 0:
|
||||
self.__queue_smtp_response(214, line)
|
||||
self.__queue_smtp_response(214, "\n.\n")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(550, "Could not find the mail id")
|
||||
|
||||
# Client wants to view the mails in their mailbox
|
||||
elif view_command_parts[0] == "LIST":
|
||||
self.__main_state = "VIEW"
|
||||
self.__sub_state = "LIST"
|
||||
|
||||
user_id = self.__current_session.get_user().get_user_id()
|
||||
|
||||
mail_box_content = ["Last 20 mailbox items:", ""]
|
||||
mail_box_items = db_connection.do_select("mail_store", "all", "mail_id",
|
||||
"to_id = :to_id",
|
||||
[
|
||||
{
|
||||
"field": "to_id",
|
||||
"value": user_id
|
||||
}
|
||||
],
|
||||
"ORDER BY mail_date_sent DESC LIMIT 0, 20")
|
||||
for row in mail_box_items:
|
||||
current_mail = Mail.Mail()
|
||||
current_mail.load_mail(row['mail_id'])
|
||||
mail_box_content.append("{:<8} | {:<25} | "
|
||||
"{:<25} | {:<15}".format(current_mail.get_mail_id(),
|
||||
current_mail.get_subject(20),
|
||||
current_mail.get_from_user()
|
||||
.get_user_fullname(),
|
||||
current_mail.get_date_sent()
|
||||
)
|
||||
)
|
||||
|
||||
if len(mail_box_content) == 2:
|
||||
mail_box_content[1] = "No Items."
|
||||
else:
|
||||
mail_box_content[1] = "{:<8} | {:<25} | {:<25} | {:<15}".format("ID", "Subject",
|
||||
"From", "Date")
|
||||
|
||||
self.__process_smtp_response(211, "OK")
|
||||
for line in mail_box_content:
|
||||
self.__queue_smtp_response(214, line)
|
||||
self.__queue_smtp_response(214, "\n.\n")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
# Process the RSET command and return to the home menu state
|
||||
elif current_command_parts[0] == "RSET":
|
||||
self.__current_mail = None
|
||||
self.__process_smtp_response(250, "Ok")
|
||||
self.__main_state = "HMEN"
|
||||
self.__sub_state = None
|
||||
|
||||
# Process the HELP request
|
||||
elif current_command_parts[0] == "HELP":
|
||||
help_content = ""
|
||||
# If no help "section" is requested get the MAIN help text
|
||||
if len(current_command_parts) == 1:
|
||||
help_content = db_connection.get_system_setting("HELPMAIN")
|
||||
# Set the state to help mode
|
||||
self.__main_state = "HELP"
|
||||
elif len(current_command_parts) == 2 and self.__main_state == "HELP":
|
||||
# The client is trying to quit the help state
|
||||
if current_command_parts[1] == "QUIT":
|
||||
self.__process_smtp_response(250, "OK")
|
||||
self.__main_state = "HMEN"
|
||||
self.__sub_state = None
|
||||
else:
|
||||
help_content = db_connection.get_system_setting("HELP" + current_command_parts[1])
|
||||
else:
|
||||
# If the client is asking for a HELP section and we arnt in the help state they're wrong
|
||||
if self.__main_state != "HELP":
|
||||
self.__process_smtp_response(503, "Bad sequence of commands")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
# If we have help content to actually send back the content
|
||||
if help_content and len(help_content) > 0:
|
||||
self.__process_smtp_response(211, "OK")
|
||||
help_content_lines = help_content.split("\n")
|
||||
for line in help_content_lines:
|
||||
self.__queue_smtp_response(214, line)
|
||||
self.__queue_smtp_response(214, "\n.\n")
|
||||
else:
|
||||
if self.__main_state == "HELP":
|
||||
self.__process_smtp_response(504, "Command parameter is not implemented")
|
||||
|
||||
# ADMIN only commands
|
||||
elif current_command_parts[0] == "ADMN":
|
||||
# Check the logged in user is actually an admin
|
||||
if self.__current_session.get_user().role.upper() == "ADMIN":
|
||||
if len(current_command_parts) == 2:
|
||||
if current_command_parts[1] == "HOME":
|
||||
self.__main_state = "ADMN"
|
||||
self.__process_smtp_response(250, "OK")
|
||||
# The client is trying to quit the admin menu
|
||||
elif current_command_parts[1] == "QUIT":
|
||||
self.__main_state = "HMEN"
|
||||
self.__sub_state = None
|
||||
self.__process_smtp_response(250, "OK QUIT")
|
||||
# Unlock a user that has been locked out
|
||||
elif current_command_parts[1].startswith("UNLK"):
|
||||
unlock_parts = current_command_parts[1].split(" ", 1)
|
||||
if len(unlock_parts) == 2:
|
||||
unlock_user = User.User(unlock_parts[1])
|
||||
if unlock_user.valid_user():
|
||||
unlock_user.unlock()
|
||||
self.__process_smtp_response(250, "OK DONE")
|
||||
else:
|
||||
self.__process_smtp_response(500, "User not found.")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
# Command to return the last 20 items from the server logs audit
|
||||
elif current_command_parts[1].startswith("AUDT"):
|
||||
server_log_content = ["Last 20 server actions:", ""]
|
||||
server_log_items = db_connection.do_select("server_logs", "all", "log_id", "", [],
|
||||
"ORDER BY log_date DESC "
|
||||
"LIMIT 0, 20")
|
||||
for row in server_log_items:
|
||||
current_log = ServerLog.ServerLog(row['log_id'])
|
||||
server_log_content.append(current_log.output_log_line("{:<14} | {:<14} "
|
||||
"| {:<15} | {:<3} "
|
||||
"| {:<30} "))
|
||||
|
||||
if len(server_log_content) == 2:
|
||||
server_log_content[1] = "No Items."
|
||||
else:
|
||||
server_log_content[1] = "{:<14} | " \
|
||||
"{:<14} | " \
|
||||
"{:<15} | " \
|
||||
"{:<3} | " \
|
||||
"{:<30} ".format("Date", "User", "IP", "Dir", "Action")
|
||||
|
||||
self.__process_smtp_response(211, "OK")
|
||||
for line in server_log_content:
|
||||
self.__queue_smtp_response(214, line)
|
||||
self.__queue_smtp_response(214, "\n.\n")
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
else:
|
||||
self.__process_smtp_response(535, "No access")
|
||||
|
||||
# The client wants to quit, send the goodbye response
|
||||
elif current_command_parts[0] == "QUIT":
|
||||
self.__process_smtp_response(221, "Bye")
|
||||
print(f"Client {repr(self.__address[0])} has quit...")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
except Exception:
|
||||
# Catch all for any errors thrown during this loop
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
# print(f"Error: {repr(ex)}")
|
||||
|
||||
else:
|
||||
self.__process_smtp_response(500, "Invalid command given")
|
||||
|
||||
def __close(self):
|
||||
"""Private method to close the server thread"""
|
||||
print("Attempting to end the Connection...")
|
||||
try:
|
||||
self.__selector.unregister(self.__socket)
|
||||
self.__socket.close()
|
||||
print(f"Connection ended with ", self.__address)
|
||||
except OSError as ex:
|
||||
print(f"Could not close the connection: {repr(ex)}")
|
||||
finally:
|
||||
self.__socket = None
|
||||
self.active = False
|
||||
self.current_status = -1
|
101
Server/Session.py
Normal file
101
Server/Session.py
Normal file
@ -0,0 +1,101 @@
|
||||
import User
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class Session:
|
||||
"""Class to hold a users login session methods and variables"""
|
||||
def __init__(self, session_ref, user: User.User or None):
|
||||
self.__session_id = None
|
||||
self.session_unique_ref = session_ref
|
||||
self.__user_id = None
|
||||
self.__session_ip = None
|
||||
self.__session_data = None
|
||||
self.__session_state = None
|
||||
self.__session_last_action_date = None
|
||||
self.__is_valid = False
|
||||
|
||||
self.__user: User.User or None = user
|
||||
|
||||
def __load_session(self) -> None:
|
||||
"""Private method to load the users session from the database"""
|
||||
db_connection = DBConn()
|
||||
session_details = db_connection.do_select("sessions", "row", "*", "session_unique_ref = :session_unique_ref",
|
||||
[
|
||||
{
|
||||
"field": "session_unique_ref",
|
||||
"value": self.session_unique_ref
|
||||
}
|
||||
])
|
||||
if session_details is not None and len(session_details) > 0:
|
||||
self.__session_id = session_details['session_id']
|
||||
self.session_unique_ref = session_details['session_unique_ref']
|
||||
self.__user_id = session_details['user_id']
|
||||
self.__session_ip = session_details['session_ip']
|
||||
self.__session_data = session_details['session_data']
|
||||
self.__session_state = session_details['session_state']
|
||||
self.__session_last_action_date = session_details['session_last_action_date']
|
||||
self.__is_valid = True
|
||||
|
||||
def session_is_valid(self):
|
||||
"""Method to check if the session has a valid record loaded"""
|
||||
return self.__is_valid
|
||||
|
||||
def load_session(self) -> None:
|
||||
"""Method to load a session if the unique reference has been set"""
|
||||
if self.session_unique_ref:
|
||||
self.__load_session()
|
||||
|
||||
def get_session_last_action_timestamp(self):
|
||||
"""Method to get the sessions last action timestamp"""
|
||||
return self.__session_last_action_date
|
||||
|
||||
def get_user(self) -> User.User or None:
|
||||
"""Method to get the user object for the variable"""
|
||||
if self.__user_id:
|
||||
# Preload the user into the object if not loaded beforehand
|
||||
if not self.__user:
|
||||
self.__user = User.User(self.__user_id)
|
||||
return self.__user
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_session(self, current_state: str, last_action: str) -> None:
|
||||
"""Method to update the session with the last action, current state and current timestamp"""
|
||||
self.__session_data = last_action
|
||||
self.__session_state = current_state
|
||||
self.__session_last_action_date = get_unix_timestamp()
|
||||
self.save_current_changes()
|
||||
|
||||
def save_current_changes(self) -> bool:
|
||||
"""Method to save the changes made to a session to the database"""
|
||||
result = False
|
||||
if self.__is_valid:
|
||||
db_connection = DBConn()
|
||||
if db_connection.do_update("sessions",
|
||||
[{"field": "session_data", "value": self.__session_data},
|
||||
{"field": "session_state", "value": self.__session_state},
|
||||
{"field": "session_last_action_date",
|
||||
"value": self.__session_last_action_date}],
|
||||
"session_id = :session_id",
|
||||
[{"field": "session_id", "value": self.__session_id}]):
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def create_session(self, user_id: int, ip: str, data="", state="") -> bool:
|
||||
"""Method to save a new session to the database"""
|
||||
result = False
|
||||
if not self.__is_valid:
|
||||
if user_id and len(ip) > 0:
|
||||
db_connection = DBConn()
|
||||
if db_connection.do_insert("sessions",
|
||||
[{"field": "session_unique_ref", "value": self.session_unique_ref},
|
||||
{"field": "user_id", "value": user_id},
|
||||
{"field": "session_ip", "value": ip},
|
||||
{"field": "session_data", "value": data},
|
||||
{"field": "session_state", "value": state},
|
||||
{"field": "session_last_action_date", "value": get_unix_timestamp()}]):
|
||||
self.__load_session()
|
||||
result = True
|
||||
|
||||
return result
|
208
Server/User.py
Normal file
208
Server/User.py
Normal file
@ -0,0 +1,208 @@
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
import SHA256Custom
|
||||
from DBConn import DBConn, get_unix_timestamp
|
||||
|
||||
|
||||
class User:
|
||||
"""Class for dealing with a User object from the database"""
|
||||
def __init__(self, user_id):
|
||||
# Check if the user id is being added as an int for the user_id or string for the username
|
||||
if isinstance(user_id, int):
|
||||
self.username = None
|
||||
self.__user_id = user_id
|
||||
elif isinstance(user_id, str):
|
||||
self.username = user_id
|
||||
self.__user_id = None
|
||||
self.first_name = ""
|
||||
self.last_name = ""
|
||||
self.email_address = ""
|
||||
self.login_ip = ""
|
||||
self.role = ""
|
||||
self.__is_valid = False
|
||||
self.__is_logged_in = False
|
||||
self.login_date = -1
|
||||
self.__load_user()
|
||||
|
||||
def user_logged_in(self):
|
||||
"""Method to check if the user is logged in"""
|
||||
return self.__is_logged_in
|
||||
|
||||
def get_user_id(self):
|
||||
"""Method to return the users id"""
|
||||
return self.__user_id
|
||||
|
||||
def get_user_fullname(self):
|
||||
"""Method to return the users full name"""
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
def valid_user(self) -> bool:
|
||||
"""Method to check that the loaded user is valid"""
|
||||
return self.__is_valid
|
||||
|
||||
def __load_user(self) -> None:
|
||||
"""Private method to load the user data from the database"""
|
||||
db_connection = DBConn()
|
||||
# Use the username if the user_id was not provided
|
||||
if not self.__user_id:
|
||||
user_details = db_connection.do_select("users", "row", "*", "username = :username",
|
||||
[
|
||||
{
|
||||
"field": "username",
|
||||
"value": self.username
|
||||
}
|
||||
])
|
||||
else:
|
||||
user_details = db_connection.do_select("users", "row", "*", "user_id = :user_id",
|
||||
[
|
||||
{
|
||||
"field": "user_id",
|
||||
"value": self.__user_id
|
||||
}
|
||||
])
|
||||
if user_details is not None and len(user_details) > 0:
|
||||
self.__user_id = user_details['user_id']
|
||||
self.username = user_details['username']
|
||||
self.first_name = user_details['user_first_name']
|
||||
self.last_name = user_details['user_last_name']
|
||||
self.email_address = user_details['user_email']
|
||||
self.login_ip = ""
|
||||
self.role = user_details['user_role']
|
||||
self.__is_valid = True
|
||||
self.login_date = int(time.time())
|
||||
|
||||
def login(self, password, ip_address) -> any:
|
||||
"""Method for attempting to log in the current user with the provided password"""
|
||||
result = {"is_valid": False, "status_code": 0, "status_msg": ""}
|
||||
# Check that the user isn't already logged in with this instance
|
||||
if not self.__is_logged_in:
|
||||
if len(password) > 0 and len(ip_address) > 0:
|
||||
# Check that the user doesnt have 5 bad login attempts recently and is not locked out
|
||||
if self.bad_login_attempt_count() < 5:
|
||||
db_connection = DBConn()
|
||||
user_details = db_connection.do_select("users", "row", "user_password, user_salt",
|
||||
"username = :username",
|
||||
[
|
||||
{
|
||||
"field": "username",
|
||||
"value": self.username
|
||||
}
|
||||
])
|
||||
if user_details is not None and len(user_details) > 0:
|
||||
stored_password = user_details['user_password']
|
||||
stored_salt = user_details['user_salt']
|
||||
|
||||
# Hash the input password with the users salt from the database
|
||||
password_hash = SHA256Custom.SHA256Custom()
|
||||
password_hash.update((password + stored_salt).encode())
|
||||
|
||||
login_password = password_hash.hexdigest()
|
||||
|
||||
# If the passwords match then login
|
||||
if stored_password == login_password:
|
||||
self.__is_logged_in = True
|
||||
self.login_date = int(time.time())
|
||||
# Log the login attempt for the server records
|
||||
self.log_login_attempt(1, ip_address)
|
||||
result = {"is_valid": True, "status_code": 1, "status_msg": "User Logged in."}
|
||||
else:
|
||||
# Log the login attempt for the server records
|
||||
self.log_login_attempt(-1, ip_address)
|
||||
result['status_msg'] = "Incorrect Password."
|
||||
else:
|
||||
result['status_msg'] = "User not found."
|
||||
else:
|
||||
result['status_msg'] = "Too many tries, please wait."
|
||||
result['status_code'] = -1
|
||||
else:
|
||||
result['status_msg'] = "No Password and/or IP address provided."
|
||||
else:
|
||||
result['status_msg'] = "User already logged in."
|
||||
|
||||
return result
|
||||
|
||||
def save_current_changes(self) -> bool:
|
||||
"""Method to save any changes made to the user to the database"""
|
||||
result = False
|
||||
if self.__is_valid:
|
||||
db_connection = DBConn()
|
||||
if db_connection.do_update("users",
|
||||
[{"field": "username", "value": self.username},
|
||||
{"field": "user_first_name", "value": self.first_name},
|
||||
{"field": "user_last_name", "value": self.last_name},
|
||||
{"field": "user_email", "value": self.email_address},
|
||||
{"field": "user_role", "value": self.role}],
|
||||
"user_id = :user_id",
|
||||
[{"field": "user_id", "value": self.__user_id}]):
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def create_user(self, first_name: str, last_name: str, password: str, email_address: str, role="User") -> bool:
|
||||
"""Method to create and save the user in the database"""
|
||||
result = False
|
||||
if not self.__is_valid:
|
||||
if len(password) > 0 and len(email_address) and len(role) > 0:
|
||||
user_salt = "".join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||
|
||||
password_hash = SHA256Custom.SHA256Custom()
|
||||
password_hash.update((password + user_salt).encode())
|
||||
|
||||
user_password = password_hash.hexdigest()
|
||||
|
||||
db_connection = DBConn()
|
||||
if db_connection.do_insert("users",
|
||||
[{"field": "username", "value": self.username},
|
||||
{"field": "user_first_name", "value": first_name},
|
||||
{"field": "user_last_name", "value": last_name},
|
||||
{"field": "user_password", "value": user_password},
|
||||
{"field": "user_salt", "value": user_salt},
|
||||
{"field": "user_email", "value": email_address},
|
||||
{"field": "user_role", "value": role},
|
||||
{"field": "user_date_created", "value": get_unix_timestamp()}]):
|
||||
self.__load_user()
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def unlock(self) -> None:
|
||||
"""Method to unlock a user if they have been locked out by bad login attempts"""
|
||||
if self.__is_valid and not self.__is_logged_in:
|
||||
db_connection = DBConn()
|
||||
# It just sets the status values to another failed integer so they no longer get counted
|
||||
db_connection.do_update("login_attempts",
|
||||
[{"field": "login_attempt_status", "value": -2}],
|
||||
"user_id = :user_id AND login_attempt_status = :bad_status",
|
||||
[{"field": "user_id", "value": self.__user_id},
|
||||
{"field": "bad_status", "value": -1}])
|
||||
|
||||
def log_login_attempt(self, status, ip_address) -> None:
|
||||
"""Method for getting the current login attempt count from the database"""
|
||||
if self.__is_valid:
|
||||
db_connection = DBConn()
|
||||
db_connection.do_insert("login_attempts",
|
||||
[{"field": "user_id", "value": self.__user_id},
|
||||
{"field": "login_attempt_ip", "value": ip_address},
|
||||
{"field": "login_attempt_status", "value": status},
|
||||
{"field": "login_attempt_timestamp", "value": get_unix_timestamp()}])
|
||||
|
||||
def bad_login_attempt_count(self) -> int:
|
||||
"""Method to count bad login attempts from the user within the set timeframe"""
|
||||
result = 0
|
||||
if self.__user_id:
|
||||
db_connection = DBConn()
|
||||
attempt_count = db_connection.do_select("login_attempts", "col", "COUNT(login_attempt_id)",
|
||||
"user_id = :user_id AND "
|
||||
"login_attempt_status = :bad_login_status AND "
|
||||
"login_attempt_timestamp BETWEEN :start_time AND :end_time",
|
||||
[{"field": "user_id", "value": self.__user_id},
|
||||
{"field": "bad_login_status", "value": -1},
|
||||
{"field": "start_time", "value": get_unix_timestamp(-300)},
|
||||
{"field": "end_time", "value": get_unix_timestamp()}])
|
||||
|
||||
if attempt_count is not None:
|
||||
result = attempt_count
|
||||
|
||||
return result
|
217
Server/dbSetup.py
Normal file
217
Server/dbSetup.py
Normal file
@ -0,0 +1,217 @@
|
||||
import Crypto
|
||||
import hashlib
|
||||
import SHA256Custom
|
||||
|
||||
from DBConn import DBConn
|
||||
from User import User
|
||||
|
||||
# This is just a setup and test script to create the initial database and to test out some of the features
|
||||
|
||||
# should create the DB file if missing
|
||||
db_connection = DBConn()
|
||||
|
||||
create_system_settings_sql = "CREATE TABLE IF NOT EXISTS system_settings(" \
|
||||
"setting_name TEXT PRIMARY KEY," \
|
||||
"setting_value TEXT" \
|
||||
");"
|
||||
|
||||
create_user_table_sql = "CREATE TABLE IF NOT EXISTS users (" \
|
||||
"user_id INTEGER PRIMARY KEY," \
|
||||
"username TEXT UNIQUE," \
|
||||
"user_first_name TEXT," \
|
||||
"user_last_name TEXT," \
|
||||
"user_password TEXT NOT NULL," \
|
||||
"user_salt TEXT NOT NULL," \
|
||||
"user_email TEXT NOT NULL," \
|
||||
"user_role TEXT NOT NULL," \
|
||||
"user_date_created INTEGER NOT NULL" \
|
||||
");"
|
||||
|
||||
create_user_login_attempt_table = "CREATE TABLE IF NOT EXISTS login_attempts(" \
|
||||
"login_attempt_id INTEGER PRIMARY KEY," \
|
||||
"user_id INTEGER NOT NULL," \
|
||||
"login_attempt_ip TEXT," \
|
||||
"login_attempt_status INTEGER NOT NULL," \
|
||||
"login_attempt_timestamp INTEGER NOT NULL," \
|
||||
"FOREIGN KEY(user_id) REFERENCES users(user_id)" \
|
||||
");"
|
||||
|
||||
create_session_table = "CREATE TABLE IF NOT EXISTS sessions(" \
|
||||
"session_id TEST PRIMARY KEY," \
|
||||
"session_unique_ref TEXT UNIQUE NOT NULL," \
|
||||
"user_id INTEGER NOT NULL," \
|
||||
"session_ip TEXT NOT NULL," \
|
||||
"session_data TEXT," \
|
||||
"session_state TEXT NOT NULL," \
|
||||
"session_last_action_date INTEGER NOT NULL," \
|
||||
"FOREIGN KEY(user_id) REFERENCES users(user_id)" \
|
||||
");"
|
||||
|
||||
create_raw_mail_table_sql = "CREATE TABLE IF NOT EXISTS raw_mail (" \
|
||||
"raw_id INTEGER PRIMARY KEY," \
|
||||
"from_id INTEGER NOT NULL," \
|
||||
"raw_mail TEXT NOT NULL," \
|
||||
"raw_date_sent INTEGER NOT NULL," \
|
||||
"FOREIGN KEY(from_id) REFERENCES users(user_id)" \
|
||||
");"
|
||||
|
||||
create_mail_table_sql = "CREATE TABLE IF NOT EXISTS mail_store (" \
|
||||
"mail_id INTEGER PRIMARY KEY," \
|
||||
"raw_id INTEGER NOT NULL," \
|
||||
"from_id INTEGER NOT NULL," \
|
||||
"to_id INTEGER," \
|
||||
"mail_subject TEXT NOT NULL," \
|
||||
"mail_body TEXT NOT NULL," \
|
||||
"mail_read TEXT DEFAULT 0," \
|
||||
"mail_date_sent INTEGER NOT NULL," \
|
||||
"FOREIGN KEY(from_id) REFERENCES users(user_id)," \
|
||||
"FOREIGN KEY(to_id) REFERENCES users(user_id)" \
|
||||
");"
|
||||
|
||||
create_server_log_sql = "CREATE TABLE IF NOT EXISTS server_logs(" \
|
||||
"log_id INTEGER PRIMARY KEY," \
|
||||
"session_ref TEXT," \
|
||||
"log_action_dir TEXT," \
|
||||
"log_action TEXT," \
|
||||
"log_client_ip TEXT," \
|
||||
"log_date INTEGER" \
|
||||
");"
|
||||
|
||||
# Create the system settings and set the actual system settings needed
|
||||
if db_connection.do_generic(create_system_settings_sql):
|
||||
db_connection.set_system_setting("SERVER_DOMAIN", "server.local")
|
||||
db_connection.set_system_setting("MOTD", "Hello welcome to my SMTP server!")
|
||||
|
||||
help_main_body = "This is the main help text, i cant think of anything to put here,\n"
|
||||
help_main_body += "so i'll fill it with some lorem ipsums, but you can also look at \n"
|
||||
help_main_body += "sub help files using [USEFUL], [LOREM] or [MAIL]."
|
||||
|
||||
db_connection.set_system_setting("HELPMAIN", help_main_body)
|
||||
|
||||
help_lorem_body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet arcu tortor. \n" \
|
||||
"Maecenas ante lacus, vehicula id volutpat sed, auctor eget nunc. Mauris id consequat \n" \
|
||||
"augue. Sed molestie diam quis sapien vehicula, non ultricies tellus interdum. \n" \
|
||||
"Pellentesque vel dui enim. Integer sed magna blandit, consectetur felis id, lacinia nisi. \n" \
|
||||
"Phasellus luctus imperdiet orci quis hendrerit. Phasellus vel mattis libero. Suspendisse \n" \
|
||||
"ornare porttitor dolor, eu aliquet quam laoreet at. Integer tincidunt pretium pretium. \n" \
|
||||
"In auctor sapien a erat suscipit, quis semper ex fermentum. Vestibulum bibendum nulla ut \n" \
|
||||
"vestibulum fringilla. Fusce est risus, rhoncus feugiat tellus vitae, feugiat finibus \n" \
|
||||
"lacus. Maecenas gravida ornare imperdiet. Mauris nec est nec mi dapibus dictum viverra \n" \
|
||||
"at libero. Morbi blandit sed lacus a semper."
|
||||
|
||||
db_connection.set_system_setting("HELPLOREM", help_lorem_body)
|
||||
|
||||
help_mail_body = "When you compose a mail message it will take you through 3 sections, the first is to \n" \
|
||||
"collect the email addresses you wish to send to, \n" \
|
||||
"leaving the prompt blank will continue to the next section.\n" \
|
||||
"The next section will ask you to enter a subject for your email, this is straight forward.\n" \
|
||||
"The last section will allow you to enter the body of the email, enter will start a new \n" \
|
||||
"paragraph, if you leave the input blank and hit enter the email will attempt to send."
|
||||
|
||||
db_connection.set_system_setting("HELPMAIL", help_mail_body)
|
||||
|
||||
help_useful_body = "Test Email account email address list:\n" \
|
||||
"admin@server.local,\n" \
|
||||
"testuser@server.local,\n" \
|
||||
"craig.smith@server.local,\n" \
|
||||
"g.lemon@server.local,\n" \
|
||||
"v.king@server.local,\n" \
|
||||
"j.chong@server.local" \
|
||||
|
||||
db_connection.set_system_setting("HELPUSEFUL", help_useful_body)
|
||||
|
||||
# Create the user table and generate the admin and some fake users
|
||||
if db_connection.do_generic(create_user_table_sql):
|
||||
if db_connection.do_generic(create_user_login_attempt_table) and db_connection.do_generic(create_session_table):
|
||||
|
||||
# Set up users and do some class tests
|
||||
admin_user = User("admin")
|
||||
if admin_user.create_user("Jeff", "Jones", "admin", "admin@server.local", "Admin"):
|
||||
print("Admin user created.")
|
||||
|
||||
normal_user = User("testuser")
|
||||
if normal_user.create_user("Adam", "David", "password", "testuser@server.local"):
|
||||
print("Test user created.")
|
||||
|
||||
user_1 = User("user1")
|
||||
if user_1.create_user("Craig", "Smith", "user", "craig.smith@server.local"):
|
||||
print("Test user created.")
|
||||
|
||||
user_2 = User("user2")
|
||||
if user_2.create_user("Greg", "Lemon", "user", "g.lemon@server.local"):
|
||||
print("Test user created.")
|
||||
|
||||
user_3 = User("user3")
|
||||
if user_3.create_user("Vadim", "King", "user", "v.king@server.local"):
|
||||
print("Test user created.")
|
||||
|
||||
user_4 = User("user4")
|
||||
if user_4.create_user("Jim", "Chong", "user", "j.chong@server.local"):
|
||||
print("Test user created.")
|
||||
|
||||
print(admin_user.__dict__)
|
||||
print(normal_user.__dict__)
|
||||
|
||||
user_test = User("admin")
|
||||
for i in range(0, 5):
|
||||
print(user_test.login("nothing", "127.0.0.1"))
|
||||
print(f"Attempt {i+1}, count: {user_test.bad_login_attempt_count()}\n")
|
||||
|
||||
print(user_test.login("admin", "127.0.0.1"))
|
||||
print(f"Attempt 6, count: {user_test.bad_login_attempt_count()}\n")
|
||||
|
||||
print("Unlocking the user account\n")
|
||||
user_test.unlock()
|
||||
|
||||
print(user_test.login("admin", "127.0.0.1"))
|
||||
print(f"Attempt 7, count: {user_test.bad_login_attempt_count()}\n")
|
||||
print(user_test.__dict__)
|
||||
|
||||
user_test.email_address = "admin@email.com"
|
||||
user_test.save_current_changes()
|
||||
print(user_test.__dict__)
|
||||
|
||||
user_test.email_address = "admin@server.local"
|
||||
user_test.save_current_changes()
|
||||
|
||||
# Create the mail store and raw mail tables for holding the emails
|
||||
if db_connection.do_generic(create_raw_mail_table_sql):
|
||||
if db_connection.do_generic(create_mail_table_sql):
|
||||
if db_connection.do_generic(create_server_log_sql):
|
||||
pass
|
||||
|
||||
# Testing the Encrypting and RSA class
|
||||
crypto_test = Crypto.Receiver()
|
||||
crypto_test.generate_keys()
|
||||
|
||||
print(f"Public: {crypto_test.get_public_key_pair()}")
|
||||
print(f"Private: {crypto_test.get_private_key_pair()}")
|
||||
|
||||
decrypto_test = Crypto.Transmitter(crypto_test.get_public_key_pair())
|
||||
|
||||
print(f"Public Sent: {crypto_test.get_public_key_pair(False)}")
|
||||
print(f"Public Recv: {decrypto_test.get_current_key_pair()}")
|
||||
|
||||
print("String in: Hello world!")
|
||||
enc_string = decrypto_test.encrypt_string(b"Hello world!")
|
||||
print(f"Encrypted String: {enc_string}")
|
||||
|
||||
dec_string = crypto_test.decrypt_string(enc_string.decode())
|
||||
print(f"Decrypted String: {dec_string}")
|
||||
|
||||
hash_test_string = "Hello World!"
|
||||
|
||||
py_hash = hashlib.sha256(hash_test_string.encode())
|
||||
hash_test_a = py_hash.hexdigest()
|
||||
|
||||
custom_hash = SHA256Custom.SHA256Custom(hash_test_string.encode())
|
||||
hash_test_b = custom_hash.hexdigest()
|
||||
|
||||
print(f"Input string: {hash_test_string}")
|
||||
print(f"Python hash: {hash_test_a}")
|
||||
print(f"Custom hash: {hash_test_b}")
|
||||
|
||||
if hash_test_a == hash_test_b:
|
||||
print("Matching Hash")
|
||||
else:
|
||||
print("Non matching Hash")
|
BIN
Server/server.db
Normal file
BIN
Server/server.db
Normal file
Binary file not shown.
33
Server/server.py
Normal file
33
Server/server.py
Normal file
@ -0,0 +1,33 @@
|
||||
from SMTPServer import SMTPServer
|
||||
|
||||
server_host = "0.0.0.0"
|
||||
server_port = 50000
|
||||
|
||||
start_attempts = 0
|
||||
max_start_attempts = 3
|
||||
server_started = False
|
||||
quit_flag = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
while not quit_flag:
|
||||
print(f"Starting the server on {server_host}:{server_port}")
|
||||
|
||||
smtp_server = SMTPServer(server_host, server_port)
|
||||
|
||||
# Give the server 3 attempts to start, otherwise exit the program
|
||||
if not server_started:
|
||||
server_started = smtp_server.start_server()
|
||||
start_attempts += 1
|
||||
|
||||
if server_started:
|
||||
# Open the server and start the main script loop
|
||||
smtp_server.open_server()
|
||||
if not smtp_server.active:
|
||||
quit_flag = True
|
||||
|
||||
else:
|
||||
if start_attempts == max_start_attempts:
|
||||
print("Could not start the server, Exiting...")
|
||||
quit_flag = True
|
||||
|
||||
print("Server Ended, Thank you.")
|
Reference in New Issue
Block a user