ExistedYear's picture
deploy
a86f101
"""
utils/encryption.py — AES-256-CBC encrypt/decrypt for secure API communication.
Shared key is stored in .env as SMS_ENCRYPTION_KEY (64-char hex = 32 bytes).
Mobile encrypts SMS body with the same key before sending to /predict_secure.
"""
import os
import base64
import secrets
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from dotenv import load_dotenv
load_dotenv()
# 32-byte (256-bit) key from env — generate once with:
# python -c "import os; print(os.urandom(32).hex())"
def _load_key_hex() -> str:
key_hex = os.getenv("SMS_ENCRYPTION_KEY")
if not key_hex:
# Fallback to known default so mobile and server stay in sync
# when .env is not yet configured. Set SMS_ENCRYPTION_KEY in .env
# before deploying to production.
key_hex = "5fc5555dfd4b23ecfbfbfda273cb82eb5fdfb8b25c5b1357a2568d6afa1f472e"
print("Warning: SMS_ENCRYPTION_KEY not set in .env; using built-in dev key.")
if len(key_hex) != 64:
raise ValueError("SMS_ENCRYPTION_KEY must be a 64-character hex string.")
bytes.fromhex(key_hex) # validate it's valid hex
return key_hex
_KEY_HEX = _load_key_hex()
def get_key() -> bytes:
return bytes.fromhex(_KEY_HEX)
def decrypt_message(encrypted_b64: str) -> str:
"""
Decrypt a base64-encoded AES-256-CBC ciphertext.
Format: base64(IV[16] + ciphertext)
Matching encrypt_message() in mobile src/utils/encryption.js
"""
data = base64.b64decode(encrypted_b64)
iv = data[:16]
ciphertext = data[16:]
key = get_key()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
# Remove PKCS7 padding
pad_len = padded[-1]
return padded[:-pad_len].decode("utf-8")
def encrypt_message(plaintext: str) -> str:
"""
Encrypt a string with AES-256-CBC (for testing / server-to-client responses).
Returns base64(IV[16] + ciphertext).
"""
key = get_key()
iv = secrets.token_bytes(16)
# PKCS7 padding
raw = plaintext.encode("utf-8")
pad = 16 - (len(raw) % 16)
raw += bytes([pad] * pad)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(raw) + encryptor.finalize()
return base64.b64encode(iv + ciphertext).decode("utf-8")
def get_key_hex() -> str:
"""Return current key as hex — sent to client on /api/encryption-key."""
return _KEY_HEX