diff --git a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java index df1c8bf..a76550f 100644 --- a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java +++ b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java @@ -100,37 +100,18 @@ public class CyberCoreSignerApplication { logger.error("ALERTA CRÍTICO: Engine criptográfica JCA/JCE não está totalmente operacional!"); } - // Se o ambiente tiver suporte gráfico e não for headless forçado, abre a interface JavaFX - boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless(); - - if (isHeadless) { - logger.info("Executando em modo console / headless (sem interface gráfica)."); - startHeadlessServer(); - } else { - try { - logger.info("Iniciando interface gráfica desktop..."); - CyberCoreDesktopApplication.setDevelopmentMode(developmentMode); - Runtime.getRuntime().addShutdownHook(new Thread(() -> - logger.warn("JVM em encerramento (sinal externo, System.exit ou fim do aplicativo)."), - "cybercore-shutdown-diagnostic")); - Application.launch(CyberCoreDesktopApplication.class, args); - } catch (Throwable t) { - logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage()); - startHeadlessServer(); - } + // 1. Inicializa o núcleo e o servidor CCSP imediatamente (alta velocidade) + CertificateManager certManager = CertificateManager.createDefault(); + if (developmentMode) { + logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema)."); } + SigningService signingService = new SigningService(certManager); + br.com.cybercore.signer.core.protocol.CcspUserPromptListener promptListener = + new br.com.cybercore.signer.ui.prompt.JavaFxPromptListener(); - logger.info("CyberCore Signer finalizado."); - } - - private static void startHeadlessServer() { try { - CertificateManager certManager = CertificateManager.createDefault(); - if (developmentMode) { - logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema)."); - } - SigningService signingService = new SigningService(certManager); - runningServer = new CcspServer(configuredPort, certManager, signingService, br.com.cybercore.signer.core.security.ApplicationRegistry.getInstance(), null); + runningServer = new CcspServer(configuredPort, certManager, signingService, + br.com.cybercore.signer.core.security.ApplicationRegistry.getInstance(), promptListener); runningServer.start(); logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort()); @@ -138,22 +119,50 @@ public class CyberCoreSignerApplication { logger.info("Encerrando servidor local CCSP/1..."); stopServer(); })); - - // Mantém processo vivo para atender requisições web, a menos que em modo no-wait - if (!noWaitMode) { - Thread.currentThread().join(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); } catch (java.net.BindException e) { logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort); } catch (Exception e) { if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) { logger.error("ALERTA DE COLISÃO DE PORTA: A porta {} já está em uso por outra instância do CyberCore Signer ou outro serviço. Para usar uma porta alternativa, utilize o parâmetro --port ou defina a variável de ambiente CYBERCORE_PORT.", configuredPort); } else { - logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e); + logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e); } } + + // 2. Se o ambiente tiver suporte gráfico e não for headless forçado, abre a interface JavaFX + boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless(); + + if (!isHeadless) { + try { + logger.info("Iniciando interface gráfica desktop desacoplada..."); + CyberCoreDesktopApplication.setDevelopmentMode(developmentMode); + CyberCoreDesktopApplication.setSharedServices(runningServer, certManager, signingService); + Thread uiThread = new Thread(() -> { + try { + Application.launch(CyberCoreDesktopApplication.class, args); + } catch (Throwable t) { + logger.warn("Interface gráfica finalizada ou indisponível: {}", t.getMessage()); + } + }, "cybercore-desktop-ui"); + uiThread.setDaemon(false); + uiThread.start(); + } catch (Throwable t) { + logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo serviço.", t.getMessage()); + } + } else { + logger.info("Executando em modo console / headless (sem interface gráfica)."); + } + + // Mantém processo vivo para atender requisições web, a menos que em modo no-wait + if (!noWaitMode) { + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + logger.info("CyberCore Signer finalizado."); } public static void parseArguments(String[] args) { diff --git a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java index 4f6be7d..4dd9e53 100644 --- a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java +++ b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java @@ -34,4 +34,17 @@ class ArchitectureTest { rule.check(importedClasses); } + + @Test + @DisplayName("O módulo Core não deve acessar o ciclo de vida do toolkit JavaFX (Application ou Platform)") + void coreShouldNotAccessJavaFxLifecycle() { + ArchRule rule = noClasses() + .that().resideInAPackage("br.com.cybercore.signer.core..") + .should().dependOnClassesThat() + .haveFullyQualifiedName("javafx.application.Application") + .orShould().dependOnClassesThat() + .haveFullyQualifiedName("javafx.application.Platform"); + + rule.check(importedClasses); + } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java index 070bdf6..19f4a89 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java @@ -75,12 +75,22 @@ public class CyberCoreDesktopApplication extends Application { private static boolean developmentMode = false; private static MainAppView mainAppView; + private static volatile CcspServer sharedCcspServer = null; + private static volatile CertificateManager sharedCertManager = null; + private static volatile SigningService sharedSigningService = null; + private CertificateManager certificateManager; private SigningService signingService; private CcspServer ccspServer; private SystemTrayManager systemTrayManager; private SmartCardWatcher smartCardWatcher; + public static void setSharedServices(CcspServer server, CertificateManager certManager, SigningService signingService) { + sharedCcspServer = server; + sharedCertManager = certManager; + sharedSigningService = signingService; + } + public static void setDevelopmentMode(boolean devMode) { developmentMode = devMode; } @@ -93,10 +103,12 @@ public class CyberCoreDesktopApplication extends Application { public void start(Stage primaryStage) { logger.info("Iniciando interface gráfica JavaFX..."); Platform.setImplicitExit(false); + br.com.cybercore.signer.ui.prompt.JavaFxPromptListener.markToolkitInitialized(); + br.com.cybercore.signer.ui.prompt.JavaFxPromptListener.setActiveStage(primaryStage); - // Inicializa serviços centrais reais da plataforma - this.certificateManager = CertificateManager.createDefault(); - this.signingService = new SigningService(this.certificateManager); + // Inicializa ou reutiliza serviços centrais reais da plataforma + this.certificateManager = (sharedCertManager != null) ? sharedCertManager : CertificateManager.createDefault(); + this.signingService = (sharedSigningService != null) ? sharedSigningService : new SigningService(this.certificateManager); if (developmentMode) { logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina)."); @@ -419,6 +431,12 @@ public class CyberCoreDesktopApplication extends Application { } private void initCcspServer(Stage stage) { + if (sharedCcspServer != null) { + this.ccspServer = sharedCcspServer; + logger.info("Vinculada interface desktop ao servidor CCSP/1 já ativo na porta {}", this.ccspServer.getPort()); + return; + } + CcspUserPromptListener promptListener = new CcspUserPromptListener() { @Override public CompletableFuture requestApplicationAuthorization(String origin, String appName, String applicationId) { diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/prompt/JavaFxPromptListener.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/prompt/JavaFxPromptListener.java new file mode 100644 index 0000000..bd1b15d --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/prompt/JavaFxPromptListener.java @@ -0,0 +1,150 @@ +package br.com.cybercore.signer.ui.prompt; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.protocol.CcspRequest; +import br.com.cybercore.signer.core.protocol.CcspUserPromptListener; +import br.com.cybercore.signer.core.security.ApplicationRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import br.com.cybercore.signer.ui.view.ApplicationApprovalDialog; +import br.com.cybercore.signer.ui.view.SignaturePromptDialog; +import javafx.application.Platform; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Listener desacoplado de prompts do CCSP para a interface JavaFX. + * Garante que o toolkit JavaFX só seja inicializado sob demanda (on-demand) + * quando houver um diálogo visual de confirmação a ser exibido ao usuário. + */ +public class JavaFxPromptListener implements CcspUserPromptListener { + + private static final Logger logger = LoggerFactory.getLogger(JavaFxPromptListener.class); + private static final AtomicBoolean FX_TOOLKIT_INITIALIZED = new AtomicBoolean(false); + + private static volatile Stage activeStage = null; + + public static void setActiveStage(Stage stage) { + activeStage = stage; + } + + public static void markToolkitInitialized() { + FX_TOOLKIT_INITIALIZED.set(true); + } + + public static void runOnFxThread(Runnable action) { + if (FX_TOOLKIT_INITIALIZED.get()) { + Platform.runLater(action); + } else { + try { + Platform.startup(() -> { + FX_TOOLKIT_INITIALIZED.set(true); + Platform.setImplicitExit(false); + action.run(); + }); + } catch (IllegalStateException e) { + FX_TOOLKIT_INITIALIZED.set(true); + Platform.runLater(action); + } + } + } + + @Override + public CompletableFuture requestApplicationAuthorization(String origin, String appName, String applicationId) { + CompletableFuture future = new CompletableFuture<>(); + runOnFxThread(() -> { + try { + ApplicationApprovalDialog dialog = new ApplicationApprovalDialog(activeStage, origin, appName, applicationId); + dialog.showAndAwait().whenComplete((approved, err) -> { + if (err != null) { + future.complete(false); + } else { + future.complete(Boolean.TRUE.equals(approved)); + } + }); + } catch (Exception ex) { + logger.error("Erro ao exibir diálogo de autorização de aplicação: {}", ex.getMessage()); + future.complete(false); + } + }); + return future; + } + + @Override + public CompletableFuture requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) { + CompletableFuture future = new CompletableFuture<>(); + runOnFxThread(() -> { + try { + byte[] digestBytes; + if (request.digest() != null && !request.digest().isBlank()) { + digestBytes = Base64.getDecoder().decode(request.digest()); + } else if (request.pdf() != null && !request.pdf().isBlank()) { + byte[] rawPdf = Base64.getDecoder().decode(request.pdf()); + digestBytes = MessageDigest.getInstance("SHA-256").digest(rawPdf); + } else if (request.challenge() != null && !request.challenge().isBlank()) { + digestBytes = MessageDigest.getInstance("SHA-256") + .digest(request.challenge().getBytes(StandardCharsets.UTF_8)); + } else { + digestBytes = new byte[32]; + } + + SigningContext context = new SigningContext( + request.requestId(), + request.applicationId(), + digestBytes, + request.algorithm() != null ? request.algorithm() : "SHA256withRSA", + certificate.id(), + Instant.now() + ); + + String appName = ApplicationRegistry.getInstance().findById(request.applicationId()) + .map(AuthorizedApplication::name) + .orElse(request.applicationId()); + + Long docSize = request.documentSize(); + if (docSize == null && request.pdf() != null) { + try { + docSize = (long) Base64.getDecoder().decode(request.pdf()).length; + } catch (IllegalArgumentException ignored) { + } + } else if (docSize == null && request.xmlBase64() != null) { + try { + docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length; + } catch (IllegalArgumentException ignored) { + } + } + + String requestOrigin = request.origin(); + if (requestOrigin == null || requestOrigin.isBlank()) { + requestOrigin = ApplicationRegistry.getInstance().findById(request.applicationId()) + .map(a -> String.join(", ", a.allowedOrigins())) + .orElse("Requisição Web Local"); + } + + SignaturePromptDialog dialog = new SignaturePromptDialog( + activeStage, appName, requestOrigin, certificate, context, + request.documentName(), request.documentType(), docSize, request.isAuthentication() + ); + dialog.showAndAwait().whenComplete((approved, err) -> { + if (err != null) { + future.complete(false); + } else { + future.complete(Boolean.TRUE.equals(approved)); + } + }); + } catch (Exception ex) { + logger.error("Erro ao exibir diálogo de confirmação de assinatura: {}", ex.getMessage()); + future.complete(false); + } + }); + return future; + } +}