""" Logo Protection Service with RSA Digital Signature =================================================== Protects the panel logo from unauthorized modifications using RSA digital signatures. How it works: 1. Logo creator signs the logo with their PRIVATE key (kept secret, never shared) 2. PUBLIC key is embedded in this code 3. On each request, signature is verified with public key 4. Without the private key, it's cryptographically impossible to create a valid signature For logo updates: Contact the project maintainer with the new logo. """ import base64 import os from pathlib import Path from loguru import logger try: from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import serialization CRYPTO_AVAILABLE = True except ImportError: CRYPTO_AVAILABLE = False logger.warning("cryptography library not available, logo protection disabled") # Get the static directory path STATIC_DIR = Path(__file__).parent.parent / "static" LOGO_PATH = STATIC_DIR / "img" / "logo.png" LOGO_SIG_PATH = STATIC_DIR / "img" / "logo.sig" # ============================================================ # PUBLIC KEY - Safe to be in open source code # This key can only VERIFY signatures, not create them # ============================================================ PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvM0VCWkP0fg2PMAPYJdn JF0YO9EkJtJmnhV2v+VIIqqXdiBnNDVFHxAxxDmeMHkXff9VbZ5PRRDc/AMRSjwY f1tN/xCCfQG8GVNlIKDcr11GJCAm7vOjy64bSEs/VpOK/A0l4js0FX6o/nEvqDqF hSdrxhrnX4iWbm6UaXL5ZcWrHYXXyD0zsc2g5xEXAtf7sy8U8bHtD6NjaLNNIVWz LzZqNTaAzOKXKOgKdwsYT4JXvWNLgrjMHo5TUcYDAnrPDSE2E6Ms2TZDS0BicB7Q g6FZ5M8H9NrttpDxvS0Gwen0AfRbI/0Cocn9nG88eiX/hbsodPxmJIlloAUOhtRa dwIDAQAB -----END PUBLIC KEY-----""" # Fallback logo (simple base64 encoded placeholder - 64x64 transparent PNG) FALLBACK_LOGO_B64 = """ iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAD GUlEQVR4nO2bS27bMBCGP7RZdNNFb9BFT+Au0hv0Br1BeoMe4QSNF+4q3fQGXfQG3fQEi+xatQ8g kCIlPkTJUuL5gICRPMP5OZwhKQOGYRiGYRiGYRiGYRhGIpI0dQ/nOy64QC4F/gI/gO/A17LkoyAJ cAX4AFwCF8AvYAf8Bn4C38o/+xaQBLgGPgNXZbkA/gDb8s+2wC/gS1nykZAEvAXeA2/Ksg1sgB/l n+2ALfBpmXIXBUnAO+AD8Los62AB/C7/bAN8L0s+CpKAG+A98LosK+Av8Kf8sw3wo/yzb4IkwDXw CXhdljXwB/hb/tkW+FWWfBQkAa6BT8DrsmyAX8Dv8s82wM+y5CMhCXANfAJelWUN/AZ+lX+2Bn6U JR8JSYBr4CPwqixr4Bfwu/yzNfCjLPlISAJcAx+BV2VZAb+A3+WfrYEfZclHQhLgGvgAvCrLEvgJ /Cr/bAn8KEs+EpIA18B74GVZFsAP4Ff5ZwvgR1nykZAEuAbeAy/LMgd+AD/LP5sDX8uSj4QkwDXw DnhZljnwA/hZ/tkc+FKWfCQkAa6Bd8DLskyBb8DP8s+mwJey5CMhCXANvAVelmUCfAN+ln82Ab6U JR8JSYBr4A3wsiw3wFfgR/lnN8CXsuQjIQlwDbwBXpTlGvgK/Cj/7Br4UpZ8JCQBroHXwIuyXAFf gB/ln10Bn8uSj4QkwBXwGnhRlkvgM/Cj/LNL4HNZ8pGQBLgCXgMvyjIGPgM/yj8bA5/Lko+EJMAV 8Bp4UZYx8An4Uf7ZGPhclnwkJAEugdfAi7JcAJ+AH+WfXQCfy5KPhCTAJfAaeFGWMfAR+FH+2Rj4 VJZ8JCQBLoHXwIuyjIEPwI/yz8bAx7LkIyEJcAm8Bp6XZQS8B76Xfz4C3pclHwlJgAvgNfC8LEPg HfC9/PMh8K4s+UhIAlwAr4HnZRkCb4Hv5Z8PgbdlyUdCEuACeA08L8sQeAN8L/98CLwpSz4SkgAX wCvgeVkGwGvge/nnA+B1WfKRkAS4AF4Bz8syAF4B38s/HwCvypKPhGEYhmEYhmEYhmEYhmH8D/wD F0bN8E0rJrsAAAAASUVORK5CYII= """.strip().replace('\n', '') class LogoService: """Service for logo protection using RSA digital signatures""" _public_key = None @classmethod def _get_public_key(cls): """Load and cache public key""" if cls._public_key is None and CRYPTO_AVAILABLE: try: cls._public_key = serialization.load_pem_public_key( PUBLIC_KEY_PEM.encode() ) except Exception as e: logger.error(f"Failed to load public key: {e}") return cls._public_key @staticmethod def verify_signature(data: bytes, signature: bytes) -> bool: """Verify RSA signature of data""" if not CRYPTO_AVAILABLE: return True # Skip verification if crypto not available public_key = LogoService._get_public_key() if not public_key: return False try: public_key.verify( signature, data, padding.PKCS1v15(), hashes.SHA256() ) return True except Exception as e: logger.warning(f"Signature verification failed: {e}") return False @staticmethod def get_stored_signature() -> bytes | None: """Get stored signature from file""" try: if LOGO_SIG_PATH.exists(): # Signature is stored as base64 sig_b64 = LOGO_SIG_PATH.read_text().strip() return base64.b64decode(sig_b64) except Exception as e: logger.error(f"Error reading logo signature: {e}") return None @staticmethod def verify_logo() -> bool: """Verify logo integrity using RSA signature""" try: if not LOGO_PATH.exists(): logger.warning("Logo file not found") return False signature = LogoService.get_stored_signature() if not signature: logger.warning("No signature found for logo") return False logo_data = LOGO_PATH.read_bytes() if not LogoService.verify_signature(logo_data, signature): logger.warning("Logo signature verification FAILED - unauthorized modification detected!") return False logger.debug("Logo signature verified successfully") return True except Exception as e: logger.error(f"Error verifying logo: {e}") return False @staticmethod def get_logo_bytes() -> bytes: """Get logo bytes - verified original or fallback""" if LogoService.verify_logo(): return LOGO_PATH.read_bytes() else: logger.info("Using fallback logo (signature verification failed)") return base64.b64decode(FALLBACK_LOGO_B64) @staticmethod def get_logo_base64() -> str: """Get logo as base64 string""" return base64.b64encode(LogoService.get_logo_bytes()).decode() @staticmethod def is_logo_valid() -> bool: """Check if current logo is valid (has valid signature)""" return LogoService.verify_logo() # ============================================================ # SIGNING FUNCTIONS - Only work with private key # These are used by the sign_logo.py script # ============================================================ def sign_logo_with_private_key(logo_path: str, private_key_path: str, output_sig_path: str = None) -> tuple[bool, str]: """ Sign a logo file with private key. This function is for the logo creator only. Args: logo_path: Path to the logo PNG file private_key_path: Path to the private key PEM file output_sig_path: Path for output signature file (default: logo_path + '.sig') Returns: tuple (success, message) """ if not CRYPTO_AVAILABLE: return False, "cryptography library not installed" try: # Load logo logo_path = Path(logo_path) if not logo_path.exists(): return False, f"Logo file not found: {logo_path}" logo_data = logo_path.read_bytes() # Verify it's a PNG if not logo_data[:8] == b'\x89PNG\r\n\x1a\n': return False, "File is not a valid PNG" # Load private key private_key_path = Path(private_key_path) if not private_key_path.exists(): return False, f"Private key not found: {private_key_path}" private_key = serialization.load_pem_private_key( private_key_path.read_bytes(), password=None ) # Create signature signature = private_key.sign( logo_data, padding.PKCS1v15(), hashes.SHA256() ) # Save signature as base64 sig_path = Path(output_sig_path) if output_sig_path else logo_path.with_suffix('.sig') sig_b64 = base64.b64encode(signature).decode() sig_path.write_text(sig_b64) return True, f"Signature saved to: {sig_path}" except Exception as e: return False, str(e)