Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/encryption_service.py

176 lines
5.3 KiB
Python
Executable file

"""
سرویس رمزنگاری و رمزگشایی اطلاعات حساس
"""
from __future__ import annotations
import base64
import os
from typing import Optional
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
from app.core.settings import get_settings
class EncryptionService:
"""
سرویس رمزنگاری با استفاده از Fernet (symmetric encryption)
"""
def __init__(self, encryption_key: Optional[str] = None):
"""
Args:
encryption_key: کلید رمزنگاری (base64). اگر None باشد از تنظیمات خوانده می‌شود.
"""
if encryption_key is None:
settings = get_settings()
raw_key = (settings.encryption_key or "").strip()
if raw_key:
if self._is_valid_fernet_key(raw_key):
encryption_key = raw_key
else:
encryption_key = self._derive_key_from_secret(raw_key)
else:
# سازگاری با داده‌های قدیمی — در production ENCRYPTION_KEY اجباری است
secret = getattr(settings, 'secret_key', None) or settings.captcha_secret
encryption_key = self._derive_key_from_secret(secret)
self.cipher = Fernet(encryption_key.encode() if isinstance(encryption_key, str) else encryption_key)
@staticmethod
def _is_valid_fernet_key(key: str | bytes) -> bool:
try:
raw = key.encode() if isinstance(key, str) else key
Fernet(raw)
return True
except (ValueError, TypeError):
return False
@staticmethod
def _derive_key_from_secret(secret: str, salt: Optional[bytes] = None) -> str:
"""
تولید کلید رمزنگاری از روی یک secret
Args:
secret: رشته secret
salt: نمک برای KDF (اگر None باشد یک salt ثابت استفاده می‌شود)
Returns:
کلید base64 برای Fernet
"""
if salt is None:
# استفاده از salt ثابت (در production باید از environment variable خوانده شود)
salt = b'hesabix-tax-salt-2025'
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
backend=default_backend()
)
key = base64.urlsafe_b64encode(kdf.derive(secret.encode()))
return key.decode()
def encrypt(self, plaintext: str) -> str:
"""
رمزنگاری متن
Args:
plaintext: متن اصلی
Returns:
متن رمزشده (base64)
"""
if not plaintext:
return ""
encrypted_bytes = self.cipher.encrypt(plaintext.encode('utf-8'))
return base64.urlsafe_b64encode(encrypted_bytes).decode('utf-8')
def decrypt(self, ciphertext: str) -> str:
"""
رمزگشایی متن
Args:
ciphertext: متن رمزشده
Returns:
متن اصلی
"""
if not ciphertext:
return ""
try:
encrypted_bytes = base64.urlsafe_b64decode(ciphertext.encode('utf-8'))
decrypted_bytes = self.cipher.decrypt(encrypted_bytes)
return decrypted_bytes.decode('utf-8')
except Exception:
# اگر رمزگشایی ناموفق بود، احتمالا متن رمز نشده است
# برای سازگاری با داده‌های قدیمی
return ciphertext
def is_encrypted(self, text: str) -> bool:
"""
بررسی اینکه آیا متن رمز شده است یا خیر
Args:
text: متن برای بررسی
Returns:
True اگر احتمالا رمز شده باشد
"""
if not text:
return False
try:
# تلاش برای decode کردن
base64.urlsafe_b64decode(text.encode('utf-8'))
# اگر decode شد، احتمالا رمز شده است
return True
except Exception:
return False
# Instance سراسری
_encryption_service: Optional[EncryptionService] = None
def get_encryption_service() -> EncryptionService:
"""دریافت instance سرویس رمزنگاری"""
global _encryption_service
if _encryption_service is None:
_encryption_service = EncryptionService()
return _encryption_service
def encrypt_private_key(private_key: str) -> str:
"""
رمزنگاری کلید خصوصی
Args:
private_key: کلید خصوصی PEM
Returns:
کلید رمز شده
"""
service = get_encryption_service()
return service.encrypt(private_key)
def decrypt_private_key(encrypted_key: str) -> str:
"""
رمزگشایی کلید خصوصی
Args:
encrypted_key: کلید رمز شده
Returns:
کلید اصلی PEM
"""
service = get_encryption_service()
return service.decrypt(encrypted_key)