File size: 2,659 Bytes
77c5607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""
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