"""Main SecPaste client API.""" from typing import Optional, Dict from urllib.parse import urlparse, urlunparse from .crypto.base import CipherBackend from .crypto.aes import AESGCMCipher from .crypto.chacha import ChaCha20Cipher from .providers.base import PastebinProvider from .providers.dpaste import DPasteProvider class SecPasteClient: """Main client for encrypted pastebin operations.""" # Registry of available cipher backends CIPHERS: Dict[str, type] = { 'aes-256-gcm': AESGCMCipher, 'chacha20-poly1305': ChaCha20Cipher, } # Registry of available providers PROVIDERS: Dict[str, type] = { 'dpaste': DPasteProvider, } def __init__( self, cipher: str = 'aes-256-gcm', provider: str = 'dpaste', cipher_backend: Optional[CipherBackend] = None, provider_backend: Optional[PastebinProvider] = None ): """ Initialize SecPaste client. Args: cipher: Name of cipher backend to use (default: aes-256-gcm) provider: Name of pastebin provider (default: dpaste) cipher_backend: Custom cipher backend instance (overrides cipher) provider_backend: Custom provider instance (overrides provider) """ # Initialize cipher backend if cipher_backend: self.cipher = cipher_backend else: cipher_class = self.CIPHERS.get(cipher) if not cipher_class: raise ValueError( f"Unknown cipher: {cipher}. " f"Available: {', '.join(self.CIPHERS.keys())}" ) self.cipher = cipher_class() # Initialize provider backend if provider_backend: self.provider = provider_backend else: provider_class = self.PROVIDERS.get(provider) if not provider_class: raise ValueError( f"Unknown provider: {provider}. " f"Available: {', '.join(self.PROVIDERS.keys())}" ) self.provider = provider_class() def paste(self, content: str, **kwargs) -> str: """ Encrypt and paste content. Args: content: Plain text content to paste **kwargs: Provider-specific options (e.g., expiry, syntax) Returns: Complete URL with encryption key in fragment (e.g., https://paste.tld/abc#key) """ # Convert to bytes plaintext = content.encode('utf-8') # Encrypt ciphertext, key = self.cipher.encrypt(plaintext) # Upload to provider paste_url = self.provider.paste(ciphertext, **kwargs) # Add cipher metadata and key to URL fragment cipher_name = self.cipher.get_name() fragment = f"{cipher_name}:{key}" full_url = self._add_fragment(paste_url, fragment) return full_url def fetch(self, url: str) -> str: """ Fetch and decrypt content from URL. Args: url: Complete URL with encryption key in fragment Returns: Decrypted plain text content """ # Parse URL to extract fragment parsed = urlparse(url) if not parsed.fragment: raise ValueError("URL missing encryption key in fragment (#key)") fragment = parsed.fragment # Parse fragment: cipher_name:key if ':' not in fragment: # Backwards compatibility: assume default cipher cipher_name = 'aes-256-gcm' key = fragment else: cipher_name, key = fragment.split(':', 1) # Get appropriate cipher backend cipher_class = self.CIPHERS.get(cipher_name) if not cipher_class: raise ValueError(f"Unknown cipher in URL: {cipher_name}") cipher = cipher_class() # Remove fragment from URL for fetching fetch_url = urlunparse(( parsed.scheme, parsed.netloc, parsed.path, parsed.params, parsed.query, '' # no fragment )) # Fetch encrypted content ciphertext = self.provider.fetch(fetch_url) # Decrypt plaintext = cipher.decrypt(ciphertext, key) return plaintext.decode('utf-8') @staticmethod def _add_fragment(url: str, fragment: str) -> str: """Add fragment to URL.""" parsed = urlparse(url) return urlunparse(( parsed.scheme, parsed.netloc, parsed.path, parsed.params, parsed.query, fragment )) @classmethod def register_cipher(cls, name: str, cipher_class: type): """Register a custom cipher backend.""" cls.CIPHERS[name] = cipher_class @classmethod def register_provider(cls, name: str, provider_class: type): """Register a custom pastebin provider.""" cls.PROVIDERS[name] = provider_class @classmethod def list_ciphers(cls) -> list: """List available cipher backends.""" return list(cls.CIPHERS.keys()) @classmethod def list_providers(cls) -> list: """List available pastebin providers.""" return list(cls.PROVIDERS.keys())