Initial Commit

This commit is contained in:
2025-07-05 14:24:22 +01:00
commit b31d19e336
19 changed files with 3858 additions and 0 deletions

7
Client/ClientTempMail.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

33
Server/server.py Normal file
View 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.")