# Crypto Utility Usage Guide This document provides examples for using the ClaudeTools encryption utilities. ## Overview The crypto utilities provide secure encryption and decryption functions for sensitive data such as: - User credentials - API keys and tokens - Passwords - OAuth secrets - Database connection strings ## Features - **AES-256 symmetric encryption** via Fernet (AES-128-CBC + HMAC) - **Authenticated encryption** to prevent tampering - **Random IV** for each encryption (same plaintext produces different ciphertexts) - **Base64 encoding** for safe storage in databases and config files - **Proper error handling** for invalid keys or corrupted data - **Type safety** with type hints ## Setup ### 1. Generate an Encryption Key ```python from api.utils.crypto import generate_encryption_key # Generate a new key (only do this once during initial setup) key = generate_encryption_key() print(f"ENCRYPTION_KEY={key}") ``` ### 2. Add to Environment Add the generated key to your `.env` file: ```bash ENCRYPTION_KEY=a59976f06d88049f7e3c2b1a8d4e5f6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2 ``` **Security Notes:** - Never commit the `.env` file to version control - Use different keys for development, staging, and production - Store production keys in a secure secrets manager - Never rotate keys without migrating existing encrypted data ## Basic Usage ### Encrypting Data ```python from api.utils.crypto import encrypt_string # Encrypt sensitive data api_key = "sk-1234567890abcdef" encrypted_api_key = encrypt_string(api_key) # Store encrypted value in database user.encrypted_api_key = encrypted_api_key db.commit() ``` ### Decrypting Data ```python from api.utils.crypto import decrypt_string # Retrieve encrypted value from database encrypted_value = user.encrypted_api_key # Decrypt it api_key = decrypt_string(encrypted_value) # Use the decrypted value response = requests.get(api_url, headers={"Authorization": f"Bearer {api_key}"}) ``` ### Error Handling with Default Values ```python from api.utils.crypto import decrypt_string # Return a default value if decryption fails api_key = decrypt_string(user.encrypted_api_key, default="") if not api_key: print("Unable to decrypt API key - may need to re-authenticate") ``` ## Advanced Examples ### Database Model with Encrypted Field ```python from sqlalchemy import Column, String, Integer from sqlalchemy.orm import declarative_base from api.utils.crypto import encrypt_string, decrypt_string Base = declarative_base() class UserCredential(Base): __tablename__ = "user_credentials" id = Column(Integer, primary_key=True) service_name = Column(String(100), nullable=False) username = Column(String(100), nullable=False) encrypted_password = Column(String(500), nullable=False) def set_password(self, password: str): """Encrypt and store the password.""" self.encrypted_password = encrypt_string(password) def get_password(self) -> str: """Decrypt and return the password.""" return decrypt_string(self.encrypted_password) # Usage credential = UserCredential( service_name="GitHub", username="user@example.com" ) credential.set_password("my_secure_password_123") # Later, retrieve the password password = credential.get_password() ``` ### API Endpoint Example ```python from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from api.utils.crypto import encrypt_string, decrypt_string from api.database import get_db router = APIRouter() @router.post("/credentials") async def create_credential( service: str, username: str, password: str, db: Session = Depends(get_db) ): """Store encrypted credentials.""" try: # Encrypt the password before storing encrypted_password = encrypt_string(password) credential = UserCredential( service_name=service, username=username, encrypted_password=encrypted_password ) db.add(credential) db.commit() return {"message": "Credentials stored securely"} except Exception as e: raise HTTPException(status_code=500, detail="Failed to encrypt credentials") @router.get("/credentials/{service}") async def get_credential(service: str, db: Session = Depends(get_db)): """Retrieve and decrypt credentials.""" credential = db.query(UserCredential).filter_by(service_name=service).first() if not credential: raise HTTPException(status_code=404, detail="Credentials not found") try: # Decrypt the password password = decrypt_string(credential.encrypted_password) return { "service": credential.service_name, "username": credential.username, "password": password # In production, consider not returning plaintext } except ValueError: raise HTTPException(status_code=500, detail="Failed to decrypt credentials") ``` ### Batch Encryption ```python from api.utils.crypto import encrypt_string def encrypt_user_secrets(user_data: dict) -> dict: """Encrypt all sensitive fields in user data.""" encrypted_data = user_data.copy() # List of fields to encrypt sensitive_fields = ['password', 'api_key', 'oauth_token', 'secret_key'] for field in sensitive_fields: if field in encrypted_data and encrypted_data[field]: encrypted_data[f'encrypted_{field}'] = encrypt_string(encrypted_data[field]) del encrypted_data[field] # Remove plaintext return encrypted_data # Usage user_data = { "username": "john_doe", "email": "john@example.com", "password": "super_secret_password", "api_key": "sk-1234567890" } encrypted_user = encrypt_user_secrets(user_data) # Result: { "username": "john_doe", "email": "john@example.com", # "encrypted_password": "gAAAAAB...", "encrypted_api_key": "gAAAAAB..." } ``` ## Security Best Practices ### DO: - Use the encryption for passwords, API keys, tokens, and sensitive credentials - Store encrypted values in database fields with adequate length (500+ chars) - Use VARCHAR or TEXT fields for encrypted data - Validate encryption key exists and is correctly formatted - Log encryption/decryption failures without logging sensitive data - Use `default` parameter for graceful degradation ### DON'T: - Don't encrypt non-sensitive data (names, emails, public info) - Don't log decrypted values - Don't commit encryption keys to version control - Don't reuse encryption keys across environments - Don't rotate keys without a migration plan - Don't encrypt large files (use this for credentials only) ## Error Handling ```python from api.utils.crypto import decrypt_string try: password = decrypt_string(encrypted_value) except ValueError as e: # Handle invalid ciphertext or wrong key logger.error(f"Decryption failed: {e}") # Prompt user to re-enter credentials # Alternative: Use default value password = decrypt_string(encrypted_value, default=None) if password is None: # Handle failed decryption request_user_credentials() ``` ## Testing ```python import pytest from api.utils.crypto import encrypt_string, decrypt_string def test_encryption_roundtrip(): """Test that encryption and decryption work correctly.""" original = "my_secret_password" encrypted = encrypt_string(original) decrypted = decrypt_string(encrypted) assert decrypted == original assert encrypted != original assert len(encrypted) > len(original) def test_encryption_randomness(): """Test that same input produces different ciphertexts.""" original = "test_password" encrypted1 = encrypt_string(original) encrypted2 = encrypt_string(original) # Different ciphertexts assert encrypted1 != encrypted2 # But both decrypt to same value assert decrypt_string(encrypted1) == original assert decrypt_string(encrypted2) == original def test_invalid_ciphertext(): """Test error handling for invalid data.""" with pytest.raises(ValueError): decrypt_string("not_valid_ciphertext") def test_type_validation(): """Test type checking.""" with pytest.raises(TypeError): encrypt_string(12345) # Not a string with pytest.raises(TypeError): decrypt_string(12345) # Not a string ``` ## Troubleshooting ### "Invalid encryption key" Error **Cause:** The `ENCRYPTION_KEY` environment variable is missing or incorrectly formatted. **Solution:** 1. Generate a new key: `python -c "from api.utils.crypto import generate_encryption_key; print(generate_encryption_key())"` 2. Add to `.env`: `ENCRYPTION_KEY=` 3. Ensure the key is exactly 64 hex characters (32 bytes) ### "Failed to decrypt data" Error **Cause:** One of the following: - Data was encrypted with a different key - Data was corrupted - Data was tampered with **Solution:** 1. Verify you're using the correct encryption key 2. Check if encryption key was rotated without migrating data 3. For corrupted data, request user to re-enter credentials ### "Encryption key must be 32 bytes" Error **Cause:** The encryption key is not the correct length. **Solution:** Ensure your `ENCRYPTION_KEY` is exactly 64 hex characters (representing 32 bytes): ```bash # Correct format (64 characters) ENCRYPTION_KEY=a59976f06d88049f7e3c2b1a8d4e5f6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2 # Incorrect format (too short) ENCRYPTION_KEY=abc123 ``` ## Performance Considerations - Encryption/decryption is fast (~microseconds per operation) - Suitable for real-time API requests - For bulk operations, consider batching in background tasks - Encrypted data is ~33% larger than original (due to base64 + auth tag) - Plan database field sizes accordingly (recommend 500+ chars for encrypted fields) ## Migration and Key Rotation If you need to rotate encryption keys: 1. Generate a new key 2. Create a migration script: ```python from api.utils.crypto import decrypt_string, encrypt_string import os def migrate_encrypted_data(old_key: str, new_key: str): """Migrate data from old key to new key.""" # Temporarily set old key os.environ['ENCRYPTION_KEY'] = old_key from api.utils.crypto import decrypt_string as old_decrypt # Get all encrypted records credentials = db.query(UserCredential).all() for cred in credentials: # Decrypt with old key old_password = old_decrypt(cred.encrypted_password) # Re-encrypt with new key os.environ['ENCRYPTION_KEY'] = new_key from api.utils.crypto import encrypt_string as new_encrypt cred.encrypted_password = new_encrypt(old_password) db.commit() ``` 3. Run migration in a maintenance window 4. Update environment variable 5. Verify all data decrypts correctly 6. Securely delete old key ## API Reference ### `encrypt_string(plaintext: str) -> str` Encrypts a string using Fernet symmetric encryption. **Parameters:** - `plaintext` (str): The string to encrypt **Returns:** - str: Base64-encoded encrypted string **Raises:** - `ValueError`: If the encryption key is invalid - `TypeError`: If plaintext is not a string ### `decrypt_string(ciphertext: str, default: Optional[str] = None) -> str` Decrypts a Fernet-encrypted string back to plaintext. **Parameters:** - `ciphertext` (str): Base64-encoded encrypted string from `encrypt_string()` - `default` (Optional[str]): Optional default value to return if decryption fails **Returns:** - str: Decrypted plaintext string **Raises:** - `ValueError`: If ciphertext is invalid or decryption fails (when `default=None`) - `TypeError`: If ciphertext is not a string ### `generate_encryption_key() -> str` Generates a new random encryption key. **Returns:** - str: 64-character hex string representing a 32-byte key **Usage:** Only use during initial setup or key rotation. Never rotate keys without migrating existing encrypted data.