rubiflow 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. rubiflow-1.0.0/PKG-INFO +136 -0
  2. rubiflow-1.0.0/README.md +124 -0
  3. rubiflow-1.0.0/pyproject.toml +20 -0
  4. rubiflow-1.0.0/rubiflow/__init__.py +34 -0
  5. rubiflow-1.0.0/rubiflow/client.py +203 -0
  6. rubiflow-1.0.0/rubiflow/crypto.py +169 -0
  7. rubiflow-1.0.0/rubiflow/exceptions.py +64 -0
  8. rubiflow-1.0.0/rubiflow/methods/__init__.py +52 -0
  9. rubiflow-1.0.0/rubiflow/methods/auth.py +158 -0
  10. rubiflow-1.0.0/rubiflow/methods/channels.py +167 -0
  11. rubiflow-1.0.0/rubiflow/methods/chats.py +81 -0
  12. rubiflow-1.0.0/rubiflow/methods/files.py +100 -0
  13. rubiflow-1.0.0/rubiflow/methods/folders.py +62 -0
  14. rubiflow-1.0.0/rubiflow/methods/groups.py +176 -0
  15. rubiflow-1.0.0/rubiflow/methods/messages.py +313 -0
  16. rubiflow-1.0.0/rubiflow/methods/rubino.py +255 -0
  17. rubiflow-1.0.0/rubiflow/methods/settings.py +62 -0
  18. rubiflow-1.0.0/rubiflow/methods/users.py +110 -0
  19. rubiflow-1.0.0/rubiflow/methods/vod.py +59 -0
  20. rubiflow-1.0.0/rubiflow/methods/wallet.py +51 -0
  21. rubiflow-1.0.0/rubiflow/models/__init__.py +9 -0
  22. rubiflow-1.0.0/rubiflow/models/chat.py +42 -0
  23. rubiflow-1.0.0/rubiflow/models/message.py +68 -0
  24. rubiflow-1.0.0/rubiflow/models/user.py +50 -0
  25. rubiflow-1.0.0/rubiflow/network.py +50 -0
  26. rubiflow-1.0.0/rubiflow/session.py +162 -0
  27. rubiflow-1.0.0/rubiflow.egg-info/PKG-INFO +136 -0
  28. rubiflow-1.0.0/rubiflow.egg-info/SOURCES.txt +31 -0
  29. rubiflow-1.0.0/rubiflow.egg-info/dependency_links.txt +1 -0
  30. rubiflow-1.0.0/rubiflow.egg-info/requires.txt +2 -0
  31. rubiflow-1.0.0/rubiflow.egg-info/top_level.txt +1 -0
  32. rubiflow-1.0.0/setup.cfg +4 -0
  33. rubiflow-1.0.0/tests/test_rubiflow.py +178 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: rubiflow
