Skip to content

Security Considerations

castlecraft-engineer provides several utilities and patterns to help you build secure applications. This document covers key aspects related to data encryption, authentication, and authorization.

1. Data Encryption

For protecting sensitive data at rest, castlecraft-engineer offers straightforward encryption and decryption utilities.

Encryption Utilities (common/crypto.py)

The castlecraft_engineer.common.crypto module provides encrypt_data and decrypt_data functions.

  • encrypt_data(key: bytes, data: str) -> str: Encrypts a string using AES-GCM.
  • decrypt_data(key: bytes, encrypted_data_hex: str) -> str: Decrypts a hex-encoded string previously encrypted with encrypt_data using the same key.

Encryption Scheme: AES-GCM

The library uses AES (Advanced Encryption Standard) in GCM (Galois/Counter Mode). AES-GCM is an authenticated encryption algorithm that provides both confidentiality (data is unreadable without the key) and integrity/authenticity (ensuring the data has not been tampered with). A random nonce (number used once) is generated for each encryption operation and prepended to the ciphertext, which is crucial for the security of GCM mode.

SECRET_ENCRYPTION_KEY Environment Variable

The security of the encryption relies heavily on the secrecy and strength of the encryption key. * Configuration: The encryption key must be provided via the SECRET_ENCRYPTION_KEY environment variable (defined in castlecraft_engineer.common.env). * Format: This key should be a 32-byte (256-bit) hexadecimal string (i.e., 64 hex characters). A key of this length is used for AES-256. * Generation: It is critical to generate a cryptographically secure random key. You can use tools like OpenSSL to generate one:

openssl rand -hex 32
* Management: Treat this key as highly sensitive. Store it securely (e.g., using environment variable managers, secrets management systems) and never commit it directly into your codebase.

If SECRET_ENCRYPTION_KEY is not set or is invalid, the encryption/decryption functions will raise an error.

Potential Use Cases

  • Encrypting sensitive fields in your SQLModels before they are persisted to the database (e.g., API keys, personal identification numbers, certain user profile details).
  • Securing configuration values that might be stored in less secure places (though dedicated secrets management is preferred for highly sensitive configs).

Exceptions

When using these encryption functions, be prepared to handle the following exceptions from castlecraft_engineer.common.crypto (also re-exported in castlecraft_engineer.exc):

  • InvalidEncryptionKey: Raised if the SECRET_ENCRYPTION_KEY is missing, not a valid hex string, or not the correct length (32 bytes / 64 hex characters).
  • InvalidEncryptionFormat: Raised during decryption if the provided encrypted data is malformed, if the decryption fails due to an incorrect key, or if the data integrity check (part of GCM) fails, indicating potential tampering.

Code Examples

import os
from castlecraft_engineer.common.crypto import encrypt_data, decrypt_data, InvalidEncryptionKey, InvalidEncryptionFormat
from castlecraft_engineer.common.env import SECRET_ENCRYPTION_KEY # To load the key

# Ensure SECRET_ENCRYPTION_KEY is set in your environment for this example to run.
# For testing, you can set it temporarily:
# os.environ["SECRET_ENCRYPTION_KEY"] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" # Example 32-byte key

def encryption_example():
    sensitive_data = "This is a very secret message!"
    encryption_key_bytes = bytes.fromhex(SECRET_ENCRYPTION_KEY)

    try:
        print(f"Original data: {sensitive_data}")

        # Encrypt data
        encrypted_hex = encrypt_data(encryption_key_bytes, sensitive_data)
        print(f"Encrypted (hex): {encrypted_hex}")

        # Decrypt data
        decrypted_data = decrypt_data(encryption_key_bytes, encrypted_hex)
        print(f"Decrypted data: {decrypted_data}")

        assert sensitive_data == decrypted_data
        print("Encryption and decryption successful!")

        # Example of trying to decrypt with a wrong key (will raise InvalidEncryptionFormat)
        wrong_key_hex = os.urandom(32).hex() # Generate a random different key
        wrong_key_bytes = bytes.fromhex(wrong_key_hex)
        try:
            decrypt_data(wrong_key_bytes, encrypted_hex)
        except InvalidEncryptionFormat as e:
            print(f"Caught expected error with wrong key: {e}")

    except InvalidEncryptionKey as e:
        print(f"Encryption key error: {e}. Ensure SECRET_ENCRYPTION_KEY is set correctly.")
    except InvalidEncryptionFormat as e:
        print(f"Encryption format/decryption error: {e}")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

# if __name__ == "__main__":
#     if not os.getenv("SECRET_ENCRYPTION_KEY"):
#         print("Warning: SECRET_ENCRYPTION_KEY not set. Using a temporary insecure key for example.")
#         os.environ["SECRET_ENCRYPTION_KEY"] = os.urandom(32).hex()
#     encryption_example()

2. Authentication

castlecraft-engineer includes an AuthenticationService (located in castlecraft_engineer.application.auth) designed primarily for OpenID Connect (OIDC) based authentication.

  • Role: The AuthenticationService is responsible for:
    • Verifying OIDC ID Tokens: It uses the PyJWT library to decode and validate JWTs (JSON Web Tokens) according to OIDC specifications. This includes checking the signature against public keys fetched from the OIDC provider's JWKS (JSON Web Key Set) URI, as well as validating claims like issuer (iss), audience (aud), and expiry (exp).
    • Fetching User Information: After successful token verification, it can optionally fetch additional user information from the OIDC provider's UserInfo endpoint.
    • Token Introspection: It can also perform token introspection by calling the OIDC provider's introspection endpoint to validate an access token and get its metadata.
  • Caching: To improve performance and reduce load on the OIDC provider, the AuthenticationService heavily relies on caching for:
    • JWKS: Public keys are cached as they change infrequently.
    • Introspection Results: Valid introspection responses for tokens are cached for the token's lifetime.
  • Key Methods:
    • async verify_id_token(token: str, audience: Optional[str] = None, issuer: Optional[str] = None) -> Optional[Dict[str, Any]]: Verifies an ID token and returns its claims if valid.
    • async introspect_token(token: str, client_id: Optional[str] = None, client_secret: Optional[str] = None) -> Optional[Dict[str, Any]]: Introspects an access token and returns its metadata if active. Can optionally fetch userinfo if the introspection response contains a subject and a userinfo endpoint is configured.

This service is typically registered with the DI container using ContainerBuilder.with_authentication() and can be injected into parts of your application that require authenticated user context (e.g., API endpoints, specific command/query handlers).

3. Authorization

Once a user or process is authenticated, authorization determines what actions they are permitted to perform or what data they are allowed to access.

castlecraft-engineer provides a dedicated authorization framework that integrates with Command and Query Handlers. This includes defining permissions, an AuthorizationService to check these permissions, and mechanisms to associate required permissions with handlers.

For detailed information on how to implement and use the authorization features, please refer to the Authorization Concepts documentation.


By understanding and utilizing these security features, you can build more robust and secure applications with castlecraft-engineer. Always follow security best practices, especially regarding key management and input validation.