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>
This commit is contained in:
175
client.py
Normal file
175
client.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user