from __future__ import annotations from pathlib import Path from govoplan_core.core.encryption import ( CAPABILITY_ENCRYPTION_CONTENT_CIPHER, CAPABILITY_ENCRYPTION_CONTENT_CIPHER_PROVIDER_PREFIX, CAPABILITY_ENCRYPTION_CONTENT_PROTECTION, CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT, CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX, CAPABILITY_ENCRYPTION_KEY_VAULT, CAPABILITY_ENCRYPTION_RECOVERY, ) from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( CapabilityDocumentation, DocumentationCondition, DocumentationLink, DocumentationTopic, FrontendModule, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleManifest, ModuleUninstallGuardResult, PermissionDefinition, RoleTemplate, ViewSurface, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.db.base import Base from govoplan_encryption.backend.db import models from govoplan_encryption.backend.dsar_provider import ( ENCRYPTION_DSAR_CAPABILITY, EncryptionDsarProvider, ) from govoplan_encryption.backend.local_provider import ( LOCAL_PROVIDER_ID, LocalAesGcmProvider, ) from govoplan_encryption.backend.service import SqlEncryptionService MODULE_ID = "encryption" MODULE_NAME = "Encryption" MODULE_VERSION = "0.1.19" USE_SCOPE = "encryption:vault:use" ADMIN_SCOPE = "encryption:vault:admin" RECOVERY_SCOPE = "encryption:recovery:approve" OPTIONAL_DEPENDENCIES = ( "access", "audit", "policy", "notifications", "files", "postbox", "campaigns", "workflow_engine", "identity_trust", ) def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Encryption", level="tenant", module_id=module_id, resource=resource, action=action, ) def _router(context: ModuleContext): from govoplan_encryption.backend.router import create_router return create_router(context.registry) def _service(context: ModuleContext) -> SqlEncryptionService: return SqlEncryptionService(context.registry) def _local_provider(context: ModuleContext) -> LocalAesGcmProvider: return LocalAesGcmProvider(getattr(context.settings, "master_key_b64", None)) def _dsar_provider(_context: ModuleContext) -> EncryptionDsarProvider: return EncryptionDsarProvider() def _disable_guard( session: object | None, _module_id: str, ) -> tuple[ModuleUninstallGuardResult, ...]: if session is None: return ( ModuleUninstallGuardResult( "blocker", "encryption_disable_unverified", "Encryption cannot be disabled without proving the state of every protection envelope.", ), ) try: report = SqlEncryptionService().assess_disable(session) except Exception as exc: return ( ModuleUninstallGuardResult( "blocker", "encryption_disable_check_failed", f"Encryption disable preflight failed: {type(exc).__name__}.", ), ) if report.allowed: return () return ( ModuleUninstallGuardResult( "blocker", "encryption_protected_content_present", f"Encryption still protects {report.unresolved_count} unresolved envelope(s). Migrate, decrypt, explicitly export, or cryptographically destroy them before disabling the module.", ), ) PERMISSIONS = ( _permission( USE_SCOPE, "Use encryption profiles", "Register and resolve protected content through configured profiles.", ), _permission( ADMIN_SCOPE, "Administer encryption", "Manage vault metadata, provider operations, rotation, migration, and policy provenance.", ), _permission( RECOVERY_SCOPE, "Approve key recovery", "Participate in a high-assurance recovery ceremony without gaining content ownership.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="encryption_user", name="Encryption user", description="Use configured content-protection profiles.", permissions=(USE_SCOPE,), ), RoleTemplate( slug="encryption_custodian", name="Encryption custodian", description="Administer encryption and participate in key recovery.", permissions=(USE_SCOPE, ADMIN_SCOPE, RECOVERY_SCOPE), ), ) manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, optional_dependencies=OPTIONAL_DEPENDENCIES, provides_interfaces=( ModuleInterfaceProvider( name=CAPABILITY_ENCRYPTION_KEY_VAULT, version="1.0.0", ), ModuleInterfaceProvider( name=CAPABILITY_ENCRYPTION_CONTENT_PROTECTION, version="1.0.0", ), ModuleInterfaceProvider( name=CAPABILITY_ENCRYPTION_CONTENT_CIPHER, version="1.0.0", ), ModuleInterfaceProvider( name=f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}{LOCAL_PROVIDER_ID}", version="1.0.0", ), ModuleInterfaceProvider( name=f"{CAPABILITY_ENCRYPTION_CONTENT_CIPHER_PROVIDER_PREFIX}{LOCAL_PROVIDER_ID}", version="1.0.0", ), ModuleInterfaceProvider( name=CAPABILITY_ENCRYPTION_RECOVERY, version="1.0.0", ), ModuleInterfaceProvider( name=CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT, version="1.0.0", ), ModuleInterfaceProvider(name=ENCRYPTION_DSAR_CAPABILITY, version="0.1.0"), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, route_factory=_router, frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/encryption-webui", view_surfaces=( ViewSurface( id="encryption.admin.operations", module_id=MODULE_ID, kind="section", label="Encryption administration", order=10, ), ViewSurface( id="encryption.admin.vaults", module_id=MODULE_ID, kind="section", label="Key vaults", parent_id="encryption.admin.operations", order=20, ), ViewSurface( id="encryption.admin.migrations", module_id=MODULE_ID, kind="section", label="Protection migrations", parent_id="encryption.admin.operations", order=30, ), ViewSurface( id="encryption.admin.recovery", module_id=MODULE_ID, kind="section", label="Recovery ceremonies", parent_id="encryption.admin.operations", order=40, ), ), ), capability_factories={ CAPABILITY_ENCRYPTION_KEY_VAULT: _service, CAPABILITY_ENCRYPTION_CONTENT_PROTECTION: _service, CAPABILITY_ENCRYPTION_CONTENT_CIPHER: _service, CAPABILITY_ENCRYPTION_RECOVERY: _service, CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT: _service, ENCRYPTION_DSAR_CAPABILITY: _dsar_provider, f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}{LOCAL_PROVIDER_ID}": _local_provider, f"{CAPABILITY_ENCRYPTION_CONTENT_CIPHER_PROVIDER_PREFIX}{LOCAL_PROVIDER_ID}": _local_provider, }, capability_documentation={ CAPABILITY_ENCRYPTION_KEY_VAULT: CapabilityDocumentation( label="Governed encryption key vault", summary=( "Orchestrates opaque, idempotent provider key references, " "lifecycle state, and policy provenance without exposing key material." ), contract_version="1.0.0", audience=("administrator", "security_officer", "auditor"), ), CAPABILITY_ENCRYPTION_CONTENT_PROTECTION: CapabilityDocumentation( label="Content-protection envelope registry", summary=( "Registers versioned protection envelopes and fail-closed, " "evidence-backed migration state for feature-owned content." ), contract_version="1.0.0", ), CAPABILITY_ENCRYPTION_CONTENT_CIPHER: CapabilityDocumentation( label="Server-side content cipher", summary=( "Protects and opens owner-module content through opaque, " "versioned envelopes without exporting key material." ), contract_version="1.0.0", audience=("module_developer", "security_officer", "auditor"), ), CAPABILITY_ENCRYPTION_RECOVERY: CapabilityDocumentation( label="Encryption recovery ceremony", summary=( "Requires recent high assurance, distinct custodians, quorum, " "expiry, and immutable evidence without changing ownership." ), contract_version="1.0.0", ), CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT: CapabilityDocumentation( label="Encryption disable preflight", summary=( "Blocks disable or uninstall while any protection envelope " "remains unresolved." ), contract_version="1.0.0", ), ENCRYPTION_DSAR_CAPABILITY: CapabilityDocumentation( label="Encryption data-subject request provider", summary=( "Exports minimized cryptographic custody attribution without key " "material, ciphertext references, or protected evidence." ), contract_version="0.1.0", ), }, migration_spec=MigrationSpec( module_id=MODULE_ID, metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( models.RecoveryApproval, models.RecoveryCeremony, models.ProtectionMigration, models.ContentProtectionRecord, models.EncryptionLocalProviderOperation, models.EncryptionLocalWrappedContentKey, models.EncryptionLocalKeyMaterial, models.EncryptionKeyOperation, models.EncryptionKeyVersion, models.EncryptionVault, label=MODULE_NAME, ), retirement_notes=( "Destructive retirement remains blocked until disable preflight " "proves that no unresolved protected envelope remains." ), ), uninstall_guard_providers=( _disable_guard, persistent_table_uninstall_guard( models.EncryptionVault, models.EncryptionKeyVersion, models.EncryptionKeyOperation, models.ContentProtectionRecord, models.ProtectionMigration, models.RecoveryCeremony, models.RecoveryApproval, models.EncryptionLocalKeyMaterial, models.EncryptionLocalWrappedContentKey, models.EncryptionLocalProviderOperation, label=MODULE_NAME, ), ), documentation=( DocumentationTopic( id="encryption.data-subject-requests", title="Encryption data-subject requests", summary=( "Export cryptographic custody participation without exposing protected " "content or key material." ), body=( "Encryption correlates only an exact tenant account identifier and can " "narrow an already verified search to a vault, key operation, envelope, " "migration, or recovery ceremony. The export reports minimized vault " "administration, key-operation requests, protection registrations, " "migrations, and recovery participation. It never returns provider or " "public key references, wrapped keys, nonces, ciphertext locations, " "resource identifiers, digests, request payloads, assurance and policy " "references, recovery reasons, idempotency keys, or provenance. Feature " "modules remain responsible for exporting the plaintext semantics of " "their own protected resources. Cryptographic lifecycle and custody " "attribution remains immutable security evidence and is retained rather " "than automatically erased." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "administrator", "security_officer", "auditor"), related_modules=("core", "identity_trust", "audit", "policy"), order=95, metadata={ "kind": "reference", "help_contexts": [ "encryption.admin.operations", "privacy.data-subject-requests", ], "consequence_classes": { "export_custody_attribution": ( "Returns minimized lifecycle activity for the exact account." ), "exclude_cryptographic_secrets": ( "Never returns key material, ciphertext references, nonces, or protected evidence." ), "retain_cryptographic_evidence": ( "Preserves immutable custody and recovery accountability." ), }, }, translations={ "de": { "title": "Datenschutzanfragen zur Verschlüsselung", "summary": ( "Mitwirkung an kryptografischer Verwahrung ausgeben, ohne geschützte Inhalte oder Schlüsselmaterial offenzulegen." ), "body": ( "Encryption gleicht ausschließlich eine exakte Mandantenkontokennung ab und kann eine bereits verifizierte " "Suche auf einen Tresor, Schlüsselvorgang, Umschlag, eine Migration oder Wiederherstellungszeremonie " "einschränken. Die Ausgabe meldet minimierte Tresoradministration, Schlüsselvorgangsanfragen, " "Schutzregistrierungen, Migrationen und Wiederherstellungsmitwirkung. Sie enthält niemals Anbieter- oder " "öffentliche Schlüsselverweise, umhüllte Schlüssel, Nonces, Chiffratorte, Ressourcenkennungen, Prüfsummen, " "Anfrageinhalte, Zusicherungs- und Regelverweise, Wiederherstellungsgründe, Idempotenzschlüssel oder Provenienz. " "Fachmodule bleiben für die Ausgabe der Klartextsemantik ihrer geschützten Ressourcen verantwortlich. " "Zuordnungen des kryptografischen Lebenszyklus und der Verwahrung bleiben unveränderliche Sicherheitsnachweise " "und werden nicht automatisch gelöscht." ), } }, structured_translation_version="1", structured_translations={ "de": { "consequence_classes": { "export_custody_attribution": "Gibt minimierte Lebenszyklusaktivität für das exakte Konto zurück.", "exclude_cryptographic_secrets": "Gibt niemals Schlüsselmaterial, Chiffratverweise, Nonces oder geschützte Nachweise zurück.", "retain_cryptographic_evidence": "Bewahrt unveränderliche Verantwortungsnachweise zu Verwahrung und Wiederherstellung.", } } }, ), DocumentationTopic( id="encryption.boundary", title="Encryption and key-custody boundary", summary=( "Optional vault, content-protection, rotation, recovery, and " "disable-assurance capabilities." ), body=( "Encryption protects feature-owned content without taking over " "its business ownership. Resource ownership recovery never " "implicitly grants cryptographic keys. High-risk lifecycle " "actions require recent Identity Trust assurance. Disabling is " "blocked until each envelope is migrated, decrypted, explicitly " "exported, or cryptographically destroyed. The bundled local " "AES-GCM provider is server-readable and requires the deployment " "master key; it does not imply end-to-end encryption." ), layer="available", documentation_types=("admin", "user"), audience=( "user", "administrator", "security_officer", "product_owner", "auditor", ), related_modules=OPTIONAL_DEPENDENCIES, order=100, links=( DocumentationLink( label="Encryption boundary and threat model", href="govoplan-encryption/docs/ENCRYPTION_BOUNDARY.md", kind="repository", ), ), metadata={"kind": "reference"}, translations={ "de": { "title": "Abgrenzung von Verschlüsselung und Schlüsselverwahrung", "summary": ( "Optionale Fähigkeiten für Tresore, Inhaltsschutz, Rotation, Wiederherstellung und Deaktivierungsnachweise." ), "body": ( "Encryption schützt fachmoduleigene Inhalte, ohne deren fachliche Eigentümerschaft zu übernehmen. Die " "Wiederherstellung von Ressourceneigentum gewährt niemals stillschweigend kryptografische Schlüssel. " "Lebenszyklusaktionen mit hohem Risiko erfordern eine aktuelle Identity-Trust-Zusicherung. Die Deaktivierung " "bleibt gesperrt, bis jeder Umschlag migriert, entschlüsselt, ausdrücklich exportiert oder kryptografisch " "vernichtet wurde. Der mitgelieferte lokale AES-GCM-Anbieter ist serverseitig lesbar und erfordert den " "Deployment-Hauptschlüssel; er begründet keine Ende-zu-Ende-Verschlüsselung." ), } }, ), DocumentationTopic( id="encryption.administration", title="Administer encryption operations", summary=( "Inspect safe vault and envelope metadata, govern key lifecycle, " "coordinate migrations, and verify disable readiness." ), body=( "Encryption administration exposes bounded tenant metadata but " "never provider key references, wrapped keys, ciphertext locations, " "or plaintext. Rotation creates a new current version while existing " "envelopes remain version-bound. Revocation and destruction cannot " "recall material already obtained and can make content unavailable. " "Migrations remain two-phase: the owning module performs the durable " "content operation and records evidence before success. Disable " "preflight blocks until every envelope has a terminal disposition." ), layer="available", documentation_types=("admin",), audience=("administrator", "security_officer", "auditor"), related_modules=OPTIONAL_DEPENDENCIES, order=110, metadata={"kind": "reference"}, translations={ "de": { "title": "Verschlüsselungsvorgänge administrieren", "summary": ( "Sichere Tresor- und Umschlagmetadaten prüfen, den Schlüssellebenszyklus steuern, Migrationen koordinieren und Deaktivierungsbereitschaft nachweisen." ), "body": ( "Die Encryption-Administration zeigt begrenzte Mandantenmetadaten, aber niemals Anbieterschlüsselverweise, " "umhüllte Schlüssel, Chiffratorte oder Klartext. Rotation erzeugt eine neue aktuelle Version, während " "bestehende Umschläge an ihre Version gebunden bleiben. Widerruf und Vernichtung können bereits erlangtes " "Material nicht zurückrufen und Inhalte unzugänglich machen. Migrationen bleiben zweiphasig: Das " "Eigentümermodul führt den dauerhaften Inhaltsvorgang aus und hält vor Erfolg Nachweise fest. Die " "Deaktivierungsvorprüfung sperrt, bis jeder Umschlag einen endgültigen Verbleib hat." ), } }, ), DocumentationTopic( id="encryption.recovery", title="Run an encryption recovery ceremony", summary=( "Request and decide time-bounded recovery with high assurance and " "a distinct-custodian quorum." ), body=( "A recovery requester supplies policy and recent high-assurance " "evidence and cannot approve the same ceremony. Each custodian can " "decide once; a rejection terminates the request and approvals must " "reach the vault quorum before expiry. Approval authorizes a later " "provider operation. It does not release key material, transfer " "resource ownership, or prove that recovery execution succeeded." ), layer="available", documentation_types=("admin", "user"), audience=("administrator", "security_officer", "auditor"), conditions=( DocumentationCondition(required_scopes=(RECOVERY_SCOPE,)), ), related_modules=("identity_trust", "access", "audit", "policy"), order=120, metadata={"kind": "workflow"}, translations={ "de": { "title": "Wiederherstellungszeremonie für Verschlüsselung durchführen", "summary": ( "Zeitlich begrenzte Wiederherstellung mit hoher Zusicherung und Quorum unterschiedlicher Verwahrender beantragen und entscheiden." ), "body": ( "Die antragstellende Person legt Regel und aktuellen Nachweis hoher Zusicherung vor und darf dieselbe " "Zeremonie nicht genehmigen. Jede verwahrende Person kann genau einmal entscheiden; eine Ablehnung beendet " "den Antrag und Genehmigungen müssen das Tresorquorum vor Ablauf erreichen. Die Genehmigung autorisiert einen " "späteren Anbietervorgang. Sie gibt kein Schlüsselmaterial frei, überträgt kein Ressourceneigentum und beweist " "nicht, dass die Wiederherstellung erfolgreich ausgeführt wurde." ), } }, ), ), architecture=declared_module_architecture( layer="institutional_foundation", kind="foundation", maturity="vertical_slice", documentation_ref="docs/ENCRYPTION_BOUNDARY.md", test_ref="tests/test_encryption.py", known_limits=( "The bundled local AES-256-GCM provider is a server-side reference provider backed by shared SQL state and MASTER_KEY_B64; it is not an HSM/KMS, client E2EE protocol, or independent certification.", "A true E2EE claim remains prohibited until a selected client/provider profile passes its threat model, interoperability fixtures, backup/restore tests, and independent review.", ), owned_concepts=( "key vault", "content-protection envelope", "cryptographic recovery ceremony", ), non_owned_concepts=( "domain content", "resource ownership", "account authentication", "provider key material", ), migration_docs=("docs/ENCRYPTION_BOUNDARY.md",), recovery_docs=("docs/ENCRYPTION_BOUNDARY.md",), security_docs=("docs/ENCRYPTION_BOUNDARY.md",), operations_docs=("README.md",), ), ) def get_manifest() -> ModuleManifest: return manifest __all__ = [ "ADMIN_SCOPE", "MODULE_ID", "MODULE_VERSION", "RECOVERY_SCOPE", "USE_SCOPE", "get_manifest", "manifest", ]