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>
176 lines
5.2 KiB
Python
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())
|