From 79a2f6e944f9c09591ea1012023bc883af6f9674 Mon Sep 17 00:00:00 2001 From: Karl Date: Mon, 14 Jul 2025 11:12:13 +0100 Subject: [PATCH] feat(security): implement AES-GCM for password encryption Replaces the `pyeasyencrypt` library with a more robust and standard encryption implementation using `cryptography.hazmat`. This commit introduces AES-256-GCM for encrypting and decrypting user account passwords. The `add_account` endpoint now properly encrypts passwords before database insertion. Error handling has been added to the `get_user_accounts` endpoint to manage decryption failures for legacy passwords, which will be returned as "DECRYPTION_FAILED". BREAKING CHANGE: The password encryption algorithm has been changed. All previously stored passwords are now invalid and cannot be decrypted. --- ktvmanager/lib/database.py | 12 +++++++++--- ktvmanager/lib/encryption.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/ktvmanager/lib/database.py b/ktvmanager/lib/database.py index db22fa7..588bd1b 100644 --- a/ktvmanager/lib/database.py +++ b/ktvmanager/lib/database.py @@ -3,7 +3,7 @@ import mysql.connector from dotenv import load_dotenv from flask import jsonify, request from ktvmanager.lib.checker import single_account_check -from ktvmanager.lib.encryption import decrypt_password +from ktvmanager.lib.encryption import encrypt_password, decrypt_password load_dotenv() @@ -42,7 +42,12 @@ def get_user_accounts(user_id): query = "SELECT * FROM userAccounts WHERE userID = %s" accounts = _execute_query(query, (user_id,)) for account in accounts: - account['password'] = decrypt_password(account['password']) + try: + account['password'] = decrypt_password(account['password']) + except Exception as e: + # Log the error to the console for debugging + print(f"Password decryption failed for account ID {account.get('id', 'N/A')}: {e}") + account['password'] = "DECRYPTION_FAILED" return jsonify(accounts) def get_stream_names(): @@ -64,8 +69,9 @@ def single_check(): def add_account(): data = request.get_json() + encrypted_password = encrypt_password(data['password']) query = "INSERT INTO userAccounts (username, stream, streamURL, expiaryDate, password, userID) VALUES (%s, %s, %s, %s, %s, %s)" - params = (data['username'], data['stream'], data['streamURL'], data['expiaryDate'], data['password'], data['userID']) + params = (data['username'], data['stream'], data['streamURL'], data['expiaryDate'], encrypted_password, data['userID']) result = _execute_query(query, params) return jsonify(result) diff --git a/ktvmanager/lib/encryption.py b/ktvmanager/lib/encryption.py index f70aed8..e1ae823 100644 --- a/ktvmanager/lib/encryption.py +++ b/ktvmanager/lib/encryption.py @@ -1,17 +1,30 @@ -from pyeasyencrypt.pyeasyencrypt import encrypt_string, decrypt_string import os -from dotenv import load_dotenv - -load_dotenv() +import hashlib +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +SECRET = "BBLBTV-DNS-PASSWORDS" +KEY = hashlib.sha256(SECRET.encode()).digest() +ALGORITHM = "aes-256-gcm" +IV_LENGTH = 16 +AUTH_TAG_LENGTH = 16 def encrypt_password(clear_string): - password = os.getenv("ENCRYPTKEY") - encrypted_string = encrypt_string(clear_string, password) - return encrypted_string - + iv = os.urandom(IV_LENGTH) + aesgcm = AESGCM(KEY) + + ciphertext_and_tag = aesgcm.encrypt(iv, clear_string.encode(), None) + ciphertext = ciphertext_and_tag[:-AUTH_TAG_LENGTH] + tag = ciphertext_and_tag[-AUTH_TAG_LENGTH:] + + return (iv + tag + ciphertext).hex() def decrypt_password(encrypted_string): - password = os.getenv("ENCRYPTKEY") - decrypted_string = decrypt_string(encrypted_string, password) - return decrypted_string + data = bytes.fromhex(encrypted_string) + + iv = data[:IV_LENGTH] + tag = data[IV_LENGTH:IV_LENGTH + AUTH_TAG_LENGTH] + ciphertext = data[IV_LENGTH + AUTH_TAG_LENGTH:] + + aesgcm = AESGCM(KEY) + decrypted_bytes = aesgcm.decrypt(iv, ciphertext + tag, None) + return decrypted_bytes.decode()