diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index 20ca199..d976d7d 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -253,7 +253,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot, traffic_label = ( str(int(traffic_amount_gb)) if float(traffic_amount_gb).is_integer() else f"{traffic_amount_gb:g}" ) - config_link_display, connect_button_url = prepare_config_links( + config_link_display, connect_button_url = await prepare_config_links( settings, activation_details.get("subscription_url") if activation_details else None ) config_link_text = config_link_display or _("config_link_not_available") diff --git a/bot/handlers/user/trial_handler.py b/bot/handlers/user/trial_handler.py index ab9d2a3..5356ec8 100644 --- a/bot/handlers/user/trial_handler.py +++ b/bot/handlers/user/trial_handler.py @@ -88,7 +88,7 @@ async def request_trial_confirmation_handler( pass end_date_obj = activation_result.get("end_date") - config_link_display_for_trial, connect_button_url_for_trial = prepare_config_links( + config_link_display_for_trial, connect_button_url_for_trial = await prepare_config_links( settings, activation_result.get("subscription_url") ) config_link_for_trial = config_link_display_for_trial or _("config_link_not_available") @@ -240,7 +240,7 @@ async def confirm_activate_trial_handler( pass end_date_obj = activation_result.get("end_date") - config_link_display_for_trial, connect_button_url_for_trial = prepare_config_links( + config_link_display_for_trial, connect_button_url_for_trial = await prepare_config_links( settings, activation_result.get("subscription_url") ) config_link_for_trial = config_link_display_for_trial or _("config_link_not_available") diff --git a/bot/services/crypto_pay_service.py b/bot/services/crypto_pay_service.py index 57dbc86..895064f 100644 --- a/bot/services/crypto_pay_service.py +++ b/bot/services/crypto_pay_service.py @@ -190,7 +190,7 @@ class CryptoPayService: _ = lambda k, **kw: i18n.gettext(lang, k, **kw) raw_config_link = activation.get("subscription_url") if activation else None - display_link, button_link = prepare_config_links(settings, raw_config_link) + display_link, button_link = await prepare_config_links(settings, raw_config_link) config_link_text = display_link or _("config_link_not_available") final_end = activation.get("end_date") applied_days = 0 diff --git a/bot/services/freekassa_service.py b/bot/services/freekassa_service.py index f899dca..5ca9fe4 100644 --- a/bot/services/freekassa_service.py +++ b/bot/services/freekassa_service.py @@ -319,7 +319,7 @@ class FreeKassaService: _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k raw_config_link = activation.get("subscription_url") if activation else None - config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) + config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link) config_link_text = config_link_display or _("config_link_not_available") final_end = activation.get("end_date") if activation else None months = payment.subscription_duration_months or 1 diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index b9e75af..a86e594 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -562,3 +562,20 @@ class PanelApiService: if response_data and not response_data.get("error") and "response" in response_data: return response_data.get("response") return None + + async def encrypt_happ_link(self, link_to_encrypt: str) -> Optional[str]: + """Encrypt a subscription link using the panel's happ crypt4 API. + + Returns the encrypted link string or None if encryption failed. + """ + payload = {"linkToEncrypt": link_to_encrypt} + response_data = await self._request( + "POST", + "/system/tools/happ/encrypt", + json=payload, + log_full_response=False + ) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response", {}).get("encryptedLink") + logging.error(f"Failed to encrypt happ link. Response: {response_data}") + return None diff --git a/bot/services/platega_service.py b/bot/services/platega_service.py index 1ad2b44..a41651e 100644 --- a/bot/services/platega_service.py +++ b/bot/services/platega_service.py @@ -215,7 +215,7 @@ class PlategaService: _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k raw_config_link = activation.get("subscription_url") if activation else None - config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) + config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link) config_link_text = config_link_display or _("config_link_not_available") final_end = activation.get("end_date") if activation else None applied_days = 0 diff --git a/bot/services/severpay_service.py b/bot/services/severpay_service.py index f87822e..35c6f49 100644 --- a/bot/services/severpay_service.py +++ b/bot/services/severpay_service.py @@ -233,7 +233,7 @@ class SeverPayService: _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k raw_config_link = activation.get("subscription_url") if activation else None - config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) + config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link) config_link_text = config_link_display or _("config_link_not_available") final_end = activation.get("end_date") if activation else None applied_days = 0 diff --git a/bot/services/stars_service.py b/bot/services/stars_service.py index efeb151..0dfbe2a 100644 --- a/bot/services/stars_service.py +++ b/bot/services/stars_service.py @@ -123,7 +123,7 @@ class StarsService: _ = lambda k, **kw: i18n.gettext(current_lang, k, **kw) if i18n else k raw_config_link = activation_details.get("subscription_url") if activation_details else None - config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) + config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link) config_link_text = config_link_display or _("config_link_not_available") if sale_mode == "traffic": diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index 1806476..6069228 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -902,7 +902,7 @@ class SubscriptionService: else None ) config_link_raw = panel_user_data.get("subscriptionUrl") - display_link, connect_button_url = prepare_config_links(self.settings, config_link_raw) + display_link, connect_button_url = await prepare_config_links(self.settings, config_link_raw) hwid_limit = panel_user_data.get("hwidDeviceLimit") if hwid_limit is None: hwid_limit = self.settings.USER_HWID_DEVICE_LIMIT diff --git a/bot/utils/config_link.py b/bot/utils/config_link.py index 992e42c..f18a342 100644 --- a/bot/utils/config_link.py +++ b/bot/utils/config_link.py @@ -1,48 +1,22 @@ -import base64 import logging -from functools import lru_cache -from pathlib import Path from typing import Optional, Tuple -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey - from config.settings import Settings +from bot.services.panel_api_service import PanelApiService CRYPT4_PREFIX = "happ://crypt4/" -@lru_cache(maxsize=1) -def _load_crypt4_public_key() -> Optional[RSAPublicKey]: - """Load and cache the happ crypt4 public key from the project root.""" - pem_path = Path(__file__).resolve().parent.parent.parent / "happ-crypt4.pem" - try: - pem_bytes = pem_path.read_bytes() - key = serialization.load_pem_public_key(pem_bytes) - return key if isinstance(key, RSAPublicKey) else None - except FileNotFoundError: - logging.error("Crypt4 public key file not found at %s", pem_path) - except Exception as exc: - logging.error("Failed to load crypt4 public key: %s", exc, exc_info=True) +async def _encrypt_raw_link(settings: Settings, raw_link: str) -> Optional[str]: + """Encrypt the raw subscription URL using the panel's happ crypt4 API.""" + async with PanelApiService(settings) as panel_service: + encrypted_link = await panel_service.encrypt_happ_link(raw_link) + if encrypted_link: + return encrypted_link return None -def _encrypt_raw_link(raw_link: str) -> Optional[str]: - """Encrypt the raw subscription URL with RSA PKCS#1 v1.5 and return base64 payload.""" - public_key = _load_crypt4_public_key() - if not public_key: - return None - - try: - encrypted = public_key.encrypt(raw_link.encode("utf-8"), padding.PKCS1v15()) - return base64.b64encode(encrypted).decode("utf-8") - except Exception as exc: - logging.error("Failed to encrypt config link with crypt4: %s", exc, exc_info=True) - return None - - -def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[Optional[str], Optional[str]]: +async def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[Optional[str], Optional[str]]: """ Build the user-facing connection key and the URL for the connect button. @@ -61,7 +35,7 @@ def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[O button_link = cleaned if settings.CRYPT4_ENABLED: - encrypted_payload = _encrypt_raw_link(cleaned) + encrypted_payload = await _encrypt_raw_link(settings, cleaned) if encrypted_payload: display_link = f"{CRYPT4_PREFIX}{encrypted_payload}" button_link = display_link diff --git a/happ-crypt4.pem b/happ-crypt4.pem deleted file mode 100644 index 08aa083..0000000 --- a/happ-crypt4.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3UZ0M3L4K+WjM3vkbQnz -ozHg/cRbEXvQ6i4A8RVN4OM3rK9kU01FdjyoIgywve8OEKsFnVwERZAQZ1Trv60B -hmaM76QQEE+EUlIOL9EpwKWGtTL5lYC1sT9XJMNP3/CI0gP5wwQI88cY/xedpOEB -W72EmOOShHUm/b/3m+HPmqwc4ugKj5zWV5SyiT829aFA5DxSjmIIFBAms7DafmSq -LFTYIQL5cShDY2u+/sqyAw9yZIOoqW2TFIgIHhLPWek/ocDU7zyOrlu1E0SmcQQb -LFqHq02fsnH6IcqTv3N5Adb/CkZDDQ6HvQVBmqbKZKf7ZdXkqsc/Zw27xhG7OfXC -tUmWsiL7zA+KoTd3avyOh93Q9ju4UQsHthL3Gs4vECYOCS9dsXXSHEY/1ngU/hjO -WFF8QEE/rYV6nA4PTyUvo5RsctSQL/9DJX7XNh3zngvif8LsCN2MPvx6X+zLouBX -zgBkQ9DFfZAGLWf9TR7KVjZC/3NsuUCDoAOcpmN8pENBbeB0puiKMMWSvll36+2M -YR1Xs0MgT8Y9TwhE2+TnnTJOhzmHi/BxiUlY/w2E0s4ax9GHAmX0wyF4zeV7kDkc -vHuEdc0d7vDmdw0oqCqWj0Xwq86HfORu6tm1A8uRATjb4SzjTKclKuoElVAVa5Jo -oh/uZMozC65SmDw+N5p6Su8CAwEAAQ== ------END PUBLIC KEY-----