ai-agent/security/encrypt.py

193 lines
No EOL
6.8 KiB
Python

import ssl
def create_tls_context(purpose=ssl.Purpose.CLIENT_AUTH):
"""
Creates an SSL context configured for TLS 1.3.
Args:
purpose: The SSL purpose (e.g., CLIENT_AUTH, SERVER_AUTH).
Returns:
An SSLContext object configured for TLS 1.3.
"""
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT if purpose == ssl.Purpose.CLIENT_AUTH else ssl.PROTOCOL_TLS_SERVER)
# Require TLS 1.3
context.minimum_version = ssl.TLSVersion.TLSv1_3
# TLS 1.3 cipher suites are handled automatically by the underlying SSL library
# when minimum_version is set to TLSv1_3. Explicitly setting them via
# set_ciphers can cause issues. The required suites (AES-256-GCM, CHACHA20)
# are typically included and preferred by default in modern OpenSSL.
# context.set_ciphers('...') # Removed
# Configure certificate loading and verification
if purpose == ssl.Purpose.SERVER_AUTH:
# Server context: Load server cert/key and require client certs for RBAC
# context.load_cert_chain(certfile="path/to/server_cert.pem", keyfile="path/to/server_key.pem") # Placeholder: Needs actual paths
context.verify_mode = ssl.CERT_REQUIRED
# context.load_verify_locations(cafile="path/to/trusted_client_ca.pem") # Placeholder: Needs actual CA path for client cert validation
elif purpose == ssl.Purpose.CLIENT_AUTH:
# Client context: Load client cert/key and verify server cert against CA
# context.load_cert_chain(certfile="path/to/client_cert.pem", keyfile="path/to/client_key.pem") # Placeholder: Needs actual paths
# context.load_verify_locations(cafile="path/to/trusted_server_ca.pem") # Placeholder: Needs actual CA path
context.verify_mode = ssl.CERT_REQUIRED # Verify the server certificate
# minimum_version = TLSv1_3 implicitly disables older protocols.
# Explicit OP_NO flags are redundant but harmless; removed for clarity.
# context.options |= ssl.OP_NO_SSLv2 # Redundant
# context.options |= ssl.OP_NO_SSLv3 # Redundant
# context.options |= ssl.OP_NO_TLSv1 # Redundant
# context.options |= ssl.OP_NO_TLSv1_1 # Redundant
# context.options |= ssl.OP_NO_TLSv1_2 # Redundant
# Options for perfect forward secrecy are generally enabled by default with TLS 1.3 ciphers
# context.options |= ssl.OP_SINGLE_DH_USE
# context.options |= ssl.OP_SINGLE_ECDH_USE
# Ensure TLS 1.3 is the minimum (already set, but good to be explicit)
context.minimum_version = ssl.TLSVersion.TLSv1_3
return context
# Example usage (can be removed or kept for demonstration)
if __name__ == '__main__':
client_context = create_tls_context(ssl.Purpose.CLIENT_AUTH)
print(f"Client Context Minimum TLS Version: {client_context.minimum_version}")
# print(f"Client Context Ciphers: {client_context.get_ciphers()}") # Requires OpenSSL 1.1.1+
server_context = create_tls_context(ssl.Purpose.SERVER_AUTH)
print(f"Server Context Minimum TLS Version: {server_context.minimum_version}")
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
class AES256Cipher:
"""AES-256-GCM encryption/decryption wrapper class."""
def __init__(self, key: bytes = None):
"""
Initialize cipher with optional key.
Args:
key: Optional 32-byte AES-256 key. If None, generates new key.
"""
self.key = key if key is not None else self.generate_key()
@staticmethod
def generate_key() -> bytes:
"""Generate a secure 256-bit AES key for encryption/decryption.
Returns:
bytes: 32-byte AES-256 key
"""
return os.urandom(32)
def encrypt(self, plaintext: bytes) -> bytes:
"""Encrypt data using AES-256-GCM.
Args:
plaintext: Data to encrypt
Returns:
bytes: Encrypted data in format (nonce + ciphertext + tag)
"""
return encrypt_data(plaintext, self.key)
def decrypt(self, encrypted_data: bytes) -> bytes:
"""Decrypt data using AES-256-GCM.
Args:
encrypted_data: Data in format (nonce + ciphertext + tag)
Returns:
bytes: Decrypted plaintext
"""
return decrypt_data(encrypted_data, self.key)
def generate_key():
"""Generate a secure 256-bit AES key for encryption/decryption.
Returns:
bytes: 32-byte AES-256 key
"""
return os.urandom(32)
def encrypt_data(plaintext: bytes, key: bytes) -> bytes:
"""Encrypt data using AES-256-GCM.
Args:
plaintext: Data to encrypt
key: 32-byte AES-256 key
Returns:
bytes: Encrypted data in format (nonce + ciphertext + tag)
Raises:
ValueError: If key length is invalid
"""
if len(key) != 32:
raise ValueError("Key must be 32 bytes for AES-256")
# Generate random 96-bit nonce
nonce = os.urandom(12)
# Create cipher and encrypt
cipher = Cipher(
algorithms.AES(key),
modes.GCM(nonce),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# Return nonce + ciphertext + tag
return nonce + ciphertext + encryptor.tag
def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes:
"""Decrypt data using AES-256-GCM.
Args:
encrypted_data: Data in format (nonce + ciphertext + tag)
key: 32-byte AES-256 key
Returns:
bytes: Decrypted plaintext
Raises:
ValueError: If key length is invalid or data is malformed
"""
if len(key) != 32:
raise ValueError("Key must be 32 bytes for AES-256")
if len(encrypted_data) < 28: # Minimum: 12 nonce + 16 tag
raise ValueError("Encrypted data too short")
# Split into components
nonce = encrypted_data[:12]
ciphertext = encrypted_data[12:-16]
tag = encrypted_data[-16:]
# Create cipher and decrypt
cipher = Cipher(
algorithms.AES(key),
modes.GCM(nonce, tag),
backend=default_backend()
)
decryptor = cipher.decryptor()
return decryptor.update(ciphertext) + decryptor.finalize()
# Example usage for AES-256-GCM functions
if __name__ == '__main__':
# Generate key
key = generate_key()
print(f"Generated AES-256 key: {key.hex()}")
# Encrypt test data
plaintext = b"Test message for AES-256-GCM implementation"
encrypted = encrypt_data(plaintext, key)
print(f"Encrypted data (hex): {encrypted.hex()}")
# Decrypt test data
decrypted = decrypt_data(encrypted, key)
print(f"Decrypted data: {decrypted.decode()}")
# print(f"Server Context Ciphers: {server_context.get_ciphers()}") # Requires OpenSSL 1.1.1+