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 withencrypt_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:
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 theSECRET_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.
- Verifying OIDC ID Tokens: It uses the
- 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.