diff --git a/expedite/client/auth.py b/expedite/client/auth.py index 1ce5c44..a3f443c 100644 --- a/expedite/client/auth.py +++ b/expedite/client/auth.py @@ -34,11 +34,26 @@ def derive_code(password: str = standard.client_pswd, salt: bytes = standard.client_salt) -> bytes: + """ + Derive byte convertion from password and salt composition + + :param password: Password provided by the clients + :param salt: Byte array for additional protection + :return: Cryptography HMAC code + """ kdfo = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000, backend=default_backend()) return kdfo.derive(password.encode()) def encr_bite(data: bytes = b"", code: bytes = standard.client_code, invc: bytes = standard.client_invc) -> bytes: + """ + Encrypt file chunks using converted cryptography HMAC code + + :param data: Chunks to read, encrypt and deliver + :param code: Generated cryptography HMAC code + :param invc: Initialization vector for added protection + :return: Encrypted bytes + """ cipher = Cipher(algorithms.AES(code), modes.CBC(invc), backend=default_backend()) encrob = cipher.encryptor() padder = padding.PKCS7(algorithms.AES.block_size).padder() @@ -47,6 +62,14 @@ def encr_bite(data: bytes = b"", code: bytes = standard.client_code, invc: bytes def decr_bite(data: bytes = b"", code: bytes = standard.client_code, invc: bytes = standard.client_invc) -> bytes: + """ + Decrypt file chunks using converted cryptography HMAC code + + :param data: Chunks to collect, decrypt and save + :param code: Generated cryptography HMAC code + :param invc: Initialization vector for added protection + :return: Decrypted bytes + """ try: cipher = Cipher(algorithms.AES(code), modes.CBC(invc), backend=default_backend()) decrob = cipher.decryptor() @@ -58,6 +81,11 @@ def decr_bite(data: bytes = b"", code: bytes = standard.client_code, invc: bytes def encr_metadata() -> bytes: + """ + Encrypt metadata to deliver before file contents + + :return: Encrypted bytes + """ standard.client_invc, standard.client_salt = os.urandom(16), os.urandom(16) data = dumps({"call": "meta", "name": standard.client_filename, "size": standard.client_filesize, "chks": len(standard.client_bind)-1}) standard.client_code = derive_code(standard.client_pswd, standard.client_salt) @@ -67,6 +95,12 @@ def encr_metadata() -> bytes: def decr_metadata(pack: bytes = b"") -> tuple: + """ + Decrypt metadata to collect before file contents + + :param pack: Chunks to decrypt + :return: Decrypted chunks + """ standard.client_invc, standard.client_salt = pack[0:16], pack[16:32] data = pack[32:] standard.client_code = derive_code(standard.client_pswd, standard.client_salt) diff --git a/expedite/client/base.py b/expedite/client/base.py index 275f2e3..2c58013 100644 --- a/expedite/client/base.py +++ b/expedite/client/base.py @@ -29,14 +29,30 @@ def find_size() -> int: + """ + Retrieve the file size using the file location + + :return: Size of the intended file + """ return getsize(standard.client_file) def find_name() -> str: + """ + Retrieve the file name using the file location + + :return: Name of the intended file + """ return basename(standard.client_file) def ease_size(size: Union[int, float]) -> str: + """ + Retrieve the file size in human-readable format + + :param size: Size in byte count format + :return: Size in human-readable format + """ unitlist = ["B", "KB", "MB", "GB", "TB", "PB"] indx, opsz = 0, size if size == 0: @@ -48,6 +64,11 @@ def ease_size(size: Union[int, float]) -> str: def bite_file() -> list: + """ + Retrieve the list of read ranges from chunk size + + :return: List of read ranges + """ init, size, bite = 0, standard.client_filesize, [] while init < size: bite.append(init) @@ -60,6 +81,13 @@ def bite_file() -> list: def read_file(init: int = 0, fina: int = 0) -> bytes: + """ + Retrieve the chunk from the provided byte range + + :param init: Starting byte index for reading + :param fina: Stopping byte index for reading + :return: Chunks to read + """ if exists(standard.client_file): with open(standard.client_file, "rb") as file: file.seek(init) @@ -72,6 +100,12 @@ def read_file(init: int = 0, fina: int = 0) -> bytes: def fuse_file(pack: bytes = b"") -> bool: + """ + Create and join the chunks on the storage device + + :param pack: Chunks to save + :return: + """ if not standard.client_fileinit: mode, standard.client_fileinit = "wb", True else: diff --git a/expedite/client/bridge/main.py b/expedite/client/bridge/main.py index 786848b..14837b9 100644 --- a/expedite/client/bridge/main.py +++ b/expedite/client/bridge/main.py @@ -31,7 +31,12 @@ from expedite.client.bridge.room import MainWindow -def load_custom_font(): +def load_custom_font() -> None: + """ + Populate the application database with custom fonts + + :return: + """ fontlist = [ ":font/font/sans-bold.ttf", ":font/font/sans-rlar.ttf", @@ -42,7 +47,12 @@ def load_custom_font(): QFontDatabase.addApplicationFont(indx) -def main(): +def main() -> None: + """ + Start the worker module to start the transfer service + + :return: + """ os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" QApplication.setStyle("Fusion") app = QApplication(sys.argv) diff --git a/expedite/client/bridge/room.py b/expedite/client/bridge/room.py index 097b167..83e19ec 100644 --- a/expedite/client/bridge/room.py +++ b/expedite/client/bridge/room.py @@ -49,7 +49,7 @@ collect_digest_checks, collect_dropping_summon, collect_metadata, - collect_permission_to_join, + collect_connection_to_server, collect_separation_from_mistaken_password, deliver_confirmation, deliver_connection_to_server, @@ -221,7 +221,7 @@ async def maintain_connection(self): if standard.client_plan in ["SEND", "RECV"]: # If the purpose of the client is either DELIVERING or COLLECTING if mesgdict["call"] == "okay": - await collect_permission_to_join(mesgdict["iden"]) + await collect_connection_to_server(mesgdict["iden"]) self.statarea.showMessage("Please share your acquired identity to begin interaction") self.identity.setText(f"{mesgdict["iden"]}") elif mesgdict["call"] in ["awry", "lone"]: diff --git a/expedite/client/bridge/util.py b/expedite/client/bridge/util.py index 0495510..612907a 100644 --- a/expedite/client/bridge/util.py +++ b/expedite/client/bridge/util.py @@ -24,9 +24,17 @@ from os.path import exists from PySide6.QtWidgets import QFileDialog +from click import Tuple def show_location_dialog(parent=None, oper: str = "") -> str: + """ + Select filepath for the intended file for delivering or collecting + + :param parent: Parent window within which the location dialog exists + :param oper: Operation intent for choosing either file or directory + :return: + """ dialog = QFileDialog() if oper == "dlvr": client_path = dialog.getOpenFileName(parent, "Select location", "", "All Files (*)")[0] @@ -36,6 +44,13 @@ def show_location_dialog(parent=None, oper: str = "") -> str: def truncate_text(text: str = "", size: int = 32) -> str: + """ + Limit text elements to a certain character count for an elegant fit + + :param text: Text elements that need to be checked for fitting + :param size: Character count within which text needs to be fit + :return: Truncated text + """ if len(text) >= size: return text[0:size-3] + "..." else: @@ -43,6 +58,11 @@ def truncate_text(text: str = "", size: int = 32) -> str: def return_detail_text() -> str: + """ + Retrieve application information text for showing on the dialog box + + :return: Application information + """ text = """ Expedite Bridge v{vers}
A simple encrypted file transfer service for humans

@@ -54,7 +74,12 @@ def return_detail_text() -> str: class ValidateFields: - def __init__(self): + def __init__(self) -> None: + """ + Initialize fields validation class for confirmation + + :return: + """ self.okay = { "size": False, "time": False, @@ -70,7 +95,13 @@ def __init__(self): "pswd": "Password cannot be an empty string" } - def verify_size(self, size): + def verify_size(self, size) -> None: + """ + Ensure that processing size must be an integer value between 1024 and 524288 + + :param size: Processing size + :return: + """ self.okay["size"] = True try: oper = int(size.strip()) @@ -79,7 +110,13 @@ def verify_size(self, size): except ValueError: self.okay["size"] = False - def verify_time(self, time): + def verify_time(self, time) -> None: + """ + Ensure that expiry window must be an integer value between 5 and 300 + + :param time: Expiry window + :return: + """ self.okay["time"] = True try: oper = int(time.strip()) @@ -88,38 +125,67 @@ def verify_time(self, time): except ValueError: self.okay["time"] = False - def verify_file(self, file): + def verify_file(self, file) -> None: + """ + Ensure that filepath for delivering or collecting contents must exist + + :param path: Load filepath + :return: + """ self.okay["file"] = True if not exists(file): self.okay["file"] = False - def verify_path(self, path): + def verify_path(self, path) -> None: + """ + Ensure that filepath for delivering or collecting contents must exist + + :param path: Save filepath + :return: + """ self.okay["path"] = True if path.strip() != "" and not exists(path): self.okay["path"] = False - def verify_pswd(self, pswd): + def verify_pswd(self, pswd) -> None: + """ + Ensure that password cannot be an empty string + + :param pswd: Password string + :return: + """ self.okay["pswd"] = True if pswd.strip() == "": self.okay["pswd"] = False - def report_dlvr(self, size, time, file, pswd): + def report_dlvr(self, size, time, file, pswd) -> tuple[tuple[bool, bool, bool, bool], str]: + """ + Retrieve field validation results for delivering intent + + :param size: Validity confirmation of processing size + :param time: Validity confirmation of waiting window + :param file: Validity confirmation of delivering filepath + :param pswd: Validity confirmation of password string + :return: Validity confirmation for required elements + """ self.verify_size(size) self.verify_time(time) self.verify_file(file) self.verify_pswd(pswd) lict = [self.text[indx] for indx in ["size", "time", "file", "pswd"] if not self.okay[indx]] - return ( - (self.okay["size"], self.okay["time"], self.okay["file"], self.okay["pswd"]), - "\n".join(lict).strip() - ) + return (self.okay["size"], self.okay["time"], self.okay["file"], self.okay["pswd"]), "\n".join(lict).strip() + + def report_clct(self, time, path, pswd) -> tuple[tuple[bool, bool, bool], str]: + """ + Retrieve field validation results for collecting intent - def report_clct(self, time, path, pswd): + :param time: Validity confirmation of waiting window + :param path: Validity confirmation of collecting filepath + :param pswd: Validity confirmation of password string + :return: Validity confirmation for required elements + """ self.verify_time(time) self.verify_path(path) self.verify_pswd(pswd) lict = [self.text[indx] for indx in ["time", "path", "pswd"] if not self.okay[indx]] - return ( - (self.okay["time"], self.okay["path"], self.okay["pswd"]), - "\n".join(lict).strip(), - ) + return (self.okay["time"], self.okay["path"], self.okay["pswd"]), "\n".join(lict).strip() diff --git a/expedite/client/conn.py b/expedite/client/conn.py index 8a484b2..6584db3 100644 --- a/expedite/client/conn.py +++ b/expedite/client/conn.py @@ -37,6 +37,12 @@ async def deliver_connection_to_server(sock: WebSocketClientProtocol) -> bool: + """ + Inform target client of connecting to the exchange server + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ general("Attempting to connect to the network.") try: await sock.send(dumps({"call": "join", "plan": standard.client_plan, "scan": standard.client_endo, "wait": standard.client_time})) @@ -46,7 +52,13 @@ async def deliver_connection_to_server(sock: WebSocketClientProtocol) -> bool: return False -async def collect_permission_to_join(iden: str = standard.client_iden) -> bool: +async def collect_connection_to_server(iden: str = standard.client_iden) -> bool: + """ + Show textual message of connecting to the exchange server + + :param iden: Network identity of person client + :return: Confirmation of the action completion + """ standard.client_iden = iden general("Successfully connected to the network.") warning(f"You are now identified as {iden} in the network.") @@ -54,6 +66,12 @@ async def collect_permission_to_join(iden: str = standard.client_iden) -> bool: async def deliver_suspension_from_expiry(sock: WebSocketClientProtocol) -> bool: + """ + Inform exchange server about the disconnection from timed expiry + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ if not standard.client_pair: general("Attempting to abandon from the network after expiry.") await sock.send(dumps({"call": "rest"})) @@ -63,6 +81,12 @@ async def deliver_suspension_from_expiry(sock: WebSocketClientProtocol) -> bool: async def collect_connection_from_pairness(iden: str = standard.client_endo) -> bool: + """ + Show textual message of collecting confirmation of pairing + + :param iden: Network identity of target client + :return: Confirmation of the action completion + """ standard.client_endo = iden standard.client_pair = True general(f"Attempting pairing with {standard.client_endo}.") @@ -70,7 +94,25 @@ async def collect_connection_from_pairness(iden: str = standard.client_endo) -> return True +async def deliver_metadata(sock: WebSocketClientProtocol) -> bool: + """ + Inform target client of encrypting and delivering the metadata + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ + general("Generating cryptography sign.") + await sock.send(encr_metadata()) + return True + + async def collect_metadata(pack: bytes = b"") -> bool: + """ + Show textual message of collecting and decrypting the metadata + + :param pack: Encrypted metadata collected from target client + :return: Confirmation of the action completion + """ try: general("Generating cryptography sign.") standard.client_filename, standard.client_filesize, standard.client_chks = decr_metadata(pack) @@ -79,24 +121,35 @@ async def collect_metadata(pack: bytes = b"") -> bool: return False -async def deliver_metadata(sock: WebSocketClientProtocol) -> bool: - general("Generating cryptography sign.") - await sock.send(encr_metadata()) - return True - - async def deliver_dropping_summon(sock: WebSocketClientProtocol) -> bool: + """ + Inform target client of starting the exchange process + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ await sock.send(dumps({"call": "drop"})) general(f"Delivering collection summon to {standard.client_endo}.") return True async def collect_dropping_summon() -> bool: + """ + Show textual message of starting the exchange process + + :return: + """ general(f"Collecting delivering summon from {standard.client_endo}.") return True async def deliver_contents(sock: WebSocketClientProtocol) -> Generator[Tuple[bytes, int], None, None]: + """ + Load contents from intended file and deliver them to target client + + :param sock: Websocket object belonging to the server session + :return: Tuple of file contents and contents length + """ for indx in range(0, len(standard.client_bind) - 1): bite = read_file(standard.client_bind[indx], standard.client_bind[indx + 1]) await sock.send(bite) @@ -105,6 +158,12 @@ async def deliver_contents(sock: WebSocketClientProtocol) -> Generator[Tuple[byt async def collect_contents(sock: WebSocketClientProtocol) -> Generator[Tuple[bytes, int], None, None]: + """ + Collect contents from target client and save them to intended file + + :param sock: Websocket object belonging to the server session + :return: Tuple of file contents and contents length + """ for _ in range(standard.client_chks - 1): mesgcont = await sock.recv() if isinstance(mesgcont, bytes): @@ -114,17 +173,35 @@ async def collect_contents(sock: WebSocketClientProtocol) -> Generator[Tuple[byt async def deliver_digest_checks(sock: WebSocketClientProtocol) -> bool: + """ + Inform target client with message digest of file contents + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ general("Delivering contents digest for confirmation.") await sock.send(dumps({"call": "hash", "data": standard.client_hash.hexdigest()})) return True async def collect_digest_checks() -> bool: + """ + Show textual message with message digest of file contents + + :return: Confirmation of the action completion + """ general("Collecting contents digest for confirmation.") return True async def deliver_confirmation(sock: WebSocketClientProtocol, data: str = standard.client_hash.hexdigest()) -> bool: + """ + Inform target client of the file contents integrity confirmation + + :param sock: Websocket object belonging to the server session + :param data: Message digest from the file contents exchanged + :return: + """ standard.client_movestop = time.time() if data == standard.client_hash.hexdigest(): general(f"Contents integrity verified (Mean {ease_size(standard.client_filesize / (standard.client_movestop - standard.client_movestrt))}/s).") @@ -137,6 +214,12 @@ async def deliver_confirmation(sock: WebSocketClientProtocol, data: str = standa async def collect_confirmation(data: int = 0) -> bool: + """ + Show textual message of the file contents integrity confirmation + + :param data: Confirmation of the message digest comparison + :return: Confirmation of the action completion + """ standard.client_movestop = time.time() if bool(data): general(f"Contents integrity verified (Mean {ease_size(standard.client_filesize / (standard.client_movestop - standard.client_movestrt))}/s).") @@ -147,11 +230,22 @@ async def collect_confirmation(data: int = 0) -> bool: async def deliver_separation_from_mistaken_password(sock: WebSocketClientProtocol) -> bool: + """ + Inform target client of disconnection due to mistaken password + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ general("Delivering status update on mistaken password.") await sock.send(dumps({"call": "flub"})) return True async def collect_separation_from_mistaken_password() -> bool: + """ + Show textual message of disconnection due to mistaken password + + :return: Confirmation of the action completion + """ general("Collecting status update on mistaken password.") return True diff --git a/expedite/client/excp.py b/expedite/client/excp.py index 8d45fb4..d8bbecb 100644 --- a/expedite/client/excp.py +++ b/expedite/client/excp.py @@ -22,5 +22,10 @@ class PasswordMistaken(Exception): - def __init__(self): + def __init__(self) -> None: + """ + Initialize an exception due to incorrect password being added on the collecting client + + :return: + """ self.name = "Password Mistaken" diff --git a/expedite/client/meet.py b/expedite/client/meet.py index c636ae9..2e09a88 100644 --- a/expedite/client/meet.py +++ b/expedite/client/meet.py @@ -27,6 +27,11 @@ def talk() -> None: + """ + Show information on the client side during startup + + :return: + """ success(f"Expedite Client v{__versdata__}") general(f"Addr. {standard.client_host}") general(f"Pass. {standard.client_pswd}") diff --git a/expedite/client/prompt/main.py b/expedite/client/prompt/main.py index ee904c4..a01d338 100644 --- a/expedite/client/prompt/main.py +++ b/expedite/client/prompt/main.py @@ -36,6 +36,11 @@ def work() -> None: + """ + Start the worker module to start the transfer service + + :return: + """ talk() try: run(oper()) @@ -89,6 +94,14 @@ def main( time: int = standard.client_time, endo: str = standard.client_endo, ): + """ + Configure the service particulars before starting it + + :param host: Location where the exchange service is hosted + :param time: Time for which the client has to wait before disconnecting + :param endo: Network identity of target client for pairing and transfer + :return: + """ standard.client_host = host standard.client_time = time standard.client_endo = endo @@ -128,6 +141,14 @@ def send( file: str = standard.client_file, size: int = standard.chunking_size, ) -> None: + """ + Configure the service particulars before delivering + + :param pswd: Password for encrypting purposes + :param file: Filepath for delivering file + :param size: Unit size for file chunking + :return: + """ standard.client_pswd = pswd standard.client_file = file standard.chunking_size = size @@ -154,6 +175,12 @@ def send( def recv( pswd: str = standard.client_pswd ) -> None: + """ + Configure the service particulars before collecting + + :param pswd: Password for decrypting purposes + :return: + """ standard.client_pswd = pswd standard.client_plan = "RECV" work() diff --git a/expedite/client/prompt/room.py b/expedite/client/prompt/room.py index 13f7603..2dea377 100644 --- a/expedite/client/prompt/room.py +++ b/expedite/client/prompt/room.py @@ -41,7 +41,7 @@ collect_digest_checks, collect_dropping_summon, collect_metadata, - collect_permission_to_join, + collect_connection_to_server, collect_separation_from_mistaken_password, deliver_confirmation, deliver_connection_to_server, @@ -56,7 +56,12 @@ from expedite.view import general -async def oper(): +async def oper() -> None: + """ + Exchange data to the target client after connecting to the exchange server + + :return: + """ try: async with connect(standard.client_host) as sock: get_event_loop().call_later(standard.client_time, lambda: ensure_future(deliver_suspension_from_expiry_prompt(sock))) @@ -68,7 +73,7 @@ async def oper(): if standard.client_plan in ["SEND", "RECV"]: # If the purpose of the client is either DELIVERING or COLLECTING if mesgdict["call"] == "okay": - await collect_permission_to_join(mesgdict["iden"]) + await collect_connection_to_server(mesgdict["iden"]) elif mesgdict["call"] in ["awry", "lone"]: await facade_exit(sock, False, mesgdict["call"]) if standard.client_plan == "SEND": @@ -113,6 +118,12 @@ async def oper(): async def show_deliver_contents(sock: WebSocketClientProtocol) -> bool: + """ + Facilitate encrypting and delivering file contents + + :param sock: Websocket object belonging to the server session + :return: Confirmation of the action completion + """ general(f"Delivering contents for '{standard.client_filename}' ({ease_size(standard.client_filesize)}) to {standard.client_endo}.") standard.client_movestrt = time.time() with logging_redirect_tqdm(): @@ -124,6 +135,13 @@ async def show_deliver_contents(sock: WebSocketClientProtocol) -> bool: async def show_collect_contents(sock: WebSocketClientProtocol, pack: bytes = b"") -> bool: + """ + Facilitate collecting and decrypting file contents + + :param sock: Websocket object belonging to the server session + :param pack: Byte chunk that is to be collected and decrypted + :return: Confirmation of the action completion + """ general(f"Collecting contents for '{standard.client_filename}' ({ease_size(standard.client_filesize)}) from {standard.client_endo}.") standard.client_movestrt = time.time() fuse_file(pack) diff --git a/expedite/client/prompt/util.py b/expedite/client/prompt/util.py index 60aef3d..3cb662d 100644 --- a/expedite/client/prompt/util.py +++ b/expedite/client/prompt/util.py @@ -31,6 +31,14 @@ async def facade_exit(sock: WebSocketClientProtocol = None, cond: bool = True, note: str = "") -> None: + """ + Terminate the websocket object elegantly before leaving the application + + :param sock: Websocket object belonging to the server session + :param cond: Condition within which the disconnection has to take place + :param note: Situation within which the disconnection was caused + :return: + """ if note != "done": warning(standard.client_note[note]) if sock: @@ -45,6 +53,12 @@ async def facade_exit(sock: WebSocketClientProtocol = None, cond: bool = True, n general("Exiting.") -async def deliver_suspension_from_expiry_prompt(sock: WebSocketClientProtocol): +async def deliver_suspension_from_expiry_prompt(sock: WebSocketClientProtocol) -> None: + """ + Terminate the websocket session elegantly after the designated timeout + + :param sock: Websocket object belonging to the server session + :return: + """ if await deliver_suspension_from_expiry(sock): await facade_exit(sock, False, "rest")