atp-protocol 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
atp/encryption.py ADDED
@@ -0,0 +1,155 @@
1
+ """
2
+ Encryption utilities for ATP Protocol.
3
+
4
+ This module provides encryption/decryption functionality to protect agent
5
+ responses until payment is verified.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import os
12
+ from typing import Any, Dict, Optional
13
+
14
+ from cryptography.fernet import Fernet
15
+ from loguru import logger
16
+
17
+
18
+ class ResponseEncryptor:
19
+ """
20
+ Encrypts and decrypts agent responses using Fernet symmetric encryption.
21
+
22
+ The encryption key is derived from a secret that should be kept secure.
23
+ In production, this should be set via environment variable.
24
+ """
25
+
26
+ def __init__(self, encryption_key: Optional[str] = None):
27
+ """
28
+ Initialize the encryptor.
29
+
30
+ Args:
31
+ encryption_key: Base64-encoded Fernet key. If not provided, generates
32
+ a new key or uses ATP_ENCRYPTION_KEY from environment.
33
+ """
34
+ if encryption_key:
35
+ self.key = encryption_key.encode()
36
+ else:
37
+ # Try to get from environment, or generate a new one
38
+ env_key = os.getenv("ATP_ENCRYPTION_KEY")
39
+ if env_key:
40
+ self.key = env_key.encode()
41
+ else:
42
+ # Generate a new key (for development/testing)
43
+ # In production, this should be set via environment variable
44
+ logger.warning(
45
+ "No ATP_ENCRYPTION_KEY found in environment. "
46
+ "Generating a new key. This key will not persist across restarts."
47
+ )
48
+ self.key = Fernet.generate_key()
49
+
50
+ try:
51
+ self.fernet = Fernet(self.key)
52
+ except Exception as e:
53
+ raise ValueError(
54
+ f"Invalid encryption key format: {e}. "
55
+ "Key must be a valid base64-encoded Fernet key."
56
+ )
57
+
58
+ def encrypt(self, data: str) -> str:
59
+ """
60
+ Encrypt a string.
61
+
62
+ Args:
63
+ data: String to encrypt.
64
+
65
+ Returns:
66
+ Base64-encoded encrypted string.
67
+ """
68
+ encrypted = self.fernet.encrypt(data.encode())
69
+ return base64.b64encode(encrypted).decode()
70
+
71
+ def decrypt(self, encrypted_data: str) -> str:
72
+ """
73
+ Decrypt a string.
74
+
75
+ Args:
76
+ encrypted_data: Base64-encoded encrypted string.
77
+
78
+ Returns:
79
+ Decrypted string.
80
+ """
81
+ try:
82
+ encrypted_bytes = base64.b64decode(encrypted_data.encode())
83
+ decrypted = self.fernet.decrypt(encrypted_bytes)
84
+ return decrypted.decode()
85
+ except Exception as e:
86
+ logger.error(f"Decryption failed: {e}")
87
+ raise ValueError(f"Failed to decrypt data: {e}")
88
+
89
+ def encrypt_response_data(
90
+ self, response_data: Dict[str, Any], fields_to_encrypt: list[str] = None
91
+ ) -> Dict[str, Any]:
92
+ """
93
+ Encrypt specific fields in a response dictionary.
94
+
95
+ Args:
96
+ response_data: Response dictionary containing agent output.
97
+ fields_to_encrypt: List of field names to encrypt. Defaults to
98
+ common output fields: ["output", "response", "result", "message"].
99
+
100
+ Returns:
101
+ Response dictionary with specified fields encrypted.
102
+ """
103
+ if fields_to_encrypt is None:
104
+ fields_to_encrypt = ["output", "response", "result", "message"]
105
+
106
+ encrypted_data = response_data.copy()
107
+
108
+ for field in fields_to_encrypt:
109
+ if field in encrypted_data and isinstance(
110
+ encrypted_data[field], str
111
+ ):
112
+ encrypted_data[field] = self.encrypt(encrypted_data[field])
113
+ # Mark as encrypted
114
+ encrypted_data[f"{field}_encrypted"] = True
115
+
116
+ return encrypted_data
117
+
118
+ def decrypt_response_data(
119
+ self, response_data: Dict[str, Any], fields_to_decrypt: list[str] = None
120
+ ) -> Dict[str, Any]:
121
+ """
122
+ Decrypt specific fields in a response dictionary.
123
+
124
+ Args:
125
+ response_data: Response dictionary with encrypted fields.
126
+ fields_to_decrypt: List of field names to decrypt. Defaults to
127
+ common output fields: ["output", "response", "result", "message"].
128
+
129
+ Returns:
130
+ Response dictionary with specified fields decrypted.
131
+ """
132
+ if fields_to_decrypt is None:
133
+ fields_to_decrypt = ["output", "response", "result", "message"]
134
+
135
+ decrypted_data = response_data.copy()
136
+
137
+ for field in fields_to_decrypt:
138
+ if (
139
+ field in decrypted_data
140
+ and isinstance(decrypted_data[field], str)
141
+ and decrypted_data.get(f"{field}_encrypted", False)
142
+ ):
143
+ try:
144
+ decrypted_data[field] = self.decrypt(decrypted_data[field])
145
+ # Remove encryption marker
146
+ decrypted_data.pop(f"{field}_encrypted", None)
147
+ except Exception as e:
148
+ logger.error(
149
+ f"Failed to decrypt field '{field}': {e}"
150
+ )
151
+ # Keep encrypted if decryption fails
152
+ pass
153
+
154
+ return decrypted_data
155
+