fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.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.
- confiture/__init__.py +48 -0
- confiture/_core.cp311-win_amd64.pyd +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1656 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +132 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +793 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +38 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""KMS (Key Management Service) integration for encryption key management.
|
|
2
|
+
|
|
3
|
+
Provides multi-cloud support for key management:
|
|
4
|
+
- AWS KMS
|
|
5
|
+
- HashiCorp Vault
|
|
6
|
+
- Azure Key Vault
|
|
7
|
+
|
|
8
|
+
Enables secure key storage, rotation, and lifecycle management.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class KMSProvider(Enum):
|
|
22
|
+
"""Supported KMS providers."""
|
|
23
|
+
|
|
24
|
+
AWS = "aws"
|
|
25
|
+
VAULT = "vault"
|
|
26
|
+
AZURE = "azure"
|
|
27
|
+
LOCAL = "local" # For testing only
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class KeyMetadata:
|
|
32
|
+
"""Metadata for an encryption key."""
|
|
33
|
+
|
|
34
|
+
key_id: str
|
|
35
|
+
provider: KMSProvider
|
|
36
|
+
algorithm: str
|
|
37
|
+
created_at: datetime
|
|
38
|
+
rotated_at: datetime | None = None
|
|
39
|
+
expires_at: datetime | None = None
|
|
40
|
+
version: int = 1
|
|
41
|
+
is_active: bool = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class KMSClient(ABC):
|
|
45
|
+
"""Abstract base class for KMS clients."""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
|
|
49
|
+
"""Encrypt plaintext using the specified key."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
|
|
54
|
+
"""Decrypt ciphertext. Key ID can be embedded in ciphertext."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def rotate_key(self, key_id: str) -> str:
|
|
59
|
+
"""Rotate a key and return the new key version."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_key_metadata(self, key_id: str) -> KeyMetadata:
|
|
64
|
+
"""Get metadata for a key."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AWSKMSClient(KMSClient):
|
|
69
|
+
"""AWS KMS client implementation."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, region: str = "us-east-1"):
|
|
72
|
+
"""Initialize AWS KMS client.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
region: AWS region (e.g., 'us-east-1')
|
|
76
|
+
"""
|
|
77
|
+
self.region = region
|
|
78
|
+
self.provider = KMSProvider.AWS
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
import boto3 # type: ignore[import-untyped]
|
|
82
|
+
|
|
83
|
+
self.client = boto3.client("kms", region_name=region)
|
|
84
|
+
except ImportError as e:
|
|
85
|
+
raise ImportError("boto3 is required for AWS KMS support") from e
|
|
86
|
+
|
|
87
|
+
def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
|
|
88
|
+
"""Encrypt plaintext using AWS KMS.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
plaintext: Data to encrypt
|
|
92
|
+
key_id: AWS KMS key ID or ARN
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Encrypted data (ciphertext)
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
Exception: If encryption fails
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
response = self.client.encrypt(
|
|
102
|
+
KeyId=key_id,
|
|
103
|
+
Plaintext=plaintext,
|
|
104
|
+
)
|
|
105
|
+
return response["CiphertextBlob"]
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"AWS KMS encryption failed: {e}")
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes: # noqa: ARG002
|
|
111
|
+
"""Decrypt ciphertext using AWS KMS.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
ciphertext: Encrypted data
|
|
115
|
+
_key_id: Not used (embedded in ciphertext by AWS)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Decrypted plaintext
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
Exception: If decryption fails
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
response = self.client.decrypt(CiphertextBlob=ciphertext)
|
|
125
|
+
return response["Plaintext"]
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"AWS KMS decryption failed: {e}")
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
def rotate_key(self, key_id: str) -> str:
|
|
131
|
+
"""Rotate an AWS KMS key.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
key_id: AWS KMS key ID
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
New key version
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
Exception: If rotation fails
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
# AWS KMS automatic key rotation (enable once)
|
|
144
|
+
response = self.client.describe_key(KeyId=key_id)
|
|
145
|
+
key_metadata = response["KeyMetadata"]
|
|
146
|
+
|
|
147
|
+
logger.info(f"Key rotation enabled for {key_id}")
|
|
148
|
+
# AWS handles rotation automatically
|
|
149
|
+
return f"{key_id}:v{key_metadata['KeyUsage']}"
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"AWS KMS key rotation failed: {e}")
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class VaultKMSClient(KMSClient):
|
|
156
|
+
"""HashiCorp Vault KMS client implementation."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, vault_url: str, token: str, engine: str = "transit"):
|
|
159
|
+
"""Initialize Vault KMS client.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
vault_url: Vault server URL (e.g., 'http://localhost:8200')
|
|
163
|
+
token: Vault authentication token
|
|
164
|
+
engine: Transit engine path (default: 'transit')
|
|
165
|
+
"""
|
|
166
|
+
self.vault_url = vault_url
|
|
167
|
+
self.token = token
|
|
168
|
+
self.engine = engine
|
|
169
|
+
self.provider = KMSProvider.VAULT
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
import hvac # type: ignore[import-untyped]
|
|
173
|
+
|
|
174
|
+
self.client = hvac.Client(url=vault_url, token=token)
|
|
175
|
+
except ImportError as e:
|
|
176
|
+
raise ImportError("hvac is required for HashiCorp Vault support") from e
|
|
177
|
+
|
|
178
|
+
def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
|
|
179
|
+
"""Encrypt plaintext using Vault.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
plaintext: Data to encrypt
|
|
183
|
+
key_id: Key name in Vault
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Encrypted data (ciphertext)
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
Exception: If encryption fails
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
# Vault expects base64 plaintext
|
|
193
|
+
import base64
|
|
194
|
+
|
|
195
|
+
plaintext_b64 = base64.b64encode(plaintext).decode("utf-8")
|
|
196
|
+
|
|
197
|
+
response = self.client.secrets.transit.encrypt_data(
|
|
198
|
+
name=key_id,
|
|
199
|
+
plaintext=plaintext_b64,
|
|
200
|
+
mount_point=self.engine,
|
|
201
|
+
)
|
|
202
|
+
return response["data"]["ciphertext"].encode()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"Vault encryption failed: {e}")
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
|
|
208
|
+
"""Decrypt ciphertext using Vault.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
ciphertext: Encrypted data
|
|
212
|
+
key_id: Key name in Vault (must be provided)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Decrypted plaintext
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If key_id not provided
|
|
219
|
+
Exception: If decryption fails
|
|
220
|
+
"""
|
|
221
|
+
if not key_id:
|
|
222
|
+
raise ValueError("key_id must be provided for Vault decryption")
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
import base64
|
|
226
|
+
|
|
227
|
+
ciphertext_str = ciphertext.decode() if isinstance(ciphertext, bytes) else ciphertext
|
|
228
|
+
|
|
229
|
+
response = self.client.secrets.transit.decrypt_data(
|
|
230
|
+
name=key_id,
|
|
231
|
+
ciphertext=ciphertext_str,
|
|
232
|
+
mount_point=self.engine,
|
|
233
|
+
)
|
|
234
|
+
plaintext_b64 = response["data"]["plaintext"]
|
|
235
|
+
return base64.b64decode(plaintext_b64)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"Vault decryption failed: {e}")
|
|
238
|
+
raise
|
|
239
|
+
|
|
240
|
+
def rotate_key(self, key_id: str) -> str:
|
|
241
|
+
"""Rotate a Vault transit key.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
key_id: Key name in Vault
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
New key version
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
Exception: If rotation fails
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
self.client.secrets.transit.rotate_key(
|
|
254
|
+
name=key_id,
|
|
255
|
+
mount_point=self.engine,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Get updated key metadata
|
|
259
|
+
metadata = self.client.secrets.transit.read_key(
|
|
260
|
+
name=key_id,
|
|
261
|
+
mount_point=self.engine,
|
|
262
|
+
)
|
|
263
|
+
new_version = metadata["data"]["latest_version"]
|
|
264
|
+
|
|
265
|
+
logger.info(f"Key {key_id} rotated to version {new_version}")
|
|
266
|
+
return f"{key_id}:v{new_version}"
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Vault key rotation failed: {e}")
|
|
269
|
+
raise
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class AzureKMSClient(KMSClient):
|
|
273
|
+
"""Azure Key Vault KMS client implementation."""
|
|
274
|
+
|
|
275
|
+
def __init__(self, vault_url: str, credential: Any):
|
|
276
|
+
"""Initialize Azure Key Vault client.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
vault_url: Azure Key Vault URL
|
|
280
|
+
credential: Azure credential object (e.g., DefaultAzureCredential)
|
|
281
|
+
"""
|
|
282
|
+
self.vault_url = vault_url
|
|
283
|
+
self.credential = credential
|
|
284
|
+
self.provider = KMSProvider.AZURE
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
from azure.keyvault.keys.crypto import ( # type: ignore[import-untyped]
|
|
288
|
+
CryptographyClient,
|
|
289
|
+
EncryptionAlgorithm,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
self.CryptographyClient = CryptographyClient
|
|
293
|
+
self.EncryptionAlgorithm = EncryptionAlgorithm
|
|
294
|
+
except ImportError as e:
|
|
295
|
+
raise ImportError(
|
|
296
|
+
"azure-identity and azure-keyvault-keys are required for Azure support"
|
|
297
|
+
) from e
|
|
298
|
+
|
|
299
|
+
def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
|
|
300
|
+
"""Encrypt plaintext using Azure Key Vault.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
plaintext: Data to encrypt
|
|
304
|
+
key_id: Key name in Key Vault
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Encrypted data (ciphertext)
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
Exception: If encryption fails
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
key_url = f"{self.vault_url}/keys/{key_id}/latest"
|
|
314
|
+
crypto_client = self.CryptographyClient(key_url, credential=self.credential)
|
|
315
|
+
|
|
316
|
+
result = crypto_client.encrypt(
|
|
317
|
+
self.EncryptionAlgorithm.rsa_oaep,
|
|
318
|
+
plaintext,
|
|
319
|
+
)
|
|
320
|
+
return result.ciphertext
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Azure Key Vault encryption failed: {e}")
|
|
323
|
+
raise
|
|
324
|
+
|
|
325
|
+
def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
|
|
326
|
+
"""Decrypt ciphertext using Azure Key Vault.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
ciphertext: Encrypted data
|
|
330
|
+
key_id: Key name in Key Vault (must be provided)
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Decrypted plaintext
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
ValueError: If key_id not provided
|
|
337
|
+
Exception: If decryption fails
|
|
338
|
+
"""
|
|
339
|
+
if not key_id:
|
|
340
|
+
raise ValueError("key_id must be provided for Azure Key Vault decryption")
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
key_url = f"{self.vault_url}/keys/{key_id}/latest"
|
|
344
|
+
crypto_client = self.CryptographyClient(key_url, credential=self.credential)
|
|
345
|
+
|
|
346
|
+
result = crypto_client.decrypt(
|
|
347
|
+
self.EncryptionAlgorithm.rsa_oaep,
|
|
348
|
+
ciphertext,
|
|
349
|
+
)
|
|
350
|
+
return result.plaintext
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"Azure Key Vault decryption failed: {e}")
|
|
353
|
+
raise
|
|
354
|
+
|
|
355
|
+
def rotate_key(self, key_id: str) -> str:
|
|
356
|
+
"""Rotate an Azure Key Vault key version.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
key_id: Key name in Key Vault
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
New key version
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
Exception: If rotation fails
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
from azure.keyvault.keys import KeyClient # type: ignore[import-untyped]
|
|
369
|
+
|
|
370
|
+
key_client = KeyClient(vault_url=self.vault_url, credential=self.credential)
|
|
371
|
+
|
|
372
|
+
# Get current key
|
|
373
|
+
key = key_client.get_key(key_id)
|
|
374
|
+
current_version = key.properties.version
|
|
375
|
+
|
|
376
|
+
logger.info(f"Key {key_id} current version: {current_version}")
|
|
377
|
+
# Note: Azure doesn't have automatic rotation like Vault
|
|
378
|
+
# Manual rotation requires re-keying data
|
|
379
|
+
return f"{key_id}:{current_version}"
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Azure Key Vault key info failed: {e}")
|
|
382
|
+
raise
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class LocalKMSClient(KMSClient):
|
|
386
|
+
"""Local KMS client for testing purposes.
|
|
387
|
+
|
|
388
|
+
WARNING: Only for testing. Never use in production.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(self, keys_dir: str | None = None):
|
|
392
|
+
"""Initialize local KMS client.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
keys_dir: Directory to store test keys (temporary)
|
|
396
|
+
"""
|
|
397
|
+
import os
|
|
398
|
+
|
|
399
|
+
self.provider = KMSProvider.LOCAL
|
|
400
|
+
self.keys = {} # In-memory key storage
|
|
401
|
+
self.keys_dir = keys_dir or "/tmp/confiture_test_keys"
|
|
402
|
+
|
|
403
|
+
if not os.path.exists(self.keys_dir):
|
|
404
|
+
os.makedirs(self.keys_dir)
|
|
405
|
+
|
|
406
|
+
logger.warning("⚠️ LocalKMSClient is for TESTING ONLY. Never use in production.")
|
|
407
|
+
|
|
408
|
+
def encrypt(self, plaintext: bytes, key_id: str) -> bytes:
|
|
409
|
+
"""Simple XOR encryption for testing.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
plaintext: Data to encrypt
|
|
413
|
+
key_id: Key identifier
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Encrypted data
|
|
417
|
+
"""
|
|
418
|
+
from cryptography.fernet import Fernet
|
|
419
|
+
|
|
420
|
+
if key_id not in self.keys:
|
|
421
|
+
# Generate key if not exists
|
|
422
|
+
self.keys[key_id] = Fernet.generate_key()
|
|
423
|
+
|
|
424
|
+
f = Fernet(self.keys[key_id])
|
|
425
|
+
return f.encrypt(plaintext)
|
|
426
|
+
|
|
427
|
+
def decrypt(self, ciphertext: bytes, key_id: str | None = None) -> bytes:
|
|
428
|
+
"""Decrypt using Fernet.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
ciphertext: Encrypted data
|
|
432
|
+
key_id: Key identifier (optional, can be embedded)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Decrypted plaintext
|
|
436
|
+
"""
|
|
437
|
+
from cryptography.fernet import Fernet
|
|
438
|
+
|
|
439
|
+
if not key_id:
|
|
440
|
+
# Try all keys
|
|
441
|
+
for _key_id, key in self.keys.items():
|
|
442
|
+
try:
|
|
443
|
+
f = Fernet(key)
|
|
444
|
+
return f.decrypt(ciphertext)
|
|
445
|
+
except Exception:
|
|
446
|
+
continue
|
|
447
|
+
raise ValueError("Could not decrypt with any available key")
|
|
448
|
+
|
|
449
|
+
if key_id not in self.keys:
|
|
450
|
+
raise ValueError(f"Key {key_id} not found")
|
|
451
|
+
|
|
452
|
+
f = Fernet(self.keys[key_id])
|
|
453
|
+
return f.decrypt(ciphertext)
|
|
454
|
+
|
|
455
|
+
def rotate_key(self, key_id: str) -> str:
|
|
456
|
+
"""Rotate a test key.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
key_id: Key identifier
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
New version identifier
|
|
463
|
+
"""
|
|
464
|
+
from cryptography.fernet import Fernet
|
|
465
|
+
|
|
466
|
+
self.keys[key_id] = Fernet.generate_key()
|
|
467
|
+
version = len([k for k in self.keys if k.startswith(key_id)])
|
|
468
|
+
return f"{key_id}:v{version}"
|
|
469
|
+
|
|
470
|
+
def get_key_metadata(self, key_id: str) -> KeyMetadata:
|
|
471
|
+
"""Get metadata for a test key."""
|
|
472
|
+
return KeyMetadata(
|
|
473
|
+
key_id=key_id,
|
|
474
|
+
provider=KMSProvider.LOCAL,
|
|
475
|
+
algorithm="Fernet",
|
|
476
|
+
created_at=datetime.now(UTC),
|
|
477
|
+
version=1,
|
|
478
|
+
is_active=True,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class KMSFactory:
|
|
483
|
+
"""Factory for creating KMS clients."""
|
|
484
|
+
|
|
485
|
+
_clients: dict[str, KMSClient] = {}
|
|
486
|
+
|
|
487
|
+
@staticmethod
|
|
488
|
+
def create(
|
|
489
|
+
provider: KMSProvider,
|
|
490
|
+
**config: Any,
|
|
491
|
+
) -> KMSClient:
|
|
492
|
+
"""Create a KMS client.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
provider: KMS provider type
|
|
496
|
+
**config: Provider-specific configuration
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Configured KMS client
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
ValueError: If provider not supported
|
|
503
|
+
"""
|
|
504
|
+
if provider == KMSProvider.AWS:
|
|
505
|
+
return AWSKMSClient(
|
|
506
|
+
region=config.get("region", "us-east-1"),
|
|
507
|
+
)
|
|
508
|
+
elif provider == KMSProvider.VAULT:
|
|
509
|
+
return VaultKMSClient(
|
|
510
|
+
vault_url=config.get("vault_url"),
|
|
511
|
+
token=config.get("token"),
|
|
512
|
+
engine=config.get("engine", "transit"),
|
|
513
|
+
)
|
|
514
|
+
elif provider == KMSProvider.AZURE:
|
|
515
|
+
return AzureKMSClient(
|
|
516
|
+
vault_url=config.get("vault_url"),
|
|
517
|
+
credential=config.get("credential"),
|
|
518
|
+
)
|
|
519
|
+
elif provider == KMSProvider.LOCAL:
|
|
520
|
+
return LocalKMSClient(
|
|
521
|
+
keys_dir=config.get("keys_dir"),
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
raise ValueError(f"Unsupported KMS provider: {provider}")
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def get_or_create(
|
|
528
|
+
provider: KMSProvider,
|
|
529
|
+
key: str | None = None,
|
|
530
|
+
**config: Any,
|
|
531
|
+
) -> KMSClient:
|
|
532
|
+
"""Get or create a cached KMS client.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
provider: KMS provider type
|
|
536
|
+
key: Cache key (optional)
|
|
537
|
+
**config: Provider-specific configuration
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Configured KMS client (cached if available)
|
|
541
|
+
"""
|
|
542
|
+
cache_key = key or str(provider)
|
|
543
|
+
|
|
544
|
+
if cache_key not in KMSFactory._clients:
|
|
545
|
+
KMSFactory._clients[cache_key] = KMSFactory.create(provider, **config)
|
|
546
|
+
|
|
547
|
+
return KMSFactory._clients[cache_key]
|