Files
secpaste/client.py
nanoclaw ff41256f2f Initial commit: SecPaste encrypted pastebin client
SecPaste is a Python library and CLI tool for sharing encrypted content via
public pastebin services with zero-knowledge architecture.

Features:
- Pluggable crypto backends (AES-256-GCM, ChaCha20-Poly1305, Kyber-768)
- Pluggable pastebin providers (dpaste.com, extensible)
- URL fragment key storage (key never sent to server)
- Both CLI and library usage
- Post-quantum cryptography support (experimental)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 22:52:32 +00:00

176 lines
5.2 KiB
Python

"""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())