From f7a45a92ae248bf34ac43fd19e79cae27393e38f Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Wed, 15 Nov 2023 23:41:04 +0530 Subject: [PATCH 01/14] Update README.md --- README.md | 60 +------------------------------------------------------ 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/README.md b/README.md index 87b9f59..f6a09ed 100644 --- a/README.md +++ b/README.md @@ -1,59 +1 @@ -# pesupy-chat - -The project aims to create an end-to-end encrypted chat platform that enables users to securely exchange text messages. It consists of a Server program, which runs on a network-connected computer, and a command-line Client application for users to connect, create accounts, and engage in secure chatting with other account holders on the server. - -### Server Program - -The Server is a socket server that accepts packets from the Client, performing various operations as requested, including account creation, login, and message transmission. Users interested in hosting their own server can execute this on their computer. - -### Client Application - -The Client program is a command-line interface that provides end-users with a straightforward experience. Upon execution, it displays a Sign Up/Login screen where users can create new accounts or log in to existing ones. Post-login, users can seamlessly communicate with others who have accounts on the server. - -### Security Measures - -Client messages are securely transmitted using end-to-end encryption, ensuring data confidentiality between the Client, Server, and recipients. Account credentials are also transported securely using the same encryption method to prevent unauthorized access to user accounts. Additionally, account credentials and chat backups are stored in an encrypted format within the Server's MySQL database, providing an extra layer of security. - -## Explanation & Installation - -### End-to-End Encryption - -End-to-end encryption (E2EE) is a robust data encryption method that ensures only the sender and intended recipient can access the data. This is achieved through a pair of mathematically linked keys: a public key used for encryption and a private key used for decryption. E2EE ensures privacy and security by never storing private keys on third-party servers, preventing even service providers from decrypting messages. - -### Server Setup - -The server setup process involves the following steps: - -1. Selection of a folder for server operation files. -2. Creation of MySQL schemas and tables. -3. Generation of an encryption keypair for secure traffic. -4. Password creation for server access protection. -5. Configuration of the network port for incoming connections. - -Once set up, the server listens for connections, enabling users to sign up, log in, and send and receive messages. - -### Key Pair Usage - -To enhance security, this project employs an additional layer of end-to-end encryption, as it does not utilize SSL/TLS due to port restrictions and certificate complexities. This ensures absolute security during data transmission. - -### Account Creation - -Users can execute the Client program to connect to a hosted server. The account creation process involves entering basic information, such as full name, email (optional), username, and password. The server securely stores this data in its MySQL database and generates a chat encryption keypair for each user. - -### Initiating a Chat - -Once logged in, users access the main interface, where they can see their chats and start new conversations. They can enter the username of the person they want to chat with and engage in secure messaging. The Client uses SQLite for local data storage. - -## Future Enhancements - -The project's future enhancements may include: - -- Development of a GUI-based Client for user-friendliness. -- Mobile platform Clients (e.g., Android). -- Support for formatted text and various message types (voice, image, video). -- Group chat and file sharing features with end-to-end encryption. -- Cross-server messaging for increased flexibility and security. - -This open-source project empowers users to control their chat server's security and functionality while providing a user-friendly experience. - -_README.md created by [Si6gma](https://github.com/Si6gma)_ +[TBD] From 3fef1e4796073f2c07319c7aeb3251d8ba1e1272 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Sun, 19 Nov 2023 02:49:55 +0530 Subject: [PATCH 02/14] Clean-up chat capabilities --- i18n.py | 2 +- server_main.py | 38 +------- server_modules/db_handler.py | 155 +------------------------------ server_modules/firstrun.py | 71 ++++++-------- server_modules/packet_handler.py | 96 +------------------ 5 files changed, 36 insertions(+), 326 deletions(-) diff --git a/i18n.py b/i18n.py index 1eb7c76..390f0cb 100644 --- a/i18n.py +++ b/i18n.py @@ -5,7 +5,7 @@ class firstrun(): config_not_found = "Configuration file not found!" exec = "Server will now run its configuration process" fix_missing = "Please enter the Server's" - welcome_message = "Welcome to PesuPy Chat Server Software!" + welcome_message = "Welcome to the Account System Demonstration" setup_server_dir = "Please enter the path to a folder where the server can store its files" keypair_setup = "Setting up Server Keypair..." initialize_db = "Setting up Databases for use..." diff --git a/server_main.py b/server_main.py index 072f30b..487c34a 100644 --- a/server_main.py +++ b/server_main.py @@ -60,7 +60,7 @@ async def catch(websocket): try: while True: result = await p.handle(SESSIONS, SERVER_CREDS, await websocket.recv(), websocket) - if result in ('CONN_CLOSED', 'USER_PACKET'): + if result in ('CONN_CLOSED',): pass else: await websocket.send(result) @@ -76,7 +76,7 @@ async def main(host, port): async with websockets.serve( catch, host=host, port=port, ping_interval=30, ping_timeout=None, close_timeout=None, - max_size=10485760 + max_size=1048576 ): await asyncio.Future() # run forever @@ -120,43 +120,9 @@ async def main(host, port): print(i18n.firstrun.exit) sys.exit() - try: - with open(f'{workingdir}/creds/queue_publickey', 'rb') as f: - pem_pubkey = f.read() - pubkey = en.deser_pem(pem_pubkey, 'public') - except FileNotFoundError: - print("Could not find packet queue public key. Server will now generate it from the private key.") - try: - with open(f'{workingdir}/creds/queue_privatekey', 'rb') as f: - en_pem_prkey = f.read() - pem_prkey = fkey.decrypt(en_pem_prkey) - prkey = en.deser_pem(pem_prkey, 'private') - pubkey = prkey.public_key() - - with open(f'{workingdir}/creds/queue_publickey', 'wb') as f: - f.write(pem_pubkey) - except FileNotFoundError: - print("Could not find packet queue keypair. Server will now generate it again.") - ch = input("This will cause previously unsent packets in the queue to be lost. Continue? (y/n) > ") - if ch.lower() == 'y': - db.clear_queue(user=None) - firstrun.save_queue_keypair(fkey, workingdir) - print(i18n.firstrun.exit) - sys.exit() - if ch.lower() == 'n': - print("Key ah kaanume enna panradhu ippo?") - exit() - else: - with open(f'{workingdir}/creds/queue_privatekey', 'rb') as f: - en_pem_prkey = f.read() - pem_prkey = fkey.decrypt(en_pem_prkey) - prkey = en.deser_pem(pem_prkey, 'private') - server_eprkey, server_epbkey = en.create_rsa_key_pair() SERVER_CREDS['server_eprkey'] = server_eprkey SERVER_CREDS['server_epbkey'] = en.ser_key_pem(server_epbkey, 'public') - SERVER_CREDS['queue_privkey'] = prkey - SERVER_CREDS['queue_pubkey'] = pubkey print("[INFO] SERVER ONLINE!") try: diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index 284a6fa..0e02660 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -6,7 +6,6 @@ initialize_ddl = """DROP SCHEMA IF EXISTS chatapp_accounts; -DROP SCHEMA IF EXISTS chatapp_chats; DROP SCHEMA IF EXISTS chatapp_internal; CREATE DATABASE IF NOT EXISTS chatapp_accounts; CREATE TABLE IF NOT EXISTS chatapp_accounts.users ( @@ -22,42 +21,13 @@ SALTED_HASHBROWN blob NOT NULL, TOKEN_SECRET tinytext ); -CREATE TABLE IF NOT EXISTS chatapp_accounts.pubkeys ( - UUID char(36) REFERENCES chatapp_accounts.users(UUID) ON DELETE CASCADE ON UPDATE CASCADE, - PUBKEY blob NOT NULL -); -CREATE DATABASE IF NOT EXISTS chatapp_chats; -CREATE TABLE IF NOT EXISTS chatapp_chats.rooms ( - ID int PRIMARY KEY AUTO_INCREMENT, - CREATOR_UUID char(36) NOT NULL REFERENCES chatapp_accounts.users (UUID), - ROOM_TYPE int NOT NULL, - MEMBERS blob NOT NULL, - CHAT_TABLE tinytext NOT NULL -); CREATE DATABASE IF NOT EXISTS chatapp_internal; CREATE TABLE IF NOT EXISTS chatapp_internal.settings (PARAM varchar(64) NOT NULL, VALUE varchar(256) NOT NULL); -CREATE TABLE IF NOT EXISTS chatapp_internal.`message_queue` ( - `timestamp` timestamp PRIMARY KEY DEFAULT CURRENT_TIMESTAMP, - `recipientUUID` char(36) NOT NULL, - `packet` mediumblob NOT NULL -);""" - -createroom = """CREATE TABLE chatapp_chats.{0} ( - `messageID` int NOT NULL AUTO_INCREMENT, - `messageUUID` char(36) UNIQUE NOT NULL, - `sender` char(36) NOT NULL, - `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `message` mediumblob NOT NULL, - `type` varchar(16) NOT NULL, - `pinned` bool NOT NULL DEFAULT false, - PRIMARY KEY (`messageID`) -);""" +""" -queries = {'initialize': initialize_ddl, 'create_room':createroom} +queries = {'initialize': initialize_ddl} fields_to_check = { - 'username':{'table':'chatapp_accounts.users','attribute':'USERNAME'}, - 'room':{'table':'chatapp_chats.rooms', 'attribute':'CHAT_TABLE'} - } + 'username':{'table':'chatapp_accounts.users','attribute':'USERNAME'}} class db: con = None cur = None @@ -109,34 +79,9 @@ def get_uuid(identifier): return 'ACCOUNT_DNE' return uuid -def queue_packet(user_uuid, de_packet, SERVER_CREDS): - en_packet = en.encrypt_packet(de_packet, SERVER_CREDS['queue_pubkey']) - db.cur.execute("INSERT INTO chatapp_internal.message_queue(recipientUUID, packet) VALUES (%s, %s)", (user_uuid, en_packet)) - db.con.commit() - -def clear_queue(user: str | None): - if user: - db.cur.execute("DELETE FROM chatapp_internal.message_queue WHERE recipientUUID = %s", (user,)) - db.con.commit() - elif not user: - db.cur.execute("DELETE FROM chatapp_internal.message_queue WHERE packet IS NOT NULL") - db.con.commit() - -def flush_queue(user_uuid): - db.cur.execute("SELECT packet FROM chatapp_internal.message_queue WHERE recipientUUID = %s", (user_uuid,)) - return db.cur.fetchall() - def close(): if db.con is not None: db.con.close() -class Config(db): - def update(setting, config): - db.cur.execute("INSERT INTO chatapp_internal.settings VALUES (%s, %s)", (setting, config)) - db.con.commit() - - def read(setting): - db.cur.execute("SELECT VALUE FROM chatapp_internal.settings WHERE PARAM = %s", (setting,)) - return db.cur.fetchall()[0][0] class Account(db): def create(username, fullname, dob, email, salted_pwd): @@ -150,21 +95,6 @@ def create(username, fullname, dob, email, salted_pwd): print(f"[DEBUG | for {uuid}]", pwd_query) db.cur.execute(pwd_query, (uuid, salted_pwd)) db.con.commit() - - def check_pubkey(uuid): - db.cur.execute(f"SELECT PUBKEY FROM chatapp_accounts.pubkeys WHERE UUID = '{uuid}'") - data = db.cur.fetchall() - if len(data) == 0: - return False - elif len(data) == 1: - return True - def set_pubkey(user, key): - db.cur.execute("INSERT INTO chatapp_accounts.pubkeys VALUES(%s, %s)", (user, key)) - db.con.commit() - def get_pubkey(user): - db.cur.execute(f"SELECT PUBKEY FROM chatapp_accounts.pubkeys WHERE UUID = '{user}'") - data = db.cur.fetchall() - return data[0][0] def check_pwd(pwd, identifier): try: @@ -187,82 +117,3 @@ def set_token(uuid, secret): def get_token_key(uuid): db.cur.execute("SELECT TOKEN_SECRET FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) return db.cur.fetchall()[0][0] - -class Room(db): - def create(creator, data): - if data['room_type'] == 0 and len(data['members']) == 0 or len(data['members']) > 1: - return 'MKROOM_ERROR' - elif len(data['members']) > 1: - return 'NOT_IMPLEMENTED' - # room_type 1 is for group, 2 for broadcast - chat_table = str(uuid4()).replace('-', '') - members_db, members_dne = [creator], [] - for user in data['members']: - flag = check_if_exists(user, 'username') - if flag == True: - members_db.append(get_uuid(user)) - elif flag == False: - members_dne.append(user) - members_insert = pickle.dumps(members_db) - try: - db.cur.execute(queries['create_room'].format(chat_table)) - query = "INSERT INTO chatapp_chats.rooms(CREATOR_UUID, ROOM_TYPE, ROOM_NAME, MEMBERS, CHAT_TABLE) VALUES (%s, %s, %s, %s, %s)" - db.cur.execute(query, (data['members'][0], data['room-type'], data['room-name'], members_insert, chat_table)) - db.con.commit() - # below line is type0 specific, error handling not implemented yet - chat_pubkey = Account.get_pubkey(members_db[1]) - match data['room-type']: - case 0: - return ['MKROOM_OK', data['room-type'], data['room-name'], chat_table, members_db, members_dne, chat_pubkey] - - except: - return 'MKROOM_ERROR' - - def fetch_info(room_uuid): - """[intid, creator_uuid, room_type, members, chat_table]""" - db.cur.execute("SELECT * FROM chatapp_chats.rooms WHERE CHAT_TABLE = %s", (room_uuid,)) - data = db.con.fetchall() - if data == []: - return 'ROOM_DNE' - return data[0] - -class Chat(db): - def save_msg(sender, data): - """ - data = room, action, actiondata(send/edit/delete/pin formats)""" - room_uuid, action, actiondata = data['room'], data['action'], data['actiondata'] - if actiondata['format'] not in ['TEXT']: # add more message formats here - return 'FORMAT_ERR' - match action: - case 'send': - db.cur.execute(f"INSERT INTO chatapp_chats.{room_uuid}(messageUUID, sender, message, type) VALUES (%s, %s, %s, %s)", (str(uuid4()), sender, actiondata['content'], actiondata['format'])) - db.con.commit() - return 'SUCCESS' - case 'edit': - msg = actiondata['msg'] - db.cur.execute(f"SELECT sender FROM chatapp_chats.{room_uuid} WHERE messageUUID = %s", (msg,)) - o_sender = db.cur.fetchall()[0][0] - if sender == o_sender: - db.cur.execute(f"UPDATE chatapp_chats.{room_uuid} SET message = %s, type = %s WHERE messageUUID = %s", (actiondata['content'], actiondata['format'], actiondata['msg'])) - db.con.commit() - return 'SUCCESS' - else: - return 'NOT_YOURS' - case 'delete': - msg = data['actiondata']['msg'] - db.cur.execute(f"SELECT sender FROM chatapp_chats.{room_uuid} WHERE messageUUID = %s", (msg,)) - o_sender = db.cur.fetchall()[0][0] - if sender == o_sender: - db.cur.execute(f"UPDATE chatapp_chats.{room_uuid} SET message = 'DELETED', type = 'sig' WHERE messageUUID = %s", (msg,)) - db.con.commit() - return 'SUCCESS' - else: - return 'NOT_YOURS' - case 'pinned': - db.cur.execute(f"UPDATE chatapp_chats.{room_uuid} SET pinned = true WHERE messageUUID = %s", (msg,)) - db.con.commit() - return 'SUCCESS' - def fetch_history(room, fromtime, totime): - db.cur.execute(f"SELECT * FROM chatapp_chats.{room} WHERE timestamp BETWEEN %s AND %s", (fromtime, totime)) - return db.cur.fetchall() - diff --git a/server_modules/firstrun.py b/server_modules/firstrun.py index 81a371a..be16505 100644 --- a/server_modules/firstrun.py +++ b/server_modules/firstrun.py @@ -13,42 +13,39 @@ class working_dir(): workingdir = '' def get_server_dir(): - while True: - try: - # Open GUI file picker if possible - print(firstrun.savedata.gui) - spath = filedialog.askdirectory() - except: - print(firstrun.savedata.nogui) - spath = input().rstrip('/\\') - finally: - break - return spath + try: + print(firstrun.savedata.gui) + return filedialog.askdirectory() + except: + print(firstrun.savedata.nogui) + return input().rstrip('/\\') + +def create_directory(path): + try: + os.mkdir(path) + print(firstrun.savedata.created) + except OSError as e: + print(f"{firstrun.savedata.error}:\n{e}") + return False + return True def setup_server_dir(): while True: - while True: - spath = get_server_dir() - # Do nothing if folder exists - if os.path.exists(spath) and os.path.isdir(spath): + spath = get_server_dir() + if os.path.exists(spath) and os.path.isdir(spath): + pass + elif os.path.exists(spath) and not os.path.isdir(spath): + spath = input(f"{firstrun.savedata.not_a_dir}:\n") + elif not os.path.exists(spath): + if not create_directory(spath): + spath = input(f"{firstrun.savedata.input_writable}:\n") + continue + + creds_path = f'{spath}/creds' + if not os.path.exists(creds_path): + if create_directory(creds_path): break - # If either the path leads to a file or is not writable (or invalid) - elif os.path.exists(spath) and not os.path.isdir(spath): - spath = input(f"{firstrun.savedata.not_a_dir}:\n") - elif not os.path.exists(spath): - print(firstrun.savedata.creating, end=' ') - try: - os.mkdir(spath) - except OSError as e: - print(f"{firstrun.savedata.error}:\n{e}") - spath = input(f"{firstrun.savedata.input_writable}:\n") - else: - print(firstrun.savedata.created) - break - if not os.path.exists(f'{spath}/creds'): - os.mkdir(f'{spath}/creds') - break - elif os.path.exists(f'{spath}/creds'): + else: print(firstrun.savedata.data_exists) return spath @@ -66,15 +63,6 @@ def save_db_credentials(fkey,workingdir): with open(f'{workingdir}/creds/db', 'wb') as f: f.write(fkey.encrypt(data)) -def save_queue_keypair(fkey, workingdir): - prkey, pubkey = e.create_rsa_key_pair() - pem_prkey, pem_pubkey = e.ser_key_pem(prkey, 'private'), e.ser_key_pem(pubkey, 'public') - en_pem_prkey = fkey.encrypt(pem_prkey) - with open(f'{workingdir}/creds/queue_privatekey', 'wb') as f: - f.write(en_pem_prkey) - with open(f'{workingdir}/creds/queue_publickey', 'wb') as f: - f.write(pem_pubkey) - def main(): print(firstrun.welcome_message) print(firstrun.setup_server_dir) @@ -82,7 +70,6 @@ def main(): setattr(working_dir, 'workingdir', workingdir) fkey = e.fernet_initkey(workingdir) save_db_credentials(fkey, workingdir) - save_queue_keypair(fkey, workingdir) del fkey host = input("Enter Server Listen Address: ") port = int(input("Enter Server Listen Port: ")) diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 9e209f8..3079f14 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -180,97 +180,6 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): elif flag == 'TOKEN_INVALID': return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_INVALID'}}) -async def get_pubkey(SESSIONS, SERVER_CREDS, ws, uuid): - flag = db.Account.check_pubkey(uuid) - if not flag: - await ws.send(await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CHAT_PUBKEY_MISSING'}})) - while True: - de_pubkey = en.decrypt_packet(await ws.recv(), SERVER_CREDS['server_eprkey']) - print(de_pubkey) - if de_pubkey['type'] == 'CHAT_ENCRYPT_C': - try: - s.load_pem_public_key(de_pubkey['data']['chat_pubkey']) - except Exception as ear: - await ws.send(await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CHAT_PUBKEY_INVALID'}})) - else: - db.Account.set_pubkey(uuid, de_pubkey['data']['chat_pubkey']) - await ws.send(await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CHAT_PUBKEY_OK'}})) - return 'CHAT_PUBKEY_OK' - else: - print(f"[INFO] CLIENT un-established {ws.remote_address} DISCONNECTED due to INVALID_PACKET") - await ws.close(code = 1008, reason = "Invalid packet structure") - return 'CONN_CLOSED' - elif flag: - return 'CHAT_PUBKEY_OK' - -async def create_room(SESSIONS, SERVER_CREDS, ws, data): - con_uuid = await identify_client(ws, SESSIONS) - if len(data['people']) < 2: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'MKROOM_INSUFFICIENT_PARTICIPANTS'}}) - else: - flag = db.Room.create(SESSIONS[con_uuid][2], data) - if flag == 'NOT_IMPLEMENTED': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'MKROOM_NOT_IMPLEMENTED'}}) - elif flag == 'MKROOM_ERROR': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'MKROOM_ERROR'}}) - elif flag[0] == 'MKROOM_OK': - roomdata = {'room_type':flag[1], 'room_name':flag[2], 'room_uuid':flag[3], 'room_key':flag[6], 'members':flag[4], 'dne':flag[5]} - await broadcast_packet(SESSIONS, SERVER_CREDS, flag[4], {'type':'JOIN_ROOM', 'data':roomdata}) - return await get_resp_packet(SESSIONS, ws, {'type':'ROOM_INFO','data':roomdata}) - -async def broadcast_packet(SESSIONS, SERVER_CREDS, members, packet): - for user in members: - await send_user_packet(SESSIONS, SERVER_CREDS, user, packet) - # send the packet to members - -async def chat_action(SESSIONS, SERVER_CREDS, ws, data): - sender = await identify_client(ws, SESSIONS) - user = SESSIONS[sender][2] - room = db.Room.fetch_info(data['room']) - - if room == 'ROOM_DNE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':'ROOM_DNE'}) - elif room[1] == user or user in pickle.loads(room[3]): - members = pickle.loads(room[3]) - action_map = { - 'send': 'recv', - 'edit': 'edited', - 'delete': 'deleted', - 'pinned': 'pinned' - } - action = data['action'] - if action in action_map: - re_data = {'action': action_map[action], 'actiondata': data['actiondata']} - de_packet = {'type':'CHAT_ACTION_S', 'data':re_data} - flag = db.Chat.save_msg(user, data) - if flag == 'SUCCESS': - await broadcast_packet(SESSIONS, SERVER_CREDS, members, de_packet) - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':{'sig':'SENT'}}) - elif flag == 'NOT_YOURS': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':{'sig':'MSG_NOT_YOURS'}}) - elif flag == 'FORMAT_ERR': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':{'sig':'UNSUPPORTED_MSG_FORMAT'}}) - else: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':'NOT_IN_ROOM'}) - -async def sync_chat(SESSIONS, SERVER_CREDS, ws, data): - sender = await identify_client(ws, SESSIONS) - user = SESSIONS[sender][2] - room = db.Room.fetch_info(data['room']) - if parse_time(data['from']) == 'PARSE_ERR' or parse_time(data['to']) == 'PARSE_ERR': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':'SYNC_INVALID_TIMESTAMP'}) - - if room == 'ROOM_DNE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':'ROOM_DNE'}) - elif room[1] == user or user in pickle.loads(room[3]): - data = db.Chat.fetch_history(user, data['room'], data['from'], data['to']) - sync_data = pickle.dumps(data) - de_packet = {'type':'SYNC_ROOM_DATA', 'data':{'room':room, 'sync_data':sync_data}} - return await get_resp_packet(SESSIONS, ws, de_packet) - else: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS', 'data':'NOT_IN_ROOM'}) - - async def captcha(SESSIONS, SERVER_CREDS, ws, data): uuid = await identify_client(ws, SESSIONS) challenge = str(randint(100000,999999)) @@ -294,10 +203,7 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): packet_map = { 'SIGNUP':signup, 'LOGIN':login, - 'AUTH_TOKEN':auth, - 'CREATE_ROOM':create_room, - 'CHAT_ACTION':chat_action, - 'SYNC_ROOM_REQ':sync_chat + 'AUTH_TOKEN':auth } async def handle(SESSIONS, SERVER_CREDS, packet, ws): From 68b91dce0fce45327082d47f580bf71f91bff30f Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Sun, 19 Nov 2023 03:02:41 +0530 Subject: [PATCH 03/14] Removed chat_pubkey check, forget not --- server_modules/packet_handler.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 3079f14..509498e 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -140,11 +140,7 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): secret, access_token = en.gen_token(uuid, 1) db.Account.set_token(uuid, secret) en_packet = await get_resp_packet(SESSIONS, ws, {'type':'TOKEN_GEN','data':{'token':access_token}}) - pubkey_resp = await get_pubkey(SESSIONS, SERVER_CREDS, ws, uuid) - if pubkey_resp == 'CONN_CLOSED': - return 'CONN_CLOSED' - elif pubkey_resp == 'CHAT_PUBKEY_OK': - return en_packet + return en_packet elif flag == False: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': From a1f09118840ae39fef24bcf58c27f066cf8989d8 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Wed, 22 Nov 2023 12:44:32 +0530 Subject: [PATCH 04/14] feature complete --- i18n.py | 2 +- server_main.py | 2 +- server_modules/db_handler.py | 8 +++++++ server_modules/encryption.py | 1 - server_modules/packet_handler.py | 36 +++++++++++++++++--------------- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/i18n.py b/i18n.py index 390f0cb..9814e92 100644 --- a/i18n.py +++ b/i18n.py @@ -5,7 +5,7 @@ class firstrun(): config_not_found = "Configuration file not found!" exec = "Server will now run its configuration process" fix_missing = "Please enter the Server's" - welcome_message = "Welcome to the Account System Demonstration" + welcome_message = "Welcome to the Account System Demonstration Backend" setup_server_dir = "Please enter the path to a folder where the server can store its files" keypair_setup = "Setting up Server Keypair..." initialize_db = "Setting up Databases for use..." diff --git a/server_main.py b/server_main.py index 487c34a..68eb6aa 100644 --- a/server_main.py +++ b/server_main.py @@ -75,7 +75,7 @@ async def catch(websocket): async def main(host, port): async with websockets.serve( catch, host=host, port=port, - ping_interval=30, ping_timeout=None, close_timeout=None, + ping_interval=120, ping_timeout=None, close_timeout=None, max_size=1048576 ): await asyncio.Future() # run forever diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index 0e02660..cbf45e2 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -109,6 +109,7 @@ def check_pwd(pwd, identifier): db.cur.execute("SELECT SALTED_HASHBROWN FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) saltedpwd = db.cur.fetchall()[0][0] flag = en.db_check_pwd(pwd, saltedpwd) + print("[DEBUG]",flag,uuid) return (flag, uuid) def set_token(uuid, secret): @@ -117,3 +118,10 @@ def set_token(uuid, secret): def get_token_key(uuid): db.cur.execute("SELECT TOKEN_SECRET FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) return db.cur.fetchall()[0][0] + def logout(uuid): + try: + db.cur.execute("DELETE TOKEN_SECRET FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) + db.con.commit() + return 'SUCCESS' + except: + return 'FAILURE' diff --git a/server_modules/encryption.py b/server_modules/encryption.py index 91a816c..e54737b 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -149,7 +149,6 @@ def salt_pwd(password): def db_check_pwd(pwd, saltedpwd): salted_pwd = pickle.loads(saltedpwd) - print(f"[DEBUG] {salted_pwd}") salt, key = salted_pwd['salt'], salted_pwd['key'] password = pwd.encode() kdf = PBKDF2HMAC( diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 509498e..d572177 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -46,7 +46,7 @@ async def establish_conn(SESSIONS, SERVER_CREDS, ws): SESSIONS[uuid] = [ws, None, None] # ws, public_key, user_uuid print(f"[INFO] Remote {ws.remote_address} initiated connection with UUID: {uuid}") - print(f"[INFO] SENDING PUBLIC KEY to {uuid}") + print(f"[INFO] Sending public key to {uuid}") # Encrypt Connection await ws.send(pickle.dumps({'type':'CONN_ENCRYPT_S','data':SERVER_CREDS['server_epbkey']})) @@ -55,12 +55,12 @@ async def establish_conn(SESSIONS, SERVER_CREDS, ws): try: client_epbkey = s.load_pem_public_key(client_epbkey['data']) except Exception as e: - print(f"[INFO] CLIENT un-established {ws.remote_address} DISCONNECTED due to INVALID_PACKET") + print(f"[INFO] Client un-established {ws.remote_address} DISCONNECTED due to INVALID_PACKET") await ws.close(code = 1008, reason = "Invalid packet structure") return 'CONN_CLOSED' SESSIONS[uuid][1] = client_epbkey - print(f"[INFO] Received public key for {ws.remote_address}") + print(f"[INFO] Received public key for {uuid}") del client_epbkey return en.encrypt_packet( {'type':'STATUS', 'data':{'sig':'CONN_OK'}}, @@ -140,7 +140,7 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): secret, access_token = en.gen_token(uuid, 1) db.Account.set_token(uuid, secret) en_packet = await get_resp_packet(SESSIONS, ws, {'type':'TOKEN_GEN','data':{'token':access_token}}) - return en_packet + await ws.send(en_packet) elif flag == False: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': @@ -161,21 +161,23 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): # tasks to run on login ws.send(await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_OK'}})) print("[INFO] User", user_uuid, "logged in from", ws.remote_address) - packet_queue = db.flush_queue(user_uuid) - for i in packet_queue: - de_packet = en.decrypt_packet(i[0], SERVER_CREDS['queue_privkey']) - if de_packet['type'] == 'decrypt_error': - continue - else: - ws.send(await get_resp_packet(SESSIONS, ws, de_packet)) - db.clear_queue(user=user_uuid) - print("[INFO] Flushed queue for user", user_uuid) - return await get_resp_packet(SESSIONS, ws, {'type':'QUEUE_END'}) elif flag == 'TOKEN_EXPIRED': return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_EXPIRED'}}) elif flag == 'TOKEN_INVALID': return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_INVALID'}}) +async def logout(SESSIONS, SERVER_CREDS, ws, data): + sender = await identify_client(ws, SESSIONS) + try: + user = SESSIONS[sender][2] + flag = db.Account.logout(user) + if flag == 'SUCCESS': + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_OK'}}) + elif flag == 'FAILURE': + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_ERR'}}) + except KeyError: + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'NOT_LOGGED_IN'}}) + async def captcha(SESSIONS, SERVER_CREDS, ws, data): uuid = await identify_client(ws, SESSIONS) challenge = str(randint(100000,999999)) @@ -188,7 +190,7 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): resp = await ws.recv() # handle possible INVALID_PACKET in next line - de_resp = pickle.loads(en.decrypt_packet(resp, SERVER_CREDS['server_eprkey'])) + de_resp = en.decrypt_packet(resp, SERVER_CREDS['server_eprkey']) de_resp = de_resp['data']['solved'] return int(de_resp) == int(challenge) @@ -199,7 +201,8 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): packet_map = { 'SIGNUP':signup, 'LOGIN':login, - 'AUTH_TOKEN':auth + 'AUTH_TOKEN':auth, + 'LOGOUT':logout } async def handle(SESSIONS, SERVER_CREDS, packet, ws): @@ -207,7 +210,6 @@ async def handle(SESSIONS, SERVER_CREDS, packet, ws): de_packet = pickle.loads(packet) else: de_packet = en.decrypt_packet(packet, SERVER_CREDS['server_eprkey']) - if de_packet['type'] == 'INVALID_PACKET': await disconnect(ws, 1008, "Invalid Packet Structure") return 'CONN_CLOSED' From 399066a7814263bc364535269a4f3cef89f3f425 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Wed, 22 Nov 2023 21:54:18 +0530 Subject: [PATCH 05/14] Fully functional, feature complete --- server_main.py | 2 +- server_modules/db_handler.py | 48 ++++++++++++++++++-------------- server_modules/encryption.py | 2 +- server_modules/packet_handler.py | 33 +++++++++++++--------- 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/server_main.py b/server_main.py index 68eb6aa..399dea6 100644 --- a/server_main.py +++ b/server_main.py @@ -62,7 +62,7 @@ async def catch(websocket): result = await p.handle(SESSIONS, SERVER_CREDS, await websocket.recv(), websocket) if result in ('CONN_CLOSED',): pass - else: + elif result: await websocket.send(result) # Handle disconnection due to any exception except Exception as err3: diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index cbf45e2..e5ec223 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -5,10 +5,9 @@ -initialize_ddl = """DROP SCHEMA IF EXISTS chatapp_accounts; -DROP SCHEMA IF EXISTS chatapp_internal; -CREATE DATABASE IF NOT EXISTS chatapp_accounts; -CREATE TABLE IF NOT EXISTS chatapp_accounts.users ( +initialize_ddl = """DROP SCHEMA IF EXISTS pfyt_accounts; +CREATE DATABASE IF NOT EXISTS pfyt_accounts; +CREATE TABLE IF NOT EXISTS pfyt_accounts.users ( UUID char(36) PRIMARY KEY, USERNAME varchar(32) UNIQUE NOT NULL, FULL_NAME varchar(80) NOT NULL, @@ -16,18 +15,16 @@ EMAIL varchar(32) DEFAULT NULL, CREATION timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE IF NOT EXISTS chatapp_accounts.auth ( - UUID char(36) NOT NULL REFERENCES chatapp_accounts.users(UUID) ON DELETE CASCADE ON UPDATE CASCADE, +CREATE TABLE IF NOT EXISTS pfyt_accounts.auth ( + UUID char(36) NOT NULL REFERENCES pfyt_accounts.users(UUID) ON DELETE CASCADE ON UPDATE CASCADE, SALTED_HASHBROWN blob NOT NULL, TOKEN_SECRET tinytext ); -CREATE DATABASE IF NOT EXISTS chatapp_internal; -CREATE TABLE IF NOT EXISTS chatapp_internal.settings (PARAM varchar(64) NOT NULL, VALUE varchar(256) NOT NULL); """ queries = {'initialize': initialize_ddl} fields_to_check = { - 'username':{'table':'chatapp_accounts.users','attribute':'USERNAME'}} + 'username':{'table':'pfyt_accounts.users','attribute':'USERNAME'}} class db: con = None cur = None @@ -42,6 +39,7 @@ def decrypt_creds(fkey, workingdir): setattr(db, 'cur', db.con.cursor()) if db.con.is_connected(): print('[INFO] Connected to database ',dict['host'],':',dict['port'],sep='') + db.cur.execute("USE pfyt_accounts") except sqltor.errors.ProgrammingError as errrr: print('[ERROR] Could not connect to database:', errrr) @@ -70,10 +68,10 @@ def check_if_exists(value, field): def get_uuid(identifier): try: if '@' not in identifier: - db.cur.execute("SELECT UUID FROM chatapp_accounts.users WHERE USERNAME=%s", (identifier,)) + db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE USERNAME=%s", (identifier,)) uuid = db.cur.fetchall()[0][0] elif '@' in identifier: - db.cur.execute("SELECT UUID FROM chatapp_accounts.users WHERE EMAIL=%s", (identifier,)) + db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE EMAIL=%s", (identifier,)) uuid = db.cur.fetchall()[0][0] except IndexError: return 'ACCOUNT_DNE' @@ -87,11 +85,11 @@ class Account(db): def create(username, fullname, dob, email, salted_pwd): # UUID, USERNAME, FULL_NAME, DOB, EMAIL, CREATION uuid = str(uuid4()) - query = "INSERT INTO chatapp_accounts.users(UUID, USERNAME, FULL_NAME, DOB, EMAIL) VALUES (%s, %s, %s, %s, %s)" + query = "INSERT INTO pfyt_accounts.users(UUID, USERNAME, FULL_NAME, DOB, EMAIL) VALUES (%s, %s, %s, %s, %s)" print(f"[DEBUG | for {uuid}]",query) db.cur.execute(query, (uuid, username, fullname, dob, email)) db.con.commit() - pwd_query = "INSERT INTO chatapp_accounts.auth(UUID, SALTED_HASHBROWN) VALUES (%s, %s)" + pwd_query = "INSERT INTO pfyt_accounts.auth(UUID, SALTED_HASHBROWN) VALUES (%s, %s)" print(f"[DEBUG | for {uuid}]", pwd_query) db.cur.execute(pwd_query, (uuid, salted_pwd)) db.con.commit() @@ -99,29 +97,37 @@ def create(username, fullname, dob, email, salted_pwd): def check_pwd(pwd, identifier): try: if '@' not in identifier: - db.cur.execute("SELECT UUID FROM chatapp_accounts.users WHERE USERNAME=%s", (identifier,)) + db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE USERNAME=%s", (identifier,)) uuid = db.cur.fetchall()[0][0] elif '@' in identifier: - db.cur.execute("SELECT UUID FROM chatapp_accounts.users WHERE EMAIL=%s", (identifier,)) + db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE EMAIL=%s", (identifier,)) uuid = db.cur.fetchall()[0][0] except IndexError: return 'ACCOUNT_DNE' - db.cur.execute("SELECT SALTED_HASHBROWN FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) + db.cur.execute("SELECT SALTED_HASHBROWN FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) saltedpwd = db.cur.fetchall()[0][0] flag = en.db_check_pwd(pwd, saltedpwd) print("[DEBUG]",flag,uuid) return (flag, uuid) def set_token(uuid, secret): - db.cur.execute("UPDATE chatapp_accounts.auth SET TOKEN_SECRET = %s WHERE UUID = %s", (secret, uuid)) + db.cur.execute("UPDATE pfyt_accounts.auth SET TOKEN_SECRET = %s WHERE UUID = %s", (secret, uuid)) db.con.commit() def get_token_key(uuid): - db.cur.execute("SELECT TOKEN_SECRET FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) - return db.cur.fetchall()[0][0] + db.cur.execute("SELECT TOKEN_SECRET FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) + try: + resp = db.cur.fetchall()[0][0] + if resp: + return resp + else: + return 'TOKEN_NOT_FOUND' + except IndexError: + return 'TOKEN_NOT_FOUND' def logout(uuid): try: - db.cur.execute("DELETE TOKEN_SECRET FROM chatapp_accounts.auth WHERE UUID = %s", (uuid,)) + db.cur.execute("UPDATE pfyt_accounts.auth SET TOKEN_SECRET = NULL WHERE UUID = %s", (uuid,)) db.con.commit() return 'SUCCESS' - except: + except Exception as err: + print(err) return 'FAILURE' diff --git a/server_modules/encryption.py b/server_modules/encryption.py index e54737b..3235ed6 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -184,7 +184,7 @@ def validate_token(key, token, user): decoded_token = jwt.decode(token, key, algorithms=["HS256"]) except: return 'TOKEN_INVALID' - timenow = datetime.datetime.utcnow() + timenow = datetime.datetime.utcnow().timestamp() if decoded_token['sub'] == user and timenow < decoded_token['exp']: return 'TOKEN_OK' elif decoded_token['sub'] == user and timenow > decoded_token['exp']: diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index d572177..97752e1 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -140,7 +140,8 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): secret, access_token = en.gen_token(uuid, 1) db.Account.set_token(uuid, secret) en_packet = await get_resp_packet(SESSIONS, ws, {'type':'TOKEN_GEN','data':{'token':access_token}}) - await ws.send(en_packet) + print("[INFO] Generated token for", uuid) + return en_packet elif flag == False: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': @@ -150,17 +151,22 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): async def auth(SESSIONS, SERVER_CREDS, ws, data): # REMINDER TO HANDLE LOGIN FROM TWO DEVICES - user = data['data']['user'] - token = data['data']['token'] + user = data['user'] + token = data['token'] con_uuid = await identify_client(ws, SESSIONS) user_uuid = db.get_uuid(user) + if user_uuid == 'ACCOUNT_DNE': + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'ACCOUNT_DNE'}}) key = db.Account.get_token_key(user_uuid) - flag = en.validate_token(key, token, con_uuid) + if key == 'TOKEN_NOT_FOUND': + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_NOT_FOUND'}}) + flag = en.validate_token(key, token, user_uuid) if flag == 'TOKEN_OK': SESSIONS[con_uuid][2] = user_uuid # tasks to run on login - ws.send(await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_OK'}})) print("[INFO] User", user_uuid, "logged in from", ws.remote_address) + print("[DEBUG] SESSIONS:", SESSIONS) + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_OK'}}) elif flag == 'TOKEN_EXPIRED': return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_EXPIRED'}}) elif flag == 'TOKEN_INVALID': @@ -168,15 +174,16 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): async def logout(SESSIONS, SERVER_CREDS, ws, data): sender = await identify_client(ws, SESSIONS) - try: - user = SESSIONS[sender][2] - flag = db.Account.logout(user) - if flag == 'SUCCESS': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_OK'}}) - elif flag == 'FAILURE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_ERR'}}) - except KeyError: + user = SESSIONS[sender][2] + if not user: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'NOT_LOGGED_IN'}}) + flag = db.Account.logout(user) + if flag == 'SUCCESS': + SESSIONS[sender][2] = None + print("[DEBUG] SESSIONS:", SESSIONS) + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_OK'}}) + elif flag == 'FAILURE': + return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_ERR'}}) async def captcha(SESSIONS, SERVER_CREDS, ws, data): uuid = await identify_client(ws, SESSIONS) From a7ce738d3638fb3c563945cc0d17270b22a9942c Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Thu, 23 Nov 2023 06:41:54 +0530 Subject: [PATCH 06/14] string authoring changes --- README.md | 7 ++++- ascii_art.txt | 40 -------------------------- i18n.py | 56 +++++++++++++++++++----------------- requirements.txt | 2 +- server_main.py | 10 +++---- server_modules/encryption.py | 7 +++-- server_modules/firstrun.py | 32 ++++++++++----------- 7 files changed, 62 insertions(+), 92 deletions(-) delete mode 100644 ascii_art.txt diff --git a/README.md b/README.md index f6a09ed..de1030b 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -[TBD] +# Account System + +## with Session Token based Authentication + +*** + diff --git a/ascii_art.txt b/ascii_art.txt deleted file mode 100644 index d8badd1..0000000 --- a/ascii_art.txt +++ /dev/null @@ -1,40 +0,0 @@ - - ..... . .. - %*---%::% @- - .:::::::::----==++**##+. %. .% *= .+% - :#@@@@@@@@@@@@@@@@%%%%%@@+ :@+++*- -+==#= - =@@*------*@@*:.... =@@+ =* .%. %. - +@@= +@@- =@@+ *: +%+++*= - *@@- .*@%- =@@+ - .*@%- .#@%=::---===*@@+ - .#@%: .#@@@@@@@@@@@@@#- -=- :-. .+##**#########%#= - :%@#: -@@*. -%@+ .#@@@###%@@@#***+- - -%@#. +@@= -@@+ .:::*@@+...-%@#. - =@@+ .::----====-. .*@%- =@@+.-#@@@@@@@@@%%%@@@#****+=++-. - +@@= :*@@@@@@@@@@@@%-:#@#: =@@+.*@@+:=%@@*=++#@@@%%@@@@@@%#: - +@@= =@@*::......=@@+-%@#. =@@+.*@%- .#@%: =@@+ .*@@=... - .#@%: =@@= :%@#=%@#. =@@+.*@%- :%@#. =@@= +@@= - :%@#. =@@= .#@%+@@*. =@@+:#@%- -@@*. +@@= .*@@- - -@@*. ..:*@@#++*****##@@#*@@#+===+++****%@@=:*@@%#%@@@####%@%: .*@#: - =@@%##%%%@@@@@@@%%%######*=::*%%%@@@%%%%%%%%#+. .=***####*####+: .*@#: - - - - ...... . .. - -======+++**##%+ %*---%::% @- - +@#+++*@@+=--:-%%. %. .% *= .+% - *@- :@# .%%. :@+++*- -+==#= - #@: -@#..:::-%%. =* .%. %. - .%%. :%%%%#####= :*- =- :%%###%%%%%+ *: +%+++*= - .@% *@- :@# ..-@@=:-@%:.. - -@* .=++++***:. %@: :@# .#@%%@@%%%@@###***= - =@+ .%@+===-=%%- %%. -@* :@# -@* -@#:*@*--. - +@= .%%. *@+ @# :@# :@# =@+ -@* =@+ - #@: .:%%======#@* @#-----==*@* -@%+#@#+=#@= +@= -.#@%%%%%%##*+++++=:. =******+++=. :===+==++- +@= - - - - - - diff --git a/i18n.py b/i18n.py index 9814e92..2d063d5 100644 --- a/i18n.py +++ b/i18n.py @@ -1,44 +1,48 @@ class firstrun(): - prompt1 = "Could not determine server's " + setting_not_found = "Could not determine server's {0}" empty_config = "Server configuration is empty. Deleting..." - prompt2 = "Is this the first time you are running the server?" + ft_question = "Is this the first time you are running the server?" config_not_found = "Configuration file not found!" exec = "Server will now run its configuration process" - fix_missing = "Please enter the Server's" + fix_missing = "Please enter the Server's {0}" welcome_message = "Welcome to the Account System Demonstration Backend" setup_server_dir = "Please enter the path to a folder where the server can store its files" keypair_setup = "Setting up Server Keypair..." initialize_db = "Setting up Databases for use..." security = "For security reasons, enter server launch password again." exit = "Server will now exit. Please run it again!" - class savedata(): - gui = "Opening file chooser dialog..." - nogui = "Cannot open file chooser! Enter the path manually:" - error = "An error occurred" - write_error = "An error occurred while trying to write server files.\nPlease choose another path" - not_a_dir = "Please enter path to a folder!" - creating = "Folder not accessible." - input_writable = "Please enter a writeable folder path" - created = "Written Server files successfully." - created_new = "Created Server folder successfully." - data_exists = "Previous Installation Detected! Please delete the files or choose another folder." - class database(): - host = 'Enter MySQL/MariaDB Server IP Address: ' - port = 'Enter MySQL/MariaDB Server Port (leave blank for 3306): ' - user = 'Enter Username of user with CREATE privilege: ' - passwd = 'Enter Password of the user: ' + listenaddr = "Enter Server Listen Address: " + listenport = "Enter Server Listen Port: " +class savedata(): + gui = "Opening file chooser dialog..." + nogui = "Cannot open file chooser! Enter the path manually:" + error = "An error occurred" + write_error = "An error occurred while trying to write server files.\nPlease choose another path" + not_a_dir = "Please enter path to a folder!" + creating = "Folder not accessible." + input_writable = "Please enter a writeable folder path" + created = "Written Server files successfully." + created_new = "Created Server folder successfully." + data_exists = "Previous Installation Detected! Please delete the files or choose another folder." +class database(): + host = 'Enter MySQL/MariaDB Server IP Address: ' + port = 'Enter MySQL/MariaDB Server Port (leave blank for 3306): ' + user = 'Enter Username of user with CREATE privilege: ' + passwd = 'Enter Password of the user: ' + creds_not_found = "Could not find database credentials. Rerunning server configuration process" - class passwd(): - explain = "\ - \ - " - input = "Enter the server's launch password: " - confirm = "Enter it again to confirm: " - retry = "Passwords do not match!" +class password(): + explain = "The server's 'Launch Password' is used to encrypt credentials.\n\ +The server will not launch without it." + input = "Enter the server's launch password: " + confirm = "Enter it again to confirm: " + retry = "Passwords do not match!" class log(): class tags(): info = '[INFO] ' warn = '[WARN] ' error = '[ERR] ' + class conn(): + disconnected = "Client {0} disconnected due to:\n\t{1}" server_start = "Server starting from path {0}...." \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 630bcae..7975005 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ asyncio captcha cffi cryptography -mysql-connector +mysql-connector-python Pillow pycparser PyYAML diff --git a/server_main.py b/server_main.py index 399dea6..dea641a 100644 --- a/server_main.py +++ b/server_main.py @@ -24,9 +24,9 @@ def execute_firstrun(): def check_missing_config(f, yaml, config): try: if yaml[config] is None: - print(i18n.firstrun.prompt1 + config) + print(i18n.firstrun.setting_not_found.format(config)) if config == 'working_directory': - print(i18n.firstrun.prompt2) + print(i18n.firstrun.ft_question) while True: choice = input("(Y / N) > ") if choice.lower() == 'y': @@ -42,12 +42,12 @@ def check_missing_config(f, yaml, config): else: fill_missing_config(f, yaml, config) except KeyError: - print(i18n.firstrun.prompt1 + config) + print(i18n.firstrun.setting_not_found.format(config)) fill_missing_config(f, yaml, config) def fill_missing_config(f, yaml, config): - print(i18n.firstrun.fix_missing, config) + print(i18n.firstrun.fix_missing.format(config)) yaml[config] = input('\n> ') if config in ['listen_port', 'any_other_int_type_config']: yaml[config] = int(yaml[config]) @@ -106,7 +106,7 @@ async def main(host, port): print(i18n.firstrun.config_not_found, i18n.firstrun.exec) execute_firstrun() except TypeError: - print("Could not find database credentials. Server will now run its configuration process again") + print(i18n.database.creds_not_found) execute_firstrun() workingdir = yaml['working_directory'] diff --git a/server_modules/encryption.py b/server_modules/encryption.py index 3235ed6..245b658 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -28,12 +28,13 @@ def create_rsa_key_pair(): def fernet_initkey(workingdir): passwd = '' while True: - passwd = getpass(i18n.firstrun.passwd.input) - confirm = getpass(i18n.firstrun.passwd.confirm) + print(i18n.firstrun.password.explain) + passwd = getpass(i18n.firstrun.password.input) + confirm = getpass(i18n.firstrun.password.confirm) if passwd == confirm: break else: - print(i18n.firstrun.passwd.retry) + print(i18n.firstrun.password.retry) # Generate a Fernet key with the password and save the salt salt = urandom(16) with open(f"{workingdir}/creds/salt", "wb") as f: diff --git a/server_modules/firstrun.py b/server_modules/firstrun.py index be16505..4d153de 100644 --- a/server_modules/firstrun.py +++ b/server_modules/firstrun.py @@ -1,6 +1,6 @@ import os import getpass -from i18n import firstrun +import i18n from . import encryption as e import pickle from yaml import dump as dumpyaml @@ -14,18 +14,18 @@ class working_dir(): def get_server_dir(): try: - print(firstrun.savedata.gui) + print(i18n.savedata.gui) return filedialog.askdirectory() except: - print(firstrun.savedata.nogui) + print(i18n.savedata.nogui) return input().rstrip('/\\') def create_directory(path): try: os.mkdir(path) - print(firstrun.savedata.created) + print(i18n.savedata.created) except OSError as e: - print(f"{firstrun.savedata.error}:\n{e}") + print(f"{i18n.savedata.error}:\n{e}") return False return True @@ -35,10 +35,10 @@ def setup_server_dir(): if os.path.exists(spath) and os.path.isdir(spath): pass elif os.path.exists(spath) and not os.path.isdir(spath): - spath = input(f"{firstrun.savedata.not_a_dir}:\n") + spath = input(f"{i18n.savedata.not_a_dir}:\n") elif not os.path.exists(spath): if not create_directory(spath): - spath = input(f"{firstrun.savedata.input_writable}:\n") + spath = input(f"{i18n.savedata.input_writable}:\n") continue creds_path = f'{spath}/creds' @@ -46,33 +46,33 @@ def setup_server_dir(): if create_directory(creds_path): break else: - print(firstrun.savedata.data_exists) + print(i18n.savedata.data_exists) return spath def save_db_credentials(fkey,workingdir): - host = input(firstrun.database.host) - port = input(firstrun.database.port) - user = input(firstrun.database.user) + host = input(i18n.database.host) + port = input(i18n.database.port) + user = input(i18n.database.user) if not port: port = 3306 else: port = int(port) - passwd = getpass.getpass(firstrun.database.passwd) + passwd = getpass.getpass(i18n.database.passwd) data = pickle.dumps({'host':host, 'port': port, 'user':user, 'passwd':passwd}) with open(f'{workingdir}/creds/db', 'wb') as f: f.write(fkey.encrypt(data)) def main(): - print(firstrun.welcome_message) - print(firstrun.setup_server_dir) + print(i18n.firstrun.welcome_message) + print(i18n.firstrun.setup_server_dir) workingdir = setup_server_dir() setattr(working_dir, 'workingdir', workingdir) fkey = e.fernet_initkey(workingdir) save_db_credentials(fkey, workingdir) del fkey - host = input("Enter Server Listen Address: ") - port = int(input("Enter Server Listen Port: ")) + host = input(i18n.firstrun.listenaddr) + port = int(input(i18n.firstrun.listenport)) with open(f'{os.path.dirname(os.path.abspath(__file__))}/../config.yml', 'w') as fi: config = {'working_directory': workingdir, 'listen_address': host, 'listen_port': port} fi.write(dumpyaml(config)) From 53716ce3d98f62a1b3b81d8726bd3063fa9e356c Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Sun, 26 Nov 2023 19:20:17 +0530 Subject: [PATCH 07/14] CLEANUP + full i18n --- i18n.py | 24 +++++++++++++++++++-- server_main.py | 18 +++++++--------- server_modules/db_handler.py | 18 +++++++++------- server_modules/encryption.py | 8 +++---- server_modules/packet_handler.py | 36 +++++++++++++------------------- 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/i18n.py b/i18n.py index 2d063d5..b1aaa3b 100644 --- a/i18n.py +++ b/i18n.py @@ -24,12 +24,14 @@ class savedata(): created = "Written Server files successfully." created_new = "Created Server folder successfully." data_exists = "Previous Installation Detected! Please delete the files or choose another folder." + class database(): host = 'Enter MySQL/MariaDB Server IP Address: ' port = 'Enter MySQL/MariaDB Server Port (leave blank for 3306): ' user = 'Enter Username of user with CREATE privilege: ' passwd = 'Enter Password of the user: ' creds_not_found = "Could not find database credentials. Rerunning server configuration process" + de_cred_fail = "Error while decrypting database credentials. Check your password\n{}" class password(): explain = "The server's 'Launch Password' is used to encrypt credentials.\n\ @@ -43,6 +45,24 @@ class tags(): info = '[INFO] ' warn = '[WARN] ' error = '[ERR] ' + debug = '[DEBUG] ' class conn(): - disconnected = "Client {0} disconnected due to:\n\t{1}" - server_start = "Server starting from path {0}...." \ No newline at end of file + attempt = "Remote {0} attempted connection" + init = "Remote {0} initiated connection with UUID: {1}" + disconnected = "Client {0} disconnected due to:\n\t{1}" + db_conn_success = "Connected to database {0}:{1}" + db_conn_err = "Could not connect to database: {}" + class db(): + init_success = 'Created schemas successfully' + init_fail = 'Failed to create schemas: {}' + class packet(): + pubkey_recv = "Received public key for {}" + signup_attempt = "Client {0} attempted SIGNUP with username '{1}'" + signup_success = "Account creation successful for '{}'." + token_gen = "Generated token for {}" + login_success = "User {0} logged in from {1}" + logout_success = "User {0} logged out from {1}" + captcha_gen = "Generated CAPTCHA for client {0} with code = {1}" + server_start = "Server starting from path {0}...." + server_online = "Server Online!" + server_exit = "Goodbye!" \ No newline at end of file diff --git a/server_main.py b/server_main.py index dea641a..7798411 100644 --- a/server_main.py +++ b/server_main.py @@ -49,7 +49,7 @@ def check_missing_config(f, yaml, config): def fill_missing_config(f, yaml, config): print(i18n.firstrun.fix_missing.format(config)) yaml[config] = input('\n> ') - if config in ['listen_port', 'any_other_int_type_config']: + if config in ('listen_port', 'any_other_int_type_config'): yaml[config] = int(yaml[config]) f.seek(0) f.write(dumpyaml(yaml)) @@ -67,7 +67,7 @@ async def catch(websocket): # Handle disconnection due to any exception except Exception as err3: client = await p.identify_client(websocket, SESSIONS) - print(f"[INFO] CLIENT {client} DISCONNECTED due to\n\t", err3) + print(i18n.log.tags.info + i18n.log.conn.disconnected.format(client, err3)) del SESSIONS[client] return None @@ -87,7 +87,7 @@ async def main(host, port): try: rootdir = os.path.dirname(os.path.abspath(__file__)) - print(i18n.log.tags.info + i18n.log.server_start.format(rootdir)) + print('\n' + i18n.log.tags.info + i18n.log.server_start.format(rootdir)) f = open(f'{rootdir}/config.yml', 'r+') yaml = loadyaml(f.read()) if not yaml: @@ -99,15 +99,13 @@ async def main(host, port): check_missing_config(f, yaml, 'listen_address') check_missing_config(f, yaml, 'listen_port') if not os.path.isfile(f"{yaml['working_directory']}/creds/db"): - raise TypeError("DB_CREDS_NOT_FOUND") + print(i18n.database.creds_not_found) + execute_firstrun() f.close() except FileNotFoundError as err: print(i18n.firstrun.config_not_found, i18n.firstrun.exec) execute_firstrun() - except TypeError: - print(i18n.database.creds_not_found) - execute_firstrun() workingdir = yaml['working_directory'] host, port = yaml['listen_address'], yaml['listen_port'] @@ -116,7 +114,7 @@ async def main(host, port): fkey = en.fermat_gen(workingdir) db.decrypt_creds(fkey, workingdir) except Exception as w: - print("Error while decrypting database credentials. Check your password\n", w) + print(i18n.database.de_cred_fail.format(w)) print(i18n.firstrun.exit) sys.exit() @@ -124,11 +122,11 @@ async def main(host, port): SERVER_CREDS['server_eprkey'] = server_eprkey SERVER_CREDS['server_epbkey'] = en.ser_key_pem(server_epbkey, 'public') - print("[INFO] SERVER ONLINE!") + print('\n' + i18n.log.tags.info + i18n.log.server_online) try: asyncio.run(main(host, port)) except KeyboardInterrupt: - print('\n[INFO] Goodbye!') + print('\n' + i18n.log.tags.info + i18n.log.server_exit) db.close() sys.exit() diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index e5ec223..44b9c52 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -2,6 +2,7 @@ from uuid import uuid4 import pickle from . import encryption as en +from i18n import log @@ -38,32 +39,33 @@ def decrypt_creds(fkey, workingdir): setattr(db, 'con', sqltor.connect(host = dict['host'], user = dict['user'], passwd = dict['passwd'])) setattr(db, 'cur', db.con.cursor()) if db.con.is_connected(): - print('[INFO] Connected to database ',dict['host'],':',dict['port'],sep='') + print(log.tags.info + log.conn.db_conn_success.format(dict['host'],dict['port'])) db.cur.execute("USE pfyt_accounts") except sqltor.errors.ProgrammingError as errrr: - print('[ERROR] Could not connect to database:', errrr) + print(log.tags.error + log.conn.db_conn_err.format(errrr)) def initialize_schemas(): try: query = queries['initialize'].rstrip(';').split(';\n') for i in query: - print('[DEBUG]',i) # print('[INFO] Created schemas successfully') db.cur.execute(i) db.con.commit() - print('[DEBUG] OK') + print(log.tags.info + log.tags.db.init_success) except Exception as error: - print('[ERROR] Failed to create schemas:', error) + print(log.tags.error + log.tags.db.init_fail.format(error)) def check_if_exists(value, field): col = fields_to_check[field]['attribute'] table = fields_to_check[field]['table'] - db.cur.execute(f"select {col} from {table} where {col} = '{value}'") + db.cur.execute(f"SELECT {col} FROM {table} WHERE {col} = '{value}'") data = db.cur.fetchall() try: if data[0][0] == value: return True except IndexError: return False + else: + return False def get_uuid(identifier): try: @@ -90,7 +92,6 @@ def create(username, fullname, dob, email, salted_pwd): db.cur.execute(query, (uuid, username, fullname, dob, email)) db.con.commit() pwd_query = "INSERT INTO pfyt_accounts.auth(UUID, SALTED_HASHBROWN) VALUES (%s, %s)" - print(f"[DEBUG | for {uuid}]", pwd_query) db.cur.execute(pwd_query, (uuid, salted_pwd)) db.con.commit() @@ -107,12 +108,12 @@ def check_pwd(pwd, identifier): db.cur.execute("SELECT SALTED_HASHBROWN FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) saltedpwd = db.cur.fetchall()[0][0] flag = en.db_check_pwd(pwd, saltedpwd) - print("[DEBUG]",flag,uuid) return (flag, uuid) def set_token(uuid, secret): db.cur.execute("UPDATE pfyt_accounts.auth SET TOKEN_SECRET = %s WHERE UUID = %s", (secret, uuid)) db.con.commit() + def get_token_key(uuid): db.cur.execute("SELECT TOKEN_SECRET FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) try: @@ -123,6 +124,7 @@ def get_token_key(uuid): return 'TOKEN_NOT_FOUND' except IndexError: return 'TOKEN_NOT_FOUND' + def logout(uuid): try: db.cur.execute("UPDATE pfyt_accounts.auth SET TOKEN_SECRET = NULL WHERE UUID = %s", (uuid,)) diff --git a/server_modules/encryption.py b/server_modules/encryption.py index 245b658..cdb2984 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -28,13 +28,13 @@ def create_rsa_key_pair(): def fernet_initkey(workingdir): passwd = '' while True: - print(i18n.firstrun.password.explain) - passwd = getpass(i18n.firstrun.password.input) - confirm = getpass(i18n.firstrun.password.confirm) + print(i18n.password.explain) + passwd = getpass(i18n.password.input) + confirm = getpass(i18n.password.confirm) if passwd == confirm: break else: - print(i18n.firstrun.password.retry) + print(i18n.password.retry) # Generate a Fernet key with the password and save the salt salt = urandom(16) with open(f"{workingdir}/creds/salt", "wb") as f: diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 97752e1..e795ca2 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -7,12 +7,13 @@ from captcha.image import ImageCaptcha from random import randint import datetime +from i18n import log async def identify_client(websocket, SESSIONS): return list(SESSIONS.keys())[[i[0] for i in list(SESSIONS.values())].index(websocket)] async def disconnect(ws, code, reason): - print(f"[INFO] CLIENT {ws.remote_address} DISCONNECTED due to",code,reason) + print(log.tags.info + log.conn.disconnected.format(ws.remote_address, code+' '+reason)) await ws.close(code=code, reason=reason) return 'CONN_CLOSED' @@ -41,11 +42,11 @@ def parse_time(time_string): async def establish_conn(SESSIONS, SERVER_CREDS, ws): - print(f"[INFO] Remote {ws.remote_address} attempted connection") + print(log.tags.info + log.conn.attempt.format(ws.remote_address)) uuid = str(UUID.uuid4()) SESSIONS[uuid] = [ws, None, None] # ws, public_key, user_uuid - print(f"[INFO] Remote {ws.remote_address} initiated connection with UUID: {uuid}") + print(log.tags.info + log.conn.init.format(ws.remote_address, uuid)) print(f"[INFO] Sending public key to {uuid}") # Encrypt Connection @@ -55,20 +56,20 @@ async def establish_conn(SESSIONS, SERVER_CREDS, ws): try: client_epbkey = s.load_pem_public_key(client_epbkey['data']) except Exception as e: - print(f"[INFO] Client un-established {ws.remote_address} DISCONNECTED due to INVALID_PACKET") + print(log.tags.info + log.conn.disconnected.format(ws.remote_address, "INVALID_PACKET")) await ws.close(code = 1008, reason = "Invalid packet structure") return 'CONN_CLOSED' SESSIONS[uuid][1] = client_epbkey - print(f"[INFO] Received public key for {uuid}") + print(log.tags.info + log.packet.pubkey_recv.format(uuid)) del client_epbkey return en.encrypt_packet( {'type':'STATUS', 'data':{'sig':'CONN_OK'}}, SESSIONS[uuid][1], ) - # If client sends bullshit instead of its PEM serialized ephemeral public key + # If client sends something else instead of its PEM serialized ephemeral public key except Exception as err2: - print(f"[INFO] CLIENT {uuid} {ws.remote_address} DISCONNECTED due to INVALID_CONN_KEY:\n\t",err2) + print(log.tags.info + log.conn.disconnected.format(ws.remote_address, err2)) await ws.close(code = 1003, reason = "Connection Public Key in invalid format") del SESSIONS[uuid] return 'CONN_CLOSED' @@ -78,15 +79,6 @@ async def get_resp_packet(SESSIONS, ws, de_packet): en_packet = en.encrypt_packet(de_packet, SESSIONS[uuid][1]) return en_packet -async def send_user_packet(SESSIONS, SERVER_CREDS, user_uuid, de_packet): - try: - con_uuid = list(SESSIONS.keys())[[i[2] for i in list(SESSIONS.values())].index(user_uuid)] - ws = SESSIONS[con_uuid][0] - ws.send(en.encrypt_packet(de_packet, SESSIONS[con_uuid[1]])) - except Exception as ear: - print("[DEBUG] Queued Packet for", user_uuid, "due to:\n\t", ear) - db.queue_packet(user_uuid, en.encrypt_packet(de_packet, SERVER_CREDS['queue_pubkey'])) - async def signup(SESSIONS, SERVER_CREDS, ws, data): uuid = list(SESSIONS.keys())[[i[0] for i in list(SESSIONS.values())].index(ws)] try: @@ -112,13 +104,14 @@ async def signup(SESSIONS, SERVER_CREDS, ws, data): return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':error_message}}) resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) if resp_captcha == True: - print(f"[INFO] CLIENT {uuid} ATTEMPTED SIGNUP WITH username {user}") + print(log.tags.info + log.packet.signup_attempt.format(uuid, user)) salted_pwd = en.salt_pwd(password) try: db.Account.create(user, fullname, dob, email, salted_pwd) except Exception as errr: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'SIGNUP_ERR','desc':errr}}) else: + print(log.tags.info + log.packet.signup_success.format(user)) return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'SIGNUP_OK'}}) elif resp_captcha == False: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CAPTCHA_WRONG'}}) @@ -140,7 +133,7 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): secret, access_token = en.gen_token(uuid, 1) db.Account.set_token(uuid, secret) en_packet = await get_resp_packet(SESSIONS, ws, {'type':'TOKEN_GEN','data':{'token':access_token}}) - print("[INFO] Generated token for", uuid) + print(log.tags.info + log.packet.token_gen.format(uuid)) return en_packet elif flag == False: return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_INCORRECT_PASSWORD'}}) @@ -164,8 +157,8 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): if flag == 'TOKEN_OK': SESSIONS[con_uuid][2] = user_uuid # tasks to run on login - print("[INFO] User", user_uuid, "logged in from", ws.remote_address) - print("[DEBUG] SESSIONS:", SESSIONS) + print(log.tags.info + log.packet.login_success.format(user_uuid, ws.remote_address)) + print("[DEBUG] SESSIONS:", SESSIONS) # This line is not meant for production return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_OK'}}) elif flag == 'TOKEN_EXPIRED': return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_EXPIRED'}}) @@ -180,6 +173,7 @@ async def logout(SESSIONS, SERVER_CREDS, ws, data): flag = db.Account.logout(user) if flag == 'SUCCESS': SESSIONS[sender][2] = None + print(log.tags.info + log.packet.logout_success.format(sender, ws.remote_address)) print("[DEBUG] SESSIONS:", SESSIONS) return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_OK'}}) elif flag == 'FAILURE': @@ -193,7 +187,7 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): packet = en.encrypt_packet({'type':'CAPTCHA', 'data':{'challenge':image}}, SESSIONS[uuid][1]) await ws.send(packet) - print(f"[INFO] GENERATED CAPTCHA FOR CLIENT {uuid} with CODE {challenge}") + print(log.tags.debug + log.packet.captcha_gen.format(uuid, challenge)) # This line is not meant for production resp = await ws.recv() # handle possible INVALID_PACKET in next line From cf8002069646258bccc078b58f46f987811b7518 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Sun, 26 Nov 2023 20:04:56 +0530 Subject: [PATCH 08/14] Refactor (thank you pycharm for yelling @me) --- i18n.py | 28 +++++--- server_main.py | 25 +++---- server_modules/db_handler.py | 24 ++++--- server_modules/encryption.py | 32 ++++++--- server_modules/firstrun.py | 23 +++--- server_modules/packet_handler.py | 117 +++++++++++++++++-------------- 6 files changed, 144 insertions(+), 105 deletions(-) diff --git a/i18n.py b/i18n.py index b1aaa3b..8c0876a 100644 --- a/i18n.py +++ b/i18n.py @@ -1,4 +1,4 @@ -class firstrun(): +class firstrun: setting_not_found = "Could not determine server's {0}" empty_config = "Server configuration is empty. Deleting..." ft_question = "Is this the first time you are running the server?" @@ -13,7 +13,9 @@ class firstrun(): exit = "Server will now exit. Please run it again!" listenaddr = "Enter Server Listen Address: " listenport = "Enter Server Listen Port: " -class savedata(): + + +class savedata: gui = "Opening file chooser dialog..." nogui = "Cannot open file chooser! Enter the path manually:" error = "An error occurred" @@ -25,7 +27,8 @@ class savedata(): created_new = "Created Server folder successfully." data_exists = "Previous Installation Detected! Please delete the files or choose another folder." -class database(): + +class database: host = 'Enter MySQL/MariaDB Server IP Address: ' port = 'Enter MySQL/MariaDB Server Port (leave blank for 3306): ' user = 'Enter Username of user with CREATE privilege: ' @@ -33,29 +36,34 @@ class database(): creds_not_found = "Could not find database credentials. Rerunning server configuration process" de_cred_fail = "Error while decrypting database credentials. Check your password\n{}" -class password(): + +class password: explain = "The server's 'Launch Password' is used to encrypt credentials.\n\ The server will not launch without it." input = "Enter the server's launch password: " confirm = "Enter it again to confirm: " retry = "Passwords do not match!" -class log(): - class tags(): + +class log: + class tags: info = '[INFO] ' warn = '[WARN] ' error = '[ERR] ' debug = '[DEBUG] ' - class conn(): + + class conn: attempt = "Remote {0} attempted connection" init = "Remote {0} initiated connection with UUID: {1}" disconnected = "Client {0} disconnected due to:\n\t{1}" db_conn_success = "Connected to database {0}:{1}" db_conn_err = "Could not connect to database: {}" - class db(): + + class db: init_success = 'Created schemas successfully' init_fail = 'Failed to create schemas: {}' - class packet(): + + class packet: pubkey_recv = "Received public key for {}" signup_attempt = "Client {0} attempted SIGNUP with username '{1}'" signup_success = "Account creation successful for '{}'." @@ -65,4 +73,4 @@ class packet(): captcha_gen = "Generated CAPTCHA for client {0} with code = {1}" server_start = "Server starting from path {0}...." server_online = "Server Online!" - server_exit = "Goodbye!" \ No newline at end of file + server_exit = "Goodbye!" diff --git a/server_main.py b/server_main.py index 7798411..dd60994 100644 --- a/server_main.py +++ b/server_main.py @@ -1,4 +1,5 @@ -import os, sys +import os +import sys import asyncio import websockets from server_modules import firstrun @@ -21,9 +22,9 @@ def execute_firstrun(): sys.exit() -def check_missing_config(f, yaml, config): +def check_missing_config(f, yamlc, config): try: - if yaml[config] is None: + if yamlc[config] is None: print(i18n.firstrun.setting_not_found.format(config)) if config == 'working_directory': print(i18n.firstrun.ft_question) @@ -37,22 +38,22 @@ def check_missing_config(f, yaml, config): print(i18n.firstrun.exit) sys.exit() elif choice.lower() == 'n': - fill_missing_config(f, yaml, 'working_directory') + fill_missing_config(f, yamlc, 'working_directory') break else: - fill_missing_config(f, yaml, config) + fill_missing_config(f, yamlc, config) except KeyError: print(i18n.firstrun.setting_not_found.format(config)) - fill_missing_config(f, yaml, config) + fill_missing_config(f, yamlc, config) -def fill_missing_config(f, yaml, config): +def fill_missing_config(f, yamlc, config): print(i18n.firstrun.fix_missing.format(config)) - yaml[config] = input('\n> ') + yamlc[config] = input('\n> ') if config in ('listen_port', 'any_other_int_type_config'): - yaml[config] = int(yaml[config]) + yamlc[config] = int(yamlc[config]) f.seek(0) - f.write(dumpyaml(yaml)) + f.write(dumpyaml(yamlc)) async def catch(websocket): @@ -72,9 +73,9 @@ async def catch(websocket): return None -async def main(host, port): +async def main(chost, cport): async with websockets.serve( - catch, host=host, port=port, + catch, host=chost, port=cport, ping_interval=120, ping_timeout=None, close_timeout=None, max_size=1048576 ): diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index 44b9c52..b8624e3 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -4,8 +4,6 @@ from . import encryption as en from i18n import log - - initialize_ddl = """DROP SCHEMA IF EXISTS pfyt_accounts; CREATE DATABASE IF NOT EXISTS pfyt_accounts; CREATE TABLE IF NOT EXISTS pfyt_accounts.users ( @@ -25,25 +23,29 @@ queries = {'initialize': initialize_ddl} fields_to_check = { - 'username':{'table':'pfyt_accounts.users','attribute':'USERNAME'}} + 'username': {'table': 'pfyt_accounts.users', 'attribute': 'USERNAME'}} + + class db: con = None cur = None + def decrypt_creds(fkey, workingdir): with open(f'{workingdir}/creds/db', 'rb') as f: data = f.read() decrypted = fkey.decrypt(data) dict = pickle.loads(decrypted) try: - setattr(db, 'con', sqltor.connect(host = dict['host'], user = dict['user'], passwd = dict['passwd'])) + setattr(db, 'con', sqltor.connect(host=dict['host'], user=dict['user'], passwd=dict['passwd'])) setattr(db, 'cur', db.con.cursor()) if db.con.is_connected(): - print(log.tags.info + log.conn.db_conn_success.format(dict['host'],dict['port'])) + print(log.tags.info + log.conn.db_conn_success.format(dict['host'], dict['port'])) db.cur.execute("USE pfyt_accounts") except sqltor.errors.ProgrammingError as errrr: print(log.tags.error + log.conn.db_conn_err.format(errrr)) + def initialize_schemas(): try: query = queries['initialize'].rstrip(';').split(';\n') @@ -54,11 +56,12 @@ def initialize_schemas(): except Exception as error: print(log.tags.error + log.tags.db.init_fail.format(error)) + def check_if_exists(value, field): col = fields_to_check[field]['attribute'] table = fields_to_check[field]['table'] db.cur.execute(f"SELECT {col} FROM {table} WHERE {col} = '{value}'") - data = db.cur.fetchall() + data = db.cur.fetchall() try: if data[0][0] == value: return True @@ -67,6 +70,7 @@ def check_if_exists(value, field): else: return False + def get_uuid(identifier): try: if '@' not in identifier: @@ -79,22 +83,24 @@ def get_uuid(identifier): return 'ACCOUNT_DNE' return uuid + def close(): if db.con is not None: db.con.close() + class Account(db): def create(username, fullname, dob, email, salted_pwd): # UUID, USERNAME, FULL_NAME, DOB, EMAIL, CREATION uuid = str(uuid4()) query = "INSERT INTO pfyt_accounts.users(UUID, USERNAME, FULL_NAME, DOB, EMAIL) VALUES (%s, %s, %s, %s, %s)" - print(f"[DEBUG | for {uuid}]",query) + print(f"[DEBUG | for {uuid}]", query) db.cur.execute(query, (uuid, username, fullname, dob, email)) db.con.commit() pwd_query = "INSERT INTO pfyt_accounts.auth(UUID, SALTED_HASHBROWN) VALUES (%s, %s)" db.cur.execute(pwd_query, (uuid, salted_pwd)) db.con.commit() - + def check_pwd(pwd, identifier): try: if '@' not in identifier: @@ -108,7 +114,7 @@ def check_pwd(pwd, identifier): db.cur.execute("SELECT SALTED_HASHBROWN FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) saltedpwd = db.cur.fetchall()[0][0] flag = en.db_check_pwd(pwd, saltedpwd) - return (flag, uuid) + return flag, uuid def set_token(uuid, secret): db.cur.execute("UPDATE pfyt_accounts.auth SET TOKEN_SECRET = %s WHERE UUID = %s", (secret, uuid)) diff --git a/server_modules/encryption.py b/server_modules/encryption.py index cdb2984..872ee02 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -5,7 +5,6 @@ from cryptography.fernet import Fernet from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.exceptions import InvalidSignature from secrets import token_hex from getpass import getpass from os import urandom @@ -15,6 +14,7 @@ import jwt import datetime + def create_rsa_key_pair(): # Generate a 2048-bit RSA private key private_key = rsa.generate_private_key( @@ -25,8 +25,8 @@ def create_rsa_key_pair(): public_key = private_key.public_key() return private_key, public_key + def fernet_initkey(workingdir): - passwd = '' while True: print(i18n.password.explain) passwd = getpass(i18n.password.input) @@ -49,6 +49,7 @@ def fernet_initkey(workingdir): key = Fernet(key) return key + def fermat_gen(workingdir): passwd = getpass("Enter Password: ") with open(f"{workingdir}/creds/salt", "rb") as f: @@ -63,6 +64,7 @@ def fermat_gen(workingdir): key = Fernet(key) return key + def ser_key_pem(key, type: str): if type == 'public': return key.public_bytes( @@ -75,12 +77,15 @@ def ser_key_pem(key, type: str): format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ) + + def deser_pem(key, type): if type == 'public': return serialization.load_pem_public_key(key) elif type == 'private': return serialization.load_pem_private_key(key, password=None) + def encrypt_packet(data, pubkey): data = pickle.dumps(data) # Generate symmetric key and encrypt it @@ -103,7 +108,8 @@ def encrypt_packet(data, pubkey): encryptor = cipher.encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() - return pickle.dumps({'skey':encrypted_skey, 'cbc':cbc, 'ciphertext':ciphertext}) + return pickle.dumps({'skey': encrypted_skey, 'cbc': cbc, 'ciphertext': ciphertext}) + def decrypt_packet(encrypted_data, privkey): try: @@ -129,8 +135,9 @@ def decrypt_packet(encrypted_data, privkey): data = unpadder.update(decrypted_data) + unpadder.finalize() return pickle.loads(data) except Exception as error: - return {'type':'decrypt_error','data':f'{error}'} - + return {'type': 'decrypt_error', 'data': f'{error}'} + + def salt_pwd(password): pwd = password.encode() salt = urandom(16) @@ -146,7 +153,8 @@ def salt_pwd(password): key = urlsafe_b64encode(kdf.derive(pwd)) # Store the salt and key in your database - return pickle.dumps({'salt':salt, 'key':key}) + return pickle.dumps({'salt': salt, 'key': key}) + def db_check_pwd(pwd, saltedpwd): salted_pwd = pickle.loads(saltedpwd) @@ -166,7 +174,8 @@ def db_check_pwd(pwd, saltedpwd): return True else: return False - + + def gen_token(user, validity): """ `validity` in days @@ -174,11 +183,12 @@ def gen_token(user, validity): secret = token_hex(32) payload = { "sub": user, - "iat": datetime.datetime.utcnow(), - "exp": datetime.datetime.utcnow() + datetime.timedelta(days = validity) + "iat": datetime.datetime.now(datetime.UTC), + "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=validity) } access_token = jwt.encode(payload, secret, algorithm="HS256") - return (secret, access_token) + return secret, access_token + def validate_token(key, token, user): try: @@ -193,4 +203,4 @@ def validate_token(key, token, user): elif decoded_token['sub'] != user: return 'TOKEN_INVALID' else: - return 'TOKEN_INVALID' \ No newline at end of file + return 'TOKEN_INVALID' diff --git a/server_modules/firstrun.py b/server_modules/firstrun.py index 4d153de..bca06bb 100644 --- a/server_modules/firstrun.py +++ b/server_modules/firstrun.py @@ -2,16 +2,19 @@ import getpass import i18n from . import encryption as e -import pickle +import pickle from yaml import dump as dumpyaml -try: + +try: from tkinter import filedialog except: pass + class working_dir(): workingdir = '' + def get_server_dir(): try: print(i18n.savedata.gui) @@ -20,15 +23,17 @@ def get_server_dir(): print(i18n.savedata.nogui) return input().rstrip('/\\') + def create_directory(path): try: os.mkdir(path) print(i18n.savedata.created) - except OSError as e: - print(f"{i18n.savedata.error}:\n{e}") + except OSError as err: + print(f"{i18n.savedata.error}:\n{err}") return False return True + def setup_server_dir(): while True: spath = get_server_dir() @@ -50,7 +55,8 @@ def setup_server_dir(): return spath -def save_db_credentials(fkey,workingdir): + +def save_db_credentials(fkey, workingdir): host = input(i18n.database.host) port = input(i18n.database.port) user = input(i18n.database.user) @@ -59,10 +65,11 @@ def save_db_credentials(fkey,workingdir): else: port = int(port) passwd = getpass.getpass(i18n.database.passwd) - data = pickle.dumps({'host':host, 'port': port, 'user':user, 'passwd':passwd}) + data = pickle.dumps({'host': host, 'port': port, 'user': user, 'passwd': passwd}) with open(f'{workingdir}/creds/db', 'wb') as f: f.write(fkey.encrypt(data)) + def main(): print(i18n.firstrun.welcome_message) print(i18n.firstrun.setup_server_dir) @@ -76,7 +83,3 @@ def main(): with open(f'{os.path.dirname(os.path.abspath(__file__))}/../config.yml', 'w') as fi: config = {'working_directory': workingdir, 'listen_address': host, 'listen_port': port} fi.write(dumpyaml(config)) - - - - diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index e795ca2..6eac221 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -9,16 +9,17 @@ import datetime from i18n import log + async def identify_client(websocket, SESSIONS): return list(SESSIONS.keys())[[i[0] for i in list(SESSIONS.values())].index(websocket)] + async def disconnect(ws, code, reason): - print(log.tags.info + log.conn.disconnected.format(ws.remote_address, code+' '+reason)) + print(log.tags.info + log.conn.disconnected.format(ws.remote_address, code + ' ' + reason)) await ws.close(code=code, reason=reason) return 'CONN_CLOSED' -#async def get_packet(ws, type): -# # packet validator + def parse_date(date_string): date_formats = ["%d/%m/%Y", "%d-%m-%Y", "%Y-%m-%d", "%Y/%m/%d"] @@ -32,6 +33,7 @@ def parse_date(date_string): else: return 'PARSE_ERR' + def parse_time(time_string): try: time_obj = datetime.datetime.strptime(time_string, '%Y-%m-%d %H:%M:%S') @@ -39,46 +41,48 @@ def parse_time(time_string): return 'PARSE_ERR' else: return 'VALID_TIME' - + async def establish_conn(SESSIONS, SERVER_CREDS, ws): print(log.tags.info + log.conn.attempt.format(ws.remote_address)) uuid = str(UUID.uuid4()) - SESSIONS[uuid] = [ws, None, None] # ws, public_key, user_uuid + SESSIONS[uuid] = [ws, None, None] # ws, public_key, user_uuid print(log.tags.info + log.conn.init.format(ws.remote_address, uuid)) print(f"[INFO] Sending public key to {uuid}") # Encrypt Connection - await ws.send(pickle.dumps({'type':'CONN_ENCRYPT_S','data':SERVER_CREDS['server_epbkey']})) + await ws.send(pickle.dumps({'type': 'CONN_ENCRYPT_S', 'data': SERVER_CREDS['server_epbkey']})) try: client_epbkey = pickle.loads(await ws.recv()) try: client_epbkey = s.load_pem_public_key(client_epbkey['data']) - except Exception as e: + except Exception: print(log.tags.info + log.conn.disconnected.format(ws.remote_address, "INVALID_PACKET")) - await ws.close(code = 1008, reason = "Invalid packet structure") + await ws.close(code=1008, reason="Invalid packet structure") return 'CONN_CLOSED' - + SESSIONS[uuid][1] = client_epbkey print(log.tags.info + log.packet.pubkey_recv.format(uuid)) del client_epbkey return en.encrypt_packet( - {'type':'STATUS', 'data':{'sig':'CONN_OK'}}, + {'type': 'STATUS', 'data': {'sig': 'CONN_OK'}}, SESSIONS[uuid][1], - ) + ) # If client sends something else instead of its PEM serialized ephemeral public key except Exception as err2: print(log.tags.info + log.conn.disconnected.format(ws.remote_address, err2)) - await ws.close(code = 1003, reason = "Connection Public Key in invalid format") + await ws.close(code=1003, reason="Connection Public Key in invalid format") del SESSIONS[uuid] return 'CONN_CLOSED' - + + async def get_resp_packet(SESSIONS, ws, de_packet): uuid = await identify_client(ws, SESSIONS) en_packet = en.encrypt_packet(de_packet, SESSIONS[uuid][1]) return en_packet + async def signup(SESSIONS, SERVER_CREDS, ws, data): uuid = list(SESSIONS.keys())[[i[0] for i in list(SESSIONS.values())].index(ws)] try: @@ -88,7 +92,8 @@ async def signup(SESSIONS, SERVER_CREDS, ws, data): dob = parse_date(data['dob']) password = data['password'] except KeyError as ero: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'SIGNUP_MISSING_CREDS','desc':ero}}) + return await get_resp_packet(SESSIONS, ws, + {'type': 'STATUS', 'data': {'sig': 'SIGNUP_MISSING_CREDS', 'desc': ero}}) # Define validation rules validation_rules = [ (len(user) > 32, 'SIGNUP_USERNAME_ABOVE_LIMIT'), @@ -101,20 +106,21 @@ async def signup(SESSIONS, SERVER_CREDS, ws, data): # Validate user input for condition, error_message in validation_rules: if condition: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':error_message}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': error_message}}) resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) - if resp_captcha == True: + if resp_captcha is True: print(log.tags.info + log.packet.signup_attempt.format(uuid, user)) salted_pwd = en.salt_pwd(password) try: db.Account.create(user, fullname, dob, email, salted_pwd) except Exception as errr: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'SIGNUP_ERR','desc':errr}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'SIGNUP_ERR', 'desc': errr}}) else: print(log.tags.info + log.packet.signup_success.format(user)) - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'SIGNUP_OK'}}) - elif resp_captcha == False: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CAPTCHA_WRONG'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'SIGNUP_OK'}}) + elif resp_captcha is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'CAPTCHA_WRONG'}}) + async def login(SESSIONS, SERVER_CREDS, ws, data): try: @@ -122,25 +128,27 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): password = data['password'] dont_ask_again = data['save'] except KeyError as ero: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_MISSING_CREDS','desc':ero}}) + return await get_resp_packet(SESSIONS, ws, + {'type': 'STATUS', 'data': {'sig': 'LOGIN_MISSING_CREDS', 'desc': ero}}) resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) - if resp_captcha == True: + if resp_captcha is True: flag, uuid = db.Account.check_pwd(password, identifier) - if flag == True: - if dont_ask_again == True: + if flag is True: + if dont_ask_again is True: secret, access_token = en.gen_token(uuid, 30) else: secret, access_token = en.gen_token(uuid, 1) db.Account.set_token(uuid, secret) - en_packet = await get_resp_packet(SESSIONS, ws, {'type':'TOKEN_GEN','data':{'token':access_token}}) + en_packet = await get_resp_packet(SESSIONS, ws, {'type': 'TOKEN_GEN', 'data': {'token': access_token}}) print(log.tags.info + log.packet.token_gen.format(uuid)) return en_packet - elif flag == False: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_INCORRECT_PASSWORD'}}) + elif flag is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGIN_INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_ACCOUNT_NOT_FOUND'}}) - elif resp_captcha == False: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'CAPTCHA_WRONG'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGIN_ACCOUNT_NOT_FOUND'}}) + elif resp_captcha is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'CAPTCHA_WRONG'}}) + async def auth(SESSIONS, SERVER_CREDS, ws, data): # REMINDER TO HANDLE LOGIN FROM TWO DEVICES @@ -149,45 +157,47 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): con_uuid = await identify_client(ws, SESSIONS) user_uuid = db.get_uuid(user) if user_uuid == 'ACCOUNT_DNE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'ACCOUNT_DNE'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACCOUNT_DNE'}}) key = db.Account.get_token_key(user_uuid) if key == 'TOKEN_NOT_FOUND': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_NOT_FOUND'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'TOKEN_NOT_FOUND'}}) flag = en.validate_token(key, token, user_uuid) if flag == 'TOKEN_OK': SESSIONS[con_uuid][2] = user_uuid # tasks to run on login print(log.tags.info + log.packet.login_success.format(user_uuid, ws.remote_address)) - print("[DEBUG] SESSIONS:", SESSIONS) # This line is not meant for production - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGIN_OK'}}) + print("[DEBUG] SESSIONS:", SESSIONS) # This line is not meant for production + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGIN_OK'}}) elif flag == 'TOKEN_EXPIRED': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_EXPIRED'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'TOKEN_EXPIRED'}}) elif flag == 'TOKEN_INVALID': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'TOKEN_INVALID'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'TOKEN_INVALID'}}) + async def logout(SESSIONS, SERVER_CREDS, ws, data): sender = await identify_client(ws, SESSIONS) user = SESSIONS[sender][2] if not user: - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'NOT_LOGGED_IN'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'NOT_LOGGED_IN'}}) flag = db.Account.logout(user) if flag == 'SUCCESS': SESSIONS[sender][2] = None print(log.tags.info + log.packet.logout_success.format(sender, ws.remote_address)) print("[DEBUG] SESSIONS:", SESSIONS) - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_OK'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGOUT_OK'}}) elif flag == 'FAILURE': - return await get_resp_packet(SESSIONS, ws, {'type':'STATUS','data':{'sig':'LOGOUT_ERR'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGOUT_ERR'}}) + async def captcha(SESSIONS, SERVER_CREDS, ws, data): uuid = await identify_client(ws, SESSIONS) - challenge = str(randint(100000,999999)) + challenge = str(randint(100000, 999999)) data = ImageCaptcha().generate(challenge) image = data.getvalue() - packet = en.encrypt_packet({'type':'CAPTCHA', 'data':{'challenge':image}}, SESSIONS[uuid][1]) + packet = en.encrypt_packet({'type': 'CAPTCHA', 'data': {'challenge': image}}, SESSIONS[uuid][1]) await ws.send(packet) - print(log.tags.debug + log.packet.captcha_gen.format(uuid, challenge)) # This line is not meant for production + print(log.tags.debug + log.packet.captcha_gen.format(uuid, challenge)) # This line is not meant for production resp = await ws.recv() # handle possible INVALID_PACKET in next line @@ -195,17 +205,19 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): de_resp = de_resp['data']['solved'] return int(de_resp) == int(challenge) + upacket_map = { - 'CONN_INIT':1, - 'CONN_ENCRYPT_C':2 + 'CONN_INIT': 1, + 'CONN_ENCRYPT_C': 2 } packet_map = { - 'SIGNUP':signup, - 'LOGIN':login, - 'AUTH_TOKEN':auth, - 'LOGOUT':logout + 'SIGNUP': signup, + 'LOGIN': login, + 'AUTH_TOKEN': auth, + 'LOGOUT': logout } + async def handle(SESSIONS, SERVER_CREDS, packet, ws): if 'type'.encode() in packet: de_packet = pickle.loads(packet) @@ -215,15 +227,14 @@ async def handle(SESSIONS, SERVER_CREDS, packet, ws): await disconnect(ws, 1008, "Invalid Packet Structure") return 'CONN_CLOSED' else: - type = de_packet['type'] + ptype = de_packet['type'] data = de_packet['data'] - if type == 'CONN_INIT': + if ptype == 'CONN_INIT': return await establish_conn(SESSIONS, SERVER_CREDS, ws) - elif type in packet_map.keys(): - func = packet_map[type] + elif ptype in packet_map.keys(): + func = packet_map[ptype] return await func(SESSIONS, SERVER_CREDS, ws, data) else: await disconnect(ws, 1008, "Invalid Packet Structure") return 'CONN_CLOSED' - From 4b9f3e5de1d3e21cd676c1704c0ddc20c2566fe1 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Thu, 30 Nov 2023 09:01:29 +0530 Subject: [PATCH 09/14] Account Delete Functionality --- i18n.py | 1 + requirements.txt | 11 ---------- server_modules/db_handler.py | 34 ++++++++++++++++--------------- server_modules/packet_handler.py | 35 ++++++++++++++++++++++++++------ 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/i18n.py b/i18n.py index 8c0876a..a928adc 100644 --- a/i18n.py +++ b/i18n.py @@ -71,6 +71,7 @@ class packet: login_success = "User {0} logged in from {1}" logout_success = "User {0} logged out from {1}" captcha_gen = "Generated CAPTCHA for client {0} with code = {1}" + acc_delete = "Account deletion successful for '{}'." server_start = "Server starting from path {0}...." server_online = "Server Online!" server_exit = "Goodbye!" diff --git a/requirements.txt b/requirements.txt index 7975005..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +0,0 @@ -asyncio -captcha -cffi -cryptography -mysql-connector-python -Pillow -pycparser -PyYAML -uuid -websockets -PyJWT \ No newline at end of file diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index b8624e3..cf73ddf 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -70,20 +70,6 @@ def check_if_exists(value, field): else: return False - -def get_uuid(identifier): - try: - if '@' not in identifier: - db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE USERNAME=%s", (identifier,)) - uuid = db.cur.fetchall()[0][0] - elif '@' in identifier: - db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE EMAIL=%s", (identifier,)) - uuid = db.cur.fetchall()[0][0] - except IndexError: - return 'ACCOUNT_DNE' - return uuid - - def close(): if db.con is not None: db.con.close() @@ -101,7 +87,7 @@ def create(username, fullname, dob, email, salted_pwd): db.cur.execute(pwd_query, (uuid, salted_pwd)) db.con.commit() - def check_pwd(pwd, identifier): + def get_uuid(identifier): try: if '@' not in identifier: db.cur.execute("SELECT UUID FROM pfyt_accounts.users WHERE USERNAME=%s", (identifier,)) @@ -111,6 +97,12 @@ def check_pwd(pwd, identifier): uuid = db.cur.fetchall()[0][0] except IndexError: return 'ACCOUNT_DNE' + return uuid + + def check_pwd(pwd, identifier): + uuid = Account.get_uuid(identifier) + if uuid == 'ACCOUNT_DNE': + return 'ACCOUNT_DNE' db.cur.execute("SELECT SALTED_HASHBROWN FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) saltedpwd = db.cur.fetchall()[0][0] flag = en.db_check_pwd(pwd, saltedpwd) @@ -137,5 +129,15 @@ def logout(uuid): db.con.commit() return 'SUCCESS' except Exception as err: - print(err) + print(log.tags.error + "LOGOUT_DB_ERROR:", err) return 'FAILURE' + + def delete(uuid): + try: + db.cur.execute("DELETE FROM pfyt_accounts.users WHERE UUID = %s", (uuid,)) + db.cur.execute("DELETE FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) + db.con.commit() + return 'SUCCESS' + except Exception as err: + print(log.tags.error + "ACC_DELETE_DB_ERROR:", err) + return 'FAILURE' \ No newline at end of file diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 6eac221..5bf614b 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -155,7 +155,7 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): user = data['user'] token = data['token'] con_uuid = await identify_client(ws, SESSIONS) - user_uuid = db.get_uuid(user) + user_uuid = db.Account.get_uuid(user) if user_uuid == 'ACCOUNT_DNE': return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACCOUNT_DNE'}}) key = db.Account.get_token_key(user_uuid) @@ -189,6 +189,32 @@ async def logout(SESSIONS, SERVER_CREDS, ws, data): return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGOUT_ERR'}}) +async def delete(SESSIONS, SERVER_CREDS, ws, data): + try: + identifier = data['id'] + password = data['password'] + except KeyError as ero: + return await get_resp_packet(SESSIONS, ws, + {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_MISSING_CREDS', 'desc': ero}}) + resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) + if resp_captcha is True: + flag, uuid = db.Account.check_pwd(password, identifier) + if flag is True: + user = db.Account.get_uuid(identifier) + dflag = db.Account.delete(user) + if dflag == 'SUCCESS': + print(log.tags.info + log.packet.acc_delete.format(identifier)) + return get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_SUCCESS'}}) + elif dflag == 'FAILURE': + return get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_ERR'}}) + elif flag is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_INCORRECT_PASSWORD'}}) + elif flag == 'ACCOUNT_DNE': + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'DELETE_NONEXISTENT_ACCOUNT'}}) + elif resp_captcha is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'CAPTCHA_WRONG'}}) + + async def captcha(SESSIONS, SERVER_CREDS, ws, data): uuid = await identify_client(ws, SESSIONS) challenge = str(randint(100000, 999999)) @@ -206,15 +232,12 @@ async def captcha(SESSIONS, SERVER_CREDS, ws, data): return int(de_resp) == int(challenge) -upacket_map = { - 'CONN_INIT': 1, - 'CONN_ENCRYPT_C': 2 -} packet_map = { 'SIGNUP': signup, 'LOGIN': login, 'AUTH_TOKEN': auth, - 'LOGOUT': logout + 'LOGOUT': logout, + 'DELETE': delete } From 0548be352e2af006e0e49593457bb2c6610e902d Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Fri, 1 Dec 2023 04:02:56 +0530 Subject: [PATCH 10/14] Reduce redundant signals --- requirements.txt | Bin 0 -> 422 bytes server_modules/db_handler.py | 17 +++++--------- server_modules/packet_handler.py | 37 ++++++++++++++++++------------- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/requirements.txt b/requirements.txt index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0ba7210e27b1edb47fadf249c9fbd77a5e2c8456 100644 GIT binary patch literal 422 zcmYk2O=|){5Jc-N@dJ&s?ygAwe3Dl^VnQ=a)79OtW`4da zwffRYEA5r^M3fq6%qjJt9`#DKI_@2r^LXL!>$+AwRSpjv41Pixri>7=ci*tDbK z-f?v2UFLjJZ*|sDeOL)cCw-84wxL#|cX+Vk`6-mqowUHgkx06RT7*mc$u4Vlq}a98 z1;5#WH)PMFc#9b;NEO{J^d|aDai3i-=y=haUhj&1WQMhOJ!`I6sQDlKBc8kvd#yt; Wv%F1R;Vhvg$OrO{Jpbk2>wW>^mpzLB literal 0 HcmV?d00001 diff --git a/server_modules/db_handler.py b/server_modules/db_handler.py index cf73ddf..42dc25a 100644 --- a/server_modules/db_handler.py +++ b/server_modules/db_handler.py @@ -15,16 +15,12 @@ CREATION timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pfyt_accounts.auth ( - UUID char(36) NOT NULL REFERENCES pfyt_accounts.users(UUID) ON DELETE CASCADE ON UPDATE CASCADE, + UUID char(36) NOT NULL REFERENCES pfyt_accounts.users(UUID) ON DELETE CASCADE, SALTED_HASHBROWN blob NOT NULL, TOKEN_SECRET tinytext ); """ -queries = {'initialize': initialize_ddl} -fields_to_check = { - 'username': {'table': 'pfyt_accounts.users', 'attribute': 'USERNAME'}} - class db: con = None @@ -48,19 +44,17 @@ def decrypt_creds(fkey, workingdir): def initialize_schemas(): try: - query = queries['initialize'].rstrip(';').split(';\n') + query = initialize_ddl.rstrip(';').split(';\n') for i in query: db.cur.execute(i) db.con.commit() - print(log.tags.info + log.tags.db.init_success) + print(log.tags.info + log.db.init_success) except Exception as error: - print(log.tags.error + log.tags.db.init_fail.format(error)) + print(log.tags.error + log.db.init_fail.format(error)) def check_if_exists(value, field): - col = fields_to_check[field]['attribute'] - table = fields_to_check[field]['table'] - db.cur.execute(f"SELECT {col} FROM {table} WHERE {col} = '{value}'") + db.cur.execute(f"SELECT USERNAME FROM pfyt_accounts.users WHERE USERNAME = %s", (value,)) data = db.cur.fetchall() try: if data[0][0] == value: @@ -135,7 +129,6 @@ def logout(uuid): def delete(uuid): try: db.cur.execute("DELETE FROM pfyt_accounts.users WHERE UUID = %s", (uuid,)) - db.cur.execute("DELETE FROM pfyt_accounts.auth WHERE UUID = %s", (uuid,)) db.con.commit() return 'SUCCESS' except Exception as err: diff --git a/server_modules/packet_handler.py b/server_modules/packet_handler.py index 5bf614b..78c6b0b 100644 --- a/server_modules/packet_handler.py +++ b/server_modules/packet_handler.py @@ -93,7 +93,7 @@ async def signup(SESSIONS, SERVER_CREDS, ws, data): password = data['password'] except KeyError as ero: return await get_resp_packet(SESSIONS, ws, - {'type': 'STATUS', 'data': {'sig': 'SIGNUP_MISSING_CREDS', 'desc': ero}}) + {'type': 'STATUS', 'data': {'sig': 'MISSING_CREDS', 'desc': ero}}) # Define validation rules validation_rules = [ (len(user) > 32, 'SIGNUP_USERNAME_ABOVE_LIMIT'), @@ -129,11 +129,12 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): dont_ask_again = data['save'] except KeyError as ero: return await get_resp_packet(SESSIONS, ws, - {'type': 'STATUS', 'data': {'sig': 'LOGIN_MISSING_CREDS', 'desc': ero}}) + {'type': 'STATUS', 'data': {'sig': 'MISSING_CREDS', 'desc': ero}}) resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) if resp_captcha is True: - flag, uuid = db.Account.check_pwd(password, identifier) - if flag is True: + flag = db.Account.check_pwd(password, identifier) + if flag[0] is True: + uuid = flag[1] if dont_ask_again is True: secret, access_token = en.gen_token(uuid, 30) else: @@ -142,10 +143,10 @@ async def login(SESSIONS, SERVER_CREDS, ws, data): en_packet = await get_resp_packet(SESSIONS, ws, {'type': 'TOKEN_GEN', 'data': {'token': access_token}}) print(log.tags.info + log.packet.token_gen.format(uuid)) return en_packet - elif flag is False: - return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGIN_INCORRECT_PASSWORD'}}) + elif flag[0] is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': - return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'LOGIN_ACCOUNT_NOT_FOUND'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACCOUNT_NOT_FOUND'}}) elif resp_captcha is False: return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'CAPTCHA_WRONG'}}) @@ -157,7 +158,7 @@ async def auth(SESSIONS, SERVER_CREDS, ws, data): con_uuid = await identify_client(ws, SESSIONS) user_uuid = db.Account.get_uuid(user) if user_uuid == 'ACCOUNT_DNE': - return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACCOUNT_DNE'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACCOUNT_NOT_FOUND'}}) key = db.Account.get_token_key(user_uuid) if key == 'TOKEN_NOT_FOUND': return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'TOKEN_NOT_FOUND'}}) @@ -195,20 +196,24 @@ async def delete(SESSIONS, SERVER_CREDS, ws, data): password = data['password'] except KeyError as ero: return await get_resp_packet(SESSIONS, ws, - {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_MISSING_CREDS', 'desc': ero}}) + {'type': 'STATUS', 'data': {'sig': 'MISSING_CREDS', 'desc': ero}}) resp_captcha = await captcha(SESSIONS, SERVER_CREDS, ws, data) if resp_captcha is True: - flag, uuid = db.Account.check_pwd(password, identifier) - if flag is True: - user = db.Account.get_uuid(identifier) + print("DEBUG - True") + flag = db.Account.check_pwd(password, identifier) + print("DEBUG - Passwd check") + if flag[0] is True: + user = flag[1] + print("DEBUG - identifier to uuid") dflag = db.Account.delete(user) + print("DEBUG - delete complete") if dflag == 'SUCCESS': print(log.tags.info + log.packet.acc_delete.format(identifier)) - return get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_SUCCESS'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_SUCCESS'}}) elif dflag == 'FAILURE': - return get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_ERR'}}) - elif flag is False: - return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_INCORRECT_PASSWORD'}}) + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'ACC_DELETE_ERR'}}) + elif flag[0] is False: + return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'INCORRECT_PASSWORD'}}) elif flag == 'ACCOUNT_DNE': return await get_resp_packet(SESSIONS, ws, {'type': 'STATUS', 'data': {'sig': 'DELETE_NONEXISTENT_ACCOUNT'}}) elif resp_captcha is False: From 1bf1d8ba0f1691a51181811ca09c537e0f21564c Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Sun, 31 Dec 2023 22:01:29 +0530 Subject: [PATCH 11/14] more cleanup + spelling mistake :grimacing: --- server_main.py | 4 ++-- server_modules/encryption.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server_main.py b/server_main.py index dd60994..b84a837 100644 --- a/server_main.py +++ b/server_main.py @@ -14,7 +14,7 @@ def execute_firstrun(): firstrun.main() print(i18n.firstrun.security) - db.decrypt_creds(en.fermat_gen(firstrun.working_dir.workingdir), firstrun.working_dir.workingdir) + db.decrypt_creds(en.fernet_gen(firstrun.working_dir.workingdir), firstrun.working_dir.workingdir) print(i18n.firstrun.initialize_db) db.initialize_schemas() db.close() @@ -112,7 +112,7 @@ async def main(chost, cport): host, port = yaml['listen_address'], yaml['listen_port'] try: - fkey = en.fermat_gen(workingdir) + fkey = en.fernet_gen(workingdir) db.decrypt_creds(fkey, workingdir) except Exception as w: print(i18n.database.de_cred_fail.format(w)) diff --git a/server_modules/encryption.py b/server_modules/encryption.py index 872ee02..e81b35f 100644 --- a/server_modules/encryption.py +++ b/server_modules/encryption.py @@ -1,4 +1,4 @@ -from cryptography.hazmat.primitives import serialization, hashes, hmac +from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import padding as spadding @@ -35,6 +35,7 @@ def fernet_initkey(workingdir): break else: print(i18n.password.retry) + # Generate a Fernet key with the password and save the salt salt = urandom(16) with open(f"{workingdir}/creds/salt", "wb") as f: @@ -50,7 +51,7 @@ def fernet_initkey(workingdir): return key -def fermat_gen(workingdir): +def fernet_gen(workingdir): passwd = getpass("Enter Password: ") with open(f"{workingdir}/creds/salt", "rb") as f: salt = f.read() @@ -98,11 +99,14 @@ def encrypt_packet(data, pubkey): label=None, ), ) + # Encrypt packet data with symmetric key cbc = urandom(16) + # Add the padding to the data padder = spadding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() + # Encrypting the padded_data cipher = Cipher(algorithms.AES(skey), modes.CBC(cbc)) encryptor = cipher.encryptor() From 0d8970dfb8a117c829ebc36ca9c1a043e1856ac1 Mon Sep 17 00:00:00 2001 From: Ilamparithi M <5023573ilamparithi@gmail.com> Date: Mon, 1 Jan 2024 15:29:57 +0530 Subject: [PATCH 12/14] Update server_main.py --- server_main.py | 70 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/server_main.py b/server_main.py index b84a837..229f9f9 100644 --- a/server_main.py +++ b/server_main.py @@ -22,30 +22,52 @@ def execute_firstrun(): sys.exit() -def check_missing_config(f, yamlc, config): - try: - if yamlc[config] is None: - print(i18n.firstrun.setting_not_found.format(config)) - if config == 'working_directory': - print(i18n.firstrun.ft_question) - while True: - choice = input("(Y / N) > ") - if choice.lower() == 'y': - print(i18n.firstrun.exec) - f.close() - os.remove(f'{rootdir}/config.yml') - firstrun.main() - print(i18n.firstrun.exit) - sys.exit() - elif choice.lower() == 'n': - fill_missing_config(f, yamlc, 'working_directory') - break - else: - fill_missing_config(f, yamlc, config) - except KeyError: - print(i18n.firstrun.setting_not_found.format(config)) - fill_missing_config(f, yamlc, config) - +# def check_missing_config(f, yamlc, config): +# try: +# if yamlc[config] is None: +# print(i18n.firstrun.setting_not_found.format(config)) +# if config == 'working_directory': +# print(i18n.firstrun.ft_question) +# while True: +# choice = input("(Y / N) > ") +# if choice.lower() == 'y': +# print(i18n.firstrun.exec) +# f.close() +# os.remove(f'{rootdir}/config.yml') +# firstrun.main() +# print(i18n.firstrun.exit) +# sys.exit() +# elif choice.lower() == 'n': +# fill_missing_config(f, yamlc, 'working_directory') +# break +# else: +# fill_missing_config(f, yamlc, config) +# except KeyError: +# print(i18n.firstrun.setting_not_found.format(config)) +# fill_missing_config(f, yamlc, config) + +def check_missing_config(file, yaml_config, config_key): + if yaml_config.get(config_key) is not None: + return + print(i18n.firstrun.setting_not_found.format(config_key)) + + if config_key != 'working_directory': + fill_missing_config(file, yaml_config, config_key) + return + print(i18n.firstrun.ft_question) + + while True: + choice = input("(Y / N) > ").lower() + if choice == 'y': + print(i18n.firstrun.exec) + file.close() + os.remove(f'{rootdir}/config.yml') + firstrun.main() + print(i18n.firstrun.exit) + sys.exit() + elif choice == 'n': + fill_missing_config(file, yaml_config, 'working_directory') + break def fill_missing_config(f, yamlc, config): print(i18n.firstrun.fix_missing.format(config)) From caa3a141f4ab913888175be010173becdde9ea7b Mon Sep 17 00:00:00 2001 From: Ilamparithi Murali Date: Sun, 11 May 2025 02:54:14 +0530 Subject: [PATCH 13/14] Update README.md copy pasted the description given in the project report --- README.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de1030b..cce5769 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,106 @@ # Account System -## with Session Token based Authentication +## with Session Token based Authentication and End-to-End Encrypted Traffic *** +### Setting up the Server + +#### Tested and written in Python 3.11. Older versions will not work. Support for arm64 subject to availability of pre-built wheels for the required packages. + +Create a venv and run `pip install -r requirements.txt` inside it before following the next steps. + +On the first execution of the server program, it asks the user for a folder where the server can store its operation files. In cases where the terminal can open a GUI, it opens a tkinter file selector dialogue and wherever it cannot do that, the user will be asked to input the file path in the command-line itself. + +The user is asked for a password to lock the server to protect it from unauthorized access. This password will be used to encrypt the database credentials so that malicious actors with unauthorized access cannot hack into the database. + +Then the database credentials, namely the MySQL/MariaDB server address, username, and password, are asked. The server then creates the necessary database and tables. + +The server program then asks for the network address and port on which the server should listen to connections. This port must be open to the public on the server side to allow connections. + +The server program will then close itself and will be ready for operation. When the server program is executed again, it asks for the password you entered. If a wrong password is entered, the server program closes at once. + +When the correct password is entered, the server starts to listen for connections. At this state, users can connect to the server and perform their operations (sign up, login, authenticate and logout). + +*** + +### Problem Definition + +To create a User Account Management System which can be used in various applications for safe and secure User Accounts. + +This project consists of a Server program which can be executed on any computer with a network connection, and an example command-line Client, which users can use to connect to a server, create an account in the server and log into it. + +The Server program is a WebSocket server which accepts packets from the client and performs the operations mentioned by the client (such as creating account, logging in, authenticating, etc.). This Server program is the base of the account management system. + +The Client program is a command-line application for demonstrating the working of the server. When executed, the program displays an interactive command-line menu from where you can create an account, log in to an existing account or log out of the current session if logged in. + +The messages exchanged between the Client and the Server are encrypted, meaning no one in the middle can intercept the packet and view user details. + +The account credentials are safely stored in the server computer’s MySQL/MariaDB database in an encrypted format, which provides an additional level of security from the server’s owner/malicious actors. + +This project aims to provide other developers with a secure way of handling accounts in their applications, helping them focus on the main functionality of their application. + +This system can be implemented in various applications, such as chat platforms, social media platforms, and any other websites that require account creation. + +### Problem Analysis + +#### Encrypted Traffic: + +In cryptography, a key is a piece of information, usually a string of numbers or letters, that when processed through a cryptographic algorithm, can encode or decode cryptographic data. The key is used to transform data from plaintext (the original data) to ciphertext (the encrypted data). There are different methods for utilizing keys and encryption: + +- Symmetric cryptography: The same key is used for both encryption and decryption. + +- Asymmetric cryptography: Separate keys are used for encrypting and decrypting. These keys are known as the public and private keys. + +Encrypting data is essential so that only the sender and intended recipient (in this case, the server) can read it. The type of encryption used in the client-server connection system of this project is based on asymmetric cryptography. The public key is used to encrypt the data, and the private key is used to decrypt it. The public key is shared with the recipient, but the private key is kept secret by the sender. The asymmetric encryption algorithm this project uses is RSA (Rivest-Shamir-Adleman). + +When a message is sent by the server, it is encrypted using the recipient's public key. The message can only be decrypted using the recipient's private key. Similarly, messages sent to the server by the client are encrypted using + +the server’s public key. This means that even if an attacker intercepts the message, they will not be able to read it without the private key. In this application, the server’s public key will change with every restart for additional security. This diagram may give you more clarity: + +![image](https://github.com/user-attachments/assets/2231548d-8090-49d5-b8e4-c42f31c5cfc9) + +This project does not utilize SSL/TLS or other methods of encrypting the connection as it requires special ports (number 80 and 443) to be open, and additional setup such as obtaining a certificate. Most consumer-oriented Internet Service Providers (ISPs) block the users from opening these ports, to prevent misuse. The process of obtaining a certificate is also tedious. This prevents the developers with not enough resources to buy a server from a hosting service/get their own enterprise network solution, from hosting this server. Therefore, the connection is encrypted with a different method to ensure security. + +#### WebSocket Server: + +WebSocket is a computer communications protocol that provides simultaneous two-way communication channels over a single Transmission Control Protocol (TCP) connection. Unlike HTTP, which is unidirectional, WebSocket is bidirectional and full duplex. This means the connection + +between the client and the server is kept alive until it is terminated by either party. WebSocket URIs start with ws:// + +It facilitates real-time data transfer from and to the server. This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client and allowing messages to be passed back and forth while keeping the connection open. + +This is implemented in python by the websockets library built on top of python’s standard asynchronous input/output framework, asyncio. + +#### Asynchronous Operations + +Asynchronous operations are tasks that can run concurrently without blocking or waiting for each other. They are useful for improving the performance and responsiveness of IO-bound applications, such as network and web servers, database connections, etc. Asynchronous operations can be executed using the async/await syntax, which allows writing code that looks like synchronous code, but is asynchronous. + +asyncio is a built-in library in Python that provides support for asynchronous programming. It offers a framework and tools for creating and managing event loops, coroutines, tasks, futures, streams, transports, protocols, queues, and synchronization primitives. It also provides compatibility with other libraries and frameworks that use callbacks or generators. + +websockets library is built on top of asyncio for its very function of asynchronous programming. WebSocket servers need to be able to handle multiple connections at once which is not possible synchronously. One connection would block the others, waiting for its own job to be complete. Asynchronous processing eliminates this block and allows for simultaneous connections and better performance. + +#### Hashing and Salting + +Hashing is a process used in password security that involves converting a password into an unrecognizable series of characters. This is done using an irreversible hash function, which is a specialized algorithm. Instead of storing the password as plain text, a mathematical algorithm converts the password into a unique code. This unique code, or hash, is what gets stored in the database. + +Salting is a technique used in password security to enhance the protection of passwords stored in a database. It involves adding a unique, random string of characters, known as a salt, to each password before it is hashed. This process changes the hash of the password, making it more secure. + +Before the password is hashed, a salt value is added to it. This salted password is then hashed and stored in the database. Salting ensures that even if two users have the same password, their hashes will be different due to the unique salts. This halts attacks using precomputed tables of hashes, known as rainbow tables. + +Moreover, salting makes it extra difficult for an attacker who gains access to password hashes to find out the original password. Even if an attacker manages to decrypt a salted password, the original password remains hidden. + +#### Authentication Token + +An authentication token is a computer-generated code that securely transmits information about user identities between applications and websites. It allows users to access services without having to enter their login credentials each time. The user logs in once, and a unique token is generated and shared with connected applications or websites to verify their identity. + +An authentication token is formed of three key components: the header (defines the token type and the signing algorithm), the payload (provides information about the user and other metadata), and the signature (verifies the authenticity of a message). + +To generate the tokens, we use the open standard JSON Web Tokens (JWT). The token’s payload has 3 parts: sub, iat, and exp. + +- sub: The ‘sub’ claim in a JWT stands for ‘subject’. It identifies the subject of the JWT, which is typically the user or the entity that the token represents. It’s a way to identify who the token is about. + +- iat: The ‘iat’ claim stands for ‘Issued At’. It identifies the time at which the JWT was issued. The value must be a NumericDate, which is defined as the number of seconds (not milliseconds) since Epoch (1970-01-01T00:00:00Z UTC). + +- exp: The ‘exp’ claim stands for ‘Expiration Time’. It identifies the expiration time on or after which the JWT must not be accepted for processing. Like ‘iat’, the value must be a NumericDate. + From 41ecc0b9e3e34c27b602107b0f40b74ebdbf702d Mon Sep 17 00:00:00 2001 From: Ilamparithi Murali Date: Sun, 11 May 2025 03:05:00 +0530 Subject: [PATCH 14/14] fix e2ee illustration --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cce5769..45ab0bd 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ When a message is sent by the server, it is encrypted using the recipient's publ the server’s public key. This means that even if an attacker intercepts the message, they will not be able to read it without the private key. In this application, the server’s public key will change with every restart for additional security. This diagram may give you more clarity: -![image](https://github.com/user-attachments/assets/2231548d-8090-49d5-b8e4-c42f31c5cfc9) +![image](https://github.com/user-attachments/assets/92820fbd-ae36-47f6-adcf-992a662fbeab) + This project does not utilize SSL/TLS or other methods of encrypting the connection as it requires special ports (number 80 and 443) to be open, and additional setup such as obtaining a certificate. Most consumer-oriented Internet Service Providers (ISPs) block the users from opening these ports, to prevent misuse. The process of obtaining a certificate is also tedious. This prevents the developers with not enough resources to buy a server from a hosting service/get their own enterprise network solution, from hosting this server. Therefore, the connection is encrypted with a different method to ensure security.