truthound 1.0.8__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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
"""Key management module for encryption.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive key management including:
|
|
4
|
+
- Key derivation functions (KDFs) for password-based encryption
|
|
5
|
+
- Key rotation and versioning
|
|
6
|
+
- In-memory and file-based key storage
|
|
7
|
+
- Envelope encryption for secure key wrapping
|
|
8
|
+
- Integration with external key management systems (AWS KMS, HashiCorp Vault)
|
|
9
|
+
|
|
10
|
+
Security Best Practices:
|
|
11
|
+
- Never hardcode keys in source code
|
|
12
|
+
- Use strong KDFs for password-derived keys
|
|
13
|
+
- Rotate keys regularly
|
|
14
|
+
- Use envelope encryption for key wrapping
|
|
15
|
+
- Clear key material from memory when done
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from truthound.stores.encryption.keys import (
|
|
19
|
+
... KeyManager,
|
|
20
|
+
... derive_key,
|
|
21
|
+
... KeyDerivation,
|
|
22
|
+
... )
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Derive key from password
|
|
25
|
+
>>> key = derive_key("my_password", kdf=KeyDerivation.ARGON2ID)
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Use key manager for key lifecycle
|
|
28
|
+
>>> manager = KeyManager()
|
|
29
|
+
>>> key_obj = manager.create_key(algorithm=EncryptionAlgorithm.AES_256_GCM)
|
|
30
|
+
>>> manager.rotate_key(key_obj.key_id)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import base64
|
|
36
|
+
import hashlib
|
|
37
|
+
import hmac
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import time
|
|
41
|
+
from abc import ABC, abstractmethod
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from datetime import datetime, timedelta, timezone
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from threading import RLock
|
|
46
|
+
from typing import Any, Callable, Iterator
|
|
47
|
+
|
|
48
|
+
from truthound.stores.encryption.base import (
|
|
49
|
+
EncryptionAlgorithm,
|
|
50
|
+
EncryptionKey,
|
|
51
|
+
KeyDerivation,
|
|
52
|
+
KeyDerivationConfig,
|
|
53
|
+
KeyDerivationError,
|
|
54
|
+
KeyError_,
|
|
55
|
+
KeyExpiredError,
|
|
56
|
+
KeyType,
|
|
57
|
+
generate_key,
|
|
58
|
+
generate_key_id,
|
|
59
|
+
generate_salt,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Key Derivation Functions
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BaseKeyDeriver(ABC):
|
|
69
|
+
"""Base class for key derivation implementations."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, kdf: KeyDerivation) -> None:
|
|
72
|
+
self._kdf = kdf
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def kdf(self) -> KeyDerivation:
|
|
76
|
+
"""Get the KDF type."""
|
|
77
|
+
return self._kdf
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def derive(
|
|
81
|
+
self,
|
|
82
|
+
password: str | bytes,
|
|
83
|
+
salt: bytes,
|
|
84
|
+
key_size: int,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> bytes:
|
|
87
|
+
"""Derive a key from password.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
password: Password or passphrase.
|
|
91
|
+
salt: Cryptographic salt.
|
|
92
|
+
key_size: Desired key size in bytes.
|
|
93
|
+
**kwargs: KDF-specific parameters.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Derived key bytes.
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Argon2KeyDeriver(BaseKeyDeriver):
|
|
102
|
+
"""Argon2 key derivation (recommended for new applications).
|
|
103
|
+
|
|
104
|
+
Argon2 is the winner of the Password Hashing Competition and
|
|
105
|
+
provides excellent resistance against GPU and ASIC attacks.
|
|
106
|
+
|
|
107
|
+
Variants:
|
|
108
|
+
- Argon2id: Hybrid mode (recommended)
|
|
109
|
+
- Argon2i: Data-independent (side-channel resistant)
|
|
110
|
+
- Argon2d: Data-dependent (higher resistance to GPU attacks)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
variant: KeyDerivation = KeyDerivation.ARGON2ID,
|
|
116
|
+
time_cost: int = 3,
|
|
117
|
+
memory_cost: int = 65536,
|
|
118
|
+
parallelism: int = 4,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Initialize Argon2 key deriver.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
variant: Argon2 variant (id, i, or d).
|
|
124
|
+
time_cost: Number of iterations.
|
|
125
|
+
memory_cost: Memory usage in KiB.
|
|
126
|
+
parallelism: Degree of parallelism.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__(variant)
|
|
129
|
+
self.time_cost = time_cost
|
|
130
|
+
self.memory_cost = memory_cost
|
|
131
|
+
self.parallelism = parallelism
|
|
132
|
+
|
|
133
|
+
def derive(
|
|
134
|
+
self,
|
|
135
|
+
password: str | bytes,
|
|
136
|
+
salt: bytes,
|
|
137
|
+
key_size: int,
|
|
138
|
+
**kwargs: Any,
|
|
139
|
+
) -> bytes:
|
|
140
|
+
"""Derive key using Argon2."""
|
|
141
|
+
try:
|
|
142
|
+
from argon2.low_level import Type, hash_secret_raw
|
|
143
|
+
except ImportError as e:
|
|
144
|
+
raise KeyDerivationError(
|
|
145
|
+
"Argon2 requires 'argon2-cffi' package: pip install argon2-cffi",
|
|
146
|
+
self._kdf.value,
|
|
147
|
+
) from e
|
|
148
|
+
|
|
149
|
+
if isinstance(password, str):
|
|
150
|
+
password = password.encode("utf-8")
|
|
151
|
+
|
|
152
|
+
# Map variant to type
|
|
153
|
+
type_map = {
|
|
154
|
+
KeyDerivation.ARGON2ID: Type.ID,
|
|
155
|
+
KeyDerivation.ARGON2I: Type.I,
|
|
156
|
+
KeyDerivation.ARGON2D: Type.D,
|
|
157
|
+
}
|
|
158
|
+
argon_type = type_map.get(self._kdf, Type.ID)
|
|
159
|
+
|
|
160
|
+
time_cost = kwargs.get("time_cost", self.time_cost)
|
|
161
|
+
memory_cost = kwargs.get("memory_cost", self.memory_cost)
|
|
162
|
+
parallelism = kwargs.get("parallelism", self.parallelism)
|
|
163
|
+
|
|
164
|
+
return hash_secret_raw(
|
|
165
|
+
secret=password,
|
|
166
|
+
salt=salt,
|
|
167
|
+
time_cost=time_cost,
|
|
168
|
+
memory_cost=memory_cost,
|
|
169
|
+
parallelism=parallelism,
|
|
170
|
+
hash_len=key_size,
|
|
171
|
+
type=argon_type,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PBKDF2KeyDeriver(BaseKeyDeriver):
|
|
176
|
+
"""PBKDF2 key derivation (widely compatible).
|
|
177
|
+
|
|
178
|
+
PBKDF2 is a widely supported KDF that is suitable for most
|
|
179
|
+
applications requiring password-based encryption.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
hash_name: str = "sha256",
|
|
185
|
+
iterations: int = 600_000,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Initialize PBKDF2 key deriver.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
hash_name: Hash function (sha256, sha512).
|
|
191
|
+
iterations: Number of iterations (higher = slower + more secure).
|
|
192
|
+
"""
|
|
193
|
+
kdf = (
|
|
194
|
+
KeyDerivation.PBKDF2_SHA512
|
|
195
|
+
if hash_name == "sha512"
|
|
196
|
+
else KeyDerivation.PBKDF2_SHA256
|
|
197
|
+
)
|
|
198
|
+
super().__init__(kdf)
|
|
199
|
+
self.hash_name = hash_name
|
|
200
|
+
self.iterations = iterations
|
|
201
|
+
|
|
202
|
+
def derive(
|
|
203
|
+
self,
|
|
204
|
+
password: str | bytes,
|
|
205
|
+
salt: bytes,
|
|
206
|
+
key_size: int,
|
|
207
|
+
**kwargs: Any,
|
|
208
|
+
) -> bytes:
|
|
209
|
+
"""Derive key using PBKDF2."""
|
|
210
|
+
if isinstance(password, str):
|
|
211
|
+
password = password.encode("utf-8")
|
|
212
|
+
|
|
213
|
+
iterations = kwargs.get("iterations", self.iterations)
|
|
214
|
+
|
|
215
|
+
return hashlib.pbkdf2_hmac(
|
|
216
|
+
self.hash_name,
|
|
217
|
+
password,
|
|
218
|
+
salt,
|
|
219
|
+
iterations,
|
|
220
|
+
dklen=key_size,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ScryptKeyDeriver(BaseKeyDeriver):
|
|
225
|
+
"""scrypt key derivation (memory-hard).
|
|
226
|
+
|
|
227
|
+
scrypt is designed to be memory-hard, making it expensive to
|
|
228
|
+
parallelize on GPUs or ASICs.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
n: int = 2**14,
|
|
234
|
+
r: int = 8,
|
|
235
|
+
p: int = 1,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Initialize scrypt key deriver.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
n: CPU/memory cost parameter (must be power of 2).
|
|
241
|
+
r: Block size parameter.
|
|
242
|
+
p: Parallelization parameter.
|
|
243
|
+
"""
|
|
244
|
+
super().__init__(KeyDerivation.SCRYPT)
|
|
245
|
+
self.n = n
|
|
246
|
+
self.r = r
|
|
247
|
+
self.p = p
|
|
248
|
+
|
|
249
|
+
def derive(
|
|
250
|
+
self,
|
|
251
|
+
password: str | bytes,
|
|
252
|
+
salt: bytes,
|
|
253
|
+
key_size: int,
|
|
254
|
+
**kwargs: Any,
|
|
255
|
+
) -> bytes:
|
|
256
|
+
"""Derive key using scrypt."""
|
|
257
|
+
if isinstance(password, str):
|
|
258
|
+
password = password.encode("utf-8")
|
|
259
|
+
|
|
260
|
+
n = kwargs.get("n", self.n)
|
|
261
|
+
r = kwargs.get("r", self.r)
|
|
262
|
+
p = kwargs.get("p", self.p)
|
|
263
|
+
|
|
264
|
+
return hashlib.scrypt(
|
|
265
|
+
password,
|
|
266
|
+
salt=salt,
|
|
267
|
+
n=n,
|
|
268
|
+
r=r,
|
|
269
|
+
p=p,
|
|
270
|
+
dklen=key_size,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class HKDFKeyDeriver(BaseKeyDeriver):
|
|
275
|
+
"""HKDF key derivation (for key expansion).
|
|
276
|
+
|
|
277
|
+
HKDF is used for key expansion and derivation from existing
|
|
278
|
+
key material (not for password-based derivation).
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def __init__(self, hash_name: str = "sha256") -> None:
|
|
282
|
+
"""Initialize HKDF key deriver.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
hash_name: Hash function (sha256, sha512).
|
|
286
|
+
"""
|
|
287
|
+
kdf = (
|
|
288
|
+
KeyDerivation.HKDF_SHA512
|
|
289
|
+
if hash_name == "sha512"
|
|
290
|
+
else KeyDerivation.HKDF_SHA256
|
|
291
|
+
)
|
|
292
|
+
super().__init__(kdf)
|
|
293
|
+
self.hash_name = hash_name
|
|
294
|
+
|
|
295
|
+
def derive(
|
|
296
|
+
self,
|
|
297
|
+
password: str | bytes,
|
|
298
|
+
salt: bytes,
|
|
299
|
+
key_size: int,
|
|
300
|
+
**kwargs: Any,
|
|
301
|
+
) -> bytes:
|
|
302
|
+
"""Derive key using HKDF."""
|
|
303
|
+
try:
|
|
304
|
+
from cryptography.hazmat.primitives import hashes
|
|
305
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
306
|
+
except ImportError as e:
|
|
307
|
+
raise KeyDerivationError(
|
|
308
|
+
"HKDF requires 'cryptography' package",
|
|
309
|
+
self._kdf.value,
|
|
310
|
+
) from e
|
|
311
|
+
|
|
312
|
+
if isinstance(password, str):
|
|
313
|
+
password = password.encode("utf-8")
|
|
314
|
+
|
|
315
|
+
hash_algo = hashes.SHA512() if self.hash_name == "sha512" else hashes.SHA256()
|
|
316
|
+
info = kwargs.get("info", b"truthound-encryption")
|
|
317
|
+
|
|
318
|
+
hkdf = HKDF(
|
|
319
|
+
algorithm=hash_algo,
|
|
320
|
+
length=key_size,
|
|
321
|
+
salt=salt if salt else None,
|
|
322
|
+
info=info,
|
|
323
|
+
)
|
|
324
|
+
return hkdf.derive(password)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# Key Deriver Registry and Factory
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
_KEY_DERIVER_REGISTRY: dict[KeyDerivation, type[BaseKeyDeriver]] = {
|
|
332
|
+
KeyDerivation.ARGON2ID: Argon2KeyDeriver,
|
|
333
|
+
KeyDerivation.ARGON2I: Argon2KeyDeriver,
|
|
334
|
+
KeyDerivation.ARGON2D: Argon2KeyDeriver,
|
|
335
|
+
KeyDerivation.PBKDF2_SHA256: PBKDF2KeyDeriver,
|
|
336
|
+
KeyDerivation.PBKDF2_SHA512: PBKDF2KeyDeriver,
|
|
337
|
+
KeyDerivation.SCRYPT: ScryptKeyDeriver,
|
|
338
|
+
KeyDerivation.HKDF_SHA256: HKDFKeyDeriver,
|
|
339
|
+
KeyDerivation.HKDF_SHA512: HKDFKeyDeriver,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_key_deriver(kdf: KeyDerivation, **kwargs: Any) -> BaseKeyDeriver:
|
|
344
|
+
"""Get a key deriver instance.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
kdf: Key derivation function.
|
|
348
|
+
**kwargs: KDF-specific parameters.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Key deriver instance.
|
|
352
|
+
"""
|
|
353
|
+
deriver_class = _KEY_DERIVER_REGISTRY.get(kdf)
|
|
354
|
+
if deriver_class is None:
|
|
355
|
+
raise KeyDerivationError(f"Unsupported KDF: {kdf.value}")
|
|
356
|
+
|
|
357
|
+
# Handle variants
|
|
358
|
+
if kdf in (KeyDerivation.ARGON2ID, KeyDerivation.ARGON2I, KeyDerivation.ARGON2D):
|
|
359
|
+
return Argon2KeyDeriver(variant=kdf, **kwargs)
|
|
360
|
+
elif kdf == KeyDerivation.PBKDF2_SHA512:
|
|
361
|
+
return PBKDF2KeyDeriver(hash_name="sha512", **kwargs)
|
|
362
|
+
elif kdf == KeyDerivation.HKDF_SHA512:
|
|
363
|
+
return HKDFKeyDeriver(hash_name="sha512")
|
|
364
|
+
|
|
365
|
+
return deriver_class(**kwargs)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def derive_key(
|
|
369
|
+
password: str | bytes,
|
|
370
|
+
salt: bytes | None = None,
|
|
371
|
+
key_size: int = 32,
|
|
372
|
+
kdf: KeyDerivation = KeyDerivation.ARGON2ID,
|
|
373
|
+
config: KeyDerivationConfig | None = None,
|
|
374
|
+
) -> tuple[bytes, bytes]:
|
|
375
|
+
"""Derive an encryption key from a password.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
password: Password or passphrase.
|
|
379
|
+
salt: Salt (generated if not provided).
|
|
380
|
+
key_size: Desired key size in bytes.
|
|
381
|
+
kdf: Key derivation function.
|
|
382
|
+
config: Detailed configuration.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Tuple of (derived_key, salt).
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> key, salt = derive_key("my_password")
|
|
389
|
+
>>> # Store salt alongside encrypted data
|
|
390
|
+
"""
|
|
391
|
+
if salt is None:
|
|
392
|
+
salt_size = config.salt_size if config else 16
|
|
393
|
+
salt = generate_salt(salt_size)
|
|
394
|
+
|
|
395
|
+
kwargs: dict[str, Any] = {}
|
|
396
|
+
if config:
|
|
397
|
+
if kdf in (KeyDerivation.ARGON2ID, KeyDerivation.ARGON2I, KeyDerivation.ARGON2D):
|
|
398
|
+
kwargs = {
|
|
399
|
+
"time_cost": config.time_cost,
|
|
400
|
+
"memory_cost": config.memory_cost,
|
|
401
|
+
"parallelism": config.parallelism,
|
|
402
|
+
}
|
|
403
|
+
elif kdf in (KeyDerivation.PBKDF2_SHA256, KeyDerivation.PBKDF2_SHA512):
|
|
404
|
+
kwargs = {"iterations": config.get_iterations()}
|
|
405
|
+
elif kdf == KeyDerivation.SCRYPT:
|
|
406
|
+
kwargs = {"n": config.n, "r": config.r, "p": config.p}
|
|
407
|
+
|
|
408
|
+
deriver = get_key_deriver(kdf, **kwargs)
|
|
409
|
+
key = deriver.derive(password, salt, key_size)
|
|
410
|
+
return key, salt
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# =============================================================================
|
|
414
|
+
# Key Storage Backends
|
|
415
|
+
# =============================================================================
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class BaseKeyStore(ABC):
|
|
419
|
+
"""Base class for key storage backends."""
|
|
420
|
+
|
|
421
|
+
@abstractmethod
|
|
422
|
+
def get(self, key_id: str) -> EncryptionKey | None:
|
|
423
|
+
"""Retrieve a key by ID."""
|
|
424
|
+
...
|
|
425
|
+
|
|
426
|
+
@abstractmethod
|
|
427
|
+
def put(self, key: EncryptionKey) -> None:
|
|
428
|
+
"""Store a key."""
|
|
429
|
+
...
|
|
430
|
+
|
|
431
|
+
@abstractmethod
|
|
432
|
+
def delete(self, key_id: str) -> bool:
|
|
433
|
+
"""Delete a key by ID."""
|
|
434
|
+
...
|
|
435
|
+
|
|
436
|
+
@abstractmethod
|
|
437
|
+
def list_keys(self) -> list[str]:
|
|
438
|
+
"""List all key IDs."""
|
|
439
|
+
...
|
|
440
|
+
|
|
441
|
+
def exists(self, key_id: str) -> bool:
|
|
442
|
+
"""Check if a key exists."""
|
|
443
|
+
return self.get(key_id) is not None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class InMemoryKeyStore(BaseKeyStore):
|
|
447
|
+
"""Thread-safe in-memory key storage.
|
|
448
|
+
|
|
449
|
+
WARNING: Keys are lost when the process exits. Use only for
|
|
450
|
+
testing or ephemeral keys.
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
def __init__(self) -> None:
|
|
454
|
+
self._keys: dict[str, EncryptionKey] = {}
|
|
455
|
+
self._lock = RLock()
|
|
456
|
+
|
|
457
|
+
def get(self, key_id: str) -> EncryptionKey | None:
|
|
458
|
+
with self._lock:
|
|
459
|
+
return self._keys.get(key_id)
|
|
460
|
+
|
|
461
|
+
def put(self, key: EncryptionKey) -> None:
|
|
462
|
+
with self._lock:
|
|
463
|
+
self._keys[key.key_id] = key
|
|
464
|
+
|
|
465
|
+
def delete(self, key_id: str) -> bool:
|
|
466
|
+
with self._lock:
|
|
467
|
+
if key_id in self._keys:
|
|
468
|
+
# Clear key material before removing
|
|
469
|
+
self._keys[key_id].clear()
|
|
470
|
+
del self._keys[key_id]
|
|
471
|
+
return True
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
def list_keys(self) -> list[str]:
|
|
475
|
+
with self._lock:
|
|
476
|
+
return list(self._keys.keys())
|
|
477
|
+
|
|
478
|
+
def clear(self) -> None:
|
|
479
|
+
"""Clear all keys from memory."""
|
|
480
|
+
with self._lock:
|
|
481
|
+
for key in self._keys.values():
|
|
482
|
+
key.clear()
|
|
483
|
+
self._keys.clear()
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class FileKeyStore(BaseKeyStore):
|
|
487
|
+
"""File-based key storage with encryption.
|
|
488
|
+
|
|
489
|
+
Keys are stored encrypted using a master key. The master key
|
|
490
|
+
should be provided externally (e.g., from environment variable).
|
|
491
|
+
|
|
492
|
+
WARNING: This is a basic implementation. For production use,
|
|
493
|
+
consider using a proper secrets manager.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
def __init__(
|
|
497
|
+
self,
|
|
498
|
+
path: str | Path,
|
|
499
|
+
master_key: bytes | None = None,
|
|
500
|
+
master_password: str | None = None,
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Initialize file key store.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
path: Directory to store keys.
|
|
506
|
+
master_key: Master key for encrypting stored keys.
|
|
507
|
+
master_password: Password to derive master key (alternative to master_key).
|
|
508
|
+
"""
|
|
509
|
+
self._path = Path(path)
|
|
510
|
+
self._path.mkdir(parents=True, exist_ok=True)
|
|
511
|
+
self._lock = RLock()
|
|
512
|
+
|
|
513
|
+
# Derive or use provided master key
|
|
514
|
+
if master_key:
|
|
515
|
+
self._master_key = master_key
|
|
516
|
+
elif master_password:
|
|
517
|
+
# Use fixed salt for master key (stored in .master_salt file)
|
|
518
|
+
salt_file = self._path / ".master_salt"
|
|
519
|
+
if salt_file.exists():
|
|
520
|
+
salt = salt_file.read_bytes()
|
|
521
|
+
else:
|
|
522
|
+
salt = generate_salt(16)
|
|
523
|
+
salt_file.write_bytes(salt)
|
|
524
|
+
self._master_key, _ = derive_key(master_password, salt=salt)
|
|
525
|
+
else:
|
|
526
|
+
# No encryption - store keys in plaintext (NOT RECOMMENDED)
|
|
527
|
+
self._master_key = None
|
|
528
|
+
|
|
529
|
+
def _get_key_path(self, key_id: str) -> Path:
|
|
530
|
+
"""Get file path for a key."""
|
|
531
|
+
# Sanitize key_id for filesystem
|
|
532
|
+
safe_id = base64.urlsafe_b64encode(key_id.encode()).decode()
|
|
533
|
+
return self._path / f"{safe_id}.key"
|
|
534
|
+
|
|
535
|
+
def _encrypt_key_data(self, data: bytes) -> bytes:
|
|
536
|
+
"""Encrypt key data with master key."""
|
|
537
|
+
if self._master_key is None:
|
|
538
|
+
return data
|
|
539
|
+
|
|
540
|
+
from truthound.stores.encryption.providers import AesGcmEncryptor
|
|
541
|
+
|
|
542
|
+
encryptor = AesGcmEncryptor(key_size=32)
|
|
543
|
+
return encryptor.encrypt(data, self._master_key)
|
|
544
|
+
|
|
545
|
+
def _decrypt_key_data(self, data: bytes) -> bytes:
|
|
546
|
+
"""Decrypt key data with master key."""
|
|
547
|
+
if self._master_key is None:
|
|
548
|
+
return data
|
|
549
|
+
|
|
550
|
+
from truthound.stores.encryption.providers import AesGcmEncryptor
|
|
551
|
+
|
|
552
|
+
encryptor = AesGcmEncryptor(key_size=32)
|
|
553
|
+
return encryptor.decrypt(data, self._master_key)
|
|
554
|
+
|
|
555
|
+
def get(self, key_id: str) -> EncryptionKey | None:
|
|
556
|
+
with self._lock:
|
|
557
|
+
key_path = self._get_key_path(key_id)
|
|
558
|
+
if not key_path.exists():
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
encrypted_data = key_path.read_bytes()
|
|
563
|
+
data = self._decrypt_key_data(encrypted_data)
|
|
564
|
+
key_dict = json.loads(data.decode())
|
|
565
|
+
|
|
566
|
+
return EncryptionKey(
|
|
567
|
+
key_id=key_dict["key_id"],
|
|
568
|
+
key_material=base64.b64decode(key_dict["key_material"]),
|
|
569
|
+
algorithm=EncryptionAlgorithm(key_dict["algorithm"]),
|
|
570
|
+
key_type=KeyType(key_dict["key_type"]),
|
|
571
|
+
created_at=datetime.fromisoformat(key_dict["created_at"]),
|
|
572
|
+
expires_at=(
|
|
573
|
+
datetime.fromisoformat(key_dict["expires_at"])
|
|
574
|
+
if key_dict.get("expires_at")
|
|
575
|
+
else None
|
|
576
|
+
),
|
|
577
|
+
version=key_dict["version"],
|
|
578
|
+
metadata=key_dict.get("metadata", {}),
|
|
579
|
+
)
|
|
580
|
+
except Exception:
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
def put(self, key: EncryptionKey) -> None:
|
|
584
|
+
with self._lock:
|
|
585
|
+
key_dict = {
|
|
586
|
+
"key_id": key.key_id,
|
|
587
|
+
"key_material": base64.b64encode(key.key_material).decode(),
|
|
588
|
+
"algorithm": key.algorithm.value,
|
|
589
|
+
"key_type": key.key_type.value,
|
|
590
|
+
"created_at": key.created_at.isoformat(),
|
|
591
|
+
"expires_at": key.expires_at.isoformat() if key.expires_at else None,
|
|
592
|
+
"version": key.version,
|
|
593
|
+
"metadata": key.metadata,
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
data = json.dumps(key_dict).encode()
|
|
597
|
+
encrypted_data = self._encrypt_key_data(data)
|
|
598
|
+
|
|
599
|
+
key_path = self._get_key_path(key.key_id)
|
|
600
|
+
key_path.write_bytes(encrypted_data)
|
|
601
|
+
|
|
602
|
+
def delete(self, key_id: str) -> bool:
|
|
603
|
+
with self._lock:
|
|
604
|
+
key_path = self._get_key_path(key_id)
|
|
605
|
+
if key_path.exists():
|
|
606
|
+
# Overwrite with zeros before deletion
|
|
607
|
+
size = key_path.stat().st_size
|
|
608
|
+
key_path.write_bytes(b"\x00" * size)
|
|
609
|
+
key_path.unlink()
|
|
610
|
+
return True
|
|
611
|
+
return False
|
|
612
|
+
|
|
613
|
+
def list_keys(self) -> list[str]:
|
|
614
|
+
with self._lock:
|
|
615
|
+
keys = []
|
|
616
|
+
for key_path in self._path.glob("*.key"):
|
|
617
|
+
try:
|
|
618
|
+
safe_id = key_path.stem
|
|
619
|
+
key_id = base64.urlsafe_b64decode(safe_id).decode()
|
|
620
|
+
keys.append(key_id)
|
|
621
|
+
except Exception:
|
|
622
|
+
continue
|
|
623
|
+
return keys
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class EnvironmentKeyStore(BaseKeyStore):
|
|
627
|
+
"""Key storage using environment variables.
|
|
628
|
+
|
|
629
|
+
Keys are stored base64-encoded in environment variables with
|
|
630
|
+
a configurable prefix.
|
|
631
|
+
|
|
632
|
+
Example:
|
|
633
|
+
TRUTHOUND_KEY_mykey=base64_encoded_key
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
def __init__(self, prefix: str = "TRUTHOUND_KEY_") -> None:
|
|
637
|
+
"""Initialize environment key store.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
prefix: Prefix for environment variable names.
|
|
641
|
+
"""
|
|
642
|
+
self._prefix = prefix
|
|
643
|
+
|
|
644
|
+
def get(self, key_id: str) -> EncryptionKey | None:
|
|
645
|
+
env_var = f"{self._prefix}{key_id}"
|
|
646
|
+
value = os.environ.get(env_var)
|
|
647
|
+
if not value:
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
key_material = base64.b64decode(value)
|
|
652
|
+
# Infer algorithm from key size
|
|
653
|
+
if len(key_material) == 16:
|
|
654
|
+
algorithm = EncryptionAlgorithm.AES_128_GCM
|
|
655
|
+
elif len(key_material) == 32:
|
|
656
|
+
algorithm = EncryptionAlgorithm.AES_256_GCM
|
|
657
|
+
elif len(key_material) == 44: # Fernet key (base64)
|
|
658
|
+
algorithm = EncryptionAlgorithm.FERNET
|
|
659
|
+
key_material = value.encode() # Fernet uses base64 string
|
|
660
|
+
else:
|
|
661
|
+
algorithm = EncryptionAlgorithm.AES_256_GCM
|
|
662
|
+
|
|
663
|
+
return EncryptionKey(
|
|
664
|
+
key_id=key_id,
|
|
665
|
+
key_material=key_material,
|
|
666
|
+
algorithm=algorithm,
|
|
667
|
+
)
|
|
668
|
+
except Exception:
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
def put(self, key: EncryptionKey) -> None:
|
|
672
|
+
env_var = f"{self._prefix}{key.key_id}"
|
|
673
|
+
if key.algorithm == EncryptionAlgorithm.FERNET:
|
|
674
|
+
value = key.key_material.decode()
|
|
675
|
+
else:
|
|
676
|
+
value = base64.b64encode(key.key_material).decode()
|
|
677
|
+
os.environ[env_var] = value
|
|
678
|
+
|
|
679
|
+
def delete(self, key_id: str) -> bool:
|
|
680
|
+
env_var = f"{self._prefix}{key_id}"
|
|
681
|
+
if env_var in os.environ:
|
|
682
|
+
del os.environ[env_var]
|
|
683
|
+
return True
|
|
684
|
+
return False
|
|
685
|
+
|
|
686
|
+
def list_keys(self) -> list[str]:
|
|
687
|
+
keys = []
|
|
688
|
+
for var in os.environ:
|
|
689
|
+
if var.startswith(self._prefix):
|
|
690
|
+
key_id = var[len(self._prefix) :]
|
|
691
|
+
keys.append(key_id)
|
|
692
|
+
return keys
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# =============================================================================
|
|
696
|
+
# Key Manager
|
|
697
|
+
# =============================================================================
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@dataclass
|
|
701
|
+
class KeyManagerConfig:
|
|
702
|
+
"""Configuration for key manager.
|
|
703
|
+
|
|
704
|
+
Attributes:
|
|
705
|
+
default_algorithm: Default encryption algorithm.
|
|
706
|
+
default_key_type: Default key type.
|
|
707
|
+
default_ttl: Default key TTL (None = no expiration).
|
|
708
|
+
auto_rotate_before_expiry: Auto-rotate keys before expiry.
|
|
709
|
+
rotation_overlap: Time to keep old key active after rotation.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
default_algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256_GCM
|
|
713
|
+
default_key_type: KeyType = KeyType.DATA_ENCRYPTION_KEY
|
|
714
|
+
default_ttl: timedelta | None = None
|
|
715
|
+
auto_rotate_before_expiry: timedelta = timedelta(days=7)
|
|
716
|
+
rotation_overlap: timedelta = timedelta(hours=24)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class KeyManager:
|
|
720
|
+
"""Comprehensive key management system.
|
|
721
|
+
|
|
722
|
+
Features:
|
|
723
|
+
- Key creation with automatic ID generation
|
|
724
|
+
- Key rotation with version tracking
|
|
725
|
+
- Multiple storage backend support
|
|
726
|
+
- Key expiration management
|
|
727
|
+
- Audit logging hooks
|
|
728
|
+
|
|
729
|
+
Example:
|
|
730
|
+
>>> manager = KeyManager()
|
|
731
|
+
>>> key = manager.create_key()
|
|
732
|
+
>>> encrypted = encrypt_data(data, key.key_material)
|
|
733
|
+
>>> manager.rotate_key(key.key_id)
|
|
734
|
+
"""
|
|
735
|
+
|
|
736
|
+
def __init__(
|
|
737
|
+
self,
|
|
738
|
+
store: BaseKeyStore | None = None,
|
|
739
|
+
config: KeyManagerConfig | None = None,
|
|
740
|
+
audit_hook: Callable[[str, dict[str, Any]], None] | None = None,
|
|
741
|
+
) -> None:
|
|
742
|
+
"""Initialize key manager.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
store: Key storage backend (defaults to in-memory).
|
|
746
|
+
config: Manager configuration.
|
|
747
|
+
audit_hook: Callback for audit events.
|
|
748
|
+
"""
|
|
749
|
+
self._store = store or InMemoryKeyStore()
|
|
750
|
+
self._config = config or KeyManagerConfig()
|
|
751
|
+
self._audit_hook = audit_hook
|
|
752
|
+
self._lock = RLock()
|
|
753
|
+
|
|
754
|
+
def _audit(self, event: str, details: dict[str, Any]) -> None:
|
|
755
|
+
"""Log audit event."""
|
|
756
|
+
if self._audit_hook:
|
|
757
|
+
self._audit_hook(event, {
|
|
758
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
759
|
+
**details,
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
def create_key(
|
|
763
|
+
self,
|
|
764
|
+
key_id: str | None = None,
|
|
765
|
+
algorithm: EncryptionAlgorithm | None = None,
|
|
766
|
+
key_type: KeyType | None = None,
|
|
767
|
+
ttl: timedelta | None = None,
|
|
768
|
+
metadata: dict[str, Any] | None = None,
|
|
769
|
+
) -> EncryptionKey:
|
|
770
|
+
"""Create a new encryption key.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
key_id: Custom key ID (auto-generated if not provided).
|
|
774
|
+
algorithm: Encryption algorithm.
|
|
775
|
+
key_type: Type of key.
|
|
776
|
+
ttl: Time to live.
|
|
777
|
+
metadata: Additional metadata.
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
New encryption key.
|
|
781
|
+
"""
|
|
782
|
+
with self._lock:
|
|
783
|
+
key_id = key_id or generate_key_id()
|
|
784
|
+
algorithm = algorithm or self._config.default_algorithm
|
|
785
|
+
key_type = key_type or self._config.default_key_type
|
|
786
|
+
|
|
787
|
+
# Calculate expiration
|
|
788
|
+
expires_at = None
|
|
789
|
+
effective_ttl = ttl or self._config.default_ttl
|
|
790
|
+
if effective_ttl:
|
|
791
|
+
expires_at = datetime.now(timezone.utc) + effective_ttl
|
|
792
|
+
|
|
793
|
+
key = EncryptionKey(
|
|
794
|
+
key_id=key_id,
|
|
795
|
+
key_material=generate_key(algorithm),
|
|
796
|
+
algorithm=algorithm,
|
|
797
|
+
key_type=key_type,
|
|
798
|
+
expires_at=expires_at,
|
|
799
|
+
metadata=metadata or {},
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
self._store.put(key)
|
|
803
|
+
self._audit("key_created", {
|
|
804
|
+
"key_id": key_id,
|
|
805
|
+
"algorithm": algorithm.value,
|
|
806
|
+
"key_type": key_type.value,
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
return key
|
|
810
|
+
|
|
811
|
+
def get_key(self, key_id: str, validate: bool = True) -> EncryptionKey:
|
|
812
|
+
"""Retrieve a key by ID.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
key_id: Key identifier.
|
|
816
|
+
validate: Whether to validate key is not expired.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
Encryption key.
|
|
820
|
+
|
|
821
|
+
Raises:
|
|
822
|
+
KeyError_: If key not found.
|
|
823
|
+
KeyExpiredError: If key has expired.
|
|
824
|
+
"""
|
|
825
|
+
key = self._store.get(key_id)
|
|
826
|
+
if key is None:
|
|
827
|
+
raise KeyError_(f"Key not found: {key_id}")
|
|
828
|
+
|
|
829
|
+
if validate:
|
|
830
|
+
key.validate()
|
|
831
|
+
|
|
832
|
+
self._audit("key_accessed", {"key_id": key_id})
|
|
833
|
+
return key
|
|
834
|
+
|
|
835
|
+
def rotate_key(
|
|
836
|
+
self,
|
|
837
|
+
key_id: str,
|
|
838
|
+
archive_old: bool = True,
|
|
839
|
+
) -> EncryptionKey:
|
|
840
|
+
"""Rotate a key (create new version).
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
key_id: Key to rotate.
|
|
844
|
+
archive_old: Whether to keep old key version.
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
New key version.
|
|
848
|
+
"""
|
|
849
|
+
with self._lock:
|
|
850
|
+
old_key = self.get_key(key_id, validate=False)
|
|
851
|
+
|
|
852
|
+
# Create new key with incremented version
|
|
853
|
+
new_key = EncryptionKey(
|
|
854
|
+
key_id=key_id,
|
|
855
|
+
key_material=generate_key(old_key.algorithm),
|
|
856
|
+
algorithm=old_key.algorithm,
|
|
857
|
+
key_type=old_key.key_type,
|
|
858
|
+
version=old_key.version + 1,
|
|
859
|
+
metadata={
|
|
860
|
+
**old_key.metadata,
|
|
861
|
+
"previous_version": old_key.version,
|
|
862
|
+
"rotated_at": datetime.now(timezone.utc).isoformat(),
|
|
863
|
+
},
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if self._config.default_ttl:
|
|
867
|
+
new_key.expires_at = datetime.now(timezone.utc) + self._config.default_ttl
|
|
868
|
+
|
|
869
|
+
# Archive old key if requested
|
|
870
|
+
if archive_old:
|
|
871
|
+
archive_id = f"{key_id}_v{old_key.version}"
|
|
872
|
+
archived_key = EncryptionKey(
|
|
873
|
+
key_id=archive_id,
|
|
874
|
+
key_material=old_key.key_material,
|
|
875
|
+
algorithm=old_key.algorithm,
|
|
876
|
+
key_type=old_key.key_type,
|
|
877
|
+
created_at=old_key.created_at,
|
|
878
|
+
expires_at=datetime.now(timezone.utc) + self._config.rotation_overlap,
|
|
879
|
+
version=old_key.version,
|
|
880
|
+
metadata={**old_key.metadata, "archived": True},
|
|
881
|
+
)
|
|
882
|
+
self._store.put(archived_key)
|
|
883
|
+
|
|
884
|
+
# Store new key
|
|
885
|
+
self._store.put(new_key)
|
|
886
|
+
self._audit("key_rotated", {
|
|
887
|
+
"key_id": key_id,
|
|
888
|
+
"old_version": old_key.version,
|
|
889
|
+
"new_version": new_key.version,
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
return new_key
|
|
893
|
+
|
|
894
|
+
def delete_key(self, key_id: str) -> bool:
|
|
895
|
+
"""Delete a key.
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
key_id: Key to delete.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
True if deleted.
|
|
902
|
+
"""
|
|
903
|
+
with self._lock:
|
|
904
|
+
result = self._store.delete(key_id)
|
|
905
|
+
if result:
|
|
906
|
+
self._audit("key_deleted", {"key_id": key_id})
|
|
907
|
+
return result
|
|
908
|
+
|
|
909
|
+
def list_keys(
|
|
910
|
+
self,
|
|
911
|
+
include_expired: bool = False,
|
|
912
|
+
key_type: KeyType | None = None,
|
|
913
|
+
) -> list[EncryptionKey]:
|
|
914
|
+
"""List all keys.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
include_expired: Include expired keys.
|
|
918
|
+
key_type: Filter by key type.
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
List of keys.
|
|
922
|
+
"""
|
|
923
|
+
keys = []
|
|
924
|
+
for key_id in self._store.list_keys():
|
|
925
|
+
key = self._store.get(key_id)
|
|
926
|
+
if key is None:
|
|
927
|
+
continue
|
|
928
|
+
if not include_expired and key.is_expired:
|
|
929
|
+
continue
|
|
930
|
+
if key_type and key.key_type != key_type:
|
|
931
|
+
continue
|
|
932
|
+
keys.append(key)
|
|
933
|
+
return keys
|
|
934
|
+
|
|
935
|
+
def get_or_create_key(
|
|
936
|
+
self,
|
|
937
|
+
key_id: str,
|
|
938
|
+
**create_kwargs: Any,
|
|
939
|
+
) -> EncryptionKey:
|
|
940
|
+
"""Get existing key or create new one.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
key_id: Key identifier.
|
|
944
|
+
**create_kwargs: Arguments for create_key if creating.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Encryption key.
|
|
948
|
+
"""
|
|
949
|
+
try:
|
|
950
|
+
return self.get_key(key_id)
|
|
951
|
+
except KeyError_:
|
|
952
|
+
return self.create_key(key_id=key_id, **create_kwargs)
|
|
953
|
+
|
|
954
|
+
def cleanup_expired(self) -> int:
|
|
955
|
+
"""Delete all expired keys.
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
Number of keys deleted.
|
|
959
|
+
"""
|
|
960
|
+
deleted = 0
|
|
961
|
+
for key_id in self._store.list_keys():
|
|
962
|
+
key = self._store.get(key_id)
|
|
963
|
+
if key and key.is_expired:
|
|
964
|
+
self.delete_key(key_id)
|
|
965
|
+
deleted += 1
|
|
966
|
+
return deleted
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
# =============================================================================
|
|
970
|
+
# Envelope Encryption
|
|
971
|
+
# =============================================================================
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@dataclass
|
|
975
|
+
class EnvelopeEncryptedData:
|
|
976
|
+
"""Data encrypted using envelope encryption.
|
|
977
|
+
|
|
978
|
+
Attributes:
|
|
979
|
+
encrypted_key: DEK encrypted with KEK.
|
|
980
|
+
encrypted_data: Data encrypted with DEK.
|
|
981
|
+
kek_id: ID of key encryption key used.
|
|
982
|
+
algorithm: Algorithm for data encryption.
|
|
983
|
+
nonce: Nonce used for data encryption.
|
|
984
|
+
tag: Authentication tag.
|
|
985
|
+
"""
|
|
986
|
+
|
|
987
|
+
encrypted_key: bytes
|
|
988
|
+
encrypted_data: bytes
|
|
989
|
+
kek_id: str
|
|
990
|
+
algorithm: EncryptionAlgorithm
|
|
991
|
+
nonce: bytes
|
|
992
|
+
tag: bytes
|
|
993
|
+
|
|
994
|
+
def to_bytes(self) -> bytes:
|
|
995
|
+
"""Serialize to bytes."""
|
|
996
|
+
header = json.dumps({
|
|
997
|
+
"kek_id": self.kek_id,
|
|
998
|
+
"algorithm": self.algorithm.value,
|
|
999
|
+
"key_len": len(self.encrypted_key),
|
|
1000
|
+
"nonce_len": len(self.nonce),
|
|
1001
|
+
"tag_len": len(self.tag),
|
|
1002
|
+
}).encode()
|
|
1003
|
+
|
|
1004
|
+
header_len = len(header).to_bytes(4, "big")
|
|
1005
|
+
return (
|
|
1006
|
+
header_len
|
|
1007
|
+
+ header
|
|
1008
|
+
+ self.encrypted_key
|
|
1009
|
+
+ self.nonce
|
|
1010
|
+
+ self.encrypted_data
|
|
1011
|
+
+ self.tag
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
@classmethod
|
|
1015
|
+
def from_bytes(cls, data: bytes) -> "EnvelopeEncryptedData":
|
|
1016
|
+
"""Deserialize from bytes."""
|
|
1017
|
+
header_len = int.from_bytes(data[:4], "big")
|
|
1018
|
+
header = json.loads(data[4 : 4 + header_len].decode())
|
|
1019
|
+
|
|
1020
|
+
offset = 4 + header_len
|
|
1021
|
+
key_len = header["key_len"]
|
|
1022
|
+
nonce_len = header["nonce_len"]
|
|
1023
|
+
tag_len = header["tag_len"]
|
|
1024
|
+
|
|
1025
|
+
encrypted_key = data[offset : offset + key_len]
|
|
1026
|
+
offset += key_len
|
|
1027
|
+
|
|
1028
|
+
nonce = data[offset : offset + nonce_len]
|
|
1029
|
+
offset += nonce_len
|
|
1030
|
+
|
|
1031
|
+
encrypted_data = data[offset : -tag_len]
|
|
1032
|
+
tag = data[-tag_len:]
|
|
1033
|
+
|
|
1034
|
+
return cls(
|
|
1035
|
+
encrypted_key=encrypted_key,
|
|
1036
|
+
encrypted_data=encrypted_data,
|
|
1037
|
+
kek_id=header["kek_id"],
|
|
1038
|
+
algorithm=EncryptionAlgorithm(header["algorithm"]),
|
|
1039
|
+
nonce=nonce,
|
|
1040
|
+
tag=tag,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
class EnvelopeEncryption:
|
|
1045
|
+
"""Envelope encryption for secure key management.
|
|
1046
|
+
|
|
1047
|
+
Envelope encryption uses two levels of keys:
|
|
1048
|
+
- Key Encryption Key (KEK): Used to encrypt data keys
|
|
1049
|
+
- Data Encryption Key (DEK): Used to encrypt actual data
|
|
1050
|
+
|
|
1051
|
+
This pattern allows:
|
|
1052
|
+
- Easy key rotation (just re-encrypt DEK)
|
|
1053
|
+
- Integration with external KMS
|
|
1054
|
+
- Fine-grained access control
|
|
1055
|
+
|
|
1056
|
+
Example:
|
|
1057
|
+
>>> envelope = EnvelopeEncryption(key_manager)
|
|
1058
|
+
>>> encrypted = envelope.encrypt(data, kek_id="master_key")
|
|
1059
|
+
>>> decrypted = envelope.decrypt(encrypted)
|
|
1060
|
+
"""
|
|
1061
|
+
|
|
1062
|
+
def __init__(
|
|
1063
|
+
self,
|
|
1064
|
+
key_manager: KeyManager,
|
|
1065
|
+
data_algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256_GCM,
|
|
1066
|
+
) -> None:
|
|
1067
|
+
"""Initialize envelope encryption.
|
|
1068
|
+
|
|
1069
|
+
Args:
|
|
1070
|
+
key_manager: Key manager for KEK storage.
|
|
1071
|
+
data_algorithm: Algorithm for data encryption.
|
|
1072
|
+
"""
|
|
1073
|
+
self._key_manager = key_manager
|
|
1074
|
+
self._data_algorithm = data_algorithm
|
|
1075
|
+
|
|
1076
|
+
def encrypt(
|
|
1077
|
+
self,
|
|
1078
|
+
plaintext: bytes,
|
|
1079
|
+
kek_id: str,
|
|
1080
|
+
aad: bytes | None = None,
|
|
1081
|
+
) -> EnvelopeEncryptedData:
|
|
1082
|
+
"""Encrypt data using envelope encryption.
|
|
1083
|
+
|
|
1084
|
+
Args:
|
|
1085
|
+
plaintext: Data to encrypt.
|
|
1086
|
+
kek_id: ID of key encryption key.
|
|
1087
|
+
aad: Additional authenticated data.
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
Envelope encrypted data.
|
|
1091
|
+
"""
|
|
1092
|
+
from truthound.stores.encryption.providers import get_encryptor
|
|
1093
|
+
|
|
1094
|
+
# Get KEK
|
|
1095
|
+
kek = self._key_manager.get_key(kek_id)
|
|
1096
|
+
|
|
1097
|
+
# Generate ephemeral DEK
|
|
1098
|
+
dek = generate_key(self._data_algorithm)
|
|
1099
|
+
|
|
1100
|
+
# Encrypt DEK with KEK
|
|
1101
|
+
kek_encryptor = get_encryptor(kek.algorithm)
|
|
1102
|
+
encrypted_dek = kek_encryptor.encrypt(dek, kek.key_material)
|
|
1103
|
+
|
|
1104
|
+
# Encrypt data with DEK
|
|
1105
|
+
dek_encryptor = get_encryptor(self._data_algorithm)
|
|
1106
|
+
result = dek_encryptor.encrypt_with_metrics(plaintext, dek, aad=aad)
|
|
1107
|
+
|
|
1108
|
+
# Clear DEK from memory
|
|
1109
|
+
dek = b"\x00" * len(dek)
|
|
1110
|
+
|
|
1111
|
+
return EnvelopeEncryptedData(
|
|
1112
|
+
encrypted_key=encrypted_dek,
|
|
1113
|
+
encrypted_data=result.ciphertext,
|
|
1114
|
+
kek_id=kek_id,
|
|
1115
|
+
algorithm=self._data_algorithm,
|
|
1116
|
+
nonce=result.nonce,
|
|
1117
|
+
tag=result.tag,
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
def decrypt(
|
|
1121
|
+
self,
|
|
1122
|
+
envelope: EnvelopeEncryptedData,
|
|
1123
|
+
aad: bytes | None = None,
|
|
1124
|
+
) -> bytes:
|
|
1125
|
+
"""Decrypt envelope encrypted data.
|
|
1126
|
+
|
|
1127
|
+
Args:
|
|
1128
|
+
envelope: Envelope encrypted data.
|
|
1129
|
+
aad: Additional authenticated data.
|
|
1130
|
+
|
|
1131
|
+
Returns:
|
|
1132
|
+
Decrypted plaintext.
|
|
1133
|
+
"""
|
|
1134
|
+
from truthound.stores.encryption.providers import get_encryptor
|
|
1135
|
+
|
|
1136
|
+
# Get KEK
|
|
1137
|
+
kek = self._key_manager.get_key(envelope.kek_id)
|
|
1138
|
+
|
|
1139
|
+
# Decrypt DEK
|
|
1140
|
+
kek_encryptor = get_encryptor(kek.algorithm)
|
|
1141
|
+
dek = kek_encryptor.decrypt(envelope.encrypted_key, kek.key_material)
|
|
1142
|
+
|
|
1143
|
+
# Decrypt data
|
|
1144
|
+
dek_encryptor = get_encryptor(envelope.algorithm)
|
|
1145
|
+
ciphertext = envelope.nonce + envelope.encrypted_data + envelope.tag
|
|
1146
|
+
plaintext = dek_encryptor.decrypt(ciphertext, dek, aad=aad)
|
|
1147
|
+
|
|
1148
|
+
# Clear DEK from memory
|
|
1149
|
+
dek = b"\x00" * len(dek)
|
|
1150
|
+
|
|
1151
|
+
return plaintext
|
|
1152
|
+
|
|
1153
|
+
def reencrypt_key(
|
|
1154
|
+
self,
|
|
1155
|
+
envelope: EnvelopeEncryptedData,
|
|
1156
|
+
new_kek_id: str,
|
|
1157
|
+
) -> EnvelopeEncryptedData:
|
|
1158
|
+
"""Re-encrypt DEK with a new KEK (for key rotation).
|
|
1159
|
+
|
|
1160
|
+
Args:
|
|
1161
|
+
envelope: Original envelope data.
|
|
1162
|
+
new_kek_id: ID of new key encryption key.
|
|
1163
|
+
|
|
1164
|
+
Returns:
|
|
1165
|
+
Envelope with re-encrypted DEK.
|
|
1166
|
+
"""
|
|
1167
|
+
from truthound.stores.encryption.providers import get_encryptor
|
|
1168
|
+
|
|
1169
|
+
# Get old and new KEKs
|
|
1170
|
+
old_kek = self._key_manager.get_key(envelope.kek_id, validate=False)
|
|
1171
|
+
new_kek = self._key_manager.get_key(new_kek_id)
|
|
1172
|
+
|
|
1173
|
+
# Decrypt DEK with old KEK
|
|
1174
|
+
old_encryptor = get_encryptor(old_kek.algorithm)
|
|
1175
|
+
dek = old_encryptor.decrypt(envelope.encrypted_key, old_kek.key_material)
|
|
1176
|
+
|
|
1177
|
+
# Encrypt DEK with new KEK
|
|
1178
|
+
new_encryptor = get_encryptor(new_kek.algorithm)
|
|
1179
|
+
new_encrypted_dek = new_encryptor.encrypt(dek, new_kek.key_material)
|
|
1180
|
+
|
|
1181
|
+
# Clear DEK from memory
|
|
1182
|
+
dek = b"\x00" * len(dek)
|
|
1183
|
+
|
|
1184
|
+
return EnvelopeEncryptedData(
|
|
1185
|
+
encrypted_key=new_encrypted_dek,
|
|
1186
|
+
encrypted_data=envelope.encrypted_data,
|
|
1187
|
+
kek_id=new_kek_id,
|
|
1188
|
+
algorithm=envelope.algorithm,
|
|
1189
|
+
nonce=envelope.nonce,
|
|
1190
|
+
tag=envelope.tag,
|
|
1191
|
+
)
|