From d758ea4cd69337caf63deb8aaf3cfb932b31c3f5 Mon Sep 17 00:00:00 2001 From: Tiago Garcia Date: Thu, 19 Dec 2024 23:46:31 +0000 Subject: [PATCH] Implement server/client security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: RĂºben Gomes Signed-off-by: Tiago Garcia --- delivery2/client/bin/lib/__init__.py | 3 +- delivery2/client/bin/lib/diffie_hellman.py | 2 +- .../client/bin/lib/symmetric_encryption.py | 23 +++ delivery2/client/bin/rep_acl_doc | 27 ++- delivery2/client/bin/rep_activate_subject | 7 +- delivery2/client/bin/rep_add_doc | 27 ++- delivery2/client/bin/rep_add_permission | 41 ++--- delivery2/client/bin/rep_add_role | 25 ++- delivery2/client/bin/rep_add_subject | 20 ++- delivery2/client/bin/rep_assume_role | 7 +- delivery2/client/bin/rep_create_org | 14 +- delivery2/client/bin/rep_create_session | 100 +++++++++-- delivery2/client/bin/rep_decrypt_file | 4 +- delivery2/client/bin/rep_delete_doc | 7 +- delivery2/client/bin/rep_drop_role | 7 +- delivery2/client/bin/rep_get_doc_file | 15 +- delivery2/client/bin/rep_get_doc_metadata | 17 +- delivery2/client/bin/rep_list_docs | 114 +++++-------- delivery2/client/bin/rep_list_orgs | 8 +- .../client/bin/rep_list_permission_roles | 17 +- .../client/bin/rep_list_role_permissions | 17 +- delivery2/client/bin/rep_list_role_subjects | 17 +- delivery2/client/bin/rep_list_roles | 15 +- delivery2/client/bin/rep_list_subject_roles | 17 +- delivery2/client/bin/rep_list_subjects | 62 ++++--- delivery2/client/bin/rep_remove_permission | 41 ++--- delivery2/server/database.py | 4 +- delivery2/server/models/session.py | 3 - delivery2/server/routes/file.py | 85 +++++----- delivery2/server/routes/org.py | 14 +- delivery2/server/routes/role.py | 51 +++--- delivery2/server/routes/user.py | 159 ++++++++++++------ delivery2/server/services/files.py | 22 --- delivery2/server/services/orgs.py | 4 +- delivery2/server/services/roles.py | 4 +- delivery2/server/services/security.py | 14 ++ delivery2/server/services/sessions.py | 31 +++- delivery2/server/utils/__init__.py | 4 +- delivery2/server/utils/comms_encryption.py | 38 +++++ delivery2/server/utils/diffie_hellman.py | 147 ++++++++++++++++ delivery2/server/utils/exceptions.py | 5 + 41 files changed, 851 insertions(+), 388 deletions(-) create mode 100644 delivery2/server/services/security.py create mode 100644 delivery2/server/utils/comms_encryption.py create mode 100644 delivery2/server/utils/diffie_hellman.py create mode 100644 delivery2/server/utils/exceptions.py diff --git a/delivery2/client/bin/lib/__init__.py b/delivery2/client/bin/lib/__init__.py index 1b2fe5b..1a4abfb 100644 --- a/delivery2/client/bin/lib/__init__.py +++ b/delivery2/client/bin/lib/__init__.py @@ -1,4 +1,5 @@ from .asymmetric_functs import * from .symmetric_encryption import * from .digest import * -from .key_pair import * \ No newline at end of file +from .key_pair import * +from .diffie_hellman import * \ No newline at end of file diff --git a/delivery2/client/bin/lib/diffie_hellman.py b/delivery2/client/bin/lib/diffie_hellman.py index d231eb8..b544ce0 100644 --- a/delivery2/client/bin/lib/diffie_hellman.py +++ b/delivery2/client/bin/lib/diffie_hellman.py @@ -60,7 +60,7 @@ def encrypt(content, key): iv = os.urandom(16) cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) encryptor = cipher.encryptor() - ciphertext = iv + encryptor.update(content) + encryptor.finalize() + ciphertext = iv + encryptor.update(content.encode()) + encryptor.finalize() return ciphertext diff --git a/delivery2/client/bin/lib/symmetric_encryption.py b/delivery2/client/bin/lib/symmetric_encryption.py index 811e8b5..8e25140 100644 --- a/delivery2/client/bin/lib/symmetric_encryption.py +++ b/delivery2/client/bin/lib/symmetric_encryption.py @@ -67,3 +67,26 @@ def decrypt_file(key, input_file, output_file=None) -> str: except UnicodeDecodeError: return plaintext_content + +def encrypt_response_with_iv(input_string: str) -> bytes: + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(iv), modes.CFB(iv)) + encryptor = cipher.encryptor() + + plaintext_bytes = input_string.encode('utf-8') + ciphertext = encryptor.update(plaintext_bytes) + encryptor.finalize() + + encrypted_data = iv + ciphertext + + return encrypted_data + +def decrypt_request_with_iv(encrypted_data: bytes) -> str: + iv = encrypted_data[:16] + ciphertext = encrypted_data[16:] + + cipher = Cipher(algorithms.AES(iv), modes.CFB(iv)) + decryptor = cipher.decryptor() + + plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize() + + return plaintext_bytes.decode('utf-8') diff --git a/delivery2/client/bin/rep_acl_doc b/delivery2/client/bin/rep_acl_doc index 5942665..7bd210c 100755 --- a/delivery2/client/bin/rep_acl_doc +++ b/delivery2/client/bin/rep_acl_doc @@ -7,6 +7,7 @@ import json import argparse from lib import digest +from lib.diffie_hellman import * from subject import main logging.basicConfig(format='%(levelname)s\t- %(message)s') @@ -63,12 +64,23 @@ def aclDoc(args): document_handle = digest.get_hash(bytes(args.name, encoding='utf-8')) - payload = {'document_handle' : document_handle, 'role' : args.role, 'perm' : args.permission, 'operation' : change} + derived_key = bytes.fromhex(args.session['derived_key']) + payload = json.dumps({'role' : args.role, 'perm' : args.permission, 'operation' : change}) + try: + payload = encrypt(payload, derived_key).hex() + except Exception: + logger.error("Failed to encrypt the content") + sys.exit(-1) + + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } try: - req = requests.post(f'http://{state['REP_ADDRESS']}/file/acl', - json=json.dumps(payload), - headers={'Authorization': args.session['token']}) + req = requests.post(f'http://{state['REP_ADDRESS']}/file/acl/{document_handle}', + data=payload, + headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: logger.error("%d: %s", req.status_code, req.json()['error']) @@ -78,8 +90,11 @@ def aclDoc(args): sys.exit(-1) # Operation success - logger.info("ACL changed succesfully.") - sys.exit(0) + if req.status_code == 200: + logger.info("ACL changed succesfully.") + sys.exit(0) + logger.error("Failed to change ACL") + sys.exit(-1) if __name__ == '__main__': aclDoc(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_activate_subject b/delivery2/client/bin/rep_activate_subject index db80828..3b5223e 100755 --- a/delivery2/client/bin/rep_activate_subject +++ b/delivery2/client/bin/rep_activate_subject @@ -53,8 +53,11 @@ def activateSubject(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info("Subject %s has been activated." % args.username) - sys.exit(0) + if req.status_code == 200: + logger.info("Subject %s has been activated." % args.username) + sys.exit(0) + logger.error("Failed to activate subject") + sys.exit(-1) if __name__ == '__main__': activateSubject(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_add_doc b/delivery2/client/bin/rep_add_doc index c3006f8..433f3a2 100755 --- a/delivery2/client/bin/rep_add_doc +++ b/delivery2/client/bin/rep_add_doc @@ -8,6 +8,8 @@ import argparse from subject import main +from lib.diffie_hellman import * + sys.path.append(os.path.abspath("../../")) from lib.symmetric_encryption import * from lib import digest @@ -53,12 +55,24 @@ def addDoc(args): #Encrypt content key, content = encrypt_file(BASE_DIR + args.file, BASE_DIR + 'encryptedText') + derived_key = bytes.fromhex(args.session['derived_key']) #Upload document metadata - doc = {'document_name' : args.name, 'key' : key.hex(), 'alg' : 'AES-CFB' } + doc = json.dumps({'document_name' : args.name, 'key' : key.hex(), 'alg' : 'AES-CFB' }) + try: + doc = encrypt(doc, derived_key).hex() + except Exception: + logger.error("Failed to encrypt the content") + sys.exit(-1) + + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } try: - req = requests.post(f'http://{state['REP_ADDRESS']}/file/upload/metadata', json=json.dumps(doc), - headers={'Authorization': args.session['token']}) + req = requests.post(f'http://{state['REP_ADDRESS']}/file/upload/metadata', + data=doc, + headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: @@ -90,8 +104,11 @@ def addDoc(args): #Delete temporary file os.remove(BASE_DIR + 'encryptedText') - logger.info("Document uploaded successfully.") - sys.exit(0) + if req.status_code == 201: + logger.info("Document uploaded successfully.") + sys.exit(0) + logger.error("Failed to add document.") + sys.exit(-1) if __name__ == '__main__': addDoc(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_add_permission b/delivery2/client/bin/rep_add_permission index 7605b1d..6a36c3e 100755 --- a/delivery2/client/bin/rep_add_permission +++ b/delivery2/client/bin/rep_add_permission @@ -61,38 +61,21 @@ def addPermission(args): ]: isPerm = True - if isPerm: - try: - req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + '/perm/add/' + args.value, - headers={'Authorization': args.session['token']}) - req.raise_for_status() + try: + req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + f'/{"perm" if isPerm else "user"}/add/' + args.value, + headers={'Authorization': args.session['token']}) + req.raise_for_status() - except requests.exceptions.HTTPError: - logger.error("%d: %s", req.status_code, req.json()['error']) - sys.exit(-1) + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server.") + sys.exit(-1) - logger.info("Permission added successfully to role.") - sys.exit(0) - else: - try: - req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + '/user/add/' + args.value, - headers={'Authorization': args.session['token']}) - req.raise_for_status() - - except requests.exceptions.HTTPError: - logger.error("%d: %s", req.status_code, req.json()['error']) - sys.exit(-1) - - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) - - logger.info("User added successfully to role.") - sys.exit(0) + logger.info("Permission added successfully to role." if isPerm else "User added successfully to role.") + sys.exit(0) if __name__ == '__main__': addPermission(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_add_role b/delivery2/client/bin/rep_add_role index 2c5fe7a..57df185 100755 --- a/delivery2/client/bin/rep_add_role +++ b/delivery2/client/bin/rep_add_role @@ -6,6 +6,7 @@ import requests import json import argparse +from lib.diffie_hellman import * from subject import main logging.basicConfig(format='%(levelname)s\t- %(message)s') @@ -44,10 +45,23 @@ def addRole(args): with open(BASE_DIR + args.session, 'r') as f: args.session = json.load(f) + derived_key = bytes.fromhex(args.session['derived_key']) + payload = json.dumps({'role' : args.role}) + try: + payload = encrypt(payload, derived_key).hex() + except Exception: + logger.error("Failed to encrypt the content") + sys.exit(-1) + + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } + try: req = requests.post(f'http://{state['REP_ADDRESS']}/role/create', - json=json.dumps({'role' : args.role}), - headers={'Authorization': args.session['token']}) + data=payload, + headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: @@ -58,8 +72,11 @@ def addRole(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info("Role %s added.", args.role) - sys.exit(0) + if req.status_code == 201: + logger.info("Role %s added.", args.role) + sys.exit(0) + logger.error('Failed to add role.') + sys.exit(-1) if __name__ == '__main__': addRole(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_add_subject b/delivery2/client/bin/rep_add_subject index ea820c9..6f14e75 100755 --- a/delivery2/client/bin/rep_add_subject +++ b/delivery2/client/bin/rep_add_subject @@ -6,6 +6,7 @@ import requests import json import argparse +from lib.diffie_hellman import * from subject import main sys.path.append(os.path.abspath('../../')) @@ -53,10 +54,23 @@ def addSubject(args): pubKey = asymmetric_functs.load_public_key(BASE_DIR + args.credentials) - subject = {'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey} - + derived_key = bytes.fromhex(args.session['derived_key']) + subject = json.dumps({'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey}) try: - req = requests.post(f'http://{state['REP_ADDRESS']}/user/create', json=json.dumps(subject), headers={'Authorization': args.session['token']}) + subject = encrypt(subject, derived_key).hex() + except Exception: + logger.error("Failed to encrypt the content") + sys.exit(-1) + + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } + + try: + req = requests.post(f'http://{state['REP_ADDRESS']}/user/create', + data=subject, + headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: diff --git a/delivery2/client/bin/rep_assume_role b/delivery2/client/bin/rep_assume_role index 3bdc6dc..339b7e2 100755 --- a/delivery2/client/bin/rep_assume_role +++ b/delivery2/client/bin/rep_assume_role @@ -55,8 +55,11 @@ def assumeRole(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info("You assumed the role %s.", args.role) - sys.exit(0) + if req.status_code == 200: + logger.info("You assumed the role %s.", args.role) + sys.exit(0) + logger.error("Failed to assume role %s.", args.role) + sys.exit(-1) if __name__ == '__main__': assumeRole(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_create_org b/delivery2/client/bin/rep_create_org index 0967381..5d75c64 100755 --- a/delivery2/client/bin/rep_create_org +++ b/delivery2/client/bin/rep_create_org @@ -7,6 +7,8 @@ import json import re import argparse +from bin.lib import encrypt_response_with_iv, decrypt_request_with_iv + sys.path.append(os.path.abspath("../")) from subject import main @@ -56,10 +58,11 @@ def createOrganization(args): # load public key from file pubKey = asymmetric_functs.load_public_key(BASE_DIR + args.pubkey) - input = {'name' : args.org, 'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey} + payload = encrypt_response_with_iv(json.dumps({'name' : args.org, 'username' : args.username, 'full_name' : args.name, 'email' : args.email, 'public_key' : pubKey})) + headers = {'Content-Type': 'application/octet-stream'} try: - req = requests.post(f'http://{state['REP_ADDRESS']}/org/create', json=json.dumps(input)) + req = requests.post(f'http://{state['REP_ADDRESS']}/org/create', data=payload, headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: @@ -71,7 +74,12 @@ def createOrganization(args): sys.exit(-1) if req.status_code == 201: - logger.info("Organization created successfully.") + try: + response_data = json.loads(decrypt_request_with_iv(bytes.fromhex(req.text))) + except Exception as e: + logger.error("Failed to decrypt the content: %s", e) + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) sys.exit(0) logger.error("Failed to create organization.") sys.exit(-1) diff --git a/delivery2/client/bin/rep_create_session b/delivery2/client/bin/rep_create_session index 26ecac3..d72e4d8 100755 --- a/delivery2/client/bin/rep_create_session +++ b/delivery2/client/bin/rep_create_session @@ -8,8 +8,9 @@ import json import requests from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes, serialization +from lib.diffie_hellman import * from subject import main # Identity attributes @@ -40,7 +41,7 @@ def createSession(args): args = parser.parse_args() - if not args.org or not args.username or not args.credentials or not args.session: + if not args.org or not args.username or not args.password or not args.credentials or not args.session: logger.error("Need organization, username, credentials and session file") sys.exit(1) @@ -48,18 +49,81 @@ def createSession(args): logger.error("File '" + args.credentials + "' not found.") sys.exit(1) - session = {'org' : args.org, 'username' : args.username} + # perform diffie-hellman key exchange + # OPTIONAL: ask for parameters + generator = 2; key_size = 1024 + parameters = generate_parameters(generator, key_size) - #print( type(json.dumps(session))) + parameters_send = parameters.parameter_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.ParameterFormat.PKCS3).hex() + # send parameters to server try: - req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps(session)) + req = requests.post(f'http://{state['REP_ADDRESS']}/user/dh', json=json.dumps({'user': args.username, + 'org': args.org, + 'parameters': parameters_send})) req.raise_for_status() + + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) + except requests.exceptions.RequestException as errex: logger.error("Failed to obtain response from server") sys.exit(-1) + private_key, public_key = generate_key_pair(parameters) + response = req.json() + server_public_key = serialization.load_pem_public_key(bytes.fromhex(response['public_key'])) + derived_key = derive_keys(private_key, server_public_key) + + # save derived key + with open(BASE_DIR + 'derived_key.pem', 'wb') as f: + f.write(derived_key) + + # send public key to server + public_key_content = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).hex() + + try: + req = requests.post(f'http://{state['REP_ADDRESS']}/user/dh', json=json.dumps({'user': args.username, + 'org': args.org, + 'public_key': public_key_content})) + req.raise_for_status() + + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) + + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server") + sys.exit(-1) + + if req.status_code == 200: + logger.info("Diffie-Hellman key exchange completed successfully") + else: + logger.error("Failed to complete Diffie-Hellman key exchange") + sys.exit(-1) + + try: + session = {'org': args.org, 'username': args.username} + req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps(session)) + req.raise_for_status() + + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) + + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server") + sys.exit(-1) + + req = decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8') + response = json.loads(req) challenge = response['challenge'] with open(BASE_DIR + args.credentials, 'rb') as f: @@ -76,7 +140,15 @@ def createSession(args): ) try: - req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps({'signature' : base64.b64encode(signature).decode('utf-8')}), headers={'Authorization': response['token']}) + json_payload = json.dumps({'signature' : base64.b64encode(signature).decode('utf-8')}) + json_payload = encrypt(json_payload, derived_key).hex() + + headers = { + 'Authorization': response['token'], + 'Content-Type': 'application/octet-stream' + } + + req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', data=json_payload, headers=headers) req.raise_for_status() except requests.exceptions.HTTPError: @@ -87,14 +159,20 @@ def createSession(args): logger.error("Failed to obtain response from server") sys.exit(-1) - with open(BASE_DIR + args.session, 'w') as f: - json.dump(req.json(), f) - if req.status_code == 201: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + session = response_data | {'derived_key': derived_key.hex()} + with open(BASE_DIR + args.session, 'w') as f: + json.dump(session, f) logger.info("Session created successfully") sys.exit(0) - logger.error("Failed to create session") - sys.exit(-1) + else: + logger.error("Failed to create session") + sys.exit(-1) if __name__ == '__main__': createSession(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_decrypt_file b/delivery2/client/bin/rep_decrypt_file index eb26340..9e6bab1 100755 --- a/delivery2/client/bin/rep_decrypt_file +++ b/delivery2/client/bin/rep_decrypt_file @@ -26,7 +26,7 @@ def decryptFile(args): args = parser.parse_args() if not args.encrypted or not args.metadata: - logger.error("Need encrypted file and it's metadata.") + logger.error("Need encrypted file and its metadata.") sys.exit(1) # If first argument is not a file or not found @@ -38,7 +38,7 @@ def decryptFile(args): content = symmetric_encryption.decrypt_file(bytes.fromhex(metadata['key']), BASE_DIR + args.encrypted) - with open(BASE_DIR + 'decryped_file.txt', 'w') as f: + with open(BASE_DIR + 'decrypted_file.txt', 'w') as f: f.write(content) # Send decrypted content to stdout diff --git a/delivery2/client/bin/rep_delete_doc b/delivery2/client/bin/rep_delete_doc index 07ebb12..fe0f450 100755 --- a/delivery2/client/bin/rep_delete_doc +++ b/delivery2/client/bin/rep_delete_doc @@ -58,8 +58,11 @@ def delDoc(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info("You deleted the document %s", args.name) - sys.exit(0) + if req.status_code == 200: + logger.info("You deleted the document %s", args.name) + sys.exit(0) + logger.error("Failed to delete document.") + sys.exit(-1) if __name__ == '__main__': delDoc(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_drop_role b/delivery2/client/bin/rep_drop_role index 8141fc2..bc8a294 100755 --- a/delivery2/client/bin/rep_drop_role +++ b/delivery2/client/bin/rep_drop_role @@ -55,8 +55,11 @@ def dropRole(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info("You dropped the role %s", args.role) - sys.exit(0) + if req.status_code == 200: + logger.info("You dropped the role %s", args.role) + sys.exit(0) + logger.error("Failed to assume role %s.", args.role) + sys.exit(-1) if __name__ == '__main__': dropRole(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_get_doc_file b/delivery2/client/bin/rep_get_doc_file index 7f659bc..c50546d 100755 --- a/delivery2/client/bin/rep_get_doc_file +++ b/delivery2/client/bin/rep_get_doc_file @@ -6,6 +6,7 @@ import requests import json import argparse +from lib.diffie_hellman import decrypt from subject import main sys.path.append(os.path.abspath("../../")) @@ -62,9 +63,15 @@ def getDoc(args): logger.error("Failed to obtain response from server.") sys.exit(1) - metadata = metadata.json() - - #Get file with file_handle provided by metadata + if metadata.status_code == 200: + try: + metadata = json.loads(decrypt(bytes.fromhex(metadata.text), bytes.fromhex(args.session['derived_key'])).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + else: + logger.error("Failed to get metadata") + sys.exit(-1) try: file = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + metadata['file_handle'] + '/content') @@ -81,7 +88,7 @@ def getDoc(args): file = file.content if not digest.get_hash(file) == metadata['file_handle']: - logger.error("Files integrity is lost.") + logger.error("File's integrity was lost.") sys.exit(-1) with open(BASE_DIR + 'encrypted_file', 'wb') as f: diff --git a/delivery2/client/bin/rep_get_doc_metadata b/delivery2/client/bin/rep_get_doc_metadata index 69676d1..479c256 100755 --- a/delivery2/client/bin/rep_get_doc_metadata +++ b/delivery2/client/bin/rep_get_doc_metadata @@ -6,6 +6,8 @@ import requests import json import argparse +from lib.diffie_hellman import decrypt + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) from lib import digest @@ -46,6 +48,7 @@ def getDocMetadata(args): args.session = json.load(f) doc_name = digest.get_hash(bytes(args.name, encoding='utf-8')) + derived_key = bytes.fromhex(args.session['derived_key']) try: metadata = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + doc_name + '/metadata', headers={'Authorization': args.session['token']}) @@ -59,10 +62,18 @@ def getDocMetadata(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - metadata = metadata.json() + if metadata.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(metadata.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + sys.stdout.write(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to retrieve metadata") + sys.exit(-1) - sys.stdout.write(json.dumps(metadata)) - sys.exit(0) if __name__ == '__main__': getDocMetadata(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_docs b/delivery2/client/bin/rep_list_docs index 1ea4539..60caba9 100755 --- a/delivery2/client/bin/rep_list_docs +++ b/delivery2/client/bin/rep_list_docs @@ -8,6 +8,7 @@ import argparse import datetime from subject import main +from lib.diffie_hellman import decrypt, encrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -36,9 +37,7 @@ def list_docs(args): parser.add_argument('session', nargs='?', default=None) parser.add_argument("-s", '--username', nargs=1, help="Username") - parser.add_argument("-dnt", '--newerThan', help="Date new than") - parser.add_argument("-dot", '--olderThan', help="Date older than") - parser.add_argument("-deq", '--equalTo', help="Date equal to") + parser.add_argument("-d", "--date", nargs=2, help="Date") args = parser.parse_args() @@ -55,89 +54,54 @@ def list_docs(args): with open(BASE_DIR + args.session, 'r') as f: args.session = json.load(f) - if args.newerThan: - #Convert date str to datetime in seconds - args.newerThan = validDate(args.newerThan) - args.newerThan = int(args.newerThan.timestamp()) + payload = {} - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list', - json=json.dumps({'username' : args.username,'datetime' : {'value' : args.newerThan, 'relation' : 'nt'}}), - headers={'Authorization': args.session['token']}) - subjects.raise_for_status() + if args.username: + payload['username'] = args.username[0] - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects.json()['error']) + if args.date: + if not args.date[0] or not args.date[1]: + logger.error("Need date.") + sys.exit(-1) + if args.date[0] in ['nt', 'ot', 'et']: + payload['datetime'] = {'value' : args.date[1], 'relation' : args.date[0]} + else: + logger.error("Invalid date relation. Use nt, ot or et.") sys.exit(-1) - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) + derived_key = bytes.fromhex(args.session['derived_key']) - subjects = subjects.json() - - elif args.equalTo: - #Convert date str to datetime in seconds - args.equalTo = validDate(args.equalTo) - args.equalTo = int(args.equalTo.timestamp()) + try: + payload = json.dumps(payload) + payload = encrypt(payload, derived_key).hex() - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list', - json=json.dumps({'username' : args.username, - 'datetime' : {'value' : args.equalTo, 'relation' : 'eq'}}), - headers={'Authorization': args.session['token']}) - subjects.raise_for_status() + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects - .json()['error']) - sys.exit(-1) + req = requests.get(f'http://{state['REP_ADDRESS']}/file/list', data=payload, headers=headers) + req.raise_for_status() - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) - subjects = subjects.json() - - elif args.olderThan: - #Convert date str to datetime in seconds - args.olderThan = validDate(args.olderThan) - args.olderThan = int(args.olderThan.timestamp()) - - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list', - json=json.dumps({'username' : args.username, - 'datetime' : {'value' : args.olderThan, 'relation' : 'ot'}}), - headers={'Authorization': args.session['token']}) - subjects.raise_for_status() - - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects.json()['error']) - sys.exit(-1) - - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) - - subjects = subjects.json() + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server.") + sys.exit(-1) + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) else: - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/file/list', json=json.dumps({}), headers={'Authorization': args.session['token']}) - subjects.raise_for_status() - - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects.json()['error']) - sys.exit(-1) - - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) - - subjects = subjects.json() - - logger.info(json.dumps(subjects, indent=4)) - sys.exit(0) + logger.error("Failed to get documents") + sys.exit(-1) if __name__ == '__main__': list_docs(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_orgs b/delivery2/client/bin/rep_list_orgs index b46eb3d..f0490d7 100755 --- a/delivery2/client/bin/rep_list_orgs +++ b/delivery2/client/bin/rep_list_orgs @@ -5,6 +5,7 @@ import logging import json import requests +from lib.symmetric_encryption import decrypt_request_with_iv from subject import main # Identity attributes @@ -19,9 +20,10 @@ state = main(sys.argv) def listOrganizations(): try: orgs = requests.get(f'http://{state['REP_ADDRESS']}/org/list') - orgs.raise_for_status() + orgs.raise_for_status() except requests.exceptions.HTTPError: + # We know this is useless, but we don't give a damn and still put it here logger.error("%d: %s", orgs.status_code, orgs.json()['error']) sys.exit(-1) @@ -29,7 +31,9 @@ def listOrganizations(): logger.error("Failed to obtain response from server.") sys.exit(-1) - logger.info(json.dumps(orgs.json(), indent=4)) + orgs = json.loads(decrypt_request_with_iv(bytes.fromhex(orgs.text))) + + logger.info(json.dumps(orgs, indent=4)) sys.exit(0) if __name__ == '__main__': diff --git a/delivery2/client/bin/rep_list_permission_roles b/delivery2/client/bin/rep_list_permission_roles index 0b3e489..4fa8eca 100755 --- a/delivery2/client/bin/rep_list_permission_roles +++ b/delivery2/client/bin/rep_list_permission_roles @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -56,10 +57,20 @@ def listPermissionRoles(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - roles = req.json() - logger.info(json.dumps(roles, indent=4)) + derived_key = bytes.fromhex(args.session['derived_key']) + + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to get roles with permission %s", args.permission) + sys.exit(-1) - sys.exit(0) if __name__ == '__main__': listPermissionRoles(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_role_permissions b/delivery2/client/bin/rep_list_role_permissions index c1ba190..9b44ee0 100755 --- a/delivery2/client/bin/rep_list_role_permissions +++ b/delivery2/client/bin/rep_list_role_permissions @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -56,10 +57,18 @@ def listRolePermissions(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - perms = req.json() - logger.info(json.dumps(perms, indent=4)) - - sys.exit(0) + derived_key = bytes.fromhex(args.session['derived_key']) + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to get permissions of role %s", args.role) + sys.exit(-1) if __name__ == '__main__': listRolePermissions(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_role_subjects b/delivery2/client/bin/rep_list_role_subjects index 79a5b8a..3fb15bf 100755 --- a/delivery2/client/bin/rep_list_role_subjects +++ b/delivery2/client/bin/rep_list_role_subjects @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -56,10 +57,18 @@ def listRoleSubjects(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - subjects = req.json() - logger.info(json.dumps(subjects, indent=4)) - - sys.exit(0) + derived_key = bytes.fromhex(args.session['derived_key']) + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to get subjects with role %s", args.role) + sys.exit(-1) if __name__ == '__main__': listRoleSubjects(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_roles b/delivery2/client/bin/rep_list_roles index b30b653..6dc3504 100755 --- a/delivery2/client/bin/rep_list_roles +++ b/delivery2/client/bin/rep_list_roles @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -56,8 +57,18 @@ def listRoles(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - roles = req.json() - logger.info(json.dumps(roles, indent=4)) + derived_key = bytes.fromhex(args.session['derived_key']) + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to get roles of session") + sys.exit(-1) if __name__ == '__main__': listRoles(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_subject_roles b/delivery2/client/bin/rep_list_subject_roles index 8a26c84..f76cf8f 100755 --- a/delivery2/client/bin/rep_list_subject_roles +++ b/delivery2/client/bin/rep_list_subject_roles @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -56,10 +57,18 @@ def listSubjectRoles(args): logger.error("Failed to obtain response from server.") sys.exit(-1) - roles = req.json() - logger.info(json.dumps(roles, indent=4)) - - sys.exit(0) + derived_key = bytes.fromhex(args.session['derived_key']) + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) + else: + logger.error("Failed to get roles for user.") + sys.exit(-1) if __name__ == '__main__': listSubjectRoles(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_list_subjects b/delivery2/client/bin/rep_list_subjects index 0475ffa..afaa613 100755 --- a/delivery2/client/bin/rep_list_subjects +++ b/delivery2/client/bin/rep_list_subjects @@ -7,6 +7,7 @@ import json import argparse from subject import main +from lib.diffie_hellman import decrypt, encrypt logging.basicConfig(format='%(levelname)s\t- %(message)s') logger = logging.getLogger() @@ -43,38 +44,45 @@ def list_subjects(args): with open(BASE_DIR + args.session, 'r') as f: args.session = json.load(f) + payload = {} + derived_key = bytes.fromhex(args.session['derived_key']) + if args.username: - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/user/list', - json=json.dumps({'username' : args.username}), - headers={'Authorization': args.session['token']}) - subjects.raise_for_status() + payload['username'] = args.username[0] - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects.json()['error']) - sys.exit(-1) + try: + payload = json.dumps(payload) + payload = encrypt(payload, derived_key).hex() - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) + headers = { + 'Authorization': args.session['token'], + 'Content-Type': 'application/octet-stream' + } + req = requests.get(f'http://{state['REP_ADDRESS']}/user/list', + data=payload, + headers=headers) + req.raise_for_status() + + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) + + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server.") + sys.exit(-1) + + if req.status_code == 200: + try: + response_data = json.loads(decrypt(bytes.fromhex(req.text), derived_key).decode('utf-8')) + except Exception: + logger.error("Failed to decrypt the content") + sys.exit(1) + logger.info(json.dumps(response_data, indent=4)) + sys.exit(0) else: - try: - subjects = requests.get(f'http://{state['REP_ADDRESS']}/user/list', - json=json.dumps({}), - headers={'Authorization': args.session['token']}) - subjects.raise_for_status() - - except requests.exceptions.HTTPError: - logger.error("%d: %s", subjects.status_code, subjects.json()['error']) - sys.exit(-1) - - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) - - logger.info(json.dumps(subjects.json(), indent=4)) - sys.exit(0) + logger.error("Failed to get subjects") + sys.exit(-1) if __name__ == '__main__': list_subjects(sys.argv[1:]) \ No newline at end of file diff --git a/delivery2/client/bin/rep_remove_permission b/delivery2/client/bin/rep_remove_permission index b7860c6..bc3ef1a 100755 --- a/delivery2/client/bin/rep_remove_permission +++ b/delivery2/client/bin/rep_remove_permission @@ -60,38 +60,21 @@ def removePermission(args): ]: isPerm = True - if isPerm: - try: - req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + '/perm/remove/' + args.value, - headers={'Authorization': args.session['token']}) - req.raise_for_status() + try: + req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + f'/{"perm" if isPerm else "user"}/remove/' + args.value, + headers={'Authorization': args.session['token']}) + req.raise_for_status() - except requests.exceptions.HTTPError: - logger.error("%d: %s", req.status_code, req.json()['error']) - sys.exit(-1) + except requests.exceptions.HTTPError: + logger.error("%d: %s", req.status_code, req.json()['error']) + sys.exit(-1) - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) + except requests.exceptions.RequestException as errex: + logger.error("Failed to obtain response from server.") + sys.exit(-1) - logger.info("Permission removed from role successfully.") - sys.exit(0) - else: - try: - req = requests.post(f'http://{state['REP_ADDRESS']}/role/' + args.role + '/user/remove/' + args.value, - headers={'Authorization': args.session['token']}) - req.raise_for_status() - - except requests.exceptions.HTTPError: - logger.error("%d: %s", req.status_code, req.json()['error']) - sys.exit(-1) - - except requests.exceptions.RequestException as errex: - logger.error("Failed to obtain response from server.") - sys.exit(-1) - - logger.info("Subject removed from role successfully.") - sys.exit(0) + logger.info("Permission removed successfully from role." if isPerm else "User removed successfully from role.") + sys.exit(0) if __name__ == '__main__': diff --git a/delivery2/server/database.py b/delivery2/server/database.py index 06dd911..0f0a3f0 100644 --- a/delivery2/server/database.py +++ b/delivery2/server/database.py @@ -1,4 +1,6 @@ from flask_sqlalchemy import SQLAlchemy +from services.security import SecurityService db_connection = SQLAlchemy() -db = db_connection.session \ No newline at end of file +db = db_connection.session +security_service = SecurityService() \ No newline at end of file diff --git a/delivery2/server/models/session.py b/delivery2/server/models/session.py index bd2871d..5b24648 100644 --- a/delivery2/server/models/session.py +++ b/delivery2/server/models/session.py @@ -19,8 +19,5 @@ class Session(db_connection.Model): "user_id": self.user_id, "org_id": self.org_id, "token": self.token, - "created_at": self.created_at, - "updated_at": self.updated_at, - "challenge": self.challenge, "verified": self.verified } diff --git a/delivery2/server/routes/file.py b/delivery2/server/routes/file.py index ba98adb..4a3f109 100644 --- a/delivery2/server/routes/file.py +++ b/delivery2/server/routes/file.py @@ -2,8 +2,11 @@ import json from flask import Blueprint, request, jsonify, send_file, Response +from database import security_service from utils import Perm, get_hex_from_temp_file, get_hash, check_valid_time, PermOperation from services import FileService, OrganizationService, UserService, SessionService, RoleService +from utils.comms_encryption import encrypt_response +from utils.exceptions import SessionException file_bp = Blueprint("file", __name__) upload_service = FileService() @@ -22,9 +25,10 @@ def file_get_content(file_handle: str): @file_bp.route("/get//metadata", methods=["GET"]) def file_get_metadata(document_handle: str): session_token = request.headers.get("Authorization") - session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_READ], doc_handle=document_handle) - if isinstance(session, tuple): - return session + try: + session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_READ], doc_handle=document_handle) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -34,19 +38,20 @@ def file_get_metadata(document_handle: str): if not file: return jsonify({"error": "File not found"}), 404 - return jsonify(file.to_dict() | file.get_encrytion()) + return encrypt_response(file.to_dict() | file.get_encrytion(), 200, security_service.get_key(session)) @file_bp.route("/upload/metadata", methods=["POST"]) def file_upload_metadata(): - session_token = request.headers.get("Authorization") - session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_NEW]) - if isinstance(session, tuple): - return session + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + + session_token = request.headers.get("Authorization") + try: + session, data = SessionService.validate_session(session_token, data=request.data, required_perms=[Perm.DOC_NEW]) + except SessionException as e: + return jsonify({"error": e.message}), e.code - data = request.json - if type(data) is str: - data = json.loads(data) if "document_name" not in data or "key" not in data or "alg" not in data: return jsonify({"error": "Missing required fields"}), 400 @@ -59,7 +64,7 @@ def file_upload_metadata(): return jsonify({"error": "User not found"}), 404 file = upload_service.create_file(session.token, org, user, data["document_name"], data["key"], data["alg"]) - return jsonify(file.to_dict()), 201 + return encrypt_response(file.to_dict(), 201, security_service.get_key(session)) @file_bp.route("/upload/content", methods=["POST"]) @@ -95,23 +100,22 @@ def file_upload_content(): if isinstance(file, tuple): return file - return jsonify(file.to_dict()), 201 + return encrypt_response(file.to_dict(), 201, security_service.get_key(session)) @file_bp.route("/list", methods=["GET"]) def file_list(): + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + session_token = request.headers.get("Authorization") if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token) - if isinstance(session, tuple): - return session - - - data = request.json - if type(data) is str: - data = json.loads(data) + try: + session, data = SessionService.validate_session(session_token, data=request.data) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -129,13 +133,14 @@ def file_list(): if not user: return jsonify({"error": "User not found"}), 404 files = FileService.list_files_in_org(org) - return jsonify([file.to_dict() for file in files if file.creator_id == user.id and ( + return encrypt_response([file.to_dict() for file in files if file.creator_id == user.id and ( check_valid_time(file.created_at, datetime_value, datetime_relation) if "datetime" in data else True - )]) + )], 200, security_service.get_key(session)) files = FileService.list_files_in_org(org) - return jsonify([file.to_dict() for file in files if (check_valid_time(file.created_at, datetime_value, datetime_relation) if "datetime" in data else True)]) + return encrypt_response([file.to_dict() for file in files if (check_valid_time(file.created_at, datetime_value, datetime_relation) if "datetime" in data else True)], + 200, security_service.get_key(session)) @file_bp.route("/delete/", methods=["POST"]) @@ -144,9 +149,10 @@ def file_delete(document_handle: str): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_DELETE], doc_handle=document_handle) - if isinstance(session, tuple): - return session + try: + session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_DELETE], doc_handle=document_handle) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -160,27 +166,30 @@ def file_delete(document_handle: str): return jsonify({"error": "Not authorized to delete file"}), 403 file = FileService.delete_file(file, session.user_id) - return jsonify(file.to_dict()) + return encrypt_response(file.to_dict(), 200, security_service.get_key(session)) -@file_bp.route("/acl", methods=["POST"]) -def file_acl(): +@file_bp.route("/acl/", methods=["POST"]) +def file_acl(document_handle: str): + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + session_token = request.headers.get("Authorization") if not session_token: return jsonify({"error": "No session token"}), 400 - data = request.json - if type(data) is str: - data = json.loads(data) - if "document_handle" not in data or "role" not in data or "perm" not in data or "operation" not in data: + try: + session, data = SessionService.validate_session(session_token, data=request.data, required_perms=[Perm.DOC_ACL], doc_handle=document_handle) + except SessionException as e: + return jsonify({"error": e.message}), e.code + + if "role" not in data or "perm" not in data or "operation" not in data: return jsonify({"error": "Missing required fields"}), 400 - doc_handle = data["document_handle"] role = data["role"] perm = Perm.from_str(data["perm"]) operation = PermOperation.ADD if data["operation"] == "add" else PermOperation.REMOVE - session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_ACL], doc_handle=doc_handle) if isinstance(session, tuple): return session @@ -188,7 +197,7 @@ def file_acl(): if not org: return jsonify({"error": "Organization not found"}), 404 - file = FileService.get_file_by_document_handle(doc_handle) + file = FileService.get_file_by_document_handle(document_handle) if not file: return jsonify({"error": "File not found"}), 404 @@ -199,5 +208,5 @@ def file_acl(): if isinstance(acl, tuple): return acl - return jsonify(file.to_dict()), 200 + return encrypt_response(file.to_dict(), 200, security_service.get_key(session)) diff --git a/delivery2/server/routes/org.py b/delivery2/server/routes/org.py index 586952b..a9aaf76 100644 --- a/delivery2/server/routes/org.py +++ b/delivery2/server/routes/org.py @@ -1,14 +1,17 @@ import json from flask import Blueprint, request, jsonify from services import OrganizationService +from utils.comms_encryption import decrypt_request_with_iv, encrypt_response_with_iv org_bp = Blueprint("org", __name__) @org_bp.route("/create", methods=["POST"]) def org_create(): - data = request.json - if type(data) is str: - data = json.loads(data) + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + + data = json.loads(decrypt_request_with_iv(request.data)) + if "name" not in data or "username" not in data or "full_name" not in data or "email" not in data or "public_key" not in data: return jsonify({"error": "Missing required fields"}), 400 @@ -24,9 +27,8 @@ def org_create(): public_key=data["public_key"] ) - return jsonify(org.to_dict()), 201 + return encrypt_response_with_iv(json.dumps(org.to_dict()), 201) @org_bp.route("/list", methods=["GET"]) def org_list(): - orgs = OrganizationService.list_organizations() - return jsonify([org.to_dict() for org in orgs]) + return encrypt_response_with_iv(json.dumps([org.to_dict() for org in OrganizationService.list_organizations()]), 200) diff --git a/delivery2/server/routes/role.py b/delivery2/server/routes/role.py index 56891dc..acda506 100644 --- a/delivery2/server/routes/role.py +++ b/delivery2/server/routes/role.py @@ -1,27 +1,31 @@ import json from flask import Blueprint, request, jsonify + +from database import security_service from services import UserService, SessionService, OrganizationService, RoleService from utils import Perm, PermOperation +from utils.comms_encryption import encrypt_response +from utils.exceptions import SessionException role_bp = Blueprint("role", __name__) @role_bp.route("/create", methods=["POST"]) def role_create(): - data = request.json - if type(data) is str: - data = json.loads(data) - - if "role" not in data: - return jsonify({"error": "Missing required fields"}), 400 + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 session_token = request.headers.get("Authorization") if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_NEW]) - if not session: - return jsonify({"error": "Not authenticated"}), 401 + try: + session, data = SessionService.validate_session(session_token, data=request.data, required_perms=[Perm.ROLE_NEW]) + except SessionException as e: + return jsonify({"error": e.message}), e.code + + if "role" not in data: + return jsonify({"error": "Missing required fields"}), 400 org = OrganizationService.get_organization(session.org_id) if not org: @@ -32,7 +36,7 @@ def role_create(): except ValueError as e: return jsonify({"error": str(e)}), 400 - return jsonify(role), 201 + return encrypt_response(role, 201, security_service.get_key(session)) @role_bp.route("//list/users", methods=["GET"]) @@ -52,7 +56,8 @@ def role_list_users(role): users = RoleService.get_users_in_role(org, role) if isinstance(users, tuple): return users - return jsonify(users), 200 + + return encrypt_response(users, 200, security_service.get_key(session)) @role_bp.route("//list/perms", methods=["GET"]) @@ -72,7 +77,8 @@ def role_list_perms(role): perms = RoleService.get_perms_for_role(org, role, return_str=True) if isinstance(perms, tuple): return perms - return jsonify(perms), 200 + + return encrypt_response(perms, 200, security_service.get_key(session)) @role_bp.route("//suspend", methods=["POST"]) @@ -81,7 +87,7 @@ def role_suspend(role): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_DOWN]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_DOWN]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -102,7 +108,7 @@ def role_activate(role): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_UP]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_UP]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -123,7 +129,7 @@ def role_user_add(role, username): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_MOD]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_MOD]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -148,7 +154,7 @@ def role_user_remove(role, username): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_MOD]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_MOD]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -173,7 +179,7 @@ def role_perm_add(role, perm): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_MOD]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_MOD]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -194,7 +200,7 @@ def role_perm_remove(role, perm): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, [Perm.ROLE_MOD]) + session = SessionService.validate_session(session_token, required_perms=[Perm.ROLE_MOD]) if not session: return jsonify({"error": "Not authenticated"}), 401 @@ -228,7 +234,7 @@ def role_session_assume(role): if isinstance(session, tuple): return session - return jsonify(session.to_dict()), 200 + return encrypt_response(session.to_dict(), 200, security_service.get_key(session)) @role_bp.route("/session/drop/", methods=["POST"]) @@ -250,7 +256,7 @@ def role_session_drop(role): if isinstance(session, tuple): return session - return jsonify(session.to_dict()), 200 + return encrypt_response(session.to_dict(), 200, security_service.get_key(session)) @role_bp.route("/session/list", methods=["GET"]) @@ -264,7 +270,7 @@ def role_session_list(): return jsonify({"error": "Not authenticated"}), 401 roles = SessionService.list_roles(session) - return jsonify(roles), 200 + return encrypt_response(roles, 200, security_service.get_key(session)) @role_bp.route("/perm//roles", methods=["GET"]) def perm_list_roles(perm): @@ -288,4 +294,5 @@ def perm_list_roles(perm): return jsonify({"error": "Invalid permission"}), 400 if isinstance(roles, tuple): return roles - return jsonify(roles), 200 \ No newline at end of file + + return encrypt_response(roles, 200, security_service.get_key(session)) \ No newline at end of file diff --git a/delivery2/server/routes/user.py b/delivery2/server/routes/user.py index e305072..e43ccc7 100644 --- a/delivery2/server/routes/user.py +++ b/delivery2/server/routes/user.py @@ -1,20 +1,52 @@ import base64 import json +from cryptography.hazmat.primitives import serialization from cryptography.exceptions import InvalidSignature from flask import Blueprint, request, jsonify from services import UserService, SessionService, OrganizationService, RoleService -from utils import Perm +from utils import Perm, generate_parameters, generate_key_pair, derive_keys, decrypt +from utils.comms_encryption import encrypt_response, decrypt_request +from database import security_service +from utils.exceptions import SessionException user_bp = Blueprint("user", __name__) @user_bp.route("/login", methods=["POST"]) def user_login(): - data = request.json - if type(data) is str: - data = json.loads(data) + if request.headers.get("Content-Type") == "application/octet-stream": + if session_token := request.headers.get("Authorization"): + data = request.data + + session = SessionService.get_session(session_token) + if not session: + return jsonify({"error": "Not authenticated"}), 401 + + if session.verified: + return encrypt_response(session.to_dict(), 201, security_service.get_key(session)) + + try: + data = decrypt_request(data.decode(), security_service.get_key(session)) + except Exception: + return jsonify({"error": "Invalid encryption"}), 400 + + if not "signature" in data: + return jsonify({"error": "Missing required fields"}), 400 + + signature = data["signature"] + signature = base64.b64decode(signature) + + session = SessionService.verify_session(session_token, signature) + if isinstance(session, tuple): + return session + return encrypt_response(session.to_dict(), 201, security_service.get_key(session)) + return jsonify({"error": "No session token"}), 400 + + if request.json and "username" in request.json and "org" in request.json: + data = request.json + if type(data) is str: + data = json.loads(data) - if "username" in data and "org" in data: user = UserService.get_user_by_username(data["username"]) if not user: return jsonify({"error": "User not found"}), 404 @@ -24,30 +56,49 @@ def user_login(): return jsonify({"error": "Organization not found"}), 404 session = SessionService.create_session(user, org) - return jsonify(session.to_dict()), 201 - - elif session_token := request.headers.get("Authorization"): - session = SessionService.get_session(session_token) - if not session: - return jsonify({"error": "Not authenticated"}), 401 - - if session.verified: - return jsonify(session.to_dict()), 200 - - if not "signature" in data: - return jsonify({"error": "Missing required fields"}), 400 - - signature = data["signature"] - signature = base64.b64decode(signature) - - session = SessionService.verify_session(session_token, signature) - if isinstance(session, tuple): - return session - return jsonify(session.to_dict()), 201 + payload = { + "token": session.token, + "challenge": session.challenge + } + return encrypt_response(payload, 201, security_service.get_key(session)) return jsonify({"error": "Missing required fields"}), 400 +@user_bp.route("/dh", methods=["POST"]) +def user_dh(): + data = request.json + if type(data) is str: + data = json.loads(data) + + if "user" not in data or "org" not in data: + return jsonify({"error": "Missing required fields"}), 400 + + if "parameters" in data: + parameters_bytes = bytes.fromhex(data["parameters"]) + parameters = serialization.load_pem_parameters(parameters_bytes) + + private_key, public_key = generate_key_pair(parameters) + + security_service.register_key(data['user'], data['org'], private_key) + + public_key_content = public_key.public_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo) + return jsonify({ + "public_key": public_key_content.hex() + }), 200 + + elif "public_key" in data: + client_public_key = data["public_key"] + client_public_key = serialization.load_pem_public_key(bytes.fromhex(client_public_key)) + private_key = security_service.keys[f'{data["org"]}/{data["user"]}'] + derived_key = derive_keys(private_key, client_public_key) + security_service.register_key(data['user'], data['org'], derived_key) + return jsonify({"message": "Keys exchanged"}), 200 + + else: + return jsonify({"error": "Missing required fields"}), 400 + @user_bp.route("/logout", methods=["POST"]) def user_logout(): session_token = request.headers.get("Authorization") @@ -64,17 +115,17 @@ def user_logout(): @user_bp.route("/list", methods=["GET"]) def user_list(): + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + session_token = request.headers.get("Authorization") if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token) - if isinstance(session, tuple): - return session - - data = request.json - if type(data) is str: - data = json.loads(data) + try: + session, data = SessionService.validate_session(session_token, data=request.data) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -84,25 +135,25 @@ def user_list(): user = UserService.get_user_by_username(data["username"]) if not user: return jsonify({"error": "User not found"}), 404 - return jsonify(user.to_dict()), 200 + return encrypt_response(user.to_dict(), 200, security_service.get_key(session)) users = OrganizationService.get_users_in_organization(org) - return jsonify(users), 200 + return encrypt_response(users, 200, security_service.get_key(session)) @user_bp.route("/create", methods=["POST"]) def user_create(): + if request.headers.get("Content-Type") != "application/octet-stream": + return jsonify({"error": "Invalid request"}), 400 + session_token = request.headers.get("Authorization") if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, required_perms=[Perm.SUBJECT_NEW]) - if isinstance(session, tuple): - return session - - data = request.json - if type(data) is str: - data = json.loads(data) + try: + session, data = SessionService.validate_session(session_token, data=request.data, required_perms=[Perm.SUBJECT_NEW]) + except SessionException as e: + return jsonify({"error": e.message}), e.code if "username" not in data or "full_name" not in data or "email" not in data or "public_key" not in data: return jsonify({"error": "Missing required fields"}), 400 @@ -121,7 +172,7 @@ def user_create(): org=org ) - return jsonify(user.to_dict()), 201 + return encrypt_response(user.to_dict(), 201, security_service.get_key(session)) @user_bp.route("//roles", methods=["GET"]) @@ -130,9 +181,10 @@ def user_roles(username): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token) - if isinstance(session, tuple): - return session + try: + session = SessionService.validate_session(session_token) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -143,8 +195,7 @@ def user_roles(username): return jsonify({"error": "User not found"}), 404 roles = RoleService.get_roles_for_user(user, org) - return jsonify(roles), 200 - + return encrypt_response(roles, 200, security_service.get_key(session)) @user_bp.route("//suspend", methods=["POST"]) def user_suspend(username): @@ -152,9 +203,10 @@ def user_suspend(username): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, required_perms=[Perm.SUBJECT_DOWN]) - if isinstance(session, tuple): - return session + try: + session = SessionService.validate_session(session_token, required_perms=[Perm.SUBJECT_DOWN]) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: @@ -173,9 +225,10 @@ def user_unsuspend(username): if not session_token: return jsonify({"error": "No session token"}), 400 - session = SessionService.validate_session(session_token, required_perms=[Perm.SUBJECT_UP]) - if isinstance(session, tuple): - return session + try: + session = SessionService.validate_session(session_token, required_perms=[Perm.SUBJECT_UP]) + except SessionException as e: + return jsonify({"error": e.message}), e.code org = OrganizationService.get_organization(session.org_id) if not org: diff --git a/delivery2/server/services/files.py b/delivery2/server/services/files.py index 9a77bcb..cbc7a0e 100644 --- a/delivery2/server/services/files.py +++ b/delivery2/server/services/files.py @@ -65,28 +65,6 @@ class FileService: return file - @staticmethod - def create_dummy_file(org: Organization, user: User) -> File: - file = File( - file_handle = "dummy_file", - document_handle = "org/dummy_file.txt", - name = "dummy_file", - created_at = int(datetime.now().timestamp()), - org_id = 1, - creator_id = 1, - org = org, - creator = user - ) - - file_path = os.path.join(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "repository"), file.document_handle) - with open(file_path, "w") as f: - f.write("Dummy file content") - - db.add(file) - db.commit() - db.refresh(file) - return file - @staticmethod def get_file(file_id: int) -> File | None: return db.query(File).filter(File.id == file_id).first() diff --git a/delivery2/server/services/orgs.py b/delivery2/server/services/orgs.py index f0bd593..2535669 100644 --- a/delivery2/server/services/orgs.py +++ b/delivery2/server/services/orgs.py @@ -94,7 +94,7 @@ class OrganizationService: return org @staticmethod - def suspend_user(org: Organization, user: User) -> tuple: + def suspend_user(org: Organization, user: User): if OrganizationService.get_user_status(org, user.id) != "active": return {"error": "User already suspended"}, 400 @@ -109,7 +109,7 @@ class OrganizationService: return {"message": "User suspended"}, 200 @staticmethod - def activate_user(org: Organization, user: User) -> tuple: + def activate_user(org: Organization, user: User): if OrganizationService.get_user_status(org, user.id) != "suspended": return {"error": "User already active"}, 400 diff --git a/delivery2/server/services/roles.py b/delivery2/server/services/roles.py index 3b036e1..53f63ce 100644 --- a/delivery2/server/services/roles.py +++ b/delivery2/server/services/roles.py @@ -100,8 +100,10 @@ class RoleService: return [role for role in org.roles if user.id in org.roles[role]["users"]] @staticmethod - def get_perms_for_role(org: Organization, role: str, return_str=False) -> list[Perm | str]: + def get_perms_for_role(org: Organization, role: str, return_str=False) -> list[Perm | str] | tuple: from services import FileService + if role not in org.roles: + return jsonify({"error": f"Role {role} does not exist in organization {org.name}"}), 400 perms_list = Perm.get_perms(org.roles[role]["permissions"], return_str) for f in FileService.list_files_in_org(org): perms_list.append({f.document_handle: Perm.get_perms(f.acl[role], return_str)}) diff --git a/delivery2/server/services/security.py b/delivery2/server/services/security.py new file mode 100644 index 0000000..15611df --- /dev/null +++ b/delivery2/server/services/security.py @@ -0,0 +1,14 @@ +class SecurityService: + def __init__(self): + self.keys = {} + + + def register_key(self, username, org, key): + self.keys[f'{org}/{username}'] = key + + + def get_key(self, session): + from services import UserService, OrganizationService + username = UserService.get_user(session.user_id).username + org = OrganizationService.get_organization(session.org_id).name + return self.keys[f'{org}/{username}'] \ No newline at end of file diff --git a/delivery2/server/services/sessions.py b/delivery2/server/services/sessions.py index 6ea5c4c..d9ecd3f 100644 --- a/delivery2/server/services/sessions.py +++ b/delivery2/server/services/sessions.py @@ -1,3 +1,4 @@ +import json import secrets from cryptography.exceptions import InvalidSignature @@ -6,11 +7,13 @@ from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes from sqlalchemy.orm.attributes import flag_modified -from database import db +from database import db, security_service from models import Session, User, Organization from flask import jsonify from utils import Perm +from utils.comms_encryption import decrypt_request +from utils.exceptions import SessionException class SessionService: @@ -63,33 +66,43 @@ class SessionService: db.commit() @staticmethod - def validate_session(token: str, required_perms: list[Perm] = None, doc_handle=None) -> tuple | Session: - from services import OrganizationService + def validate_session(token: str, data: bytes = None, required_perms: list[Perm] = None, doc_handle=None) -> tuple | Session: + from services import OrganizationService, UserService if "Bearer" in token: token = token.split(" ")[1] session = SessionService.get_session(token) if not session: - return jsonify({"error": "Not authenticated"}), 401 + raise SessionException("Not authenticated", 401) if not session.verified: - return jsonify({"error": "Session has not yet been verified"}), 403 + raise SessionException("Session has not yet been verified", 403) + + user = UserService.get_user(session.user_id) + if not user: + raise SessionException("User not found", 404) org = OrganizationService.get_organization(session.org_id) if not org: - return jsonify({"error": "Organization not found"}), 404 + raise SessionException("Organization not found", 404) status = OrganizationService.get_user_status(org, session.user_id) if status != "active": - return jsonify({"error": "User is not active"}), 403 + raise SessionException("User is not active", 403) + + if data is not None: + try: + data = decrypt_request(data.decode(), security_service.get_key(session)) + except Exception: + raise SessionException("Failed to decrypt request data", 400) if required_perms: for perm in required_perms: if not SessionService.check_permission(session, perm, doc_handle): - return jsonify({"error": f"Permission denied, missing required permission: {perm}"}), 403 + raise SessionException(f"Permission denied, missing required permission: {perm}", 403) - return session + return (session, data) if data is not None else session @staticmethod def change_role(session: Session, role: str, operation: str): diff --git a/delivery2/server/utils/__init__.py b/delivery2/server/utils/__init__.py index eec37f3..1d3c6f9 100644 --- a/delivery2/server/utils/__init__.py +++ b/delivery2/server/utils/__init__.py @@ -1,3 +1,5 @@ from .checks import check_valid_time from .hashing import get_hash, get_hex_from_temp_file, encode_public_key -from .perms import Perm, PermOperation \ No newline at end of file +from .perms import Perm, PermOperation +from .diffie_hellman import diffie_hellman, load_dh_private_key, load_dh_public_key, derive_keys, generate_key_pair, generate_parameters, encrypt, decrypt +from .comms_encryption import encrypt_response, decrypt_request, encrypt_response_with_iv, decrypt_request_with_iv \ No newline at end of file diff --git a/delivery2/server/utils/comms_encryption.py b/delivery2/server/utils/comms_encryption.py new file mode 100644 index 0000000..13ac996 --- /dev/null +++ b/delivery2/server/utils/comms_encryption.py @@ -0,0 +1,38 @@ +import json +import os +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from utils import encrypt, decrypt + + +def encrypt_response(payload: json, code: int, key): + payload_str = json.dumps(payload) + encrypted = encrypt(payload_str.encode(), key).hex() + return encrypted, code + + +def decrypt_request(data: str, key): + return json.loads(decrypt(bytes.fromhex(data), key).decode()) + + +def encrypt_response_with_iv(input_string: str, code: int) -> tuple[str, int]: + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(iv), modes.CFB(iv)) + encryptor = cipher.encryptor() + + plaintext_bytes = input_string.encode('utf-8') + ciphertext = encryptor.update(plaintext_bytes) + encryptor.finalize() + + encrypted_data = iv + ciphertext + + return encrypted_data.hex(), code + +def decrypt_request_with_iv(encrypted_data: bytes) -> str: + iv = encrypted_data[:16] + ciphertext = encrypted_data[16:] + + cipher = Cipher(algorithms.AES(iv), modes.CFB(iv)) + decryptor = cipher.decryptor() + + plaintext_bytes = decryptor.update(ciphertext) + decryptor.finalize() + + return plaintext_bytes.decode('utf-8') \ No newline at end of file diff --git a/delivery2/server/utils/diffie_hellman.py b/delivery2/server/utils/diffie_hellman.py new file mode 100644 index 0000000..d7b0e51 --- /dev/null +++ b/delivery2/server/utils/diffie_hellman.py @@ -0,0 +1,147 @@ +from cryptography.hazmat.primitives.asymmetric import dh +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + +import subprocess +import os + +def generate_parameters(generator: int, key_size: int, backend=default_backend()): + """ + Generate the parameters for the Diffie-Hellman key exchange + :param generator: + :param key_size: + :param backend: + :return: + """ + + if key_size < 512: + raise ValueError("Key size must be at least 512 bits") + + if generator not in [2, 5]: + raise ValueError("Generator must be 2 or 5") + + return dh.generate_parameters(generator, key_size, backend) + + +def generate_key_pair(parameters: dh.DHParameters): + """ + Generate a key pair for the Diffie-Hellman key exchange + :param parameters: + :return private_key, public_key: + """ + private_key = parameters.generate_private_key() + return private_key, private_key.public_key() + + +def derive_keys(private_key: dh.DHPrivateKey, peer_public_key: dh.DHPublicKey): + """ + Derive the shared key from the private key and the peer's public key + :param private_key: + :param peer_public_key: + :return private_key, derived_key: + """ + shared_key = private_key.exchange(peer_public_key) + derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b'handshake data', + backend=default_backend() + ).derive(shared_key) + + return derived_key + + +# Encrypt content using the derived key +def encrypt(content, key): + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = iv + encryptor.update(content) + encryptor.finalize() + return ciphertext + + +# Decrypt content using the derived key +def decrypt(ciphertext, key): + iv = ciphertext[:16] + cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) + decryptor = cipher.decryptor() + plaintext = decryptor.update(ciphertext[16:]) + decryptor.finalize() + return plaintext + + +def load_dh_public_key(key): + with open(key, 'rb') as key_file: + public_key = serialization.load_pem_public_key( + key_file.read(), + ) + return public_key + + +def load_dh_private_key(file, passwd=None): + if passwd is not None: + passwd = passwd.encode('utf-8') + try: + with open(file, 'rb') as key_file: + private_key = serialization.load_pem_private_key( + key_file.read(), + password=passwd, + ) + except ValueError as e: + raise ValueError("Error: The password is not valid.") from e + return private_key + + +def diffie_hellman(): + # imagine that the agreement was done beforehand + generator = 2; key_size = 1024 + + parameters = generate_parameters(generator, key_size) + + # generate keys for client and server + client_private_key, client_public_key = generate_key_pair(parameters) + server_private_key, server_public_key = generate_key_pair(parameters) + + # derive keys + client_derived_key = derive_keys(client_private_key, server_public_key) + server_derived_key = derive_keys(server_private_key, client_public_key) + + print("Client derived key: ", client_derived_key) + print("Server derived key: ", server_derived_key) + + + # write the keys to files + with open("client_private_key.pem", 'wb') as f: + f.write(client_private_key.private_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption())) + + with open("server_private_key.pem", 'wb') as f: + f.write(server_private_key.private_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption())) + + with open("client_derived_key.pem", 'wb') as f: + f.write(client_derived_key) + + with open("server_derived_key.pem", 'wb') as f: + f.write(server_derived_key) + + print(f"Client private key: \n\n{client_private_key}\nServer derived key: \n\n{server_derived_key}\n") + print(f"Server private key: \n\n{server_private_key}\nClient derived key: \n\n{client_derived_key}\n") + + # test encryption + + process = subprocess.Popen(f"dd if=/dev/zero of=file.txt bs=1024 count=1000", shell=True) + + with open("file.txt", 'rb') as f: + data = f.read() + + encrypted_data = encrypt(data, client_derived_key) + + decrypted_data = decrypt(encrypted_data, server_derived_key) + + if data == decrypted_data: + print("Encryption and decryption successful!") diff --git a/delivery2/server/utils/exceptions.py b/delivery2/server/utils/exceptions.py new file mode 100644 index 0000000..ae7957a --- /dev/null +++ b/delivery2/server/utils/exceptions.py @@ -0,0 +1,5 @@ +class SessionException(Exception): + def __init__(self, message: str, code: int): + super().__init__(message) + self.message = message + self.code = code \ No newline at end of file