This commit is contained in:
user committed 2025-07-27 03:12:33 +03:00
1 parent 8b68b0f1e3
commit 274c8f1f09
8 files changed
+2126 -6

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
"""
MY Network v3.0 - Cryptographic Module for uploader-bot
Модуль криптографических операций для защиты inter-node коммуникаций.
"""
from .ed25519_manager import Ed25519Manager, get_ed25519_manager, init_ed25519_manager
__all__ = [
'Ed25519Manager',
'get_ed25519_manager',
'init_ed25519_manager'
]
+316
View File
@@ -0,0 +1,316 @@
"""
MY Network v3.0 - Ed25519 Cryptographic Manager for uploader-bot
Модуль для работы с ed25519 ключами и подписями.
Все inter-node сообщения должны быть подписаны и проверены.
"""
import os
import base64
import json
import hashlib
from typing import Dict, Any, Optional, Tuple
from pathlib import Path
import logging
import time
try:
import ed25519
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519 as crypto_ed25519
except ImportError as e:
logging.error(f"Required cryptographic libraries not found: {e}")
raise ImportError("Please install: pip install ed25519 cryptography")
logger = logging.getLogger(__name__)
class Ed25519Manager:
"""Менеджер для ed25519 криптографических операций в uploader-bot"""
def __init__(self, private_key_path: Optional[str] = None, public_key_path: Optional[str] = None):
"""
Инициализация Ed25519Manager
Args:
private_key_path: Путь к приватному ключу
public_key_path: Путь к публичному ключу
"""
self.private_key_path = private_key_path or os.getenv('NODE_PRIVATE_KEY_PATH')
self.public_key_path = public_key_path or os.getenv('NODE_PUBLIC_KEY_PATH')
self._private_key = None
self._public_key = None
self._node_id = None
# Загружаем ключи при инициализации
self._load_keys()
def _load_keys(self) -> None:
"""Загрузка ключей из файлов"""
try:
# Загрузка приватного ключа
if self.private_key_path and os.path.exists(self.private_key_path):
with open(self.private_key_path, 'rb') as f:
private_key_data = f.read()
# Загружаем PEM ключ
self._private_key = serialization.load_pem_private_key(
private_key_data,
password=None
)
# Получаем публичный ключ из приватного
self._public_key = self._private_key.public_key()
# Генерируем NODE_ID из публичного ключа
self._node_id = self._generate_node_id()
logger.info(f"Ed25519 ключи загружены. Node ID: {self._node_id}")
else:
logger.warning(f"Private key file not found: {self.private_key_path}")
except Exception as e:
logger.error(f"Error loading Ed25519 keys: {e}")
raise
def _generate_node_id(self) -> str:
"""Генерация NODE_ID из публичного ключа"""
if not self._public_key:
raise ValueError("Public key not loaded")
# Получаем raw bytes публичного ключа
public_key_bytes = self._public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
# Создаем упрощенный base58-подобный NODE_ID
# В реальной реализации здесь должен быть полный base58
hex_key = public_key_bytes.hex()
return f"node-{hex_key[:16]}"
@property
def node_id(self) -> str:
"""Получить NODE_ID"""
if not self._node_id:
raise ValueError("Node ID not generated. Check if keys are loaded.")
return self._node_id
@property
def public_key_hex(self) -> str:
"""Получить публичный ключ в hex формате"""
if not self._public_key:
raise ValueError("Public key not loaded")
public_key_bytes = self._public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
return public_key_bytes.hex()
def sign_message(self, message: Dict[str, Any]) -> str:
"""
Подписать сообщение ed25519 ключом
Args:
message: Словарь с данными для подписи
Returns:
base64-encoded подпись
"""
if not self._private_key:
raise ValueError("Private key not loaded")
# Сериализуем сообщение в JSON для подписи
message_json = json.dumps(message, sort_keys=True, ensure_ascii=False)
message_bytes = message_json.encode('utf-8')
# Создаем хеш сообщения для подписи
message_hash = hashlib.sha256(message_bytes).digest()
# Подписываем хеш
signature = self._private_key.sign(message_hash)
# Возвращаем подпись в base64
return base64.b64encode(signature).decode('ascii')
def verify_signature(self, message: Dict[str, Any], signature: str, public_key_hex: str) -> bool:
"""
Проверить подпись сообщения
Args:
message: Словарь с данными
signature: base64-encoded подпись
public_key_hex: Публичный ключ в hex формате
Returns:
True если подпись валидна
"""
try:
# Восстанавливаем публичный ключ из hex
public_key_bytes = bytes.fromhex(public_key_hex)
public_key = crypto_ed25519.Ed25519PublicKey.from_public_bytes(public_key_bytes)
# Сериализуем сообщение так же как при подписи
message_json = json.dumps(message, sort_keys=True, ensure_ascii=False)
message_bytes = message_json.encode('utf-8')
message_hash = hashlib.sha256(message_bytes).digest()
# Декодируем подпись
signature_bytes = base64.b64decode(signature.encode('ascii'))
# Проверяем подпись
public_key.verify(signature_bytes, message_hash)
return True
except Exception as e:
logger.warning(f"Signature verification failed: {e}")
return False
def create_signed_message(self, message_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Создать подписанное сообщение для отправки
Args:
message_type: Тип сообщения (handshake, sync_request, etc.)
data: Данные сообщения
Returns:
Подписанное сообщение
"""
# Основная структура сообщения
message = {
"type": message_type,
"node_id": self.node_id,
"public_key": self.public_key_hex,
"timestamp": int(time.time()),
"data": data
}
# Подписываем сообщение
signature = self.sign_message(message)
# Добавляем подпись
signed_message = message.copy()
signed_message["signature"] = signature
return signed_message
def verify_incoming_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
"""
Проверить входящее подписанное сообщение
Args:
message: Входящее сообщение
Returns:
(is_valid, error_message)
"""
try:
# Проверяем обязательные поля
required_fields = ["type", "node_id", "public_key", "timestamp", "data", "signature"]
for field in required_fields:
if field not in message:
return False, f"Missing required field: {field}"
# Извлекаем подпись и создаем сообщение без подписи для проверки
signature = message.pop("signature")
# Проверяем подпись
is_valid = self.verify_signature(message, signature, message["public_key"])
if not is_valid:
return False, "Invalid signature"
# Проверяем временную метку (не старше 5 минут)
current_time = int(time.time())
if abs(current_time - message["timestamp"]) > 300:
return False, "Message timestamp too old"
return True, None
except Exception as e:
return False, f"Verification error: {str(e)}"
def create_handshake_message(self, target_node_id: str, additional_data: Optional[Dict] = None) -> Dict[str, Any]:
"""
Создать сообщение для handshake с другой нодой
Args:
target_node_id: ID целевой ноды
additional_data: Дополнительные данные
Returns:
Подписанное handshake сообщение
"""
handshake_data = {
"target_node_id": target_node_id,
"protocol_version": "3.0",
"node_type": os.getenv("NODE_TYPE", "uploader"),
"capabilities": ["upload", "content_streaming", "conversion", "storage"]
}
if additional_data:
handshake_data.update(additional_data)
return self.create_signed_message("handshake", handshake_data)
def create_upload_message(self, content_hash: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
"""
Создать подписанное сообщение для загрузки контента
Args:
content_hash: Хеш контента
metadata: Метаданные файла
Returns:
Подписанное upload сообщение
"""
upload_data = {
"content_hash": content_hash,
"metadata": metadata,
"uploader_node": self.node_id,
"upload_timestamp": int(time.time())
}
return self.create_signed_message("content_upload", upload_data)
def create_sync_message(self, content_list: list, operation: str = "announce") -> Dict[str, Any]:
"""
Создать сообщение для синхронизации контента
Args:
content_list: Список контента для синхронизации
operation: Тип операции (announce, request, response)
Returns:
Подписанное sync сообщение
"""
sync_data = {
"operation": operation,
"content_list": content_list,
"sync_id": hashlib.sha256(
(self.node_id + str(int(time.time()))).encode()
).hexdigest()[:16]
}
return self.create_signed_message("content_sync", sync_data)
# Глобальный экземпляр менеджера
_ed25519_manager = None
def get_ed25519_manager() -> Ed25519Manager:
"""Получить глобальный экземпляр Ed25519Manager"""
global _ed25519_manager
if _ed25519_manager is None:
_ed25519_manager = Ed25519Manager()
return _ed25519_manager
def init_ed25519_manager(private_key_path: str, public_key_path: str) -> Ed25519Manager:
"""Инициализировать Ed25519Manager с путями к ключам"""
global _ed25519_manager
_ed25519_manager = Ed25519Manager(private_key_path, public_key_path)
return _ed25519_manager
+486
View File
@@ -0,0 +1,486 @@
"""
Клиент для межузлового общения с ed25519 подписями
"""
import asyncio
import json
import aiohttp
from typing import Dict, Any, Optional, List
from datetime import datetime
from urllib.parse import urljoin
from app.core.crypto import get_ed25519_manager
from app.core.logging import get_logger
logger = get_logger(__name__)
class NodeClient:
"""Клиент для подписанного межузлового общения"""
def __init__(self, timeout: int = 30):
self.timeout = aiohttp.ClientTimeout(total=timeout)
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
"""Async context manager entry"""
self.session = aiohttp.ClientSession(timeout=self.timeout)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit"""
if self.session:
await self.session.close()
async def _create_signed_request(
self,
action: str,
data: Dict[str, Any],
target_url: str
) -> Dict[str, Any]:
"""
Создать подписанный запрос для межузлового общения
Args:
action: Тип действия (handshake, content_sync, ping, etc.)
data: Данные сообщения
target_url: URL целевой ноды
Returns:
Заголовки и тело запроса
"""
crypto_manager = get_ed25519_manager()
# Создаем сообщение
message = {
"action": action,
"timestamp": datetime.utcnow().isoformat(),
**data
}
# Подписываем сообщение
signature = crypto_manager.sign_message(message)
# Создаем заголовки
headers = {
"Content-Type": "application/json",
"X-Node-Communication": "true",
"X-Node-ID": crypto_manager.node_id,
"X-Node-Public-Key": crypto_manager.public_key_hex,
"X-Node-Signature": signature
}
return {
"headers": headers,
"json": message
}
async def send_handshake(
self,
target_url: str,
our_node_info: Dict[str, Any]
) -> Dict[str, Any]:
"""
Отправить хэндшейк ноде
Args:
target_url: URL целевой ноды (например, "http://node.example.com:8000")
our_node_info: Информация о нашей ноде
Returns:
Ответ от ноды или информация об ошибке
"""
endpoint_url = urljoin(target_url, "/api/node/handshake")
try:
request_data = await self._create_signed_request(
"handshake",
{"node_info": our_node_info},
target_url
)
logger.info(f"Sending handshake to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.info(f"Handshake successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Handshake failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except asyncio.TimeoutError:
logger.warning(f"Handshake timeout with {target_url}")
return {
"success": False,
"error": "timeout",
"node_url": target_url
}
except Exception as e:
logger.error(f"Handshake error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_content_sync(
self,
target_url: str,
sync_type: str,
content_info: Dict[str, Any]
) -> Dict[str, Any]:
"""
Отправить запрос синхронизации контента
Args:
target_url: URL целевой ноды
sync_type: Тип синхронизации (new_content, content_list, content_request)
content_info: Информация о контенте
Returns:
Ответ от ноды
"""
endpoint_url = urljoin(target_url, "/api/node/content/sync")
try:
request_data = await self._create_signed_request(
"content_sync",
{
"sync_type": sync_type,
"content_info": content_info
},
target_url
)
logger.info(f"Sending content sync ({sync_type}) to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.debug(f"Content sync successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Content sync failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Content sync error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_ping(self, target_url: str) -> Dict[str, Any]:
"""
Отправить пинг ноде
Args:
target_url: URL целевой ноды
Returns:
Ответ от ноды (pong)
"""
endpoint_url = urljoin(target_url, "/api/node/network/ping")
try:
request_data = await self._create_signed_request(
"ping",
{"data": {"test": True}},
target_url
)
start_time = datetime.utcnow()
async with self.session.post(endpoint_url, **request_data) as response:
end_time = datetime.utcnow()
duration = (end_time - start_time).total_seconds() * 1000 # ms
response_data = await response.json()
if response.status == 200:
return {
"success": True,
"data": response_data,
"latency_ms": round(duration, 2),
"node_url": target_url
}
else:
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Ping error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def get_node_status(self, target_url: str) -> Dict[str, Any]:
"""
Получить статус ноды (GET запрос без подписи)
Args:
target_url: URL целевой ноды
Returns:
Статус ноды
"""
endpoint_url = urljoin(target_url, "/api/node/network/status")
try:
async with self.session.get(endpoint_url) as response:
response_data = await response.json()
if response.status == 200:
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Status request error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
async def send_discovery(
self,
target_url: str,
known_nodes: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Отправить запрос обнаружения нод
Args:
target_url: URL целевой ноды
known_nodes: Список известных нам нод
Returns:
Список нод от целевой ноды
"""
endpoint_url = urljoin(target_url, "/api/node/network/discover")
try:
request_data = await self._create_signed_request(
"discover",
{"known_nodes": known_nodes},
target_url
)
logger.info(f"Sending discovery request to {target_url}")
async with self.session.post(endpoint_url, **request_data) as response:
response_data = await response.json()
if response.status == 200:
logger.debug(f"Discovery successful with {target_url}")
return {
"success": True,
"data": response_data,
"node_url": target_url
}
else:
logger.warning(f"Discovery failed with {target_url}: {response.status}")
return {
"success": False,
"error": f"HTTP {response.status}",
"data": response_data,
"node_url": target_url
}
except Exception as e:
logger.error(f"Discovery error with {target_url}: {e}")
return {
"success": False,
"error": str(e),
"node_url": target_url
}
class NodeNetworkManager:
"""Менеджер для работы с сетью нод"""
def __init__(self):
self.known_nodes: List[str] = []
self.active_nodes: List[str] = []
async def discover_nodes(self, bootstrap_nodes: List[str]) -> List[str]:
"""
Обнаружить ноды в сети через bootstrap ноды
Args:
bootstrap_nodes: Список bootstrap нод для начального подключения
Returns:
Список обнаруженных активных нод
"""
discovered_nodes = set()
async with NodeClient() as client:
# Получить информацию о нашей ноде
crypto_manager = get_ed25519_manager()
our_node_info = {
"node_id": crypto_manager.node_id,
"version": "3.0.0",
"capabilities": [
"content_upload",
"content_sync",
"decentralized_filtering",
"ed25519_signatures"
],
"network_info": {
"public_key": crypto_manager.public_key_hex,
"protocol_version": "1.0"
}
}
# Попробовать подключиться к bootstrap нодам
for node_url in bootstrap_nodes:
try:
# Выполнить хэндшейк
handshake_result = await client.send_handshake(node_url, our_node_info)
if handshake_result["success"]:
discovered_nodes.add(node_url)
# Запросить список известных нод
discovery_result = await client.send_discovery(node_url, list(discovered_nodes))
if discovery_result["success"]:
# Добавить ноды из ответа
known_nodes = discovery_result["data"]["data"]["known_nodes"]
for node_info in known_nodes:
if "url" in node_info:
discovered_nodes.add(node_info["url"])
except Exception as e:
logger.warning(f"Failed to discover through {node_url}: {e}")
self.known_nodes = list(discovered_nodes)
return self.known_nodes
async def check_node_health(self, nodes: List[str]) -> Dict[str, Dict[str, Any]]:
"""
Проверить состояние нод
Args:
nodes: Список нод для проверки
Returns:
Словарь с результатами проверки для каждой ноды
"""
results = {}
async with NodeClient() as client:
# Создаем задачи для параллельной проверки
tasks = []
for node_url in nodes:
task = asyncio.create_task(client.send_ping(node_url))
tasks.append((node_url, task))
# Ждем завершения всех задач
for node_url, task in tasks:
try:
result = await task
results[node_url] = result
except Exception as e:
results[node_url] = {
"success": False,
"error": str(e),
"node_url": node_url
}
# Обновляем список активных нод
self.active_nodes = [
node_url for node_url, result in results.items()
if result.get("success", False)
]
return results
async def broadcast_content(
self,
content_info: Dict[str, Any],
target_nodes: Optional[List[str]] = None
) -> Dict[str, Dict[str, Any]]:
"""
Транслировать информацию о новом контенте всем активным нодам
Args:
content_info: Информация о контенте
target_nodes: Список целевых нод (по умолчанию все активные)
Returns:
Результаты трансляции для каждой ноды
"""
nodes = target_nodes or self.active_nodes
results = {}
async with NodeClient() as client:
# Создаем задачи для параллельной отправки
tasks = []
for node_url in nodes:
task = asyncio.create_task(
client.send_content_sync(node_url, "new_content", content_info)
)
tasks.append((node_url, task))
# Ждем завершения всех задач
for node_url, task in tasks:
try:
result = await task
results[node_url] = result
except Exception as e:
results[node_url] = {
"success": False,
"error": str(e),
"node_url": node_url
}
return results
# Глобальный экземпляр менеджера сети
network_manager = NodeNetworkManager()
async def get_network_manager() -> NodeNetworkManager:
"""Получить глобальный экземпляр менеджера сети"""
return network_manager