From e108d2394548650ea22e382fecaf2fa25ba9a25e Mon Sep 17 00:00:00 2001 From: TySP-Dev Date: Wed, 6 Aug 2025 19:15:05 -1000 Subject: [PATCH] Phase 1 in progress --- application_data/requirements.txt | 18 +- application_data/settings.json | 1 + paccrypt_algos/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 168 bytes .../__pycache__/aes_cbc.cpython-313.pyc | Bin 0 -> 8128 bytes .../__pycache__/aes_gcm.cpython-313.pyc | Bin 0 -> 3425 bytes .../pqcrypto_hybrid.cpython-313.pyc | Bin 0 -> 6035 bytes .../__pycache__/rsa_hybrid.cpython-313.pyc | Bin 0 -> 8795 bytes .../__pycache__/xchacha.cpython-313.pyc | Bin 0 -> 4790 bytes paccrypt_algos/aes_cbc.py | 136 +++++++++++++ paccrypt_algos/aes_gcm.py | 68 +++++++ paccrypt_algos/pqcrypto_hybrid.py | 99 ++++++++++ paccrypt_algos/rsa_hybrid.py | 151 +++++++++++++++ paccrypt_algos/test_algos.py | 178 ++++++++++++++++++ paccrypt_algos/xchacha.py | 92 +++++++++ 15 files changed, 737 insertions(+), 6 deletions(-) create mode 100644 application_data/settings.json create mode 100644 paccrypt_algos/__init__.py create mode 100644 paccrypt_algos/__pycache__/__init__.cpython-313.pyc create mode 100644 paccrypt_algos/__pycache__/aes_cbc.cpython-313.pyc create mode 100644 paccrypt_algos/__pycache__/aes_gcm.cpython-313.pyc create mode 100644 paccrypt_algos/__pycache__/pqcrypto_hybrid.cpython-313.pyc create mode 100644 paccrypt_algos/__pycache__/rsa_hybrid.cpython-313.pyc create mode 100644 paccrypt_algos/__pycache__/xchacha.cpython-313.pyc create mode 100644 paccrypt_algos/aes_cbc.py create mode 100644 paccrypt_algos/aes_gcm.py create mode 100644 paccrypt_algos/pqcrypto_hybrid.py create mode 100644 paccrypt_algos/rsa_hybrid.py create mode 100644 paccrypt_algos/test_algos.py create mode 100644 paccrypt_algos/xchacha.py diff --git a/application_data/requirements.txt b/application_data/requirements.txt index 83ce7fe..f3daa0c 100644 --- a/application_data/requirements.txt +++ b/application_data/requirements.txt @@ -1,11 +1,17 @@ ### **requirements.txt** -flask==3.0.3 +# Core Flask stack +flask flask-cors -cryptography==42.0.5 -waitress==2.1.2 -werkzeug==3.0.1 -psutil>=5.9.0,<6.0.0 +waitress +werkzeug + +# Encryption engines +cryptography +pycryptodome +pqcrypto + +# Utility +psutil -# nginx - Only needed for Nginx integration, not installed via pip # Run pip install -r application_data/requirements.txt diff --git a/application_data/settings.json b/application_data/settings.json new file mode 100644 index 0000000..0702930 --- /dev/null +++ b/application_data/settings.json @@ -0,0 +1 @@ +{"upload_folder": "pacshare", "max_file_age_days": 14, "max_file_size_bytes": 26843545600} \ No newline at end of file diff --git a/paccrypt_algos/__init__.py b/paccrypt_algos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paccrypt_algos/__pycache__/__init__.cpython-313.pyc b/paccrypt_algos/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ef8248754ccc75b6230d0f4b02faba89d485f75 GIT binary patch literal 168 zcmey&%ge<81X-d}GC=fW5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~imenx(7s(wjj zvA#=wa%paAUP-ZjKv8~HYBGqCnCx6sSx}-Io|=?cP@rFsn4Apa$0z2b=NIe8$7kkc nmc+;F6;$5hu*uC&Da}c>D`Ewj3$nKu#Q4a}$jDg43}gWSwI(Wa literal 0 HcmV?d00001 diff --git a/paccrypt_algos/__pycache__/aes_cbc.cpython-313.pyc b/paccrypt_algos/__pycache__/aes_cbc.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f61bdecec3c98c48f6db1cef898eebc4893981cf GIT binary patch literal 8128 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 MIdXB_01JzM10>$TZvX%Q literal 0 HcmV?d00001 diff --git a/paccrypt_algos/__pycache__/pqcrypto_hybrid.cpython-313.pyc b/paccrypt_algos/__pycache__/pqcrypto_hybrid.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d37451535dd97f520d11217b320ca66a2f0dd1c6 GIT binary patch literal 6035 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 HY?=NS&ADv) literal 0 HcmV?d00001 diff --git a/paccrypt_algos/__pycache__/xchacha.cpython-313.pyc b/paccrypt_algos/__pycache__/xchacha.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1f7814f94bb61acc6e812af076fd287bb0caf4b GIT binary patch literal 4790 zcmcgw-A`M|6`$*S{dKP$Fy8?KhLSYS2N065n<%?YVjuyhB*M6HWwh4CzWzwSw&q@v z5cR=*s7lmsmsJy$N>Q6il}glo$U{}NeOjge0IXSo?uKfm<|Tb%o3xRlKJ=WqzSqFA z?e41T7@s+F=FE)Gncq1x!wauhBvAg7ZQp@ywCmj(VlQu@`K#Dy$+7YQ|(xynm z4rwEkHb>f-X44cu)nW$&VeV$^xz5r3d-Szb`dTCVTM2EO@<&?F#0ZhS1Z{2$w~uwu zPG*}w92o1OT>+BeGF-6ZIXZ7V$my;tMJ-pz%QL|HLRlI92Kp0M3p4Y_PxPMxVQogy z5>(Eo3bTpnc}-D4=D2zxJTx+ZT;$UEfm4IeYhk+QEl2`Da_77}(S?+($-09o+8oU% zVUw)F1Bc%imx0_TajRp(X0aC2O`_yfW%LC4WLJrBW^WI9WE+2NpVQLE-VE}(V_H^~ z!IQdsdhn!@PZm;&?no(EKjJ8$iKsYhKp*ML7G{+`ZC>pgDkSG-mAt0*U8RNVN)j14 z8KUz=?btPCS}qp*igGfE?Swp&DX4w7lUW)5dW-Ya2eUDPjM@RDM4n2%b*b$mscmVf zEOo6*hgPLS%gtr!_=<4+1!#~RenIQLl3)I_KdOGA`(WlEPjQ==KMX(U4#O|B9^{5M zXalgc*(BIf{Xlk@^2_kg!^_UHzuz!~HiClgD$1&Qt3Xq_pvp5^a{E@gK!F)H01Rxn z1%?320JB>cQwq)9 zR1!CodD;bXM6`<5G4mG>eITx#39r4AT9uR)K{4!h7iZ*LUQ=%GG}+qAzrpbBCRYtM z_a|?wI;83ZtCx@CZ)PP{cdFv9ybwpHvNFMvq}ej+ZW0TD^K5y0)II^}V0VdK>-o3=78Y7w>=1#r9-8m zryl=XH15Q!>U zYJ$p4CLVJE2CG5Pz~80*00_WJtY7?gS?n%_Hl%$E-@7NRNdD5$hNtG9^dS7f(N+K8 zisy~e@P_F9@tuVm3o|QX8>spk?!|w7cD1qZcdd`572ny?*fR@?Pj^GHwrRaKxLO-r zK3uLnQyTqLYJj6?6^^2dWvORH=&7XbSw)laq)u8p3_lRDz!LUPK8H3qZ!0;-hPV+X zP;XZ`RcYt#a1DWvIU+#fh+|f&7&5a5;HcVbWjCwW8OKALmDoXZM#w`JwkGhD+f@K% zKaAKQ4}JlLuG!7`=P=X#wwj5fT!c4QsaT4~AmmfDXc6{RI$!zT3NyS=83t{)gN~L*vgz-XHntr3LpZo`~%)9R#xM z*UGo$rZbpipTX?eC1K6o{?NDKa=+)i>s%Dx_blDPNC>Ug_N}@4ajA|Kcl)bGSn^}H z|MjZ|5Pa-zf30Hvc-ME~0RP0+dSQ@%azMOr!ue!S3<=J?1LXJr;{dq^Ayd5sU~-Mx ztL<%^m9B9!K+g$!R?t*ZxNFDQ9V52u!f5xN!6p9J;Q9>{8{7q^)`inJP_PL^`Ahtc#h*fA?{CzcayYjk|Ues@FqF9Njdta_}z#MWni4|jYyxkaG*L+r!UAK&>0 qvY&PA`H@7)|1*m4oUvv8-(@81-z{6!epvJ;_|LG9n|xt*8* literal 0 HcmV?d00001 diff --git a/paccrypt_algos/aes_cbc.py b/paccrypt_algos/aes_cbc.py new file mode 100644 index 0000000..a36f2e4 --- /dev/null +++ b/paccrypt_algos/aes_cbc.py @@ -0,0 +1,136 @@ +import os +import base64 +from typing import Optional + +from cryptography.hazmat.primitives import padding, hashes, hmac +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from cryptography.exceptions import InvalidSignature + +# === Constants === +SALT_LENGTH = 16 +IV_LENGTH = 16 +PBKDF2_ITERATIONS = 200_000 +KEY_LENGTH = 32 +HMAC_KEY_LENGTH = 32 # For HMAC-SHA256 +HMAC_LENGTH = 32 # Output size of SHA256 + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_LENGTH + HMAC_KEY_LENGTH, + salt=salt, + iterations=PBKDF2_ITERATIONS, + backend=default_backend() + ) + full_key = kdf.derive(password.encode('utf-8')) + return full_key[:KEY_LENGTH], full_key[KEY_LENGTH:] + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + aes_key, hmac_key = derive_key(password, salt) + + padder = padding.PKCS7(128).padder() + padded = padder.update(plaintext.encode('utf-8')) + padder.finalize() + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + + payload = salt + iv + ciphertext + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(payload) + mac = h.finalize() + + return b64encode(payload + mac) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH] + mac = raw[-HMAC_LENGTH:] + + aes_key, hmac_key = derive_key(password, salt) + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(raw[:-HMAC_LENGTH]) + h.verify(mac) + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + plaintext = unpadder.update(padded) + unpadder.finalize() + + return plaintext.decode('utf-8') + +# === Encrypt File === +def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + plaintext = f.read() + + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + aes_key, hmac_key = derive_key(password, salt) + + padder = padding.PKCS7(128).padder() + padded = padder.update(plaintext) + padder.finalize() + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(padded) + encryptor.finalize() + + payload = salt + iv + ciphertext + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(payload) + mac = h.finalize() + + with open(out_path, 'wb') as f: + f.write(payload + mac) + +# === Decrypt File === +def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + raw = f.read() + + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:-HMAC_LENGTH] + mac = raw[-HMAC_LENGTH:] + + aes_key, hmac_key = derive_key(password, salt) + + h = hmac.HMAC(hmac_key, hashes.SHA256(), backend=default_backend()) + h.update(raw[:-HMAC_LENGTH]) + h.verify(mac) + + cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + padded = decryptor.update(ciphertext) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + plaintext = unpadder.update(padded) + unpadder.finalize() + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Algo Name === +def get_name(): + return "AES-CBC" diff --git a/paccrypt_algos/aes_gcm.py b/paccrypt_algos/aes_gcm.py new file mode 100644 index 0000000..c68f49f --- /dev/null +++ b/paccrypt_algos/aes_gcm.py @@ -0,0 +1,68 @@ +import os +import base64 +import json +from typing import Optional + +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +# === Constants === +SALT_LENGTH = 16 +IV_LENGTH = 12 +PBKDF2_ITERATIONS = 200_000 +KEY_LENGTH = 32 # 256 bits + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=KEY_LENGTH, + salt=salt, + iterations=PBKDF2_ITERATIONS, + backend=default_backend() + ) + return kdf.derive(password.encode('utf-8')) + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = os.urandom(SALT_LENGTH) + iv = os.urandom(IV_LENGTH) + key = derive_key(password, salt) + + aesgcm = AESGCM(key) + ciphertext = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None) + + payload = salt + iv + ciphertext + return b64encode(payload) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + salt = raw[:SALT_LENGTH] + iv = raw[SALT_LENGTH:SALT_LENGTH + IV_LENGTH] + ciphertext = raw[SALT_LENGTH + IV_LENGTH:] + + key = derive_key(password, salt) + aesgcm = AESGCM(key) + plaintext = aesgcm.decrypt(iv, ciphertext, None) + return plaintext.decode('utf-8') + +# === Metadata-less file interface (optional placeholders) === +def encrypt_file(in_path, out_path, key, metadata: Optional[dict] = None): + raise NotImplementedError("File encryption not implemented yet.") + +def decrypt_file(in_path, out_path, key, metadata: Optional[dict] = None): + raise NotImplementedError("File decryption not implemented yet.") + +# === Engine Name === +def get_name(): + return "AES-GCM" diff --git a/paccrypt_algos/pqcrypto_hybrid.py b/paccrypt_algos/pqcrypto_hybrid.py new file mode 100644 index 0000000..c9f2d07 --- /dev/null +++ b/paccrypt_algos/pqcrypto_hybrid.py @@ -0,0 +1,99 @@ +import os +import base64 +import json +import importlib +import sys +from pathlib import Path +from typing import Optional + +from pqcrypto.kem.ml_kem_768 import generate_keypair, encrypt as kem_encapsulate, decrypt as kem_decapsulate + +# === Allow Hybrid Selector === +PARENT_DIR = Path(__file__).resolve().parent.parent +if str(PARENT_DIR) not in sys.path: + sys.path.append(str(PARENT_DIR)) + +# === Constants === +KEM_ALG = "ML-KEM-768" +AES_KEY_SIZE = 32 # 256-bit +SYMMETRIC_DEFAULT = "aes_gcm" + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode("utf-8") + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode("utf-8")) + +# === Dynamic Engine Loader === +def load_engine(engine_name: str): + try: + return importlib.import_module(f'paccrypt_algos.{engine_name}') + except ModuleNotFoundError: + raise ValueError(f"Encryption engine '{engine_name}' not found.") + +# === Encrypt Text === +def encrypt_text(plaintext: str, public_key: bytes, engine_name: str = SYMMETRIC_DEFAULT) -> str: + engine = load_engine(engine_name) + kem_ciphertext, shared_secret = kem_encapsulate(public_key) + aes_key = shared_secret[:AES_KEY_SIZE] + + encrypted_data = engine.encrypt_text(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(kem_ciphertext).to_bytes(2, 'big') + kem_ciphertext + header + b'\0' + encrypted_data.encode() + return b64encode(payload) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, private_key: bytes) -> str: + raw = b64decode(encrypted_b64) + kem_len = int.from_bytes(raw[:2], 'big') + kem_ct = raw[2:2 + kem_len] + rest = raw[2 + kem_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + shared_secret = kem_decapsulate(private_key, kem_ct) + aes_key = shared_secret[:AES_KEY_SIZE] + + engine = load_engine(engine_name) + return engine.decrypt_text(encrypted_data.decode(), aes_key.hex()) + +# === Encrypt File === +def encrypt_file(in_path: str, out_path: str, public_key: bytes, engine_name: str = SYMMETRIC_DEFAULT): + engine = load_engine(engine_name) + kem_ciphertext, shared_secret = kem_encapsulate(public_key) + aes_key = shared_secret[:AES_KEY_SIZE] + + with open(in_path, 'rb') as f: + plaintext = f.read() + + encrypted = engine.encrypt_file_bytes(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(kem_ciphertext).to_bytes(2, 'big') + kem_ciphertext + header + b'\0' + encrypted + + with open(out_path, 'wb') as f: + f.write(payload) + +# === Decrypt File === +def decrypt_file(in_path: str, out_path: str, private_key: bytes): + with open(in_path, 'rb') as f: + raw = f.read() + + kem_len = int.from_bytes(raw[:2], 'big') + kem_ct = raw[2:2 + kem_len] + rest = raw[2 + kem_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + shared_secret = kem_decapsulate(private_key, kem_ct) + aes_key = shared_secret[:AES_KEY_SIZE] + + engine = load_engine(engine_name) + plaintext = engine.decrypt_file_bytes(encrypted_data, aes_key.hex()) + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Engine Name === +def get_name(): + return "PQCrypto Hybrid" diff --git a/paccrypt_algos/rsa_hybrid.py b/paccrypt_algos/rsa_hybrid.py new file mode 100644 index 0000000..43ceb9d --- /dev/null +++ b/paccrypt_algos/rsa_hybrid.py @@ -0,0 +1,151 @@ +import os +import base64 +import json +import importlib +from typing import Optional, Tuple +import sys +from pathlib import Path + +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.backends import default_backend + +PARENT_DIR = Path(__file__).resolve().parent.parent +if str(PARENT_DIR) not in sys.path: + sys.path.append(str(PARENT_DIR)) + +# === Constants === +RSA_KEY_SIZE = 4096 +AES_KEY_SIZE = 32 # 256-bit + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode("utf-8") + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode("utf-8")) + +# === RSA Key Generation === +def generate_key_pair() -> Tuple[bytes, bytes]: + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=RSA_KEY_SIZE, + backend=default_backend() + ) + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + return private_pem, public_pem + +# === Dynamic Engine Loader === +def load_engine(engine_name: str): + try: + return importlib.import_module(f'paccrypt_algos.{engine_name}') + except ModuleNotFoundError: + raise ValueError(f"Encryption engine '{engine_name}' not found.") + +# === Encrypt Text === +def encrypt_text(plaintext: str, public_key_pem: str, engine_name: str = "aes_gcm") -> str: + engine = load_engine(engine_name) + aes_key = os.urandom(AES_KEY_SIZE) + + public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend()) + encrypted_key = public_key.encrypt( + aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + encrypted_data = engine.encrypt_text(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data.encode() + return b64encode(payload) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, private_key_pem: str) -> str: + private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend()) + raw = b64decode(encrypted_b64) + + enc_key_len = int.from_bytes(raw[:2], 'big') + enc_key = raw[2:2 + enc_key_len] + rest = raw[2 + enc_key_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + aes_key = private_key.decrypt( + enc_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + engine = load_engine(engine_name) + return engine.decrypt_text(encrypted_data.decode(), aes_key.hex()) + +# === Encrypt File === +def encrypt_file(in_path: str, out_path: str, public_key_pem: str, engine_name: str = "aes_gcm"): + engine = load_engine(engine_name) + aes_key = os.urandom(AES_KEY_SIZE) + + public_key = serialization.load_pem_public_key(public_key_pem.encode(), backend=default_backend()) + encrypted_key = public_key.encrypt( + aes_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + with open(in_path, 'rb') as f: + plaintext = f.read() + + encrypted_data = engine.encrypt_file_bytes(plaintext, aes_key.hex()) + header = json.dumps({"alg": engine_name}).encode() + payload = len(encrypted_key).to_bytes(2, 'big') + encrypted_key + header + b'\0' + encrypted_data + + with open(out_path, 'wb') as f: + f.write(payload) + +# === Decrypt File === +def decrypt_file(in_path: str, out_path: str, private_key_pem: str): + private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None, backend=default_backend()) + + with open(in_path, 'rb') as f: + raw = f.read() + + enc_key_len = int.from_bytes(raw[:2], 'big') + enc_key = raw[2:2 + enc_key_len] + rest = raw[2 + enc_key_len:] + header_data, encrypted_data = rest.split(b'\0', 1) + engine_name = json.loads(header_data.decode()).get("alg") + + aes_key = private_key.decrypt( + enc_key, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + + engine = load_engine(engine_name) + plaintext = engine.decrypt_file_bytes(encrypted_data, aes_key.hex()) + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Engine Name === +def get_name(): + return "RSA Hybrid" diff --git a/paccrypt_algos/test_algos.py b/paccrypt_algos/test_algos.py new file mode 100644 index 0000000..557524a --- /dev/null +++ b/paccrypt_algos/test_algos.py @@ -0,0 +1,178 @@ +import os +import sys + +from aes_gcm import encrypt_text as aesgcm_encrypt_text, decrypt_text as aesgcm_decrypt_text, \ + encrypt_file as aesgcm_encrypt_file, decrypt_file as aesgcm_decrypt_file +from aes_cbc import encrypt_text as aescbc_encrypt_text, decrypt_text as aescbc_decrypt_text, \ + encrypt_file as aescbc_encrypt_file, decrypt_file as aescbc_decrypt_file +from xchacha import encrypt_text as xchacha_encrypt_text, decrypt_text as xchacha_decrypt_text, \ + encrypt_file as xchacha_encrypt_file, decrypt_file as xchacha_decrypt_file +import rsa_hybrid +import pqcrypto_hybrid + +def load_text(path, binary=False): + with open(path, 'rb' if binary else 'r') as f: + return f.read() + +def save_text(path, data, binary=False): + with open(path, 'wb' if binary else 'w') as f: + f.write(data) + +def select_symmetric(): + print("\nšŸ”€ Select symmetric engine:") + choices = ["aes_gcm", "aes_cbc", "xchacha"] + for i, c in enumerate(choices): + print(f" [{i}] {c}") + while True: + try: + choice = int(input("Choice: ")) + return choices[choice] + except (ValueError, IndexError): + print("āŒ Invalid choice. Try again.") + +def hybrid_cli(name, module, key_ext, symmetric_engine, is_pem=False): + while True: + print(f"\n=== PacCrypt {name} Debug Mode ({symmetric_engine.upper()}) ===") + print("Choose:") + print(" [g] Generate keypair") + print(" [e] Encrypt text") + print(" [d] Decrypt text") + print(" [ef] Encrypt file") + print(" [df] Decrypt file") + print(" [b] Back to engine menu") + print(" [q] Quit script") + + mode = input("\nMode (g/e/d/ef/df/b/q): ").strip().lower() + + if mode == 'q': + return 'quit' + elif mode == 'b': + return 'back' + + try: + if mode == 'g': + priv, pub = module.generate_key_pair() if hasattr(module, 'generate_key_pair') else module.generate_keypair() + save_text(f"{name}_public.{key_ext}", pub, binary=True) + save_text(f"{name}_private.{key_ext}", priv, binary=True) + print(f"āœ… Keypair saved to {name}_public.{key_ext} / {name}_private.{key_ext}") + + elif mode == 'e': + plaintext = input("Text to encrypt: ") + pub_path = input("Public key path: ").strip() + pub = load_text(pub_path, binary=not is_pem) + result = module.encrypt_text(plaintext, pub, symmetric_engine) + print(f"\nšŸ” Encrypted Base64:\n{result}") + + elif mode == 'd': + encrypted = input("Encrypted Base64 input: ") + priv_path = input("Private key path: ").strip() + priv = load_text(priv_path, binary=not is_pem) + result = module.decrypt_text(encrypted, priv) + print(f"\nšŸ“ Decrypted:\n{result}") + + elif mode == 'ef': + in_path = input("Input file path: ").strip() + out_path = in_path + ".paccrypt" + pub_path = input("Public key path: ").strip() + pub = load_text(pub_path, binary=not is_pem) + module.encrypt_file(in_path, out_path, pub, symmetric_engine) + print(f"āœ… File encrypted and saved to: {out_path}") + + elif mode == 'df': + in_path = input("Encrypted file path: ").strip() + out_path = in_path.replace(".paccrypt", "") + priv_path = input("Private key path: ").strip() + priv = load_text(priv_path, binary=not is_pem) + module.decrypt_file(in_path, out_path, priv) + print(f"āœ… File decrypted and saved to: {out_path}") + else: + print("āŒ Invalid option.") + except Exception as e: + print(f"āŒ Error: {e}") + +def simple_cli(name, encrypt_text, decrypt_text, encrypt_file, decrypt_file): + while True: + print(f"\n=== PacCrypt {name} Debug Mode ===") + print("Choose:") + print(" [e] Encrypt text") + print(" [d] Decrypt text") + print(" [ef] Encrypt file") + print(" [df] Decrypt file") + print(" [b] Back to engine menu") + print(" [q] Quit script") + + mode = input("\nMode (e/d/ef/df/b/q): ").strip().lower() + + if mode == 'q': + return 'quit' + elif mode == 'b': + return 'back' + + try: + if mode == 'e': + plaintext = input("Plaintext to encrypt: ") + password = input("Password: ") + result = encrypt_text(plaintext, password) + print(f"\nšŸ” Encrypted Base64:\n{result}") + + elif mode == 'd': + encrypted = input("Encrypted Base64 input: ") + password = input("Password: ") + result = decrypt_text(encrypted, password) + print(f"\nšŸ“ Decrypted:\n{result}") + + elif mode == 'ef': + in_path = input("Input file path: ").strip() + out_path = in_path + ".paccrypt" + password = input("Password: ") + encrypt_file(in_path, out_path, password) + print(f"āœ… File encrypted and saved to: {out_path}") + + elif mode == 'df': + in_path = input("Encrypted file path: ").strip() + out_path = in_path.replace(".paccrypt", "") + password = input("Password: ") + decrypt_file(in_path, out_path, password) + print(f"āœ… File decrypted and saved to: {out_path}") + else: + print("āŒ Invalid option.") + except Exception as e: + print(f"āŒ Error: {e}") + + +# === PacCrypt CLI Entry === +while True: + print("\n=== PacCrypt Hardcoded CLI ===") + print("Pick an engine:") + print(" [0] AES-GCM") + print(" [1] AES-CBC") + print(" [2] XChaCha20-Poly1305") + print(" [3] RSA Hybrid (with selectable symmetric)") + print(" [4] PQCrypto Hybrid (with selectable symmetric)") + print(" [q] Quit") + + choice = input("Choice: ").strip().lower() + if choice == 'q': + print("šŸ‘‹ Bye.") + sys.exit(0) + + symmetric_engine = None + if choice in ['3', '4']: + symmetric_engine = select_symmetric() + + engines = { + '0': lambda: simple_cli("AES-GCM", aesgcm_encrypt_text, aesgcm_decrypt_text, aesgcm_encrypt_file, aesgcm_decrypt_file), + '1': lambda: simple_cli("AES-CBC", aescbc_encrypt_text, aescbc_decrypt_text, aescbc_encrypt_file, aescbc_decrypt_file), + '2': lambda: simple_cli("XChaCha20-Poly1305", xchacha_encrypt_text, xchacha_decrypt_text, xchacha_encrypt_file, xchacha_decrypt_file), + '3': lambda: hybrid_cli("RSA_Hybrid", rsa_hybrid, "pem", symmetric_engine, is_pem=True), + '4': lambda: hybrid_cli("PQCrypto_Hybrid", pqcrypto_hybrid, "bin", symmetric_engine), + } + + if choice in engines: + result = engines[choice]() + if result == 'quit': + print("šŸ‘‹ Quitting.") + sys.exit(0) + # If 'back', just loops again to show engine menu + else: + print("āŒ Invalid choice.") diff --git a/paccrypt_algos/xchacha.py b/paccrypt_algos/xchacha.py new file mode 100644 index 0000000..0f4a933 --- /dev/null +++ b/paccrypt_algos/xchacha.py @@ -0,0 +1,92 @@ +import os +import base64 +from typing import Optional +from Crypto.Cipher import ChaCha20_Poly1305 +from Crypto.Random import get_random_bytes +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Hash import SHA256 + +# === Constants === +SALT_LENGTH = 16 +NONCE_LENGTH = 24 +KEY_LENGTH = 32 +PBKDF2_ITERATIONS = 200_000 +TAG_LENGTH = 16 + +# === Base64 Helpers === +def b64encode(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') + +def b64decode(data: str) -> bytes: + return base64.b64decode(data.encode('utf-8')) + +# === Key Derivation === +def derive_key(password: str, salt: bytes) -> bytes: + return PBKDF2(password, salt, dkLen=KEY_LENGTH, count=PBKDF2_ITERATIONS, hmac_hash_module=SHA256) + +# === Encrypt Text === +def encrypt_text(plaintext: str, password: str) -> str: + salt = get_random_bytes(SALT_LENGTH) + nonce = get_random_bytes(NONCE_LENGTH) + key = derive_key(password, salt) + + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8')) + + final = salt + nonce + ciphertext + tag + return b64encode(final) + +# === Decrypt Text === +def decrypt_text(encrypted_b64: str, password: str) -> str: + raw = b64decode(encrypted_b64) + salt = raw[:SALT_LENGTH] + nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH] + tag = raw[-TAG_LENGTH:] + ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH] + + key = derive_key(password, salt) + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + plaintext = cipher.decrypt_and_verify(ciphertext, tag) + + return plaintext.decode('utf-8') + +# === Encrypt File === +def encrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + plaintext = f.read() + + salt = get_random_bytes(SALT_LENGTH) + nonce = get_random_bytes(NONCE_LENGTH) + key = derive_key(password, salt) + + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + ciphertext, tag = cipher.encrypt_and_digest(plaintext) + + with open(out_path, 'wb') as f: + f.write(salt + nonce + ciphertext + tag) + +# === Decrypt File === +def decrypt_file(in_path, out_path, password: str, metadata: Optional[dict] = None): + with open(in_path, 'rb') as f: + raw = f.read() + + salt = raw[:SALT_LENGTH] + nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH] + tag = raw[-TAG_LENGTH:] + ciphertext = raw[SALT_LENGTH + NONCE_LENGTH:-TAG_LENGTH] + + key = derive_key(password, salt) + cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce) + plaintext = cipher.decrypt_and_verify(ciphertext, tag) + + with open(out_path, 'wb') as f: + f.write(plaintext) + +# === Engine Name === +def get_name(): + return "XChaCha20-Poly1305" + + +if __name__ == "__main__": + from Crypto.Cipher.ChaCha20_Poly1305 import ChaCha20Poly1305Cipher as _test # Force import to validate availability + from cryptography.exceptions import InvalidTag # Still catchable for consistency