3
+ Version: 1.0.0
4
+ Summary: High-Performance Async Python Framework for Rubika Messenger and SuperApp
5
+ Author: RubiFlow Developers
6
+ License-Expression: MIT
7
+ Keywords: rubika,rubiflow,rubika-bot,async-rubika,messenger,superapp
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: aiohttp>=3.9.0
11
+ Requires-Dist: cryptography>=41.0.0
12
+
13
+ # ⚡ RubiFlow (روبی‌فلو)
14
+
15
+ > **فریم‌ورک فوق‌سریع، ناهمگام (Async) و مدرن پایتون برای پیام‌رسان و سوپراپ روبیکا**
16
+ > *Next-Generation High-Performance Async Python Framework for Rubika Messenger & SuperApp*
17
+
18
+ ---
19
+
20
+ ## 🌟 چرا RubiFlow؟
21
+
22
+ - **نام کاملاً اختصاصی و جدید:** نامی تک و ثبت‌نشده در PyPI که به جریان روان، غیرهمزمان و پرسرعت اشاره دارد.
23
+ - **Async-First و با بالاترین بازده:** با موتور شبکه `aiohttp`، مدیریت Connection Pooling و رمزنگاری با توابع C در `cryptography`.
24
+ - **رعایت کامل استانداردهای PEP 8 و Clean Architecture:** استفاده دقیق از Type Hints، مدل‌های داده‌ای (`dataclasses`) و مدیریت خطا با استثناهای اختصاصی.
25
+ - **شناسایی خودکار سشن فایرفاکس:** توانایی اتصال آنی به حساب فعال در فایرفاکس (بدون نیاز به دریافت کد پیامکی مجدد).
26
+ - **پشتیبانی از StringSession:** خروجی سشن به صورت یک رشته متنی فشرده و امن با پیشوند `RubiFlow$...` جهت استقرار روی سرورها.
27
+ - **پوشش جامع بیش از ۳۰۰ متد:** پشتیبانی از پیام‌رسان کامل، روبینو (Rubino)، فیلم و سریال (VOD) و کیف پول.
28
+ - **توابع کمکی و شی‌گرایی پیشرفته:** ارسال آسان پیام، ریپلای مستقیم با `message.reply("...")`، حذف و ویرایش، دریافت چت‌ها و پیام‌ها.
29
+
30
+ ---
31
+
32
+ ## 📦 نصب
33
+
34
+ ```bash
35
+ pip install -e .
36
+ ```
37
+
38
+ یا پیش‌نیازها:
39
+ ```bash
40
+ pip install aiohttp cryptography
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 🚀 شروع سریع (Quickstart)
46
+
47
+ ### ۱. اتصال خودکار از طریق سشن فایرفاکس
48
+ ```python
49
+ import asyncio
50
+ from rubiflow import Client
51
+
52
+ async def main():
53
+ # اتصال مستقیم به سشن مرورگر فایرفاکس شما
54
+ app = Client.from_firefox()
55
+
56
+ async with app:
57
+ me = await app.get_me()
58
+ print(f"Logged in as: {me.full_name} (@{me.username})")
59
+
60
+ # ارسال پیام
61
+ msg = await app.send_message("u0...", "سلام از روبی‌فلو!")
62
+ print(f"پیام ارسال شد با آیدی: {msg.message_id}")
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ---
68
+
69
+ ### ۲. ساخت بات پاسخگوی خودکار (Event-Driven Echo Bot)
70
+ ```python
71
+ from rubiflow import Client, Message
72
+
73
+ app = Client.from_firefox()
74
+
75
+ @app.on_message()
76
+ async def handle_message(msg: Message):
77
+ if msg.text == "سلام":
78
+ await msg.reply("درود! پیام شما توسط بات RubiFlow دریافت شد.")
79
+
80
+ # اجرای مداوم بات
81
+ app.run()
82
+ ```
83
+
84
+ ---
85
+
86
+ ### ۳. استفاده از رشته سشن (StringSession) برای سرور
87
+ ```python
88
+ import asyncio
89
+ from rubiflow import Client
90
+
91
+ async def export_session():
92
+ app = Client.from_firefox()
93
+ async with app:
94
+ # دریافت رشته امن برای قرار دادن روی سرور یا متغیرهای محیطی
95
+ string_session = app.session.to_string()
96
+ print("Your StringSession:", string_session)
97
+
98
+ # بازیابی مجدد سشن
99
+ new_app = Client(session=string_session)
100
+ async with new_app:
101
+ me = await new_app.get_me()
102
+ print("Connected with StringSession as:", me.username)
103
+
104
+ asyncio.run(export_session())
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 📂 ساختار ماژولار پروژه
110
+
111
+ ```
112
+ rubika-lib/
113
+ ├── pyproject.toml # پیکربندی استاندارد بسته پایتون
114
+ ├── README.md # راهنمای کامل
115
+ ├── rubiflow/
116
+ │ ├── __init__.py # اکسپورت کلاس‌ها و توابع اصلی
117
+ │ ├── client.py # هسته کلاینت و متدهای سطح بالا
118
+ │ ├── crypto.py # موتور پرسرعت رمزنگاری روبیکا V6 (AES-256 + RSA-SHA256)
119
+ │ ├── session.py # مدیریت سشن (Firefox, StringSession, FileSession)
120
+ │ ├── network.py # ارتباطات ناهمگام با Connection Pooling
121
+ │ ├── exceptions.py # استثناهای اختصاصی (InvalidAuthError, ApiError, ...)
122
+ │ └── models/
123
+ │ ├── user.py # کلاس داده کاربر (User)
124
+ │ ├── chat.py # کلاس داده گفتگو (Chat)
125
+ │ └── message.py # کلاس داده پیام همراه متدهای reply, delete, edit
126
+ ├── docs/
127
+ │ ├── RUBIKA_API_FULL_REFERENCE.md # کاتالوگ جامع بیش از ۳۰۰ متد روبیکا
128
+ │ └── RUBIKA_CRYPTO_AND_PROTOCOL.md # جزئیات مهندسی معکوس و رمزنگاری
129
+ └── examples/
130
+ └── rubiflow_quickstart.py # نمونه کد تست زنده و جامع
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🛡️ لایسنس
136
+ توسعه داده شده تحت لایسنس MIT.
@@ -0,0 +1,124 @@
1
+ # ⚡ RubiFlow (روبی‌فلو)
2
+
3
+ > **فریم‌ورک فوق‌سریع، ناهمگام (Async) و مدرن پایتون برای پیام‌رسان و سوپراپ روبیکا**
4
+ > *Next-Generation High-Performance Async Python Framework for Rubika Messenger & SuperApp*
5
+
6
+ ---
7
+
8
+ ## 🌟 چرا RubiFlow؟
9
+
10
+ - **نام کاملاً اختصاصی و جدید:** نامی تک و ثبت‌نشده در PyPI که به جریان روان، غیرهمزمان و پرسرعت اشاره دارد.
11
+ - **Async-First و با بالاترین بازده:** با موتور شبکه `aiohttp`، مدیریت Connection Pooling و رمزنگاری با توابع C در `cryptography`.
12
+ - **رعایت کامل استانداردهای PEP 8 و Clean Architecture:** استفاده دقیق از Type Hints، مدل‌های داده‌ای (`dataclasses`) و مدیریت خطا با استثناهای اختصاصی.
13
+ - **شناسایی خودکار سشن فایرفاکس:** توانایی اتصال آنی به حساب فعال در فایرفاکس (بدون نیاز به دریافت کد پیامکی مجدد).
14
+ - **پشتیبانی از StringSession:** خروجی سشن به صورت یک رشته متنی فشرده و امن با پیشوند `RubiFlow$...` جهت استقرار روی سرورها.
15
+ - **پوشش جامع بیش از ۳۰۰ متد:** پشتیبانی از پیام‌رسان کامل، روبینو (Rubino)، فیلم و سریال (VOD) و کیف پول.
16
+ - **توابع کمکی و شی‌گرایی پیشرفته:** ارسال آسان پیام، ریپلای مستقیم با `message.reply("...")`، حذف و ویرایش، دریافت چت‌ها و پیام‌ها.
17
+
18
+ ---
19
+
20
+ ## 📦 نصب
21
+
22
+ ```bash
23
+ pip install -e .
24
+ ```
25
+
26
+ یا پیش‌نیازها:
27
+ ```bash
28
+ pip install aiohttp cryptography
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 🚀 شروع سریع (Quickstart)
34
+
35
+ ### ۱. اتصال خودکار از طریق سشن فایرفاکس
36
+ ```python
37
+ import asyncio
38
+ from rubiflow import Client
39
+
40
+ async def main():
41
+ # اتصال مستقیم به سشن مرورگر فایرفاکس شما
42
+ app = Client.from_firefox()
43
+
44
+ async with app:
45
+ me = await app.get_me()
46
+ print(f"Logged in as: {me.full_name} (@{me.username})")
47
+
48
+ # ارسال پیام
49
+ msg = await app.send_message("u0...", "سلام از روبی‌فلو!")
50
+ print(f"پیام ارسال شد با آیدی: {msg.message_id}")
51
+
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ ---
56
+
57
+ ### ۲. ساخت بات پاسخگوی خودکار (Event-Driven Echo Bot)
58
+ ```python
59
+ from rubiflow import Client, Message
60
+
61
+ app = Client.from_firefox()
62
+
63
+ @app.on_message()
64
+ async def handle_message(msg: Message):
65
+ if msg.text == "سلام":
66
+ await msg.reply("درود! پیام شما توسط بات RubiFlow دریافت شد.")
67
+
68
+ # اجرای مداوم بات
69
+ app.run()
70
+ ```
71
+
72
+ ---
73
+
74
+ ### ۳. استفاده از رشته سشن (StringSession) برای سرور
75
+ ```python
76
+ import asyncio
77
+ from rubiflow import Client
78
+
79
+ async def export_session():
80
+ app = Client.from_firefox()
81
+ async with app:
82
+ # دریافت رشته امن برای قرار دادن روی سرور یا متغیرهای محیطی
83
+ string_session = app.session.to_string()
84
+ print("Your StringSession:", string_session)
85
+
86
+ # بازیابی مجدد سشن
87
+ new_app = Client(session=string_session)
88
+ async with new_app:
89
+ me = await new_app.get_me()
90
+ print("Connected with StringSession as:", me.username)
91
+
92
+ asyncio.run(export_session())
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 📂 ساختار ماژولار پروژه
98
+
99
+ ```
100
+ rubika-lib/
101
+ ├── pyproject.toml # پیکربندی استاندارد بسته پایتون
102
+ ├── README.md # راهنمای کامل
103
+ ├── rubiflow/
104
+ │ ├── __init__.py # اکسپورت کلاس‌ها و توابع اصلی
105
+ │ ├── client.py # هسته کلاینت و متدهای سطح بالا
106
+ │ ├── crypto.py # موتور پرسرعت رمزنگاری روبیکا V6 (AES-256 + RSA-SHA256)
107
+ │ ├── session.py # مدیریت سشن (Firefox, StringSession, FileSession)
108
+ │ ├── network.py # ارتباطات ناهمگام با Connection Pooling
109
+ │ ├── exceptions.py # استثناهای اختصاصی (InvalidAuthError, ApiError, ...)
110
+ │ └── models/
111
+ │ ├── user.py # کلاس داده کاربر (User)
112
+ │ ├── chat.py # کلاس داده گفتگو (Chat)
113
+ │ └── message.py # کلاس داده پیام همراه متدهای reply, delete, edit
114
+ ├── docs/
115
+ │ ├── RUBIKA_API_FULL_REFERENCE.md # کاتالوگ جامع بیش از ۳۰۰ متد روبیکا
116
+ │ └── RUBIKA_CRYPTO_AND_PROTOCOL.md # جزئیات مهندسی معکوس و رمزنگاری
117
+ └── examples/
118
+ └── rubiflow_quickstart.py # نمونه کد تست زنده و جامع
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🛡️ لایسنس
124
+ توسعه داده شده تحت لایسنس MIT.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rubiflow"
7
+ version = "1.0.0"
8
+ description = "High-Performance Async Python Framework for Rubika Messenger and SuperApp"
9
+ readme = "README.md"
10
+ authors = [{ name = "RubiFlow Developers" }]
11
+ license = "MIT"
12
+ requires-python = ">=3.9"
13
+ dependencies = [
14
+ "aiohttp>=3.9.0",
15
+ "cryptography>=41.0.0"
16
+ ]
17
+ keywords = ["rubika", "rubiflow", "rubika-bot", "async-rubika", "messenger", "superapp"]
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["rubiflow*"]
@@ -0,0 +1,34 @@
1
+ """
2
+ RubiFlow: Next-Generation High-Performance Async Python Framework for Rubika.
3
+ """
4
+
5
+ from .client import Client
6
+ from .crypto import RubiFlowCrypto
7
+ from .exceptions import (
8
+ ApiError,
9
+ CryptoError,
10
+ InvalidAuthError,
11
+ NetworkError,
12
+ NotFoundError,
13
+ RubiFlowError,
14
+ )
15
+ from .models import Chat, Message, User
16
+ from .session import Session
17
+
18
+ __version__ = "1.0.0"
19
+ __author__ = "RubiFlow Team"
20
+
21
+ __all__ = [
22
+ "Client",
23
+ "Session",
24
+ "RubiFlowCrypto",
25
+ "User",
26
+ "Chat",
27
+ "Message",
28
+ "RubiFlowError",
29
+ "ApiError",
30
+ "InvalidAuthError",
31
+ "NetworkError",
32
+ "CryptoError",
33
+ "NotFoundError",
34
+ ]
@@ -0,0 +1,203 @@
1
+ """
2
+ Core asynchronous Client for RubiFlow.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from typing import Any, Callable, Dict, List, Optional, Union
9
+
10
+ from .crypto import RubiFlowCrypto
11
+ from .exceptions import RubiFlowError, check_api_status
12
+ from .methods import Methods
13
+ from .models import Message, User
14
+ from .network import AsyncTransport
15
+ from .session import Session
16
+
17
+ logger = logging.getLogger("rubiflow")
18
+
19
+
20
+ class Client(Methods):
21
+ """
22
+ High-performance, asynchronous Rubika client framework.
23
+ Inherits 300+ methods covering Messenger, Rubino, VOD, and Wallet.
24
+ """
25
+
26
+ DEFAULT_DC_URL = "https://messengerg2c332.iranlms.ir"
27
+ LIB_NAME = "RubiFlow"
28
+ LIB_VERSION = "1.0.0"
29
+
30
+ # Rubika protocol standard client signature
31
+ _INTERNAL_CLIENT = {
32
+ "app_name": "Main",
33
+ "app_version": "2.5.9",
34
+ "platform": "PWA",
35
+ "package": "m.rubika.ir",
36
+ "lang_code": "fa",
37
+ }
38
+
39
+ def __init__(
40
+ self,
41
+ session: Optional[Union[Session, str]] = None,
42
+ auth: Optional[str] = None,
43
+ private_key_pem: Optional[str] = None,
44
+ dc_url: Optional[str] = None,
45
+ timeout: float = 20.0,
46
+ ):
47
+ if isinstance(session, str):
48
+ if session.startswith("RubiFlow$") or session.startswith("Rubix$"):
49
+ self.session = Session.from_string(session)
50
+ else:
51
+ self.session = Session.load_from_file(session)
52
+ elif isinstance(session, Session):
53
+ self.session = session
54
+ elif auth and private_key_pem:
55
+ self.session = Session(
56
+ auth=auth,
57
+ private_key_pem=private_key_pem,
58
+ dc_url=dc_url or self.DEFAULT_DC_URL,
59
+ )
60
+ else:
61
+ # Temporary unauthenticated session
62
+ priv, _ = RubiFlowCrypto.generate_rsa_key_pair()
63
+ tmp = RubiFlowCrypto.generate_tmp_session(32)
64
+ self.session = Session(
65
+ auth=tmp,
66
+ private_key_pem=priv,
67
+ dc_url=dc_url or self.DEFAULT_DC_URL,
68
+ )
69
+
70
+ self.transport = AsyncTransport(timeout_seconds=timeout)
71
+ self._me: Optional[User] = None
72
+ self._message_handlers: List[Callable[[Message], Any]] = []
73
+ self._is_running = False
74
+
75
+ @classmethod
76
+ def from_firefox(cls) -> "Client":
77
+ """
78
+ Creates a Client connected directly to the user's active session in Firefox.
79
+ """
80
+ session = Session.from_firefox()
81
+ return cls(session=session)
82
+
83
+ async def __aenter__(self) -> "Client":
84
+ await self.start()
85
+ return self
86
+
87
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
88
+ await self.stop()
89
+
90
+ async def start(self) -> None:
91
+ """Initializes client and verifies session authentication."""
92
+ try:
93
+ me = await self.get_me()
94
+ self._me = me
95
+ logger.info("RubiFlow connected as @%s (%s)", me.username, me.guid)
96
+ except Exception as exc:
97
+ logger.warning("RubiFlow started with unverified session: %s", exc)
98
+
99
+ async def stop(self) -> None:
100
+ """Closes transport and background tasks."""
101
+ self._is_running = False
102
+ await self.transport.close()
103
+
104
+ # --- Core RPC Dispatcher ---
105
+
106
+ async def execute(
107
+ self,
108
+ method: str,
109
+ input_data: Optional[Dict[str, Any]] = None,
110
+ api_version: str = "6",
111
+ ) -> Dict[str, Any]:
112
+ """
113
+ Executes an encrypted and RSA-signed Rubika V6 RPC method.
114
+ """
115
+ inner_payload = {
116
+ "method": method,
117
+ "input": input_data or {},
118
+ "client": self._INTERNAL_CLIENT,
119
+ }
120
+
121
+ # 1. AES encrypt inner payload with derived secret
122
+ data_enc = RubiFlowCrypto.encrypt_aes(inner_payload, self.session.auth)
123
+
124
+ # 2. RSA-SHA256 signature
125
+ sign = RubiFlowCrypto.sign_rsa_sha256(data_enc, self.session.private_key_pem)
126
+
127
+ # 3. Disguise auth token
128
+ disguised_auth = RubiFlowCrypto.disguise_auth(self.session.auth)
129
+
130
+ request_body = {
131
+ "api_version": api_version,
132
+ "auth": disguised_auth,
133
+ "data_enc": data_enc,
134
+ "sign": sign,
135
+ }
136
+
137
+ response = await self.transport.post_json(self.session.dc_url, request_body)
138
+
139
+ if "data_enc" in response:
140
+ decrypted = RubiFlowCrypto.decrypt_aes(response["data_enc"], self.session.auth)
141
+ if isinstance(decrypted, str):
142
+ decrypted = json.loads(decrypted)
143
+ check_api_status(decrypted)
144
+ return decrypted
145
+
146
+ check_api_status(response)
147
+ return response
148
+
149
+ async def invoke(self, method: str, **kwargs) -> Dict[str, Any]:
150
+ """
151
+ Directly invokes any Rubika RPC method with arbitrary input arguments.
152
+ Example: await app.invoke('sendMessage', object_guid='u0...', text='Hello')
153
+ """
154
+ return await self.execute(method, kwargs)
155
+
156
+ # --- Event Handling & Bot Loop ---
157
+
158
+ def on_message(self) -> Callable[[Callable[[Message], Any]], Callable[[Message], Any]]:
159
+ """Decorator to register an asynchronous handler for incoming messages."""
160
+ def decorator(func: Callable[[Message], Any]) -> Callable[[Message], Any]:
161
+ self._message_handlers.append(func)
162
+ return func
163
+ return decorator
164
+
165
+ async def _polling_loop(self) -> None:
166
+ """Background poller for message updates."""
167
+ while self._is_running:
168
+ try:
169
+ chats = await self.get_chats()
170
+ for chat in chats:
171
+ if chat.unread_count > 0:
172
+ msgs = await self.get_messages(chat.guid, limit=chat.unread_count)
173
+ for msg in msgs:
174
+ for handler in self._message_handlers:
175
+ if asyncio.iscoroutinefunction(handler):
176
+ await handler(msg)
177
+ else:
178
+ handler(msg)
179
+ await asyncio.sleep(2)
180
+ except Exception as exc:
181
+ logger.error("Polling error: %s", exc)
182
+ await asyncio.sleep(5)
183
+
184
+ def run(self, coro: Optional[Any] = None) -> None:
185
+ """
186
+ Convenience runner for scripts and bot processes.
187
+ """
188
+ loop = asyncio.get_event_loop()
189
+ if coro is not None:
190
+ loop.run_until_complete(coro)
191
+ return
192
+
193
+ async def _main():
194
+ async with self:
195
+ self._is_running = True
196
+ await self._polling_loop()
197
+
198
+ try:
199
+ loop.run_until_complete(_main())
200
+ except (KeyboardInterrupt, SystemExit):
201
+ pass
202
+ finally:
203
+ loop.run_until_complete(self.stop())
@@ -0,0 +1,169 @@
1
+ """
2
+ Cryptographic engine for RubiFlow (Rubika V6 Protocol).
3
+ Implements AES-256-CBC, custom key derivation, substitution auth disguising,
4
+ and RSA-SHA256 asymmetric signatures.
5
+ """
6
+
7
+ import base64
8
+ import json
9
+ import random
10
+ import string
11
+ from typing import Any, Dict, Union
12
+
13
+ from cryptography.hazmat.primitives import hashes, padding
14
+ from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
15
+ from cryptography.hazmat.primitives.asymmetric import rsa
16
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
17
+ from cryptography.hazmat.primitives.serialization import (
18
+ Encoding,
19
+ NoEncryption,
20
+ PrivateFormat,
21
+ PublicFormat,
22
+ load_pem_private_key,
23
+ )
24
+
25
+ from .exceptions import CryptoError
26
+
27
+
28
+ class RubiFlowCrypto:
29
+ """Cryptographic operations required by the Rubika V6 protocol."""
30
+
31
+ STATIC_IV = b"\x00" * 16
32
+
33
+ @staticmethod
34
+ def generate_rsa_key_pair() -> tuple[str, str]:
35
+ """
36
+ Generates a new 1024-bit RSA key pair in PEM format.
37
+ Returns: (private_key_pem, public_key_pem)
38
+ """
39
+ private_key = rsa.generate_private_key(
40
+ public_exponent=65537,
41
+ key_size=1024,
42
+ )
43
+ private_pem = private_key.private_bytes(
44
+ encoding=Encoding.PEM,
45
+ format=PrivateFormat.PKCS8,
46
+ encryption_algorithm=NoEncryption(),
47
+ ).decode("utf-8")
48
+
49
+ public_pem = private_key.public_key().public_bytes(
50
+ encoding=Encoding.PEM,
51
+ format=PublicFormat.SubjectPublicKeyInfo,
52
+ ).decode("utf-8")
53
+
54
+ return private_pem, public_pem
55
+
56
+ @staticmethod
57
+ def generate_tmp_session(length: int = 32) -> str:
58
+ """Generates a random lowercase ASCII string for unauthenticated sessions."""
59
+ return "".join(random.choices(string.ascii_lowercase, k=length))
60
+
61
+ @staticmethod
62
+ def create_secret_passphrase(auth: str) -> str:
63
+ """
64
+ Derives the 32-character AES secret key from an auth token or tmp_session.
65
+ """
66
+ if len(auth) < 32:
67
+ raise CryptoError("Auth key or tmp_session must have at least 32 characters.")
68
+
69
+ t = auth[0:8]
70
+ i = auth[8:16]
71
+ p1 = auth[16:24]
72
+ p2 = auth[24:32]
73
+
74
+ chars = list(p1 + t + p2 + i)
75
+ for idx, char in enumerate(chars):
76
+ if "0" <= char <= "9":
77
+ chars[idx] = chr((ord(char) - ord("0") + 5) % 10 + ord("0"))
78
+ elif "a" <= char <= "z":
79
+ chars[idx] = chr((ord(char) - ord("a") + 9) % 26 + ord("a"))
80
+
81
+ return "".join(chars)
82
+
83
+ @staticmethod
84
+ def disguise_auth(auth: str) -> str:
85
+ """
86
+ Applies the Rubika V6 character substitution cipher to the auth string.
87
+ """
88
+ result = []
89
+ for char in auth:
90
+ if "0" <= char <= "9":
91
+ result.append(chr((13 + (ord(char) - ord("0"))) % 10 + ord("0")))
92
+ elif "A" <= char <= "Z":
93
+ result.append(chr((29 - (ord(char) - ord("A"))) % 26 + ord("A")))
94
+ elif "a" <= char <= "z":
95
+ result.append(chr((32 - (ord(char) - ord("a"))) % 26 + ord("a")))
96
+ else:
97
+ result.append(char)
98
+ return "".join(result)
99
+
100
+ @classmethod
101
+ def encrypt_aes(cls, data: Union[Dict[str, Any], str], auth: str) -> str:
102
+ """
103
+ Encrypts a dictionary or string payload using AES-256-CBC and returns Base64.
104
+ """
105
+ try:
106
+ passphrase = cls.create_secret_passphrase(auth).encode("utf-8")
107
+ if isinstance(data, dict):
108
+ raw_bytes = json.dumps(
109
+ data, separators=(",", ":"), ensure_ascii=False
110
+ ).encode("utf-8")
111
+ else:
112
+ raw_bytes = data.encode("utf-8")
113
+
114
+ padder = padding.PKCS7(128).padder()
115
+ padded = padder.update(raw_bytes) + padder.finalize()
116
+
117
+ cipher = Cipher(algorithms.AES(passphrase), modes.CBC(cls.STATIC_IV))
118
+ encryptor = cipher.encryptor()
119
+ ciphertext = encryptor.update(padded) + encryptor.finalize()
120
+
121
+ return base64.b64encode(ciphertext).decode("utf-8")
122
+ except Exception as exc:
123
+ raise CryptoError(f"AES encryption failed: {exc}") from exc
124
+
125
+ @classmethod
126
+ def decrypt_aes(cls, encrypted_b64: str, auth: str) -> Union[Dict[str, Any], str]:
127
+ """
128
+ Decrypts a Base64 AES-256-CBC ciphertext and parses it as JSON if possible.
129
+ """
130
+ try:
131
+ passphrase = cls.create_secret_passphrase(auth).encode("utf-8")
132
+ raw_ciphertext = base64.b64decode(encrypted_b64)
133
+
134
+ cipher = Cipher(algorithms.AES(passphrase), modes.CBC(cls.STATIC_IV))
135
+ decryptor = cipher.decryptor()
136
+ padded_plain = decryptor.update(raw_ciphertext) + decryptor.finalize()
137
+
138
+ unpadder = padding.PKCS7(128).unpadder()
139
+ plain_bytes = unpadder.update(padded_plain) + unpadder.finalize()
140
+ plain_text = plain_bytes.decode("utf-8")
141
+
142
+ try:
143
+ return json.loads(plain_text)
144
+ except json.JSONDecodeError:
145
+ return plain_text
146
+ except Exception as exc:
147
+ raise CryptoError(f"AES decryption failed: {exc}") from exc
148
+
149
+ @staticmethod
150
+ def sign_rsa_sha256(data_enc_b64: str, private_key_pem: str) -> str:
151
+ """
152
+ Signs the data_enc string using RSA-SHA256 with the client's private key.
153
+ Returns the Base64-encoded signature.
154
+ """
155
+ try:
156
+ pem_bytes = (
157
+ private_key_pem.encode("utf-8")
158
+ if isinstance(private_key_pem, str)
159
+ else private_key_pem
160
+ )
161
+ private_key = load_pem_private_key(pem_bytes, password=None)
162
+ signature = private_key.sign(
163
+ data_enc_b64.encode("utf-8"),
164
+ asym_padding.PKCS1v15(),
165
+ hashes.SHA256(),
166
+ )
167
+ return base64.b64encode(signature).decode("utf-8")
168
+ except Exception as exc:
169
+ raise CryptoError(f"RSA signature failed: {exc}") from exc