Move source files into nested secpaste/ directory to fix module imports. This allows the package to be properly installed with pip and imported as 'import secpaste' or 'python -m secpaste.cli'. Changes: - Move all Python files into secpaste/secpaste/ structure - Add .gitignore for Python artifacts Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
66 lines
1.9 KiB
Python
66 lines
1.9 KiB
Python
"""AES-256-GCM cipher backend."""
|
|
|
|
import os
|
|
import base64
|
|
from typing import Tuple
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
from .base import CipherBackend
|
|
|
|
|
|
class AESGCMCipher(CipherBackend):
|
|
"""AES-256-GCM authenticated encryption."""
|
|
|
|
def encrypt(self, plaintext: bytes) -> Tuple[bytes, str]:
|
|
"""
|
|
Encrypt using AES-256-GCM.
|
|
|
|
Returns:
|
|
Tuple of (nonce + ciphertext, base64url-encoded key)
|
|
"""
|
|
# Generate random 256-bit key
|
|
key = AESGCM.generate_key(bit_length=256)
|
|
aesgcm = AESGCM(key)
|
|
|
|
# Generate random 96-bit nonce (recommended for GCM)
|
|
nonce = os.urandom(12)
|
|
|
|
# Encrypt and authenticate
|
|
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
|
|
|
|
# Prepend nonce to ciphertext for storage
|
|
encrypted_data = nonce + ciphertext
|
|
|
|
# Encode key for URL fragment
|
|
key_encoded = base64.urlsafe_b64encode(key).decode('ascii').rstrip('=')
|
|
|
|
return encrypted_data, key_encoded
|
|
|
|
def decrypt(self, ciphertext: bytes, key: str) -> bytes:
|
|
"""
|
|
Decrypt using AES-256-GCM.
|
|
|
|
Args:
|
|
ciphertext: Nonce (12 bytes) + encrypted data
|
|
key: Base64url-encoded 256-bit key
|
|
"""
|
|
# Decode key (add padding if needed)
|
|
padding = '=' * (4 - len(key) % 4) if len(key) % 4 else ''
|
|
key_bytes = base64.urlsafe_b64decode(key + padding)
|
|
|
|
if len(key_bytes) != 32:
|
|
raise ValueError("Invalid key length. Expected 32 bytes for AES-256.")
|
|
|
|
# Extract nonce and ciphertext
|
|
nonce = ciphertext[:12]
|
|
encrypted = ciphertext[12:]
|
|
|
|
# Decrypt
|
|
aesgcm = AESGCM(key_bytes)
|
|
plaintext = aesgcm.decrypt(nonce, encrypted, None)
|
|
|
|
return plaintext
|
|
|
|
def get_name(self) -> str:
|
|
"""Return cipher name."""
|
|
return "aes-256-gcm"
|