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.
This commit is contained in:
Karl 2025-07-14 11:12:13 +01:00
parent 4352004ed3
commit 79a2f6e944
2 changed files with 33 additions and 14 deletions

View File

@ -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)

View File

@ -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()