feat: initial commit - cybercore signer desktop agent

This commit is contained in:
Claudecio Martins
2026-10-11 08:20:24 -03:00
commit ccfa358f97
206 changed files with 23952 additions and 0 deletions
@@ -0,0 +1,496 @@
package br.com.cybercore.signer.ui;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.diagnostics.DiagnosticService;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.CertificateProviderException;
import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider;
import br.com.cybercore.signer.ui.view.A1PasswordDialog;
import br.com.cybercore.signer.ui.view.CertificateDetailsDialog;
import br.com.cybercore.signer.ui.view.MainAppView;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import br.com.cybercore.signer.core.protocol.CcspRequest;
import br.com.cybercore.signer.core.protocol.CcspServer;
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.core.security.SmartCardWatcher;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.view.PdfSignDialog;
import br.com.cybercore.signer.ui.view.XmlSignDialog;
import br.com.cybercore.signer.ui.tray.SystemTrayManager;
import br.com.cybercore.signer.ui.view.AboutView;
import br.com.cybercore.signer.ui.view.ApplicationApprovalDialog;
import br.com.cybercore.signer.ui.view.BatchSigningDialog;
import br.com.cybercore.signer.ui.view.CertificatesManagerDialog;
import br.com.cybercore.signer.ui.view.FileSignDialog;
import br.com.cybercore.signer.ui.view.LogsViewerDialog;
import br.com.cybercore.signer.ui.view.SealDesignerDialog;
import br.com.cybercore.signer.ui.view.SealManagementDialog;
import br.com.cybercore.signer.ui.view.SettingsView;
import br.com.cybercore.signer.ui.view.SignaturePromptDialog;
import br.com.cybercore.signer.ui.view.SignatureValidationDialog;
import br.com.cybercore.signer.ui.view.TutorialDialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* Ponto de entrada JavaFX da interface desktop do CyberCore Signer.
* Conectado diretamente ao CertificateManager e SigningService reais do sistema operacional.
*/
public class CyberCoreDesktopApplication extends Application {
private static final Logger logger = LoggerFactory.getLogger(CyberCoreDesktopApplication.class);
private static boolean developmentMode = false;
private static MainAppView mainAppView;
private CertificateManager certificateManager;
private SigningService signingService;
private CcspServer ccspServer;
private SystemTrayManager systemTrayManager;
private SmartCardWatcher smartCardWatcher;
public static void setDevelopmentMode(boolean devMode) {
developmentMode = devMode;
}
public static MainAppView getMainAppView() {
return mainAppView;
}
@Override
public void start(Stage primaryStage) {
logger.info("Iniciando interface gráfica JavaFX...");
Platform.setImplicitExit(false);
// Inicializa serviços centrais reais da plataforma
this.certificateManager = CertificateManager.createDefault();
if (developmentMode) {
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
}
mainAppView = new MainAppView(primaryStage);
// Ações da Grade de Assinatura e Validação
mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
mainAppView.setOnCoSign(() -> new FileSignDialog(primaryStage, "Co-Assinar Documento", "*.*", SvgIcons.coSignIcon(), certificateManager, signingService).show());
mainAppView.setOnTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage, false).show());
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
mainAppView.setOnExit(() -> {
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
primaryStage.close();
Platform.exit();
System.exit(0);
});
// Ações do Painel Lateral
mainAppView.setOnLogs(() -> new LogsViewerDialog(primaryStage).show());
mainAppView.setOnSettings(() -> openSettingsDialog(primaryStage));
mainAppView.setOnTutorial(() -> new TutorialDialog(primaryStage).show());
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
initCcspServer(primaryStage);
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
initSmartCardWatcher();
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
this.systemTrayManager.setup();
// Carrega os certificados instalados na máquina do usuário
refreshCertificates();
mainAppView.show();
logger.info("Janela principal JavaFX exibida.");
}
private void refreshCertificates() {
try {
List<ManagedCertificate> certs = certificateManager.discoverCertificates();
logger.info("Descoberta concluída: {} certificado(s) real(is) identificado(s).", certs.size());
} catch (Exception e) {
logger.error("Erro ao atualizar lista de certificados: {}", e.getMessage(), e);
}
}
private void openSettingsDialog(Stage owner) {
Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(owner);
dlg.setTitle("CyberCore Signer — Configurações");
dlg.setResizable(true);
dlg.setMinWidth(660);
dlg.setMinHeight(520);
SettingsView settingsView = new SettingsView();
settingsView.updateApplications(ApplicationRegistry.getInstance().listAll());
settingsView.setOnRevokeApplication(appId -> {
logger.info("Revogando autorização da aplicação web [id={}]", appId);
ApplicationRegistry.getInstance().revoke(appId);
settingsView.updateApplications(ApplicationRegistry.getInstance().listAll());
});
ScrollPane scrollPane = new ScrollPane(settingsView);
scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
footer.setAlignment(Pos.CENTER_RIGHT);
footer.setPadding(new Insets(10, 24, 16, 24));
VBox layout = new VBox(scrollPane, footer);
layout.getStyleClass().add("dialog-container");
Scene sc = new Scene(layout, 720, 580);
ThemeManager.applyTheme(sc);
dlg.setScene(sc);
dlg.show();
}
private void openAboutDialog(Stage owner) {
Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(owner);
dlg.setTitle("CyberCore Signer — Sobre");
dlg.setResizable(true);
dlg.setMinWidth(540);
dlg.setMinHeight(420);
AboutView aboutView = new AboutView();
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
footer.setAlignment(Pos.CENTER);
footer.setPadding(new Insets(10, 24, 20, 24));
VBox layout = new VBox(aboutView, footer);
layout.getStyleClass().add("dialog-container");
Scene sc = new Scene(layout, 560, 440);
ThemeManager.applyTheme(sc);
dlg.setScene(sc);
dlg.show();
}
private void showCertificateDetails(Stage owner, ManagedCertificate cert) {
CertificateDetailsDialog dialog = new CertificateDetailsDialog(owner, cert);
dialog.show();
}
private void handleAddA1Certificate(Stage owner) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Selecionar Certificado Digital A1 (.pfx, .p12)");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Certificados Digitais PKCS#12 (*.pfx, *.p12)", "*.pfx", "*.p12"),
new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
);
File selectedFile = fileChooser.showOpenDialog(owner);
if (selectedFile == null) {
logger.info("Seleção de certificado A1 cancelada pelo usuário.");
return;
}
A1PasswordDialog pwdDialog = new A1PasswordDialog(owner, selectedFile.getName());
pwdDialog.showAndAwait().thenAccept(password -> {
if (password == null) {
logger.info("Importação de certificado A1 cancelada na etapa de senha.");
return;
}
try {
byte[] pfxData = Files.readAllBytes(selectedFile.toPath());
String providerId = "provider-a1-" + Integer.toHexString(selectedFile.getAbsolutePath().hashCode());
Pkcs12CertificateProvider a1Provider = new Pkcs12CertificateProvider(providerId, selectedFile.getName());
a1Provider.loadFromBytes(pfxData, password);
// Limpa senha da memória
Arrays.fill(password, ' ');
certificateManager.registerProvider(a1Provider);
refreshCertificates();
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Certificado A1 importado com sucesso!", ButtonType.OK);
alert.initOwner(owner);
alert.setTitle("CyberCore Signer — Certificado A1");
alert.setHeaderText("Importação Concluída");
alert.show();
});
logger.info("Certificado A1 '{}' importado e registrado com sucesso.", selectedFile.getName());
} catch (CertificateProviderException cpe) {
Arrays.fill(password, ' ');
Platform.runLater(() -> {
String msg = cpe.getErrorCode() == CcspErrorCode.PIN_INCORRECT
? "A senha informada para o certificado A1 está incorreta."
: "Erro ao processar certificado: " + cpe.getMessage();
Alert alert = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
alert.initOwner(owner);
alert.setTitle("CyberCore Signer — Erro");
alert.setHeaderText("Falha na Importação");
alert.show();
});
} catch (Exception e) {
Arrays.fill(password, ' ');
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, "Não foi possível ler o arquivo: " + e.getMessage(), ButtonType.OK);
alert.initOwner(owner);
alert.setTitle("CyberCore Signer — Erro");
alert.setHeaderText("Falha na Leitura");
alert.show();
});
}
});
}
private void runSelfTestSignature(Stage stage) {
List<ManagedCertificate> available = certificateManager.getAvailableCertificates();
if (available.isEmpty()) {
logger.warn("Nenhum certificado digital disponível com chave privada para teste de assinatura.");
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.WARNING,
"Nenhum certificado digital disponível com chave privada para teste de assinatura.\n\n" +
"Conecte um token USB/Smart Card ou importe um certificado A1.",
ButtonType.OK);
alert.initOwner(stage);
alert.setTitle("CyberCore Signer — Auto-teste");
alert.setHeaderText("Certificado Não Encontrado");
alert.show();
});
return;
}
ManagedCertificate target = available.get(0);
openSignaturePrompt(stage, target);
}
private void openSignaturePrompt(Stage owner, ManagedCertificate cert) {
try {
byte[] docDigest = MessageDigest.getInstance("SHA-256")
.digest(("Documento_Teste_CyberCore_" + Instant.now()).getBytes(StandardCharsets.UTF_8));
SigningContext context = new SigningContext(
"req_test_" + System.currentTimeMillis(),
"app_teste_local",
docDigest,
"SHA256withRSA",
cert.id(),
Instant.now()
);
SignaturePromptDialog dialog = new SignaturePromptDialog(
owner, "CyberCore Agente Local", "http://localhost", cert, context
);
dialog.showAndAwait().thenAccept(approved -> {
if (Boolean.TRUE.equals(approved)) {
logger.info("Usuário aprovou a assinatura para o certificado: {}", cert.alias());
try {
SigningResult result = signingService.sign(context);
logger.info("Assinatura real gerada com sucesso! Algoritmo={}, Tamanho={}",
result.algorithm(), result.signatureBytes().length);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION,
"Documento assinado com sucesso via " + result.algorithm() + "!\n\n" +
"Tamanho da assinatura: " + result.signatureBytes().length + " bytes\n" +
"Certificado: " + cert.commonName(),
ButtonType.OK);
alert.initOwner(owner);
alert.setTitle("CyberCore Signer — Sucesso");
alert.setHeaderText("Assinatura Realizada");
alert.show();
});
} catch (Exception e) {
logger.error("Falha ao executar assinatura: {}", e.getMessage(), e);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR,
"Falha ao realizar assinatura digital:\n\n" + e.getMessage(),
ButtonType.OK);
alert.initOwner(owner);
alert.setTitle("CyberCore Signer — Erro");
alert.setHeaderText("Falha na Assinatura");
alert.show();
});
}
} else {
logger.info("Usuário cancelou a assinatura para o certificado: {}", cert.alias());
}
});
} catch (Exception e) {
logger.error("Erro ao abrir diálogo de solicitação de assinatura: {}", e.getMessage(), e);
}
}
private void initCcspServer(Stage stage) {
CcspUserPromptListener promptListener = new CcspUserPromptListener() {
@Override
public CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Platform.runLater(() -> {
try {
ApplicationApprovalDialog dialog = new ApplicationApprovalDialog(stage, 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 abrir diálogo de autorização de aplicação: {}", ex.getMessage());
future.complete(false);
}
});
return future;
}
@Override
public CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Platform.runLater(() -> {
try {
byte[] digestBytes;
if (request.digest() != null && !request.digest().isBlank()) {
digestBytes = Base64.getDecoder().decode(request.digest());
} 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());
SignaturePromptDialog dialog = new SignaturePromptDialog(
stage, appName, "Requisição Web CCSP", certificate, context,
request.documentName(), request.documentType(), 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 abrir diálogo de confirmação de assinatura: {}", ex.getMessage());
future.complete(false);
}
});
return future;
}
};
try {
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
this.ccspServer.start();
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
} catch (Exception e) {
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
}
}
private void initSmartCardWatcher() {
try {
this.smartCardWatcher = new SmartCardWatcher();
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
@Override
public void onCardInserted(String terminalName) {
logger.info("Novo dispositivo detectado no terminal '{}'. Atualizando certificados...", terminalName);
refreshCertificates();
}
@Override
public void onCardRemoved(String terminalName) {
logger.warn("Dispositivo desconectado do terminal '{}'. Atualizando certificados...", terminalName);
refreshCertificates();
}
});
this.smartCardWatcher.start();
} catch (Throwable t) {
logger.warn("Não foi possível iniciar monitor dinâmico de smart cards: {}", t.getMessage());
}
}
@Override
public void stop() {
logger.info("Encerrando componentes da interface JavaFX...");
if (smartCardWatcher != null) {
smartCardWatcher.stop();
}
if (systemTrayManager != null) {
systemTrayManager.remove();
}
if (ccspServer != null) {
ccspServer.stop();
}
}
}
@@ -0,0 +1,17 @@
package br.com.cybercore.signer.ui;
/**
* Constantes visuais e de apresentação da interface do CyberCore Signer.
*/
public final class CyberCoreUiConstants {
public static final String APP_NAME = "CyberCore Signer";
public static final String APP_VERSION = "0.1.0";
public static final String APP_STATUS_READY = "Serviço ativo e pronto para assinar";
// Temas e estilos
public static final String MAIN_CSS_PATH = "/styles/theme.css";
private CyberCoreUiConstants() {
}
}
@@ -0,0 +1,449 @@
package br.com.cybercore.signer.ui.assets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Gerenciador centralizado de recursos visuais (ícones e imagens).
* Permite que o usuário customize ou substitua facilmente qualquer ícone (SVG/PNG) ou imagem (PNG/JPG)
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
*/
public final class AssetManager {
private static final Logger logger = LoggerFactory.getLogger(AssetManager.class);
private static final String[] RASTER_EXTENSIONS = {".png", ".jpg", ".jpeg"};
private AssetManager() {
}
/**
* Retorna o caminho do diretório externo prioritário de assets.
*/
public static Path getExternalAssetsDir() {
Path localAssets = Path.of("assets");
if (Files.isDirectory(localAssets)) {
return localAssets;
}
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore", "assets");
}
/**
* Tenta carregar uma imagem bitmap personalizada (.png, .jpg) pelo nome nas pastas de assets
* ou nos recursos empacotados.
*/
public static Optional<Image> loadImage(String category, String baseName, double width, double height) {
if (baseName == null || baseName.isBlank()) {
return Optional.empty();
}
String cleanName = cleanBaseName(baseName);
// 1. Procura no diretório local ./assets/{category}/
Path localDir = Path.of("assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = localDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap do diretório local: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage());
}
}
}
// 2. Procura em ~/.cybercore/assets/{category}/
String userHome = System.getProperty("user.home", ".");
Path userDir = Path.of(userHome, ".cybercore", "assets", category);
for (String ext : RASTER_EXTENSIONS) {
Path file = userDir.resolve(cleanName + ext);
if (Files.isRegularFile(file)) {
try (InputStream in = new FileInputStream(file.toFile())) {
logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file);
return Optional.of(new Image(in, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage());
}
}
}
// 3. Procura no Classpath interno (/assets/{category}/{name})
for (String ext : RASTER_EXTENSIONS) {
String resPath = "/assets/" + category + "/" + cleanName + ext;
InputStream resStream = AssetManager.class.getResourceAsStream(resPath);
if (resStream != null) {
try (resStream) {
return Optional.of(new Image(resStream, width, height, true, true));
} catch (Exception e) {
logger.warn("Falha ao ler recurso interno {}: {}", resPath, e.getMessage());
}
}
}
return Optional.empty();
}
/**
* Retorna um Node de ícone customizado (SVG ou PNG) ou invoca o fallback caso não exista imagem customizada.
*/
public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) {
if (iconName != null && !iconName.isBlank()) {
String cleanName = cleanBaseName(iconName);
// 1. Prioridade para SVG em ./assets/icons/
Optional<Node> localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size);
if (localSvg.isPresent()) {
return localSvg.get();
}
// 2. Procura SVG em ~/.cybercore/assets/icons/
String userHome = System.getProperty("user.home", ".");
Optional<Node> userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size);
if (userSvg.isPresent()) {
return userSvg.get();
}
// 3. Se não houver SVG, tenta imagem bitmap (.png, .jpg) nas pastas externas
Optional<Image> customImg = loadImage("icons", cleanName, size, size);
if (customImg.isPresent() && !customImg.get().isError()) {
ImageView iv = new ImageView(customImg.get());
iv.setFitWidth(size);
iv.setFitHeight(size);
iv.setPreserveRatio(true);
return iv;
}
}
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
}
/**
* Retorna a imagem do logotipo do sistema ou o fallback vetorial.
*/
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
// Tenta carregar app-logo bitmap (.png) primeiro
Optional<Image> customLogo = loadImage("images", "app-logo", width, height);
if (customLogo.isPresent() && !customLogo.get().isError()) {
ImageView iv = new ImageView(customLogo.get());
iv.setFitWidth(width);
iv.setFitHeight(height);
iv.setPreserveRatio(true);
return iv;
}
// Tenta carregar SVG de app-logo se existir
Optional<Node> svgLogo = loadSvgIconFromPath(Path.of("assets", "images", "app-logo.svg"), height);
if (svgLogo.isPresent()) {
return svgLogo.get();
}
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
}
private static String cleanBaseName(String name) {
String clean = name;
int dotIdx = clean.lastIndexOf('.');
if (dotIdx > 0) {
clean = clean.substring(0, dotIdx);
}
return clean;
}
private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
if (Files.isRegularFile(path)) {
try (InputStream in = new FileInputStream(path.toFile())) {
Node node = parseSvgToNode(in, size);
if (node != null) {
logger.debug("Carregado ícone SVG customizado de: {}", path);
return Optional.of(node);
}
} catch (Exception e) {
logger.warn("Falha ao carregar ícone SVG {}: {}", path, e.getMessage());
}
}
return Optional.empty();
}
/**
* Parser analítico leve de SVG padrão para nós gráficos JavaFX com preservação geométrica exata.
*/
public static Node parseSvgToNode(InputStream in, double targetSize) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(false);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(in);
Element svg = doc.getDocumentElement();
if (!"svg".equalsIgnoreCase(svg.getNodeName())) {
return null;
}
double vbMinX = 0;
double vbMinY = 0;
double vbWidth = 48;
double vbHeight = 48;
String viewBox = svg.getAttribute("viewBox");
if (viewBox != null && !viewBox.isBlank()) {
String[] parts = viewBox.trim().split("[,\\s]+");
if (parts.length >= 4) {
try {
vbMinX = Double.parseDouble(parts[0]);
vbMinY = Double.parseDouble(parts[1]);
vbWidth = Double.parseDouble(parts[2]);
vbHeight = Double.parseDouble(parts[3]);
} catch (NumberFormatException ignored) {
}
}
} else {
String wAttr = svg.getAttribute("width");
String hAttr = svg.getAttribute("height");
if (!wAttr.isBlank() && !hAttr.isBlank()) {
try {
vbWidth = Double.parseDouble(wAttr.replaceAll("[^0-9.]", ""));
vbHeight = Double.parseDouble(hAttr.replaceAll("[^0-9.]", ""));
} catch (NumberFormatException ignored) {
}
}
}
if (vbWidth <= 0) vbWidth = 48;
if (vbHeight <= 0) vbHeight = 48;
Group contentGroup = new Group();
renderChildren(svg, contentGroup);
double maxDim = Math.max(vbWidth, vbHeight);
double scale = (targetSize * 0.90) / maxDim;
double drawnW = vbWidth * scale;
double drawnH = vbHeight * scale;
double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale);
contentGroup.getTransforms().setAll(
new javafx.scene.transform.Translate(tx, ty),
new javafx.scene.transform.Scale(scale, scale, 0, 0)
);
StackPane container = new StackPane(contentGroup);
container.setPrefSize(targetSize, targetSize);
container.setMaxSize(targetSize, targetSize);
container.setMinSize(targetSize, targetSize);
container.setAlignment(Pos.CENTER);
return container;
} catch (Exception e) {
logger.warn("Erro ao parsear arquivo SVG: {}", e.getMessage());
return null;
}
}
private static void renderChildren(Element parent, Group targetGroup) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node n = children.item(i);
if (n instanceof Element el) {
Node jfxNode = createElementNode(el);
if (jfxNode != null) {
targetGroup.getChildren().add(jfxNode);
}
}
}
}
private static Node createElementNode(Element el) {
String tagName = el.getTagName().toLowerCase();
Shape shape = null;
switch (tagName) {
case "path" -> {
String d = el.getAttribute("d");
if (!d.isBlank()) {
SVGPath p = new SVGPath();
p.setContent(d);
shape = p;
}
}
case "rect" -> {
double x = parseDouble(el.getAttribute("x"), 0);
double y = parseDouble(el.getAttribute("y"), 0);
double w = parseDouble(el.getAttribute("width"), 0);
double h = parseDouble(el.getAttribute("height"), 0);
double rx = parseDouble(el.getAttribute("rx"), 0);
Rectangle r = new Rectangle(x, y, w, h);
if (rx > 0) {
r.setArcWidth(rx * 2);
r.setArcHeight(rx * 2);
}
shape = r;
}
case "circle" -> {
double cx = parseDouble(el.getAttribute("cx"), 0);
double cy = parseDouble(el.getAttribute("cy"), 0);
double rad = parseDouble(el.getAttribute("r"), 0);
shape = new Circle(cx, cy, rad);
}
case "line" -> {
double x1 = parseDouble(el.getAttribute("x1"), 0);
double y1 = parseDouble(el.getAttribute("y1"), 0);
double x2 = parseDouble(el.getAttribute("x2"), 0);
double y2 = parseDouble(el.getAttribute("y2"), 0);
shape = new Line(x1, y1, x2, y2);
}
case "polygon" -> {
String pts = el.getAttribute("points");
if (!pts.isBlank()) {
Polygon poly = new Polygon();
parsePoints(pts, poly.getPoints());
shape = poly;
}
}
case "polyline" -> {
String pts = el.getAttribute("points");
if (!pts.isBlank()) {
Polyline poly = new Polyline();
parsePoints(pts, poly.getPoints());
shape = poly;
}
}
case "text" -> {
double x = parseDouble(el.getAttribute("x"), 0);
double y = parseDouble(el.getAttribute("y"), 0);
String content = el.getTextContent();
Text t = new Text(x, y, content);
double fs = parseDouble(el.getAttribute("font-size"), 10);
String fw = el.getAttribute("font-weight");
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
if (el.hasAttribute("fill")) {
t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK));
}
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
t.setTextOrigin(javafx.geometry.VPos.CENTER);
}
return t;
}
case "g" -> {
Group g = new Group();
renderChildren(el, g);
if (el.hasAttribute("stroke")) {
Color strokeCol = parseColor(el.getAttribute("stroke"), null);
double sw = parseDouble(el.getAttribute("stroke-width"), 1.0);
for (Node child : g.getChildren()) {
if (child instanceof Shape s) {
if (strokeCol != null) s.setStroke(strokeCol);
s.setStrokeWidth(sw);
}
}
}
return g;
}
}
if (shape != null) {
applyShapeStyles(el, shape);
return shape;
}
return null;
}
private static void applyShapeStyles(Element el, Shape shape) {
if (el.hasAttribute("fill")) {
shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT));
} else if (shape instanceof Line) {
shape.setFill(Color.TRANSPARENT);
}
if (el.hasAttribute("stroke")) {
shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT));
}
if (el.hasAttribute("stroke-width")) {
shape.setStrokeWidth(parseDouble(el.getAttribute("stroke-width"), 1.0));
}
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
if ("evenodd".equalsIgnoreCase(el.getAttribute("fill-rule"))) {
sp.setFillRule(javafx.scene.shape.FillRule.EVEN_ODD);
}
}
}
private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) {
String[] tokens = ptsStr.trim().split("[,\\s]+");
for (String t : tokens) {
if (!t.isBlank()) {
try {
pointsList.add(Double.parseDouble(t));
} catch (NumberFormatException ignored) {
}
}
}
}
private static Color parseColor(String str, Color fallback) {
if (str == null || str.isBlank() || "none".equalsIgnoreCase(str)) {
return Color.TRANSPARENT;
}
String trimmed = str.trim();
if ("currentColor".equalsIgnoreCase(trimmed)) {
return Color.web("#0288d1");
}
try {
return Color.web(trimmed);
} catch (Exception e) {
return fallback != null ? fallback : Color.BLACK;
}
}
private static double parseDouble(String str, double defaultValue) {
if (str == null || str.isBlank()) {
return defaultValue;
}
try {
return Double.parseDouble(str.replaceAll("[^0-9.-]", ""));
} catch (Exception e) {
return defaultValue;
}
}
}
@@ -0,0 +1,129 @@
package br.com.cybercore.signer.ui.components;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* Componente visual reutilizável para exibição de um certificado digital.
*/
@SuppressWarnings("this-escape")
public final class CertificateCardNode extends VBox {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy")
.withZone(ZoneId.systemDefault());
private final ManagedCertificate certificate;
public CertificateCardNode(ManagedCertificate certificate) {
this.certificate = certificate;
initLayout();
}
private void initLayout() {
getStyleClass().add("cert-card");
setCursor(javafx.scene.Cursor.HAND);
setSpacing(8);
// Linha 1: Nome do Titular e Badge A1/A3
HBox topRow = new HBox(10);
topRow.setAlignment(Pos.CENTER_LEFT);
Label titleLabel = new Label(certificate.commonName() != null ? certificate.commonName() : certificate.alias());
titleLabel.getStyleClass().add("cert-title");
HBox.setHgrow(titleLabel, Priority.ALWAYS);
Label badgeLabel = new Label();
if (certificate.type() == CertificateType.A1) {
badgeLabel.setText("A1 • Certificado local");
badgeLabel.getStyleClass().add("cert-badge-a1");
} else {
String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB";
badgeLabel.setText("A3 • " + tokenDesc);
badgeLabel.getStyleClass().add("cert-badge-a3");
}
topRow.getChildren().addAll(titleLabel, badgeLabel);
// Linha 2: Documento (CPF / CNPJ) e Emissor
HBox midRow = new HBox(12);
midRow.setAlignment(Pos.CENTER_LEFT);
if (certificate.cpf() != null) {
Label docLabel = new Label("CPF: " + formatCpf(certificate.cpf()));
docLabel.getStyleClass().add("cert-detail");
midRow.getChildren().add(docLabel);
} else if (certificate.cnpj() != null) {
Label docLabel = new Label("CNPJ: " + formatCnpj(certificate.cnpj()));
docLabel.getStyleClass().add("cert-detail");
midRow.getChildren().add(docLabel);
}
String issuerCn = extractIssuerCn(certificate.issuerDn());
Label issuerLabel = new Label("Emissor: " + issuerCn);
issuerLabel.getStyleClass().add("cert-detail");
midRow.getChildren().add(issuerLabel);
// Linha 3: Validade e Status de disponibilidade
HBox bottomRow = new HBox(12);
bottomRow.setAlignment(Pos.CENTER_LEFT);
String validityText = "Válido até " + (certificate.notAfter() != null ? DATE_FORMATTER.format(certificate.notAfter()) : "N/D");
Label validityLabel = new Label(validityText);
validityLabel.getStyleClass().add("cert-detail");
HBox.setHgrow(validityLabel, Priority.ALWAYS);
Label statusLabel = new Label();
if (certificate.status() == CertificateStatus.AVAILABLE && certificate.isCurrentlyValid()) {
statusLabel.setText("● Disponível");
statusLabel.getStyleClass().add("cert-status-available");
} else {
statusLabel.setText("● " + certificate.status().getDescription());
statusLabel.getStyleClass().add("cert-status-expired");
}
bottomRow.getChildren().addAll(validityLabel, statusLabel);
getChildren().addAll(topRow, midRow, bottomRow);
}
public ManagedCertificate getCertificate() {
return certificate;
}
private String formatCpf(String cpf) {
if (cpf != null && cpf.length() == 11) {
return cpf.substring(0, 3) + "." + cpf.substring(3, 6) + "." + cpf.substring(6, 9) + "-" + cpf.substring(9);
}
return cpf;
}
private String formatCnpj(String cnpj) {
if (cnpj != null && cnpj.length() == 14) {
return cnpj.substring(0, 2) + "." + cnpj.substring(2, 5) + "." + cnpj.substring(5, 8) + "/" + cnpj.substring(8, 12) + "-" + cnpj.substring(12);
}
return cnpj;
}
private String extractIssuerCn(String dn) {
if (dn == null) return "Desconhecido";
int idx = dn.indexOf("CN=");
if (idx >= 0) {
int end = dn.indexOf(',', idx);
if (end > idx) {
return dn.substring(idx + 3, end).trim();
}
return dn.substring(idx + 3).trim();
}
return dn;
}
}
@@ -0,0 +1,732 @@
package br.com.cybercore.signer.ui.components;
import br.com.cybercore.signer.ui.assets.AssetManager;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
/**
* Fábrica de ícones e grafismos vetoriais SVG nítidos e escaláveis para a interface Serpro do CyberCore Signer.
* Integra-se ao AssetManager para carregar dinamicamente arquivos .svg ou .png da pasta 'assets/icons/'.
*/
public final class SvgIcons {
private SvgIcons() {
}
/**
* Logotipo central de cristal/diamante azul tridimensional.
*/
public static Node createAppCrystalLogo(double size) {
return AssetManager.getIconOrDefault("app-icon", size, () -> buildAppCrystalLogo(size));
}
public static Node buildAppCrystalLogo(double size) {
StackPane root = new StackPane();
root.setPrefSize(size, size);
root.setMaxSize(size, size);
Rectangle bg = new Rectangle(size, size);
bg.setArcWidth(size * 0.35);
bg.setArcHeight(size * 0.35);
LinearGradient bgGrad = new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#1976d2")),
new Stop(0.5, Color.web("#0d47a1")),
new Stop(1, Color.web("#01579b"))
);
bg.setFill(bgGrad);
double s = size;
Polygon facetTop = new Polygon(
s * 0.5, s * 0.15,
s * 0.82, s * 0.42,
s * 0.5, s * 0.55,
s * 0.18, s * 0.42
);
facetTop.setFill(new LinearGradient(
0, 0, 0, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#ffffff", 0.85)),
new Stop(1, Color.web("#80d8ff", 0.4))
));
Polygon facetBottomLeft = new Polygon(
s * 0.18, s * 0.42,
s * 0.5, s * 0.55,
s * 0.5, s * 0.85,
s * 0.28, s * 0.70
);
facetBottomLeft.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#40c4ff", 0.6)),
new Stop(1, Color.web("#0288d1", 0.2))
));
Polygon facetBottomRight = new Polygon(
s * 0.82, s * 0.42,
s * 0.5, s * 0.55,
s * 0.5, s * 0.85,
s * 0.72, s * 0.70
);
facetBottomRight.setFill(new LinearGradient(
1, 0, 0, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#e1f5fe", 0.75)),
new Stop(1, Color.web("#29b6f6", 0.3))
));
Circle glow = new Circle(s * 0.5, s * 0.5, s * 0.12);
glow.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#ffffff", 0.9)),
new Stop(1, Color.web("#ffffff", 0.0))
));
Group crystal = new Group(bg, facetTop, facetBottomLeft, facetBottomRight, glow);
DropShadow shadow = new DropShadow();
shadow.setColor(Color.web("#0d47a1", 0.4));
shadow.setRadius(12);
shadow.setOffsetY(4);
crystal.setEffect(shadow);
root.getChildren().add(crystal);
return root;
}
/**
* Ícone de Assinar PDF.
*/
public static Node pdfIcon() {
return AssetManager.getIconOrDefault("pdf", 38, SvgIcons::buildPdfIcon);
}
public static Node buildPdfIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#d32f2f"));
doc.setStrokeWidth(1.8);
SVGPath fold = new SVGPath();
fold.setContent("M 24 2 L 24 10 L 32 10 Z");
fold.setFill(Color.web("#ffcdd2"));
fold.setStroke(Color.web("#d32f2f"));
fold.setStrokeWidth(1.2);
Rectangle tag = new Rectangle(20, 10);
tag.setArcWidth(4);
tag.setArcHeight(4);
tag.setFill(Color.web("#e53935"));
tag.setTranslateY(4);
Text txt = new Text("PDF");
txt.setFont(Font.font("Segoe UI", FontWeight.BOLD, 7.5));
txt.setFill(Color.WHITE);
txt.setTranslateY(4);
box.getChildren().addAll(doc, fold, tag, txt);
return box;
}
/**
* Ícone de Assinar XML.
*/
public static Node xmlIcon() {
return AssetManager.getIconOrDefault("xml", 38, SvgIcons::buildXmlIcon);
}
public static Node buildXmlIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#1976d2"));
doc.setStrokeWidth(1.8);
SVGPath fold = new SVGPath();
fold.setContent("M 24 2 L 24 10 L 32 10 Z");
fold.setFill(Color.web("#bbdefb"));
fold.setStroke(Color.web("#1976d2"));
fold.setStrokeWidth(1.2);
Rectangle tag = new Rectangle(22, 10);
tag.setArcWidth(4);
tag.setArcHeight(4);
tag.setFill(Color.web("#0288d1"));
tag.setTranslateY(4);
Text txt = new Text("<XML>");
txt.setFont(Font.font("Segoe UI", FontWeight.BOLD, 6.8));
txt.setFill(Color.WHITE);
txt.setTranslateY(4);
box.getChildren().addAll(doc, fold, tag, txt);
return box;
}
/**
* Ícone de Assinar Arquivo genérico.
*/
public static Node fileIcon() {
return AssetManager.getIconOrDefault("file", 38, SvgIcons::buildFileIcon);
}
public static Node buildFileIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#0288d1"));
doc.setStrokeWidth(1.8);
SVGPath fold = new SVGPath();
fold.setContent("M 24 2 L 24 10 L 32 10 Z");
fold.setFill(Color.web("#b3e5fc"));
fold.setStroke(Color.web("#0288d1"));
fold.setStrokeWidth(1.2);
SVGPath pen = new SVGPath();
pen.setContent("M 10 24 L 22 12 L 26 16 L 14 28 L 8 29 Z");
pen.setFill(Color.web("#0288d1"));
box.getChildren().addAll(doc, fold, pen);
return box;
}
/**
* Ícone de Assinar em Lote.
*/
public static Node batchIcon() {
return AssetManager.getIconOrDefault("batch", 38, SvgIcons::buildBatchIcon);
}
public static Node buildBatchIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc3 = new SVGPath();
doc3.setContent("M 12 2 L 30 2 L 30 26 L 12 26 Z");
doc3.setFill(Color.web("#e1f5fe"));
doc3.setStroke(Color.web("#4fc3f7"));
doc3.setStrokeWidth(1.2);
doc3.setTranslateX(4);
doc3.setTranslateY(-4);
SVGPath doc2 = new SVGPath();
doc2.setContent("M 8 6 L 28 6 L 28 30 L 8 30 Z");
doc2.setFill(Color.web("#b3e5fc"));
doc2.setStroke(Color.web("#0288d1"));
doc2.setStrokeWidth(1.4);
doc2.setTranslateX(2);
doc2.setTranslateY(-2);
SVGPath doc1 = new SVGPath();
doc1.setContent("M 4 10 L 24 10 L 24 34 L 4 34 Z");
doc1.setFill(Color.web("#ffffff"));
doc1.setStroke(Color.web("#01579b"));
doc1.setStrokeWidth(1.6);
SVGPath check = new SVGPath();
check.setContent("M 8 22 L 13 27 L 21 16");
check.setStroke(Color.web("#00c853"));
check.setStrokeWidth(2.2);
check.setFill(Color.TRANSPARENT);
box.getChildren().addAll(doc3, doc2, doc1, check);
return box;
}
/**
* Ícone de Assinatura Atachada.
*/
public static Node attachedIcon() {
return AssetManager.getIconOrDefault("attached", 38, SvgIcons::buildAttachedIcon);
}
public static Node buildAttachedIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 8 4 L 28 4 L 28 34 L 8 34 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#0288d1"));
doc.setStrokeWidth(1.6);
SVGPath lines = new SVGPath();
lines.setContent("M 12 12 L 24 12 M 12 17 L 24 17 M 12 22 L 20 22");
lines.setStroke(Color.web("#90caf9"));
lines.setStrokeWidth(1.4);
SVGPath clip = new SVGPath();
clip.setContent("M 10 2 C 7 2 6 5 6 8 L 6 20 C 6 23 8 25 11 25 C 14 25 16 23 16 20 L 16 9 C 16 7 15 6 13 6 C 11 6 10 7 10 9 L 10 19");
clip.setFill(Color.TRANSPARENT);
clip.setStroke(Color.web("#546e7a"));
clip.setStrokeWidth(1.8);
clip.setTranslateX(-8);
clip.setTranslateY(-4);
box.getChildren().addAll(doc, lines, clip);
return box;
}
/**
* Ícone de Co-Assinar.
*/
public static Node coSignIcon() {
return AssetManager.getIconOrDefault("co-sign", 38, SvgIcons::buildCoSignIcon);
}
public static Node buildCoSignIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc1 = new SVGPath();
doc1.setContent("M 4 8 L 22 8 L 22 32 L 4 32 Z");
doc1.setFill(Color.web("#ffffff"));
doc1.setStroke(Color.web("#1976d2"));
doc1.setStrokeWidth(1.5);
SVGPath doc2 = new SVGPath();
doc2.setContent("M 14 4 L 32 4 L 32 28 L 14 28 Z");
doc2.setFill(Color.web("#e3f2fd"));
doc2.setStroke(Color.web("#0288d1"));
doc2.setStrokeWidth(1.5);
SVGPath people = new SVGPath();
people.setContent("M 10 26 C 10 22 14 22 16 22 C 18 22 22 22 22 26 M 16 17 C 14.5 17 14.5 14 16 14 C 17.5 14 17.5 17 16 17");
people.setStroke(Color.web("#0d47a1"));
people.setStrokeWidth(1.6);
people.setFill(Color.TRANSPARENT);
box.getChildren().addAll(doc2, doc1, people);
return box;
}
/**
* Ícone de Carimbo do Tempo.
*/
public static Node timestampIcon() {
return AssetManager.getIconOrDefault("timestamp", 38, SvgIcons::buildTimestampIcon);
}
public static Node buildTimestampIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 2 L 24 2 L 32 10 L 32 34 L 6 34 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#0288d1"));
doc.setStrokeWidth(1.5);
Circle clockBg = new Circle(18, 24, 9);
clockBg.setFill(Color.web("#e1f5fe"));
clockBg.setStroke(Color.web("#0288d1"));
clockBg.setStrokeWidth(1.6);
SVGPath clockHands = new SVGPath();
clockHands.setContent("M 18 19 L 18 24 L 22 24");
clockHands.setStroke(Color.web("#01579b"));
clockHands.setStrokeWidth(1.6);
clockHands.setFill(Color.TRANSPARENT);
box.getChildren().addAll(doc, clockBg, clockHands);
return box;
}
/**
* Ícone de Assinatura de Longa Duração (LTV).
*/
public static Node ltvIcon() {
return AssetManager.getIconOrDefault("ltv", 38, SvgIcons::buildLtvIcon);
}
public static Node buildLtvIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 2 L 24 2 L 32 10 L 32 34 L 6 34 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#00838f"));
doc.setStrokeWidth(1.5);
Circle shield = new Circle(20, 24, 8);
shield.setFill(Color.web("#e0f2f1"));
shield.setStroke(Color.web("#00897b"));
shield.setStrokeWidth(1.6);
Text ltv = new Text("LTV");
ltv.setFont(Font.font("Segoe UI", FontWeight.BOLD, 6.8));
ltv.setFill(Color.web("#00695c"));
ltv.setTranslateX(2);
ltv.setTranslateY(6);
box.getChildren().addAll(doc, shield, ltv);
return box;
}
/**
* Ícone de Validar Assinatura.
*/
public static Node validateIcon() {
return AssetManager.getIconOrDefault("validate", 38, SvgIcons::buildValidateIcon);
}
public static Node buildValidateIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath doc = new SVGPath();
doc.setContent("M 6 4 L 26 4 L 26 34 L 6 34 Z");
doc.setFill(Color.web("#ffffff"));
doc.setStroke(Color.web("#0288d1"));
doc.setStrokeWidth(1.5);
SVGPath check = new SVGPath();
check.setContent("M 8 20 L 16 28 L 32 10");
check.setStroke(Color.web("#2e7d32"));
check.setStrokeWidth(2.6);
check.setFill(Color.TRANSPARENT);
box.getChildren().addAll(doc, check);
return box;
}
/**
* Ícone de Validar Carimbo do Tempo.
*/
public static Node validateTimestampIcon() {
return AssetManager.getIconOrDefault("validate-timestamp", 38, SvgIcons::buildValidateTimestampIcon);
}
public static Node buildValidateTimestampIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
Circle clock = new Circle(16, 16, 12);
clock.setFill(Color.web("#f1f8e9"));
clock.setStroke(Color.web("#558b2f"));
clock.setStrokeWidth(1.6);
SVGPath check = new SVGPath();
check.setContent("M 10 16 L 15 21 L 24 11");
check.setStroke(Color.web("#2e7d32"));
check.setStrokeWidth(2.2);
check.setFill(Color.TRANSPARENT);
box.getChildren().addAll(clock, check);
return box;
}
/**
* Ícone de Sair do Agente.
*/
public static Node exitIcon() {
return AssetManager.getIconOrDefault("exit", 38, SvgIcons::buildExitIcon);
}
public static Node buildExitIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath door = new SVGPath();
door.setContent("M 18 6 L 8 6 C 6.5 6 6 7.5 6 9 L 6 27 C 6 28.5 6.5 30 8 30 L 18 30");
door.setStroke(Color.web("#546e7a"));
door.setStrokeWidth(2);
door.setFill(Color.TRANSPARENT);
SVGPath arrow = new SVGPath();
arrow.setContent("M 14 18 L 30 18 M 24 12 L 30 18 L 24 24");
arrow.setStroke(Color.web("#d32f2f"));
arrow.setStrokeWidth(2);
arrow.setFill(Color.TRANSPARENT);
box.getChildren().addAll(door, arrow);
return box;
}
/**
* Ícone de Logs.
*/
public static Node logsIcon() {
return AssetManager.getIconOrDefault("logs", 24, SvgIcons::buildLogsIcon);
}
public static Node buildLogsIcon() {
StackPane box = new StackPane();
box.setPrefSize(24, 24);
Text code = new Text("</>");
code.setFont(Font.font("Segoe UI", FontWeight.BOLD, 12));
code.setFill(Color.web("#0288d1"));
box.getChildren().add(code);
return box;
}
/**
* Ícone de Configurações.
*/
public static Node settingsIcon() {
return AssetManager.getIconOrDefault("settings", 24, SvgIcons::buildSettingsIcon);
}
public static Node buildSettingsIcon() {
StackPane box = new StackPane();
box.setPrefSize(24, 24);
SVGPath gear = new SVGPath();
gear.setContent("M 12 1 C 11 1 10.5 2 10.5 3 C 9.5 3.3 8.6 3.8 7.8 4.5 L 6.2 3.6 C 5.5 3.2 4.6 3.5 4.1 4.1 L 2.6 6.6 C 2.1 7.2 2.3 8.1 3 8.6 L 4.4 9.6 C 4.2 10.4 4.1 11.2 4.1 12 C 4.1 12.8 4.2 13.6 4.4 14.4 L 3 15.4 C 2.3 15.9 2.1 16.8 2.6 17.4 L 4.1 19.9 C 4.6 20.5 5.5 20.8 6.2 20.4 L 7.8 19.5 C 8.6 20.2 9.5 20.7 10.5 21 C 10.5 22 11 23 12 23 C 13 23 13.5 22 13.5 21 C 14.5 20.7 15.4 20.2 16.2 19.5 L 17.8 20.4 C 18.5 20.8 19.4 20.5 19.9 19.9 L 21.4 17.4 C 21.9 16.8 21.7 15.9 21 15.4 L 19.6 14.4 C 19.8 13.6 19.9 12.8 19.9 12 C 19.9 11.2 19.8 10.4 19.6 9.6 L 21 8.6 C 21.7 8.1 21.9 7.2 21.4 6.6 L 19.9 4.1 C 19.4 3.5 18.5 3.2 17.8 3.6 L 16.2 4.5 C 15.4 3.8 14.5 3.3 13.5 3 C 13.5 2 13 1 12 1 Z M 12 8 C 14.2 8 16 9.8 16 12 C 16 14.2 14.2 16 12 16 C 9.8 16 8 14.2 8 12 C 8 9.8 9.8 8 12 8 Z");
gear.setFill(Color.web("#0288d1"));
gear.setScaleX(0.85);
gear.setScaleY(0.85);
box.getChildren().add(gear);
return box;
}
/**
* Ícone de Tutorial.
*/
public static Node tutorialIcon() {
return AssetManager.getIconOrDefault("tutorial", 24, SvgIcons::buildTutorialIcon);
}
public static Node buildTutorialIcon() {
StackPane box = new StackPane();
box.setPrefSize(24, 24);
SVGPath monitor = new SVGPath();
monitor.setContent("M 2 4 L 22 4 L 22 17 L 2 17 Z M 8 17 L 6 21 L 18 21 L 16 17 M 12 17 L 12 21");
monitor.setStroke(Color.web("#0288d1"));
monitor.setStrokeWidth(1.6);
monitor.setFill(Color.TRANSPARENT);
box.getChildren().add(monitor);
return box;
}
/**
* Ícone de Certificados.
*/
public static Node certificatesIcon() {
return AssetManager.getIconOrDefault("cert", 24, SvgIcons::buildCertificatesIcon);
}
public static Node buildCertificatesIcon() {
StackPane box = new StackPane();
box.setPrefSize(24, 24);
SVGPath folder = new SVGPath();
folder.setContent("M 2 5 L 8 5 L 10 7 L 22 7 L 22 20 L 2 20 Z");
folder.setFill(Color.web("#e1f5fe"));
folder.setStroke(Color.web("#0288d1"));
folder.setStrokeWidth(1.5);
Rectangle lock = new Rectangle(7, 6);
lock.setArcWidth(2);
lock.setArcHeight(2);
lock.setFill(Color.web("#0288d1"));
lock.setTranslateX(3);
lock.setTranslateY(2);
box.getChildren().addAll(folder, lock);
return box;
}
/**
* Ícone de Sobre.
*/
public static Node aboutIcon() {
return AssetManager.getIconOrDefault("about", 24, SvgIcons::buildAboutIcon);
}
public static Node buildAboutIcon() {
StackPane box = new StackPane();
box.setPrefSize(24, 24);
Circle circle = new Circle(10);
circle.setFill(Color.web("#0288d1"));
Text i = new Text("i");
i.setFont(Font.font("Segoe UI", FontWeight.BOLD, 13));
i.setFill(Color.WHITE);
box.getChildren().addAll(circle, i);
return box;
}
/**
* Ícone de Sol (Light Mode).
*/
public static Node sunIcon(double size) {
return AssetManager.getIconOrDefault("sun", size, () -> buildSunIcon(size));
}
public static Node buildSunIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
Circle sunCore = new Circle(size * 0.5, size * 0.5, size * 0.24);
sunCore.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#f59e0b")),
new Stop(1, Color.web("#d97706"))
));
SVGPath rays = new SVGPath();
rays.setContent("M 12 2 L 12 5 M 12 19 L 12 22 M 2 12 L 5 12 M 19 12 L 22 12 " +
"M 4.93 4.93 L 7.05 7.05 M 16.95 16.95 L 19.07 19.07 " +
"M 4.93 19.07 L 7.05 16.95 M 16.95 7.05 L 19.07 4.93");
rays.setStroke(Color.web("#f59e0b"));
rays.setStrokeWidth(2.0);
rays.setScaleX(size / 24.0);
rays.setScaleY(size / 24.0);
box.getChildren().addAll(rays, sunCore);
return box;
}
/**
* Ícone de Lua (Dark Mode).
*/
public static Node moonIcon(double size) {
return AssetManager.getIconOrDefault("moon", size, () -> buildMoonIcon(size));
}
public static Node buildMoonIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
SVGPath moon = new SVGPath();
moon.setContent("M 21 12.79 A 9 9 0 1 1 11.21 3 A 7 7 0 0 0 21 12.79 Z");
moon.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#38bdf8")),
new Stop(1, Color.web("#818cf8"))
));
moon.setScaleX(size / 24.0);
moon.setScaleY(size / 24.0);
box.getChildren().add(moon);
return box;
}
/**
* Logotipo vetorial CyberShield.
*/
public static Node cyberShieldLogo(double size) {
return AssetManager.getIconOrDefault("shield", size, () -> buildCyberShieldLogo(size));
}
public static Node buildCyberShieldLogo(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
SVGPath shield = new SVGPath();
shield.setContent("M 12 2 L 3 6 L 3 13 C 3 18.5 7 23.5 12 25 C 17 23.5 21 18.5 21 13 L 21 6 Z");
shield.setFill(new LinearGradient(
0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.web("#0284c7")),
new Stop(0.5, Color.web("#0369a1")),
new Stop(1, Color.web("#075985"))
));
shield.setStroke(Color.web("#38bdf8"));
shield.setStrokeWidth(1.2);
shield.setScaleX(size / 26.0);
shield.setScaleY(size / 26.0);
SVGPath key = new SVGPath();
key.setContent("M 12 7 C 10.3 7 9 8.3 9 10 C 9 11.3 9.8 12.4 11 12.8 L 11 18 L 13 18 L 13 15 L 14 15 L 14 13 L 13 13 L 13 12.8 C 14.2 12.4 15 11.3 15 10 C 15 8.3 13.7 7 12 7 Z M 12 9 C 12.6 9 13 9.4 13 10 C 13 10.6 12.6 11 12 11 C 11.4 11 11 10.6 11 10 C 11 9.4 11.4 9 12 9 Z");
key.setFill(Color.web("#ffffff"));
key.setScaleX(size / 26.0);
key.setScaleY(size / 26.0);
box.getChildren().addAll(shield, key);
return box;
}
/**
* Ícone de Gerenciador/Criador de Selos Visuais de Assinatura.
*/
public static Node sealIcon() {
return AssetManager.getIconOrDefault("seal", 38, SvgIcons::buildSealIcon);
}
public static Node buildSealIcon() {
StackPane box = new StackPane();
box.setPrefSize(38, 38);
SVGPath ribbon = new SVGPath();
ribbon.setContent("M 14 26 L 19 36 L 24 26 Z M 9 26 L 12 34 L 15 26 Z");
ribbon.setFill(Color.web("#f59e0b"));
Circle sealCircle = new Circle(13);
sealCircle.setFill(Color.web("#d97706"));
sealCircle.setStroke(Color.web("#fbbf24"));
sealCircle.setStrokeWidth(1.6);
sealCircle.setTranslateY(-4);
SVGPath star = new SVGPath();
star.setContent("M 19 8 L 21 13 L 26 13 L 22 16 L 24 21 L 19 18 L 14 21 L 16 16 L 12 13 L 17 13 Z");
star.setFill(Color.web("#ffffff"));
star.setTranslateX(-9);
star.setTranslateY(-14);
box.getChildren().addAll(ribbon, sealCircle, star);
return box;
}
/**
* Ícone vetorial de Fechar / Cancelar (cruz geométrica moderna e limpa).
*/
public static Node closeIcon() {
return closeIcon(14);
}
public static Node closeIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath cross = new SVGPath();
cross.setContent("M 3.5 3.5 L 12.5 12.5 M 12.5 3.5 L 3.5 12.5");
cross.setStroke(Color.web("#64748b"));
cross.setStrokeWidth(2.0);
cross.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
cross.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
cross.setScaleX(scale);
cross.setScaleY(scale);
box.getChildren().add(cross);
return box;
}
}
@@ -0,0 +1,120 @@
package br.com.cybercore.signer.ui.theme;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import javafx.scene.Parent;
import javafx.scene.Scene;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Gerenciador centralizado de temas visuais do CyberCore Signer.
* Permite alternar dinamicamente entre Tema Claro (Light) e Tema Escuro (Dark)
* aplicando as classes CSS e folhas de estilo correspondentes em qualquer Scene.
*/
public final class ThemeManager {
public enum AppTheme {
LIGHT("theme-light", "Modo Claro"),
DARK("theme-dark", "Modo Escuro");
private final String cssClass;
private final String displayName;
AppTheme(String cssClass, String displayName) {
this.cssClass = cssClass;
this.displayName = displayName;
}
public String getCssClass() {
return cssClass;
}
public String getDisplayName() {
return displayName;
}
}
private static AppTheme currentTheme = AppTheme.LIGHT;
private static final List<Consumer<AppTheme>> listeners = new ArrayList<>();
private ThemeManager() {
}
public static AppTheme getCurrentTheme() {
return currentTheme;
}
public static boolean isDark() {
return currentTheme == AppTheme.DARK;
}
public static void setCurrentTheme(AppTheme theme) {
if (theme == null || theme == currentTheme) {
return;
}
currentTheme = theme;
notifyListeners();
}
public static AppTheme toggleTheme() {
currentTheme = (currentTheme == AppTheme.LIGHT) ? AppTheme.DARK : AppTheme.LIGHT;
notifyListeners();
return currentTheme;
}
/**
* Aplica o tema atualmente ativo a uma Scene, adicionando a folha de estilo global
* e ajustando a classe CSS raiz (theme-light / theme-dark).
*/
public static void applyTheme(Scene scene) {
if (scene == null) {
return;
}
String cssUrl = Objects.requireNonNull(
ThemeManager.class.getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
).toExternalForm();
if (!scene.getStylesheets().contains(cssUrl)) {
scene.getStylesheets().add(cssUrl);
}
Parent root = scene.getRoot();
if (root != null) {
applyThemeToNode(root);
}
}
/**
* Aplica a classe de estilo do tema ativo no nó raiz fornecido.
*/
public static void applyThemeToNode(Parent node) {
if (node == null) {
return;
}
node.getStyleClass().removeAll(AppTheme.LIGHT.getCssClass(), AppTheme.DARK.getCssClass());
node.getStyleClass().add(currentTheme.getCssClass());
}
public static void addListener(Consumer<AppTheme> listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public static void removeListener(Consumer<AppTheme> listener) {
listeners.remove(listener);
}
private static void notifyListeners() {
for (Consumer<AppTheme> listener : new ArrayList<>(listeners)) {
try {
listener.accept(currentTheme);
} catch (Exception ignored) {
}
}
}
}
@@ -0,0 +1,169 @@
package br.com.cybercore.signer.ui.tray;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.RenderingHints;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.image.BufferedImage;
import java.util.Objects;
/**
* Gerencia a integração nativa com o System Tray (Área de Notificação / Bandeja do Windows).
* Permite que o CyberCore Signer permaneça em execução transparente em segundo plano,
* garantindo o atendimento ininterrupto às requisições do protocolo CCSP/1.
*/
public class SystemTrayManager {
private static final Logger logger = LoggerFactory.getLogger(SystemTrayManager.class);
private final Stage stage;
private final Runnable onRefreshCertificates;
private TrayIcon trayIcon;
private boolean initialized = false;
public SystemTrayManager(Stage stage, Runnable onRefreshCertificates) {
this.stage = Objects.requireNonNull(stage, "stage must not be null");
this.onRefreshCertificates = onRefreshCertificates;
}
/**
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
*/
public synchronized boolean setup() {
if (!SystemTray.isSupported()) {
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
return false;
}
try {
SystemTray tray = SystemTray.getSystemTray();
Image iconImage = createTrayIconImage();
PopupMenu popup = new PopupMenu();
MenuItem openItem = new MenuItem("Abrir CyberCore Signer");
openItem.addActionListener(e -> Platform.runLater(this::showStage));
MenuItem refreshItem = new MenuItem("Atualizar Certificados");
refreshItem.addActionListener(e -> {
if (onRefreshCertificates != null) {
onRefreshCertificates.run();
}
});
MenuItem exitItem = new MenuItem("Encerrar Agente");
exitItem.addActionListener(e -> {
logger.info("Encerramento solicitado através do menu da bandeja.");
shutdown();
});
popup.add(openItem);
popup.add(refreshItem);
popup.addSeparator();
popup.add(exitItem);
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
this.trayIcon.setImageAutoSize(true);
// Clique no ícone restaura a janela
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
tray.add(this.trayIcon);
this.initialized = true;
// Intercepta fechamento da janela para ocultar em vez de encerrar
Platform.runLater(() -> {
stage.setOnCloseRequest(event -> {
event.consume();
stage.hide();
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
});
});
logger.info("System Tray configurado com sucesso.");
return true;
} catch (AWTException e) {
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
return false;
} catch (Throwable t) {
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
return false;
}
}
public void showStage() {
if (stage != null) {
stage.show();
stage.setIconified(false);
stage.toFront();
}
}
public void showMessage(String title, String message, TrayIcon.MessageType type) {
if (initialized && trayIcon != null) {
try {
trayIcon.displayMessage(title, message, type);
} catch (Throwable t) {
logger.debug("Não foi possível exibir notificação no Tray: {}", t.getMessage());
}
}
}
public synchronized void remove() {
if (initialized && trayIcon != null) {
try {
SystemTray.getSystemTray().remove(trayIcon);
} catch (Throwable ignored) {
}
initialized = false;
}
}
private void shutdown() {
remove();
Platform.runLater(() -> {
stage.close();
Platform.exit();
System.exit(0);
});
}
/**
* Gera dinamicamente um ícone vetorial de 32x32 com o padrão de cores CyberCore (#38bdf8).
*/
private Image createTrayIconImage() {
int size = 32;
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Fundo em círculo azul escuro CyberCore
g.setColor(new Color(15, 23, 42));
g.fillRoundRect(2, 2, size - 4, size - 4, 8, 8);
// Borda ciano com gradiente
g.setColor(new Color(56, 189, 248));
g.drawRoundRect(2, 2, size - 4, size - 4, 8, 8);
// Letra C central estilizada
g.setColor(new Color(56, 189, 248));
g.setFont(new Font("SansSerif", Font.BOLD, 18));
g.drawString("C", 9, 23);
g.dispose();
return image;
}
}
@@ -0,0 +1,103 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* Diálogo modal para inserção segura da senha do certificado A1 (.pfx / .p12).
*/
public class A1PasswordDialog {
private final Stage stage;
private final CompletableFuture<char[]> passwordFuture = new CompletableFuture<>();
public A1PasswordDialog(Stage owner, String fileName) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Senha do Certificado A1");
this.stage.setResizable(false);
initLayout(fileName);
}
private void initLayout(String fileName) {
VBox root = new VBox(16);
root.getStyleClass().add("dialog-container");
root.setPadding(new Insets(24));
root.setPrefWidth(440);
Label header = new Label("IMPORTAR CERTIFICADO A1");
header.getStyleClass().add("dialog-header");
Label desc = new Label("Digite a senha do arquivo para desbloquear o certificado digital.");
desc.getStyleClass().add("brand-subtitle");
VBox fileBox = new VBox(4);
fileBox.getStyleClass().add("dialog-box");
Label fileTitle = new Label("Arquivo selecionado:");
fileTitle.getStyleClass().add("dialog-field-title");
Label fileNameLabel = new Label(fileName);
fileNameLabel.getStyleClass().add("detail-value");
fileBox.getChildren().addAll(fileTitle, fileNameLabel);
VBox inputGroup = new VBox(8);
Label pwdTitle = new Label("Senha do Certificado:");
pwdTitle.getStyleClass().add("dialog-field-title");
PasswordField passwordField = new PasswordField();
passwordField.setPromptText("Digite a senha...");
inputGroup.getChildren().addAll(pwdTitle, passwordField);
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.setOnAction(e -> {
passwordFuture.complete(null);
stage.close();
});
Button btnConfirm = new Button("Importar");
btnConfirm.getStyleClass().add("btn-primary");
btnConfirm.setDefaultButton(true);
btnConfirm.setOnAction(e -> {
String text = passwordField.getText();
passwordFuture.complete(text != null ? text.toCharArray() : new char[0]);
stage.close();
});
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
root.getChildren().addAll(header, desc, fileBox, inputGroup, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.setOnCloseRequest(e -> passwordFuture.complete(null));
}
public CompletableFuture<char[]> showAndAwait() {
stage.show();
return passwordFuture;
}
}
@@ -0,0 +1,52 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
/**
* Painel informativo Sobre o CyberCore Signer.
*/
@SuppressWarnings("this-escape")
public final class AboutView extends VBox {
public AboutView() {
initView();
}
private void initView() {
setSpacing(16);
setAlignment(Pos.CENTER);
getStyleClass().add("content-pane");
Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME);
brandLabel.getStyleClass().add("brand-title");
brandLabel.setStyle("-fx-font-size: 24px;");
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
versionLabel.getStyleClass().add("status-badge-active");
Label descLabel = new Label("Agente desktop profissional de assinatura digital segura para aplicações web.");
descLabel.getStyleClass().add("section-subtitle");
VBox detailsBox = new VBox(8);
detailsBox.setAlignment(Pos.CENTER);
detailsBox.getStyleClass().add("dialog-box");
Label p1 = new Label("• Criptografia local via JCA/JCE e PKCS#11.");
Label p2 = new Label("• A chave privada e o PIN nunca saem deste computador.");
Label p3 = new Label("• Comunicação restrita à interface local loopback (127.0.0.1).");
Label p4 = new Label("• Proteção contra replay attacks e validação estrita de origem.");
for (Label lbl : new Label[]{p1, p2, p3, p4}) {
lbl.getStyleClass().add("cert-detail");
detailsBox.getChildren().add(lbl);
}
Label copyLabel = new Label("© 2026 CyberCore. Todos os direitos reservados.");
copyLabel.getStyleClass().add("dialog-subtitle");
getChildren().addAll(brandLabel, versionLabel, descLabel, detailsBox, copyLabel);
}
}
@@ -0,0 +1,123 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* Diálogo modal para solicitar autorização explícita do usuário quando uma nova aplicação web
* tenta se conectar ao CyberCore Signer pela primeira vez.
*/
public class ApplicationApprovalDialog {
private final Stage stage;
private final CompletableFuture<Boolean> approvalFuture = new CompletableFuture<>();
public ApplicationApprovalDialog(Stage ownerStage, String origin, String appName, String applicationId) {
stage = new Stage();
stage.initOwner(ownerStage);
stage.initModality(Modality.APPLICATION_MODAL);
stage.initStyle(StageStyle.UTILITY);
stage.setTitle("CyberCore Signer — Solicitação de Conexão Web");
stage.setWidth(520);
stage.setResizable(false);
VBox root = new VBox(18);
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
// Cabeçalho
VBox headerBox = new VBox(4);
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
titleLabel.getStyleClass().add("dialog-header");
Label subtitleLabel = new Label("Um sistema web está tentando se conectar ao agente local CyberCore Signer.");
subtitleLabel.getStyleClass().add("dialog-subtitle");
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
// Painel de Detalhes da Aplicação
VBox detailsBox = new VBox(10);
detailsBox.getStyleClass().add("dialog-box");
VBox appRow = new VBox(2);
Label appTitle = new Label("APLICAÇÃO SOLICITANTE");
appTitle.getStyleClass().add("dialog-field-title");
Label appValue = new Label(appName != null && !appName.isBlank() ? appName : "Aplicação Web Externa");
appValue.getStyleClass().add("dialog-field-value");
appValue.setStyle("-fx-font-weight: bold;");
appRow.getChildren().addAll(appTitle, appValue);
VBox originRow = new VBox(2);
Label originTitle = new Label("ORIGEM WEB (URL)");
originTitle.getStyleClass().add("dialog-field-title");
Label originValue = new Label(origin != null ? origin : "Origem desconhecida");
originValue.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 13px; -fx-font-family: monospace;");
originRow.getChildren().addAll(originTitle, originValue);
VBox idRow = new VBox(2);
Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO");
idTitle.getStyleClass().add("dialog-field-title");
Label idValue = new Label(applicationId);
idValue.getStyleClass().add("dialog-field-value");
idValue.setStyle("-fx-font-size: 12px; -fx-font-family: monospace;");
idRow.getChildren().addAll(idTitle, idValue);
detailsBox.getChildren().addAll(appRow, originRow, idRow);
// Aviso de Segurança
Label secWarning = new Label(
"🛡️ Segurança: Aplicações autorizadas podem consultar a lista pública de certificados " +
"instalados e solicitar assinaturas. Nenhuma assinatura será realizada sem a sua confirmação explícita na tela."
);
secWarning.setWrapText(true);
secWarning.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11px; -fx-line-spacing: 2px;");
// Botões de Ação
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnReject = new Button("Recusar Acesso");
btnReject.getStyleClass().add("btn-secondary");
btnReject.setOnAction(e -> {
approvalFuture.complete(false);
stage.close();
});
Button btnAuthorize = new Button("Autorizar Conexão");
btnAuthorize.getStyleClass().add("btn-primary");
btnAuthorize.setDefaultButton(true);
btnAuthorize.setOnAction(e -> {
approvalFuture.complete(true);
stage.close();
});
actionsRow.getChildren().addAll(btnReject, btnAuthorize);
root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
}
public CompletableFuture<Boolean> showAndAwait() {
stage.show();
stage.toFront();
return approvalFuture;
}
}
@@ -0,0 +1,491 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* Diálogo modal para execução de Assinatura Digital em Lote de múltiplos arquivos (PDF, XML e arquivos diversos).
*/
@SuppressWarnings("this-escape")
public final class BatchSigningDialog {
private static final Logger logger = LoggerFactory.getLogger(BatchSigningDialog.class);
private final Stage stage;
private final CertificateManager certManager;
private final SigningService signingService;
private final ObservableList<BatchItem> fileItems = FXCollections.observableArrayList();
private TableView<BatchItem> tableView;
private ComboBox<ManagedCertificate> certCombo;
private TextField txtOutputDir;
private ProgressBar progressBar;
private Label lblProgressStatus;
private Button btnStart;
private Button btnAddFiles;
private Button btnClear;
private Button btnChooseOutputDir;
private volatile boolean isRunning = false;
public static class BatchItem {
private final File file;
private final SimpleStringProperty fileName = new SimpleStringProperty();
private final SimpleStringProperty fileSize = new SimpleStringProperty();
private final SimpleStringProperty status = new SimpleStringProperty("Aguardando");
public BatchItem(File file) {
this.file = file;
this.fileName.set(file.getName());
this.fileSize.set(formatSize(file.length()));
}
public File getFile() {
return file;
}
public String getFileName() {
return fileName.get();
}
public SimpleStringProperty fileNameProperty() {
return fileName;
}
public String getFileSize() {
return fileSize.get();
}
public SimpleStringProperty fileSizeProperty() {
return fileSize;
}
public String getStatus() {
return status.get();
}
public void setStatus(String val) {
this.status.set(val);
}
public SimpleStringProperty statusProperty() {
return status;
}
private static String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024));
char pre = "KMGTPE".charAt(exp - 1);
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
}
}
public BatchSigningDialog(Stage owner, CertificateManager certManager, SigningService signingService) {
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.signingService = Objects.requireNonNull(signingService, "SigningService não pode ser nulo.");
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Assinatura Digital em Lote");
this.stage.setResizable(true);
this.stage.setMinWidth(750);
this.stage.setMinHeight(580);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(20));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.batchIcon(),
createHeaderLabel("ASSINATURA DIGITAL EM LOTE", "Assine múltiplos arquivos de uma só vez utilizando seu Certificado Digital ICP-Brasil")
);
// Barra de Ação de Arquivos
HBox fileActions = new HBox(10);
fileActions.setAlignment(Pos.CENTER_LEFT);
btnAddFiles = new Button("Adicionar Arquivos...");
btnAddFiles.getStyleClass().add("btn-primary");
btnAddFiles.setOnAction(e -> handleAddFiles());
btnClear = new Button("Limpar Lista");
btnClear.getStyleClass().add("btn-secondary");
btnClear.setOnAction(e -> {
if (!isRunning) fileItems.clear();
});
fileActions.getChildren().addAll(btnAddFiles, btnClear);
// Tabela de Arquivos
tableView = new TableView<>(fileItems);
VBox.setVgrow(tableView, Priority.ALWAYS);
TableColumn<BatchItem, String> colName = new TableColumn<>("Nome do Arquivo");
colName.setCellValueFactory(c -> c.getValue().fileNameProperty());
colName.setPrefWidth(320);
TableColumn<BatchItem, String> colSize = new TableColumn<>("Tamanho");
colSize.setCellValueFactory(c -> c.getValue().fileSizeProperty());
colSize.setPrefWidth(120);
TableColumn<BatchItem, String> colStatus = new TableColumn<>("Status");
colStatus.setCellValueFactory(c -> c.getValue().statusProperty());
tableView.getColumns().add(colName);
tableView.getColumns().add(colSize);
tableView.getColumns().add(colStatus);
tableView.setPlaceholder(new Label("Nenhum arquivo adicionado. Clique em 'Adicionar Arquivos...' para selecionar."));
// Seção de Configuração do Certificado e Destino
VBox configBox = new VBox(10);
configBox.getStyleClass().add("dialog-box");
// Seleção do Certificado
HBox certRow = new HBox(10);
certRow.setAlignment(Pos.CENTER_LEFT);
Label lblCert = new Label("Certificado Digital:");
lblCert.getStyleClass().add("dialog-field-title");
lblCert.setStyle("-fx-min-width: 140;");
certCombo = new ComboBox<>();
certCombo.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(certCombo, Priority.ALWAYS);
populateCertificates();
certCombo.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String doc = item.cpf() != null ? "CPF: " + item.cpf() : (item.cnpj() != null ? "CNPJ: " + item.cnpj() : "");
setText(item.commonName() + " (" + item.type() + ") " + doc);
}
}
});
certCombo.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText("Selecione um Certificado Digital...");
} else {
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " • " + item.type());
}
}
});
certRow.getChildren().addAll(lblCert, certCombo);
// Pasta de Saída
HBox outputRow = new HBox(10);
outputRow.setAlignment(Pos.CENTER_LEFT);
Label lblOutput = new Label("Pasta de Destino:");
lblOutput.getStyleClass().add("dialog-field-title");
lblOutput.setStyle("-fx-min-width: 140;");
txtOutputDir = new TextField();
txtOutputDir.setPromptText("Salvar na mesma pasta dos arquivos originais");
txtOutputDir.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(txtOutputDir, Priority.ALWAYS);
btnChooseOutputDir = new Button("Selecionar...");
btnChooseOutputDir.getStyleClass().add("btn-secondary");
btnChooseOutputDir.setOnAction(e -> handleChooseOutputDir());
outputRow.getChildren().addAll(lblOutput, txtOutputDir, btnChooseOutputDir);
configBox.getChildren().addAll(certRow, outputRow);
// Progresso e Ações Finais
VBox progressBox = new VBox(6);
progressBar = new ProgressBar(0);
progressBar.setMaxWidth(Double.MAX_VALUE);
progressBar.setVisible(false);
lblProgressStatus = new Label("");
lblProgressStatus.getStyleClass().add("dialog-subtitle");
progressBox.getChildren().addAll(progressBar, lblProgressStatus);
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button("Fechar");
btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> {
if (!isRunning) stage.close();
});
btnStart = new Button("Iniciar Assinatura em Lote");
btnStart.getStyleClass().add("btn-primary");
btnStart.setOnAction(e -> startBatchSigning());
footer.getChildren().addAll(btnClose, btnStart);
root.getChildren().addAll(header, fileActions, tableView, configBox, progressBox, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void handleAddFiles() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Selecionar Arquivos para Assinatura em Lote");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Todos os Arquivos Suportados", "*.pdf", "*.xml", "*.txt", "*.doc", "*.docx", "*.*"),
new FileChooser.ExtensionFilter("Arquivos PDF (*.pdf)", "*.pdf"),
new FileChooser.ExtensionFilter("Arquivos XML (*.xml)", "*.xml")
);
List<File> selected = fileChooser.showOpenMultipleDialog(stage);
if (selected != null) {
for (File f : selected) {
boolean exists = fileItems.stream().anyMatch(item -> item.getFile().getAbsolutePath().equals(f.getAbsolutePath()));
if (!exists) {
fileItems.add(new BatchItem(f));
}
}
}
}
private void handleChooseOutputDir() {
DirectoryChooser dirChooser = new DirectoryChooser();
dirChooser.setTitle("Selecionar Pasta para Salvar Arquivos Assinados");
File dir = dirChooser.showDialog(stage);
if (dir != null) {
txtOutputDir.setText(dir.getAbsolutePath());
}
}
private void populateCertificates() {
try {
List<ManagedCertificate> certs = certManager.getAvailableCertificates();
certCombo.setItems(FXCollections.observableArrayList(certs));
if (!certs.isEmpty()) {
certCombo.getSelectionModel().select(0);
}
} catch (Exception e) {
logger.warn("Erro ao listar certificados para lote: {}", e.getMessage());
}
}
private void startBatchSigning() {
if (fileItems.isEmpty()) {
showAlert("Nenhum Arquivo", "Por favor, adicione pelo menos um arquivo para assinar.");
return;
}
ManagedCertificate selectedCert = certCombo.getValue();
if (selectedCert == null) {
showAlert("Certificado Não Selecionado", "Selecione o Certificado Digital que será utilizado para as assinaturas.");
return;
}
if (selectedCert.status() != CertificateStatus.AVAILABLE) {
showAlert("Certificado Indisponível", "O certificado selecionado não está pronto ou disponível para assinatura.");
return;
}
isRunning = true;
setControlsDisabled(true);
progressBar.setVisible(true);
progressBar.setProgress(0);
List<BatchItem> itemsToProcess = new ArrayList<>(fileItems);
String customOutDir = txtOutputDir.getText().trim();
Task<Void> batchTask = new Task<>() {
@Override
protected Void call() throws Exception {
int total = itemsToProcess.size();
int successCount = 0;
for (int i = 0; i < total; i++) {
BatchItem item = itemsToProcess.get(i);
final int currentIdx = i + 1;
Platform.runLater(() -> {
item.setStatus("Assinando...");
progressBar.setProgress((double) currentIdx / total);
lblProgressStatus.setText(String.format("Processando arquivo %d de %d: %s", currentIdx, total, item.getFileName()));
});
try {
// Determina local de gravação
Path targetDir = customOutDir.isEmpty()
? item.getFile().toPath().getParent()
: Path.of(customOutDir);
String lowerName = item.getFileName().toLowerCase();
if (lowerName.endsWith(".pdf")) {
// Assinatura PAdES de PDF com selo visual
br.com.cybercore.signer.core.pdf.PdfSigningService pdfService =
new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager);
br.com.cybercore.signer.core.pdf.PdfSignatureOptions options =
br.com.cybercore.signer.core.pdf.PdfSignatureOptions.builder(selectedCert.id())
.visibleSeal(true)
.build();
String outPdfName = item.getFileName().substring(0, item.getFileName().length() - 4) + "_assinado.pdf";
Path dest = targetDir.resolve(outPdfName);
pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
} else if (lowerName.endsWith(".xml")) {
// Assinatura XMLDSig de documento XML
br.com.cybercore.signer.core.xml.XmlSigningService xmlService =
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
br.com.cybercore.signer.core.xml.XmlSignatureOptions.wholeDocument(selectedCert.id(), null);
String outXmlName = item.getFileName().substring(0, item.getFileName().length() - 4) + "_assinado.xml";
Path dest = targetDir.resolve(outXmlName);
xmlService.signXmlFile(item.getFile(), dest.toFile(), options);
} else {
// Assinatura genérica por digest (.p7s)
byte[] fileBytes = Files.readAllBytes(item.getFile().toPath());
byte[] digest = MessageDigest.getInstance("SHA-256").digest(fileBytes);
SigningContext context = new SigningContext(
UUID.randomUUID().toString(),
"cybercore-desktop-batch",
digest,
"SHA256withRSA",
selectedCert.id(),
Instant.now()
);
SigningResult result = signingService.sign(context);
String outFileName = item.getFileName() + ".p7s";
Path dest = targetDir.resolve(outFileName);
Files.write(dest, result.signatureBytes());
}
Platform.runLater(() -> item.setStatus("✓ Assinado com Sucesso"));
successCount++;
} catch (Exception ex) {
logger.error("Falha ao assinar arquivo {}: {}", item.getFileName(), ex.getMessage(), ex);
Platform.runLater(() -> item.setStatus("✗ Erro: " + ex.getMessage()));
}
}
final int finalSuccess = successCount;
Platform.runLater(() -> {
lblProgressStatus.setText(String.format("Lote concluído: %d de %d arquivos assinados com sucesso.", finalSuccess, total));
showAlert("Assinatura em Lote Concluída",
String.format("Processamento finalizado com sucesso!\n\n• Arquivos assinados: %d\n• Total processado: %d", finalSuccess, total));
});
return null;
}
@Override
protected void succeeded() {
isRunning = false;
setControlsDisabled(false);
}
@Override
protected void failed() {
isRunning = false;
setControlsDisabled(false);
showAlert("Erro no Lote", "Ocorreu uma falha durante o processamento do lote: " + getException().getMessage());
}
};
Thread thread = new Thread(batchTask, "Batch-Signer-Worker");
thread.setDaemon(true);
thread.start();
}
private void setControlsDisabled(boolean disabled) {
btnStart.setDisable(disabled);
btnAddFiles.setDisable(disabled);
btnClear.setDisable(disabled);
certCombo.setDisable(disabled);
btnChooseOutputDir.setDisable(disabled);
}
private void showAlert(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.initOwner(stage);
if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) {
ThemeManager.applyTheme(alert.getDialogPane().getScene());
}
alert.showAndWait();
}
private VBox createHeaderLabel(String title, String subtitle) {
VBox box = new VBox(3);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
Label s = new Label(subtitle);
s.getStyleClass().add("dialog-subtitle");
box.getChildren().addAll(t, s);
return box;
}
public void show() {
stage.show();
}
public Stage getStage() {
return stage;
}
}
@@ -0,0 +1,213 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Diálogo modal para exibição de detalhes e metadados de um certificado digital.
*/
public class CertificateDetailsDialog {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault());
private final Stage stage;
private final ManagedCertificate cert;
public CertificateDetailsDialog(Stage owner, ManagedCertificate cert) {
this.cert = Objects.requireNonNull(cert, "Certificado não pode ser nulo.");
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Detalhes do Certificado");
this.stage.setResizable(true);
this.stage.setMinWidth(600);
this.stage.setMinHeight(520);
initLayout();
}
private void initLayout() {
VBox root = new VBox(18);
root.getStyleClass().add("dialog-container");
root.setPadding(new Insets(24));
// Cabeçalho
HBox headerRow = new HBox(12);
headerRow.setAlignment(Pos.CENTER_LEFT);
Label headerTitle = new Label("DETALHES DO CERTIFICADO");
headerTitle.getStyleClass().add("dialog-header");
HBox.setHgrow(headerTitle, Priority.ALWAYS);
Label badge = new Label(cert.type() == CertificateType.A1 ? "A1 • SOFTWARE" : "A3 • HARDWARE/TOKEN");
badge.getStyleClass().add(cert.type() == CertificateType.A1 ? "cert-badge-a1" : "cert-badge-a3");
headerRow.getChildren().addAll(headerTitle, badge);
// Nome do Titular em destaque
VBox titleBox = new VBox(4);
Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias());
nameLabel.getStyleClass().add("cert-title");
nameLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
String docInfo = "";
if (cert.cpf() != null) {
docInfo = "CPF: " + formatCpf(cert.cpf());
} else if (cert.cnpj() != null) {
docInfo = "CNPJ: " + formatCnpj(cert.cnpj());
}
Label docLabel = new Label(docInfo.isEmpty() ? "Alias: " + cert.alias() : docInfo);
docLabel.getStyleClass().add("brand-subtitle");
titleBox.getChildren().addAll(nameLabel, docLabel);
// Grade de Informações Técnicas
GridPane grid = new GridPane();
grid.setHgap(16);
grid.setVgap(10);
grid.setPadding(new Insets(8, 0, 8, 0));
int row = 0;
addField(grid, row++, "Status:", formatStatus(cert));
addField(grid, row++, "Dispositivo / Origem:", cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.providerId());
addField(grid, row++, "Emissor (AC):", extractCn(cert.issuerDn()));
addField(grid, row++, "Válido de:", cert.notBefore() != null ? DATE_TIME_FORMATTER.format(cert.notBefore()) : "N/D");
addField(grid, row++, "Válido até:", formatExpiration(cert.notAfter()));
addField(grid, row++, "Algoritmo da Chave:", cert.algorithm() + " (" + cert.keySize() + " bits)");
addField(grid, row++, "Número de Série:", cert.serialNumber() != null ? cert.serialNumber() : "N/D");
addField(grid, row++, "SHA-1 Thumbprint:", cert.sha1Fingerprint() != null ? formatFingerprint(cert.sha1Fingerprint()) : "N/D");
addField(grid, row++, "SHA-256 Thumbprint:", cert.sha256Fingerprint() != null ? formatFingerprint(cert.sha256Fingerprint()) : "N/D");
VBox detailsBox = new VBox(10);
detailsBox.getStyleClass().add("dialog-box");
detailsBox.getChildren().add(grid);
// Subject DN expansível
VBox dnBox = new VBox(4);
Label dnTitle = new Label("Subject Distinguished Name (DN):");
dnTitle.getStyleClass().add("dialog-field-title");
Label dnValue = new Label(cert.subjectDn());
dnValue.getStyleClass().add("dialog-hash-value");
dnValue.setWrapText(true);
dnBox.getChildren().addAll(dnTitle, dnValue);
detailsBox.getChildren().add(dnBox);
ScrollPane scroll = new ScrollPane(detailsBox);
scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scroll, Priority.ALWAYS);
// Botões de Ação
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> stage.close());
actionsRow.getChildren().add(btnClose);
root.getChildren().addAll(headerRow, titleBox, scroll, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
public void show() {
stage.show();
}
private void addField(GridPane grid, int row, String label, String value) {
Label lbl = new Label(label);
lbl.getStyleClass().add("dialog-field-title");
Label val = new Label(value);
val.getStyleClass().add("detail-value");
val.setWrapText(true);
grid.add(lbl, 0, row);
grid.add(val, 1, row);
}
private String formatStatus(ManagedCertificate c) {
if (c.status() == CertificateStatus.AVAILABLE && c.isCurrentlyValid()) {
return "● Disponível para assinatura";
}
return "● " + c.status().getDescription();
}
private String formatExpiration(Instant notAfter) {
if (notAfter == null) return "N/D";
String dateStr = DATE_TIME_FORMATTER.format(notAfter);
Instant now = Instant.now();
if (now.isAfter(notAfter)) {
return dateStr + " (EXPIRADO)";
}
long daysRemaining = ChronoUnit.DAYS.between(now, notAfter);
return dateStr + " (" + daysRemaining + " dias restantes)";
}
private String formatCpf(String cpf) {
if (cpf != null && cpf.length() == 11) {
return cpf.substring(0, 3) + "." + cpf.substring(3, 6) + "." + cpf.substring(6, 9) + "-" + cpf.substring(9);
}
return cpf;
}
private String formatCnpj(String cnpj) {
if (cnpj != null && cnpj.length() == 14) {
return cnpj.substring(0, 2) + "." + cnpj.substring(2, 5) + "." + cnpj.substring(5, 8) + "/" + cnpj.substring(8, 12) + "-" + cnpj.substring(12);
}
return cnpj;
}
private String extractCn(String dn) {
if (dn == null) return "Desconhecido";
int idx = dn.indexOf("CN=");
if (idx >= 0) {
int end = dn.indexOf(',', idx);
return end > idx ? dn.substring(idx + 3, end).trim() : dn.substring(idx + 3).trim();
}
return dn;
}
private String formatFingerprint(String hex) {
if (hex == null || hex.length() < 4) return hex;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hex.length(); i += 2) {
if (i > 0) sb.append(":");
sb.append(hex, i, Math.min(i + 2, hex.length()));
}
return sb.toString();
}
}
@@ -0,0 +1,192 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import br.com.cybercore.signer.ui.components.CertificateCardNode;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;
/**
* Diálogo de gerenciamento de Certificados Digitais e Arquivos Seguros.
* Permite listar certificados instalados, inspecionar detalhes técnicos (sem teste de assinatura) e importar A1 (.pfx/.p12).
*/
@SuppressWarnings("this-escape")
public final class CertificatesManagerDialog {
private static final Logger logger = LoggerFactory.getLogger(CertificatesManagerDialog.class);
private final Stage stage;
private final CertificateManager certManager;
private final VBox certificatesContainer = new VBox(10);
public CertificatesManagerDialog(Stage owner, CertificateManager certManager) {
this.certManager = Objects.requireNonNull(certManager);
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Certificados Digitais & Arquivos Seguros");
this.stage.setResizable(true);
this.stage.setMinWidth(720);
this.stage.setMinHeight(520);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(20));
root.getStyleClass().add("dialog-container");
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.certificatesIcon(),
createHeaderLabel("CERTIFICADOS DIGITAIS & ARQUIVOS SEGUROS", "Gerencie os certificados ICP-Brasil instalados e importe arquivos A1 (.pfx / .p12)")
);
// Barra de Ações
HBox actions = new HBox(10);
actions.setAlignment(Pos.CENTER_LEFT);
Button btnAddA1 = new Button("Importar A1 (.pfx / .p12)...");
btnAddA1.getStyleClass().add("btn-primary");
btnAddA1.setOnAction(e -> handleImportA1());
Button btnRefresh = new Button("Atualizar Lista");
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> refreshList());
actions.getChildren().addAll(btnAddA1, btnRefresh);
// Container com Scroll
ScrollPane scroll = new ScrollPane(certificatesContainer);
scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scroll, Priority.ALWAYS);
refreshList();
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button("Fechar");
btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
root.getChildren().addAll(header, actions, scroll, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void refreshList() {
certificatesContainer.getChildren().clear();
try {
List<ManagedCertificate> certs = certManager.discoverCertificates();
if (certs.isEmpty()) {
Label emptyLabel = new Label("Nenhum certificado digital identificado.\nConecte seu token USB/Smart Card A3 ou importe um certificado A1 (.pfx/.p12).");
emptyLabel.getStyleClass().add("dialog-subtitle");
emptyLabel.setStyle("-fx-padding: 20;");
certificatesContainer.getChildren().add(emptyLabel);
} else {
for (ManagedCertificate cert : certs) {
CertificateCardNode card = new CertificateCardNode(cert);
card.setOnMouseClicked(e -> {
// Abre os detalhes SEM botão de teste de assinatura
CertificateDetailsDialog details = new CertificateDetailsDialog(stage, cert);
details.show();
});
certificatesContainer.getChildren().add(card);
}
}
} catch (Exception ex) {
logger.error("Erro ao listar certificados: {}", ex.getMessage(), ex);
}
}
private void handleImportA1() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Certificado A1 (.pfx, .p12)");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Certificados PKCS#12 (*.pfx, *.p12)", "*.pfx", "*.p12"));
File file = chooser.showOpenDialog(stage);
if (file != null) {
A1PasswordDialog pwdDialog = new A1PasswordDialog(stage, file.getName());
pwdDialog.showAndAwait().thenAccept(password -> {
if (password == null) return;
try {
byte[] data = Files.readAllBytes(file.toPath());
String providerId = "provider-a1-" + Integer.toHexString(file.getAbsolutePath().hashCode());
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider(providerId, file.getName());
provider.loadFromBytes(data, password);
certManager.registerProvider(provider);
Platform.runLater(this::refreshList);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Certificado Importado");
alert.setHeaderText(null);
alert.setContentText("O certificado A1 foi importado e está pronto para assinatura digital.");
alert.initOwner(stage);
if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) {
ThemeManager.applyTheme(alert.getDialogPane().getScene());
}
alert.showAndWait();
});
} catch (Exception ex) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Falha na Importação");
alert.setHeaderText(null);
alert.setContentText("Não foi possível carregar o certificado A1: " + ex.getMessage());
alert.initOwner(stage);
if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) {
ThemeManager.applyTheme(alert.getDialogPane().getScene());
}
alert.showAndWait();
});
}
});
}
}
private VBox createHeaderLabel(String title, String subtitle) {
VBox box = new VBox(3);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
Label s = new Label(subtitle);
s.getStyleClass().add("dialog-subtitle");
box.getChildren().addAll(t, s);
return box;
}
public void show() {
stage.show();
}
}
@@ -0,0 +1,121 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.diagnostics.DiagnosticReport;
import br.com.cybercore.signer.core.diagnostics.DiagnosticService;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.function.Consumer;
/**
* Painel de Diagnóstico do CyberCore Signer.
*/
@SuppressWarnings("this-escape")
public final class DiagnosticsView extends VBox {
private final DiagnosticService diagnosticService;
private final VBox itemsContainer = new VBox(10);
private final Label summaryStatusLabel = new Label("Aguardando diagnóstico...");
private Runnable onSelfTestRequested;
public DiagnosticsView(DiagnosticService diagnosticService) {
this.diagnosticService = diagnosticService != null ? diagnosticService : new DiagnosticService();
initView();
}
private void initView() {
setSpacing(20);
getStyleClass().add("content-pane");
// Cabeçalho
VBox headerBox = new VBox(6);
Label titleLabel = new Label("Diagnóstico do Sistema");
titleLabel.getStyleClass().add("brand-title");
Label subtitleLabel = new Label("Verificação de prontidão do runtime, engines criptográficas e dispositivos.");
subtitleLabel.getStyleClass().add("brand-subtitle");
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
// Barra de Ações
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_LEFT);
Button btnRun = new Button("Executar Teste Completo");
btnRun.getStyleClass().add("btn-primary");
btnRun.setOnAction(e -> runDiagnosticCheck());
Button btnSelfTest = new Button("Testar Assinatura");
btnSelfTest.getStyleClass().add("btn-secondary");
btnSelfTest.setOnAction(e -> {
if (onSelfTestRequested != null) {
onSelfTestRequested.run();
}
});
summaryStatusLabel.getStyleClass().add("section-subtitle");
HBox.setHgrow(summaryStatusLabel, Priority.ALWAYS);
actionsRow.getChildren().addAll(btnRun, btnSelfTest, summaryStatusLabel);
// Container de Itens
ScrollPane scrollPane = new ScrollPane(itemsContainer);
scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scrollPane, Priority.ALWAYS);
getChildren().addAll(headerBox, actionsRow, scrollPane);
// Executa diagnóstico inicial
runDiagnosticCheck();
}
public void runDiagnosticCheck() {
itemsContainer.getChildren().clear();
DiagnosticReport report = diagnosticService.runDiagnostic();
addDiagnosticRow("Java Runtime", report.javaVersion() + " (" + report.javaVendor() + ")", true);
addDiagnosticRow("Crypto Engine (JCA/JCE)", report.cryptoEngineOk() ? "✓ Algoritmos operacionais" : "✗ Falha nos algoritmos", report.cryptoEngineOk());
addDiagnosticRow("Sistema Operacional", report.osName() + " " + report.osVersion() + " (" + report.osArch() + ")", true);
String winStoreText = report.windowsStoreSupported() ? "✓ Disponível (SunMSCAPI)" : (report.isWindows() ? "Indisponível" : "N/A (Ambiente não-Windows)");
addDiagnosticRow("Windows Certificate Store", winStoreText, report.windowsStoreSupported() || !report.isWindows());
String pkcs11Text = report.pkcs11ModuleAvailable() ? "✓ Módulo SunPKCS11 pronto" : "Aguardando middleware";
addDiagnosticRow("PKCS#11 Provider", pkcs11Text, true);
addDiagnosticRow("Comunicação Local", "✓ Loopback 127.0.0.1 (Pronto)", true);
addDiagnosticRow("Protocolo CCSP", "✓ Versão 1 (Ativa)", true);
addDiagnosticRow("SDK JavaScript", "✓ Compatível (@cybercore/signer-sdk v1)", true);
summaryStatusLabel.setText("Última verificação concluída com sucesso.");
}
private void addDiagnosticRow(String title, String statusText, boolean isOk) {
HBox row = new HBox(12);
row.setAlignment(Pos.CENTER_LEFT);
row.getStyleClass().add("diagnostic-item");
Label lblTitle = new Label(title);
lblTitle.getStyleClass().add("diagnostic-label");
HBox.setHgrow(lblTitle, Priority.ALWAYS);
Label lblStatus = new Label(statusText);
lblStatus.getStyleClass().add(isOk ? "diagnostic-value-ok" : "diagnostic-value-warn");
row.getChildren().addAll(lblTitle, lblStatus);
itemsContainer.getChildren().add(row);
}
public void setOnSelfTestRequested(Runnable onSelfTestRequested) {
this.onSelfTestRequested = onSelfTestRequested;
}
}
@@ -0,0 +1,260 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* Diálogo unificado para assinatura digital individual de documentos genéricos (Arquivos, Atachada e Co-Assinar).
*/
@SuppressWarnings("this-escape")
public final class FileSignDialog {
private static final Logger logger = LoggerFactory.getLogger(FileSignDialog.class);
private final Stage stage;
private final CertificateManager certManager;
private final SigningService signingService;
private final String operationTitle;
private final String fileExtensionFilter;
private final Node icon;
private TextField txtFilePath;
private ComboBox<ManagedCertificate> certCombo;
private File selectedFile;
public FileSignDialog(Stage owner, String operationTitle, String fileExtensionFilter,
Node icon, CertificateManager certManager, SigningService signingService) {
this.certManager = Objects.requireNonNull(certManager);
this.signingService = Objects.requireNonNull(signingService);
this.operationTitle = operationTitle;
this.fileExtensionFilter = fileExtensionFilter;
this.icon = icon;
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — " + operationTitle);
this.stage.setResizable(true);
this.stage.setMinWidth(620);
this.stage.setMinHeight(380);
initLayout();
}
private void initLayout() {
VBox root = new VBox(18);
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
// Topo
HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4);
Label lblTitle = new Label(operationTitle.toUpperCase());
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Selecione o documento e o seu certificado digital para assinar.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
if (icon != null) {
header.getChildren().addAll(icon, titleBox);
} else {
header.getChildren().add(titleBox);
}
// Seleção de Arquivo
VBox fileBox = new VBox(8);
fileBox.getStyleClass().add("dialog-box");
Label lblFile = new Label("Documento a Assinar:");
lblFile.getStyleClass().add("dialog-field-title");
HBox fileRow = new HBox(10);
fileRow.setAlignment(Pos.CENTER_LEFT);
txtFilePath = new TextField();
txtFilePath.setPromptText("Selecione o arquivo...");
txtFilePath.setEditable(false);
HBox.setHgrow(txtFilePath, Priority.ALWAYS);
Button btnBrowse = new Button("Procurar...");
btnBrowse.getStyleClass().add("btn-secondary");
btnBrowse.setOnAction(e -> handleBrowseFile());
fileRow.getChildren().addAll(txtFilePath, btnBrowse);
fileBox.getChildren().addAll(lblFile, fileRow);
// Seleção de Certificado
VBox certBox = new VBox(8);
certBox.getStyleClass().add("dialog-box");
Label lblCert = new Label("Certificado Digital:");
lblCert.getStyleClass().add("dialog-field-title");
certCombo = new ComboBox<>();
certCombo.setMaxWidth(Double.MAX_VALUE);
populateCertificates();
certCombo.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")");
}
}
});
certCombo.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")");
}
}
});
certBox.getChildren().addAll(lblCert, certCombo);
// Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.setOnAction(e -> stage.close());
Button btnSign = new Button("Assinar Documento");
btnSign.getStyleClass().add("btn-primary");
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeSignature());
footer.getChildren().addAll(btnCancel, btnSign);
root.getChildren().addAll(header, fileBox, certBox, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void handleBrowseFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar " + operationTitle);
if ("*.pdf".equalsIgnoreCase(fileExtensionFilter)) {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
} else if ("*.xml".equalsIgnoreCase(fileExtensionFilter)) {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos XML (*.xml)", "*.xml"));
} else {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*"));
}
File file = chooser.showOpenDialog(stage);
if (file != null) {
selectedFile = file;
txtFilePath.setText(file.getAbsolutePath());
}
}
private void populateCertificates() {
List<ManagedCertificate> certs = certManager.getAvailableCertificates();
certCombo.setItems(FXCollections.observableArrayList(certs));
if (!certs.isEmpty()) {
certCombo.getSelectionModel().select(0);
}
}
private void executeSignature() {
if (selectedFile == null || !selectedFile.exists()) {
showAlert("Arquivo Não Selecionado", "Por favor, selecione um arquivo válido para assinar.");
return;
}
ManagedCertificate cert = certCombo.getValue();
if (cert == null || cert.status() != CertificateStatus.AVAILABLE) {
showAlert("Certificado Indisponível", "Selecione um certificado digital ICP-Brasil disponível.");
return;
}
try {
byte[] fileBytes = Files.readAllBytes(selectedFile.toPath());
byte[] digest = MessageDigest.getInstance("SHA-256").digest(fileBytes);
SigningContext context = new SigningContext(
UUID.randomUUID().toString(),
"cybercore-desktop-app",
digest,
"SHA256withRSA",
cert.id(),
Instant.now()
);
SigningResult result = signingService.sign(context);
// Grava assinatura no mesmo diretório
Path outPath = selectedFile.toPath().getParent().resolve(selectedFile.getName() + ".p7s");
Files.write(outPath, result.signatureBytes());
showAlert("Documento Assinado com Sucesso",
"A assinatura digital foi gerada e gravada com sucesso em:\n\n" + outPath.toAbsolutePath());
stage.close();
} catch (Exception ex) {
logger.error("Erro ao assinar documento {}: {}", selectedFile.getName(), ex.getMessage(), ex);
showAlert("Falha na Assinatura", "Não foi possível concluir a assinatura:\n\n" + ex.getMessage());
}
}
private void showAlert(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.initOwner(stage);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
alert.showAndWait();
}
public void show() {
stage.show();
}
}
@@ -0,0 +1,108 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Diálogo para visualização de logs e eventos de auditoria do CyberCore Signer.
*/
@SuppressWarnings("this-escape")
public final class LogsViewerDialog {
private final Stage stage;
private final TextArea txtLogs = new TextArea();
public LogsViewerDialog(Stage owner) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Visualizador de Logs e Auditoria");
this.stage.setResizable(true);
this.stage.setMinWidth(680);
this.stage.setMinHeight(460);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(20));
root.getStyleClass().add("dialog-container");
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.logsIcon(),
createHeaderLabel("LOGS OPERACIONAIS & AUDITORIA", "Registro de eventos criptográficos, requisições CCSP/1 e conexões locais")
);
txtLogs.setEditable(false);
txtLogs.getStyleClass().add("log-console-area");
VBox.setVgrow(txtLogs, Priority.ALWAYS);
loadInitialLogs();
HBox footer = new HBox(10);
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnRefresh = new Button("Atualizar");
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> loadInitialLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose);
root.getChildren().addAll(header, txtLogs, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void loadInitialLogs() {
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" +
"[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" +
"[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" +
"[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\n" +
"[" + now + "] [INFO] Política anti-replay: nonces protegidos em memória.\n" +
"[" + now + "] [INFO] Registro de aplicações web ativo e pronto para autorização segura.\n";
txtLogs.setText(sb);
}
private VBox createHeaderLabel(String title, String subtitle) {
VBox box = new VBox(3);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
Label s = new Label(subtitle);
s.getStyleClass().add("dialog-subtitle");
box.getChildren().addAll(t, s);
return box;
}
public void show() {
stage.show();
}
}
@@ -0,0 +1,316 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Objects;
/**
* Janela principal moderna do CyberCore Signer.
* Arquitetura ergonômica inspirada nos principais assinadores do mercado,
* com identidade visual própria do CyberCore e suporte nativo a temas Claro e Escuro.
*/
@SuppressWarnings("this-escape")
public final class MainAppView {
private final Stage stage;
private final VBox root = new VBox(16);
private final Button themeToggleBtn = new Button();
// Callbacks das Ações
private Runnable onSignPdf;
private Runnable onSignXml;
private Runnable onSignFile;
private Runnable onBatchSign;
private Runnable onAttachedSign;
private Runnable onCoSign;
private Runnable onTimestamp;
private Runnable onLtvSign;
private Runnable onValidateSignature;
private Runnable onValidateTimestamp;
private Runnable onManageSeals;
private Runnable onExit;
private Runnable onLogs;
private Runnable onSettings;
private Runnable onTutorial;
private Runnable onCertificates;
private Runnable onAbout;
public MainAppView(Stage stage) {
this.stage = stage;
initLayout();
}
private void initLayout() {
root.getStyleClass().addAll("cyber-root", "serpro-root");
root.setAlignment(Pos.TOP_CENTER);
// 1. Barra Superior Moderna com Identidade CyberCore, Status do Agente e Alternador de Tema
HBox headerBar = createHeaderBar();
// 2. Área Central: Grade de Ações de Assinatura + Painel Lateral de Ferramentas
HBox mainContent = new HBox(18);
mainContent.setPadding(new Insets(4, 24, 20, 24));
mainContent.setAlignment(Pos.CENTER);
VBox.setVgrow(mainContent, Priority.ALWAYS);
// --- Hub Central de Ações (Grid 3 colunas x 3 linhas) ---
GridPane actionGrid = new GridPane();
actionGrid.setHgap(12);
actionGrid.setVgap(12);
actionGrid.setAlignment(Pos.CENTER);
HBox.setHgrow(actionGrid, Priority.ALWAYS);
// Linha 0: Operações de Assinatura
actionGrid.add(createActionButton(SvgIcons.pdfIcon(), "Assinar\nPDF", () -> trigger(onSignPdf)), 0, 0);
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
// Linha 1: Formatos Avançados, Temporais e Identidade Visual
actionGrid.add(createActionButton(SvgIcons.coSignIcon(), "Co-Assinar\nDocumento", () -> trigger(onCoSign)), 0, 1);
actionGrid.add(createActionButton(SvgIcons.timestampIcon(), "Carimbo\ndo Tempo", () -> trigger(onTimestamp)), 1, 1);
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 2, 1);
// Linha 2: Auditoria, Validação e Saída
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 0, 2);
actionGrid.add(createActionButton(SvgIcons.validateTimestampIcon(), "Validar\nCarimbo", () -> trigger(onValidateTimestamp)), 1, 2);
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 2);
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
VBox sideColumn = new VBox(10);
sideColumn.getStyleClass().add("cyber-side-column");
sideColumn.setPrefWidth(195);
sideColumn.setMinWidth(190);
sideColumn.setMaxWidth(210);
sideColumn.setAlignment(Pos.TOP_CENTER);
sideColumn.getChildren().addAll(
createSideButton(SvgIcons.logsIcon(), "Logs", () -> trigger(onLogs)),
createSideButton(SvgIcons.settingsIcon(), "Configurações", () -> trigger(onSettings)),
createSideButton(SvgIcons.tutorialIcon(), "Tutorial", () -> trigger(onTutorial)),
createSideButton(SvgIcons.certificatesIcon(), "Certificados", () -> trigger(onCertificates)),
createSideButton(SvgIcons.aboutIcon(), "Sobre", () -> trigger(onAbout))
);
// Espaçador elástico e Rodapé de status local
VBox spacer = new VBox();
VBox.setVgrow(spacer, Priority.ALWAYS);
sideColumn.getChildren().add(spacer);
VBox footerBox = new VBox(4);
footerBox.setAlignment(Pos.CENTER);
HBox brandFooter = new HBox(6);
brandFooter.setAlignment(Pos.CENTER);
Node smallLogo = SvgIcons.cyberShieldLogo(16);
Label lblBrandFoot = new Label("CyberCore");
lblBrandFoot.setStyle("-fx-font-weight: bold; -fx-font-size: 11.5px;");
brandFooter.getChildren().addAll(smallLogo, lblBrandFoot);
Label lblVersion = new Label("v" + CyberCoreUiConstants.APP_VERSION);
lblVersion.getStyleClass().addAll("cyber-version-label", "serpro-version-label");
footerBox.getChildren().addAll(brandFooter, lblVersion);
sideColumn.getChildren().add(footerBox);
mainContent.getChildren().addAll(actionGrid, sideColumn);
root.getChildren().addAll(headerBar, mainContent);
Scene scene = new Scene(root, 800, 500);
scene.getStylesheets().add(Objects.requireNonNull(
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
).toExternalForm());
// Configuração e inicialização do Tema
ThemeManager.applyTheme(scene);
updateThemeToggleButton();
ThemeManager.addListener(theme -> updateThemeToggleButton());
stage.setTitle(CyberCoreUiConstants.APP_NAME);
stage.setScene(scene);
stage.setResizable(false);
stage.setMinWidth(780);
stage.setMinHeight(480);
stage.setMaxWidth(820);
stage.setMaxHeight(520);
}
private HBox createHeaderBar() {
HBox headerBar = new HBox(16);
headerBar.getStyleClass().add("cyber-header-bar");
headerBar.setAlignment(Pos.CENTER_LEFT);
// Logotipo e Título
Node brandLogo = SvgIcons.createAppCrystalLogo(38);
VBox titleBox = new VBox(1);
titleBox.setAlignment(Pos.CENTER_LEFT);
HBox brandRow = new HBox(6);
brandRow.setAlignment(Pos.CENTER_LEFT);
Label lblAssinador = new Label("ASSINADOR");
lblAssinador.getStyleClass().addAll("cyber-brand-title", "serpro-header-title");
lblAssinador.setStyle("-fx-font-weight: 300; -fx-opacity: 0.85;");
Label lblSep = new Label("|");
lblSep.setStyle("-fx-font-size: 16px; -fx-opacity: 0.4;");
Label lblBrand = new Label("CyberCore");
lblBrand.getStyleClass().addAll("cyber-brand-title", "serpro-header-brand");
lblBrand.setStyle("-fx-font-weight: bold;");
brandRow.getChildren().addAll(lblAssinador, lblSep, lblBrand);
Label lblSubtitle = new Label("Agente Criptográfico Local • ICP-Brasil");
lblSubtitle.getStyleClass().add("cyber-brand-subtitle");
titleBox.getChildren().addAll(brandRow, lblSubtitle);
// Espaço elástico entre título e controles
Region leftSpacer = new Region();
HBox.setHgrow(leftSpacer, Priority.ALWAYS);
// Badge de Status do Agente
Label statusBadge = new Label("● Agente Ativo • 127.0.0.1:21325");
statusBadge.getStyleClass().add("cyber-status-badge");
// Botão Alternador de Tema (Claro / Escuro)
themeToggleBtn.getStyleClass().add("theme-toggle-btn");
themeToggleBtn.setOnAction(e -> {
ThemeManager.toggleTheme();
if (stage.getScene() != null) {
ThemeManager.applyTheme(stage.getScene());
}
});
headerBar.getChildren().addAll(brandLogo, titleBox, leftSpacer, statusBadge, themeToggleBtn);
return headerBar;
}
private void updateThemeToggleButton() {
boolean dark = ThemeManager.isDark();
HBox content = new HBox(6);
content.setAlignment(Pos.CENTER);
Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15);
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
content.getChildren().addAll(icon, label);
themeToggleBtn.setGraphic(content);
}
private Button createActionButton(Node icon, String text, Runnable onAction) {
Button btn = new Button();
btn.getStyleClass().addAll("cyber-action-card", "serpro-action-btn");
btn.setPrefWidth(162);
btn.setMinWidth(158);
btn.setPrefHeight(68);
btn.setMinHeight(68);
HBox content = new HBox(10);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
lbl.setWrapText(true);
HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) {
content.getChildren().addAll(icon, lbl);
} else {
content.getChildren().add(lbl);
}
btn.setGraphic(content);
btn.setOnAction(e -> {
if (onAction != null) onAction.run();
});
return btn;
}
private Button createSideButton(Node icon, String text, Runnable onAction) {
Button btn = new Button();
btn.getStyleClass().addAll("cyber-side-card", "serpro-side-btn");
btn.setPrefHeight(42);
btn.setMinHeight(42);
btn.setMaxWidth(Double.MAX_VALUE);
HBox content = new HBox(10);
content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text");
lbl.setWrapText(true);
HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) {
content.getChildren().addAll(icon, lbl);
} else {
content.getChildren().add(lbl);
}
btn.setGraphic(content);
btn.setOnAction(e -> {
if (onAction != null) onAction.run();
});
return btn;
}
private void trigger(Runnable r) {
if (r != null) r.run();
}
public void show() {
stage.centerOnScreen();
stage.show();
stage.toFront();
}
public void hide() {
stage.hide();
}
public boolean isShowing() {
return stage.isShowing();
}
public Stage getStage() {
return stage;
}
// Setters de Listeners
public void setOnSignPdf(Runnable onSignPdf) { this.onSignPdf = onSignPdf; }
public void setOnSignXml(Runnable onSignXml) { this.onSignXml = onSignXml; }
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
public void setOnCoSign(Runnable onCoSign) { this.onCoSign = onCoSign; }
public void setOnTimestamp(Runnable onTimestamp) { this.onTimestamp = onTimestamp; }
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
public void setOnValidateTimestamp(Runnable onValidateTimestamp) { this.onValidateTimestamp = onValidateTimestamp; }
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
public void setOnLogs(Runnable onLogs) { this.onLogs = onLogs; }
public void setOnSettings(Runnable onSettings) { this.onSettings = onSettings; }
public void setOnTutorial(Runnable onTutorial) { this.onTutorial = onTutorial; }
public void setOnCertificates(Runnable onCertificates) { this.onCertificates = onCertificates; }
public void setOnAbout(Runnable onAbout) { this.onAbout = onAbout; }
}
@@ -0,0 +1,127 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.ui.components.CertificateCardNode;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.function.Consumer;
/**
* Painel principal (Dashboard) do CyberCore Signer.
*/
@SuppressWarnings("this-escape")
public final class MainDashboardView extends VBox {
private final VBox certsContainer = new VBox(12);
private final Label countLabel = new Label("0 encontrados");
private final boolean devMode;
private Runnable onRefreshRequested;
private Runnable onAddA1Requested;
private Consumer<ManagedCertificate> onCertificateSelected;
public MainDashboardView(boolean devMode) {
this.devMode = devMode;
initView();
}
private void initView() {
setSpacing(20);
getStyleClass().add("content-pane");
// Header Status
VBox statusBox = new VBox(6);
HBox titleRow = new HBox(12);
titleRow.setAlignment(Pos.CENTER_LEFT);
Label titleLabel = new Label("CyberCore Signer");
titleLabel.getStyleClass().add("brand-title");
Label badge = new Label(devMode ? "● MODO DESENVOLVIMENTO" : "● Serviço ativo");
badge.getStyleClass().add(devMode ? "status-badge-dev" : "status-badge-active");
titleRow.getChildren().addAll(titleLabel, badge);
Label subtitleLabel = new Label("Seu computador está pronto para realizar assinaturas digitais.");
subtitleLabel.getStyleClass().add("brand-subtitle");
statusBox.getChildren().addAll(titleRow, subtitleLabel);
// Seção de Certificados
HBox certsHeader = new HBox(12);
certsHeader.setAlignment(Pos.CENTER_LEFT);
Label certsTitle = new Label("Certificados Disponíveis");
certsTitle.getStyleClass().add("section-title");
HBox.setHgrow(certsTitle, Priority.ALWAYS);
countLabel.getStyleClass().add("section-subtitle");
Button btnRefresh = new Button("Atualizar");
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> {
if (onRefreshRequested != null) onRefreshRequested.run();
});
Button btnAddA1 = new Button("+ Adicionar A1");
btnAddA1.getStyleClass().add("btn-primary");
btnAddA1.setOnAction(e -> {
if (onAddA1Requested != null) onAddA1Requested.run();
});
certsHeader.getChildren().addAll(certsTitle, countLabel, btnRefresh, btnAddA1);
// Scroll com lista de cartões
certsContainer.setPadding(new Insets(4, 0, 4, 0));
ScrollPane scrollPane = new ScrollPane(certsContainer);
scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scrollPane, Priority.ALWAYS);
getChildren().addAll(statusBox, certsHeader, scrollPane);
}
public void updateCertificates(List<ManagedCertificate> certificates) {
certsContainer.getChildren().clear();
if (certificates == null || certificates.isEmpty()) {
countLabel.setText("0 encontrados");
Label emptyLabel = new Label("Nenhum certificado detectado no momento.\nConecte um token USB/smart card ou adicione um certificado A1.");
emptyLabel.getStyleClass().add("section-subtitle");
emptyLabel.setPadding(new Insets(30, 0, 0, 0));
certsContainer.getChildren().add(emptyLabel);
return;
}
countLabel.setText(certificates.size() + " encontrado" + (certificates.size() > 1 ? "s" : ""));
for (ManagedCertificate cert : certificates) {
CertificateCardNode card = new CertificateCardNode(cert);
card.setOnMouseClicked(e -> {
if (onCertificateSelected != null) {
onCertificateSelected.accept(cert);
}
});
certsContainer.getChildren().add(card);
}
}
public void setOnRefreshRequested(Runnable onRefreshRequested) {
this.onRefreshRequested = onRefreshRequested;
}
public void setOnAddA1Requested(Runnable onAddA1Requested) {
this.onAddA1Requested = onAddA1Requested;
}
public void setOnCertificateSelected(Consumer<ManagedCertificate> onCertificateSelected) {
this.onCertificateSelected = onCertificateSelected;
}
}
@@ -0,0 +1,643 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.io.ByteArrayInputStream;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Base64;
import java.util.UUID;
import java.util.function.Consumer;
/**
* Diálogo Interativo para Criação, Customização, Importação e Instalação de Selos Digitais.
* Permite ao usuário definir cores, textos, dimensões, opções de CPF (completo, mascarado, oculto),
* data/hora da assinatura e upload de logotipo personalizado, visualizando em tempo real antes de instalar.
*/
@SuppressWarnings("this-escape")
public final class SealDesignerDialog {
private static final Logger logger = LoggerFactory.getLogger(SealDesignerDialog.class);
private final Stage stage;
private final Consumer<PdfSealTheme> onSealInstalled;
private TextField txtName;
private TextField txtCustomHeader;
private TextField txtCustomFooter;
private ColorPicker cpPrimary;
private ColorPicker cpSecondary;
private ColorPicker cpBackground;
private ColorPicker cpBorder;
private ColorPicker cpText;
private ComboBox<String> cbIconStyle;
private String customLogoBase64;
private Label lblLogoStatus;
private Button btnClearLogo;
private TextField txtCustomFieldLabel;
private TextField txtCustomFieldValue;
private CheckBox chkIncludeCpf;
private RadioButton rbCpfMasked;
private RadioButton rbCpfFull;
private CheckBox chkIncludeDateTime;
private ImageView previewImageView;
public SealDesignerDialog(Stage owner, Consumer<PdfSealTheme> onSealInstalled) {
this(owner, null, onSealInstalled);
}
public SealDesignerDialog(Stage owner, PdfSealTheme initialTheme, Consumer<PdfSealTheme> onSealInstalled) {
this.onSealInstalled = onSealInstalled;
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Criador e Instalador de Selos Digitais");
this.stage.setResizable(true);
this.stage.setMinWidth(860);
this.stage.setMinHeight(560);
initLayout();
if (initialTheme != null) {
loadThemeIntoForm(initialTheme);
}
updatePreview();
}
private void initLayout() {
VBox root = new VBox(14);
root.setPadding(new Insets(16));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(3);
Label lblTitle = new Label("CRIADOR E INSTALADOR DE SELOS VISUAIS");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Crie sua identidade visual personalizada para assinaturas em PDF e instale no sistema.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.sealIcon(), titleBox);
// Painel Principal: Formulário à esquerda, Preview ao vivo à direita
HBox mainSplit = new HBox(16);
VBox.setVgrow(mainSplit, Priority.ALWAYS);
// --- Coluna Esquerda: Controles (Formulário) ---
VBox formPane = new VBox(12);
formPane.setPrefWidth(430);
formPane.setMinWidth(400);
ScrollPane scrollForm = new ScrollPane(formPane);
scrollForm.setFitToWidth(true);
VBox.setVgrow(scrollForm, Priority.ALWAYS);
scrollForm.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
HBox.setHgrow(scrollForm, Priority.ALWAYS);
// 1. Identificação do Selo e Campos Adicionais
VBox idBox = new VBox(8);
idBox.getStyleClass().add("dialog-box");
Label lblIdSection = new Label("IDENTIFICAÇÃO DO SELO E TITULAR");
lblIdSection.getStyleClass().add("dialog-field-title");
Label lblName = new Label("Nome do Selo:");
lblName.getStyleClass().add("form-label");
txtName = new TextField("Meu Selo Personalizado");
txtName.setPromptText("Nome do selo (ex: Escritório Silva)");
txtName.textProperty().addListener((obs, oldV, newV) -> updatePreview());
Label lblCustomHeaderTitle = new Label("Texto de Cabeçalho:");
lblCustomHeaderTitle.getStyleClass().add("form-label");
txtCustomHeader = new TextField("MEU ESCRITÓRIO • ASSINADO DIGITALMENTE");
txtCustomHeader.setPromptText("Cabeçalho (ex: MINHA EMPRESA)");
txtCustomHeader.textProperty().addListener((obs, oldV, newV) -> updatePreview());
Label lblCustomFooterTitle = new Label("Texto de Rodapé:");
lblCustomFooterTitle.getStyleClass().add("form-label");
txtCustomFooter = new TextField("Validade jurídica plena conforme MP nº 2.200-2/2001");
txtCustomFooter.setPromptText("Rodapé institucional");
txtCustomFooter.textProperty().addListener((obs, oldV, newV) -> updatePreview());
// Campos Adicionais Customizáveis (Matrícula, Cargo, OAB, etc.)
Label lblCustomFieldSection = new Label("Campo Personalizado Adicional (Opcional - Matrícula, Cargo, OAB):");
lblCustomFieldSection.getStyleClass().add("form-label");
GridPane customFieldGrid = new GridPane();
customFieldGrid.setHgap(10);
customFieldGrid.setVgap(6);
txtCustomFieldLabel = new TextField("Matrícula");
txtCustomFieldLabel.setPromptText("Rótulo (ex: Matrícula, Cargo, OAB)");
txtCustomFieldLabel.setPrefWidth(150);
txtCustomFieldLabel.textProperty().addListener((obs, oldV, newV) -> updatePreview());
txtCustomFieldValue = new TextField("104829");
txtCustomFieldValue.setPromptText("Valor (ex: 104829, Auditor Fiscal)");
GridPane.setHgrow(txtCustomFieldValue, Priority.ALWAYS);
txtCustomFieldValue.textProperty().addListener((obs, oldV, newV) -> updatePreview());
Label lblCustomFieldLabelText = new Label("Rótulo do Campo:");
lblCustomFieldLabelText.getStyleClass().add("form-label");
Label lblCustomFieldValueText = new Label("Valor do Campo:");
lblCustomFieldValueText.getStyleClass().add("form-label");
customFieldGrid.add(lblCustomFieldLabelText, 0, 0);
customFieldGrid.add(txtCustomFieldLabel, 0, 1);
customFieldGrid.add(lblCustomFieldValueText, 1, 0);
customFieldGrid.add(txtCustomFieldValue, 1, 1);
idBox.getChildren().addAll(lblIdSection,
lblName, txtName,
lblCustomHeaderTitle, txtCustomHeader,
lblCustomFooterTitle, txtCustomFooter,
lblCustomFieldSection, customFieldGrid);
// 2. Cores e Emblema / Logotipo Personalizado
VBox styleBox = new VBox(12);
styleBox.getStyleClass().add("dialog-box");
Label lblStyleSection = new Label("CORES E EMBLEMA / LOGOTIPO");
lblStyleSection.getStyleClass().add("dialog-field-title");
GridPane colorGrid = new GridPane();
colorGrid.setHgap(12);
colorGrid.setVgap(10);
ColumnConstraints colLabels = new ColumnConstraints();
colLabels.setMinWidth(130);
colLabels.setPrefWidth(140);
ColumnConstraints colPickers = new ColumnConstraints();
colPickers.setHgrow(Priority.ALWAYS);
colorGrid.getColumnConstraints().addAll(colLabels, colPickers);
cpPrimary = new ColorPicker(Color.web("#0d47a1"));
cpPrimary.setMaxWidth(Double.MAX_VALUE);
cpPrimary.setOnAction(e -> updatePreview());
cpSecondary = new ColorPicker(Color.web("#0284c7"));
cpSecondary.setMaxWidth(Double.MAX_VALUE);
cpSecondary.setOnAction(e -> updatePreview());
cpBackground = new ColorPicker(Color.web("#ffffff"));
cpBackground.setMaxWidth(Double.MAX_VALUE);
cpBackground.setOnAction(e -> updatePreview());
cpBorder = new ColorPicker(Color.web("#cbd5e1"));
cpBorder.setMaxWidth(Double.MAX_VALUE);
cpBorder.setOnAction(e -> updatePreview());
cpText = new ColorPicker(Color.web("#0f172a"));
cpText.setMaxWidth(Double.MAX_VALUE);
cpText.setOnAction(e -> updatePreview());
Label lblCpPrimary = new Label("Cor Primária:");
lblCpPrimary.getStyleClass().add("form-label");
Label lblCpSecondary = new Label("Cor Secundária:");
lblCpSecondary.getStyleClass().add("form-label");
Label lblCpBackground = new Label("Cor de Fundo:");
lblCpBackground.getStyleClass().add("form-label");
Label lblCpBorder = new Label("Cor da Borda:");
lblCpBorder.getStyleClass().add("form-label");
Label lblCpText = new Label("Cor do Texto:");
lblCpText.getStyleClass().add("form-label");
colorGrid.add(lblCpPrimary, 0, 0);
colorGrid.add(cpPrimary, 1, 0);
colorGrid.add(lblCpSecondary, 0, 1);
colorGrid.add(cpSecondary, 1, 1);
colorGrid.add(lblCpBackground, 0, 2);
colorGrid.add(cpBackground, 1, 2);
colorGrid.add(lblCpBorder, 0, 3);
colorGrid.add(cpBorder, 1, 3);
colorGrid.add(lblCpText, 0, 4);
colorGrid.add(cpText, 1, 4);
// Upload de Logotipo ou Emblema Próprio
Label lblLogoHeader = new Label("Upload de Emblema / Logotipo Próprio:");
lblLogoHeader.getStyleClass().add("form-label");
Button btnUploadLogo = new Button("📁 Escolher Imagem (PNG, JPG)...");
btnUploadLogo.getStyleClass().add("btn-secondary");
btnUploadLogo.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(btnUploadLogo, Priority.ALWAYS);
btnUploadLogo.setOnAction(e -> handleUploadLogo());
btnClearLogo = new Button("✖ Remover");
btnClearLogo.getStyleClass().setAll("btn-danger");
btnClearLogo.setMinWidth(Region.USE_PREF_SIZE);
btnClearLogo.setDisable(true);
btnClearLogo.setOnAction(e -> handleClearLogo());
HBox logoBtnRow = new HBox(8, btnUploadLogo, btnClearLogo);
logoBtnRow.setAlignment(Pos.CENTER_LEFT);
lblLogoStatus = new Label("Nenhum logotipo carregado (usando emblema vetorial)");
lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;");
Label lblFallbackIcon = new Label("Emblema Vetorial Alternativo (caso sem logo própria):");
lblFallbackIcon.getStyleClass().add("form-label");
cbIconStyle = new ComboBox<>();
cbIconStyle.getItems().addAll("Escudo de Segurança (ICP-Brasil)", "Cristal Tecnológico (CyberCore)", "Carimbo Notarial (Cartório)", "Brasão Estelar (Governamental)");
cbIconStyle.setValue("Escudo de Segurança (ICP-Brasil)");
cbIconStyle.setMaxWidth(Double.MAX_VALUE);
cbIconStyle.setOnAction(e -> updatePreview());
styleBox.getChildren().addAll(lblStyleSection, colorGrid, lblLogoHeader, logoBtnRow, lblLogoStatus, lblFallbackIcon, cbIconStyle);
// 3. Regras do Usuário: CPF e Data/Hora
VBox rulesBox = new VBox(10);
rulesBox.getStyleClass().add("dialog-box");
Label lblRulesSection = new Label("PRIVACIDADE DO CPF E DATA/HORA");
lblRulesSection.getStyleClass().add("dialog-field-title");
chkIncludeCpf = new CheckBox("Incluir CPF no Selo");
chkIncludeCpf.getStyleClass().add("form-label");
chkIncludeCpf.setSelected(true);
chkIncludeCpf.setOnAction(e -> {
boolean sel = chkIncludeCpf.isSelected();
rbCpfMasked.setDisable(!sel);
rbCpfFull.setDisable(!sel);
updatePreview();
});
ToggleGroup cpfGroup = new ToggleGroup();
rbCpfMasked = new RadioButton("CPF Mascarado (***.456.789-**)");
rbCpfMasked.getStyleClass().add("form-label");
rbCpfMasked.setToggleGroup(cpfGroup);
rbCpfMasked.setSelected(true);
rbCpfMasked.setOnAction(e -> updatePreview());
rbCpfFull = new RadioButton("CPF Completo (123.456.789-00)");
rbCpfFull.getStyleClass().add("form-label");
rbCpfFull.setToggleGroup(cpfGroup);
rbCpfFull.setOnAction(e -> updatePreview());
VBox cpfSubOptions = new VBox(6);
cpfSubOptions.setPadding(new Insets(2, 0, 4, 18));
cpfSubOptions.getChildren().addAll(rbCpfMasked, rbCpfFull);
chkIncludeDateTime = new CheckBox("Incluir Data e Hora da Assinatura");
chkIncludeDateTime.getStyleClass().add("form-label");
chkIncludeDateTime.setSelected(true);
chkIncludeDateTime.setOnAction(e -> updatePreview());
rulesBox.getChildren().addAll(lblRulesSection, chkIncludeCpf, cpfSubOptions, chkIncludeDateTime);
formPane.getChildren().addAll(idBox, styleBox, rulesBox);
// --- Coluna Direita: Live Preview e Ações ---
VBox previewPane = new VBox(12);
previewPane.setAlignment(Pos.TOP_CENTER);
previewPane.setPrefWidth(380);
previewPane.setMinWidth(360);
previewPane.getStyleClass().add("dialog-box");
Label lblPreviewTitle = new Label("PRÉ-VISUALIZAÇÃO EM TEMPO REAL");
lblPreviewTitle.getStyleClass().add("dialog-field-title");
Label lblPreviewNotice = new Label("O selo sempre exibe o nome completo do signatário com destaque e máxima legibilidade.");
lblPreviewNotice.setStyle("-fx-font-size: 11px; -fx-text-fill: #94a3b8;");
lblPreviewNotice.setWrapText(true);
previewImageView = new ImageView();
previewImageView.setPreserveRatio(true);
previewImageView.setFitWidth(360);
HBox previewContainer = new HBox(previewImageView);
previewContainer.setAlignment(Pos.CENTER);
previewContainer.setPadding(new Insets(16, 8, 16, 8));
previewContainer.setStyle("-fx-background-color: #090d16; -fx-background-radius: 8px; -fx-border-color: #334155; -fx-border-radius: 8px;");
VBox previewContentBox = new VBox(10);
previewContentBox.setAlignment(Pos.CENTER);
previewContentBox.getChildren().addAll(lblPreviewTitle, lblPreviewNotice, previewContainer);
VBox.setVgrow(previewContentBox, Priority.ALWAYS);
// Botões de Importar / Exportar
HBox importExportRow = new HBox(10);
importExportRow.setAlignment(Pos.CENTER);
Button btnImport = new Button("📥 Importar Selo...");
btnImport.getStyleClass().add("btn-secondary");
btnImport.setPrefHeight(32);
btnImport.setOnAction(e -> handleImportSeal());
Button btnExport = new Button("📤 Exportar Selo...");
btnExport.getStyleClass().add("btn-secondary");
btnExport.setPrefHeight(32);
btnExport.setOnAction(e -> handleExportSeal());
importExportRow.getChildren().addAll(btnImport, btnExport);
previewPane.getChildren().addAll(previewContentBox, importExportRow);
mainSplit.getChildren().addAll(scrollForm, previewPane);
// Rodapé de Ações Finais (Fixado na base da janela, sempre visível e destacado)
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
footer.setPadding(new Insets(6, 0, 0, 0));
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.setPrefHeight(34);
btnCancel.setOnAction(e -> stage.close());
Button btnInstall = new Button("💾 Instalar Selo no Sistema");
btnInstall.getStyleClass().add("btn-primary");
btnInstall.setPrefHeight(34);
btnInstall.setDefaultButton(true);
btnInstall.setOnAction(e -> handleInstallSeal());
footer.getChildren().addAll(btnCancel, btnInstall);
root.getChildren().addAll(header, mainSplit, footer);
Scene scene = new Scene(root, 880, 580);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void updatePreview() {
Platform.runLater(() -> {
try {
PdfSealTheme theme = buildThemeFromForm("preview-temp");
CpfDisplayMode cpfMode = resolveCpfMode();
PdfSealData sampleData = new PdfSealData(
"Dr. Carlos Alberto de Oliveira Santos Júnior",
"CPF: 12345678900",
null,
"AC CyberCore JUS v5",
"8A7F4B2C91D0E3F5",
"Assinatura de Documento Digital",
"São Paulo, SP",
cpfMode,
chkIncludeDateTime.isSelected()
);
byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, sampleData);
previewImageView.setImage(new Image(new ByteArrayInputStream(pngBytes)));
} catch (Exception ex) {
logger.warn("Erro ao gerar preview de selo: {}", ex.getMessage());
}
});
}
private void handleUploadLogo() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Emblema / Logotipo do Selo");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Imagens (*.png, *.jpg, *.jpeg, *.bmp)", "*.png", "*.jpg", "*.jpeg", "*.bmp"),
new FileChooser.ExtensionFilter("Todos os Arquivos", "*.*")
);
File file = chooser.showOpenDialog(stage);
if (file != null) {
try {
byte[] bytes = Files.readAllBytes(file.toPath());
customLogoBase64 = Base64.getEncoder().encodeToString(bytes);
lblLogoStatus.setText("✓ Imagem carregada: " + file.getName());
lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #16a34a; -fx-font-weight: bold;");
btnClearLogo.setDisable(false);
updatePreview();
} catch (Exception ex) {
showAlert(Alert.AlertType.ERROR, "Erro ao Carregar Imagem", "Não foi possível carregar o arquivo de imagem: " + ex.getMessage());
}
}
}
private void handleClearLogo() {
customLogoBase64 = null;
lblLogoStatus.setText("Nenhum logotipo carregado (usando emblema vetorial)");
lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;");
btnClearLogo.setDisable(true);
updatePreview();
}
private PdfSealTheme buildThemeFromForm(String themeId) {
String name = txtName.getText().trim();
if (name.isBlank()) name = "Meu Selo Personalizado";
String icon = switch (cbIconStyle.getValue()) {
case "Cristal Tecnológico (CyberCore)" -> "crystal";
case "Carimbo Notarial (Cartório)" -> "notary";
case "Brasão Estelar (Governamental)" -> "star";
default -> "shield";
};
return new PdfSealTheme(
themeId,
name,
"Selo personalizado instalado pelo usuário",
320,
82,
toHex(cpPrimary.getValue()),
toHex(cpSecondary.getValue()),
toHex(cpBackground.getValue()),
toHex(cpBorder.getValue()),
toHex(cpText.getValue()),
null,
txtCustomHeader.getText().trim(),
txtCustomFooter.getText().trim(),
icon,
resolveCpfMode(),
chkIncludeDateTime.isSelected(),
true,
customLogoBase64,
txtCustomFieldLabel.getText().trim(),
txtCustomFieldValue.getText().trim()
);
}
private void loadThemeIntoForm(PdfSealTheme theme) {
if (theme == null) return;
if (theme.name() != null) txtName.setText(theme.name());
if (theme.customHeader() != null) txtCustomHeader.setText(theme.customHeader());
if (theme.customFooter() != null) txtCustomFooter.setText(theme.customFooter());
if (theme.customFieldLabel() != null) txtCustomFieldLabel.setText(theme.customFieldLabel());
if (theme.customFieldValue() != null) txtCustomFieldValue.setText(theme.customFieldValue());
if (theme.primaryColor() != null) cpPrimary.setValue(Color.web(theme.primaryColor()));
if (theme.secondaryColor() != null) cpSecondary.setValue(Color.web(theme.secondaryColor()));
if (theme.backgroundColor() != null) cpBackground.setValue(Color.web(theme.backgroundColor()));
if (theme.borderColor() != null) cpBorder.setValue(Color.web(theme.borderColor()));
if (theme.textColor() != null) cpText.setValue(Color.web(theme.textColor()));
if (theme.defaultCpfMode() != null) {
chkIncludeCpf.setSelected(theme.defaultCpfMode() != CpfDisplayMode.NONE);
if (theme.defaultCpfMode() == CpfDisplayMode.MASKED) {
rbCpfMasked.setSelected(true);
} else if (theme.defaultCpfMode() == CpfDisplayMode.FULL) {
rbCpfFull.setSelected(true);
}
rbCpfMasked.setDisable(!chkIncludeCpf.isSelected());
rbCpfFull.setDisable(!chkIncludeCpf.isSelected());
}
chkIncludeDateTime.setSelected(theme.defaultIncludeDateTime());
if (theme.customLogoBase64() != null && !theme.customLogoBase64().isBlank()) {
this.customLogoBase64 = theme.customLogoBase64();
lblLogoStatus.setText("✓ Logotipo carregado com sucesso!");
lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #16a34a; -fx-font-weight: bold;");
btnClearLogo.setDisable(false);
} else {
this.customLogoBase64 = null;
lblLogoStatus.setText("Nenhum logotipo carregado (usando emblema vetorial)");
lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;");
btnClearLogo.setDisable(true);
}
if (theme.iconStyle() != null) {
switch (theme.iconStyle().toLowerCase()) {
case "crystal" -> cbIconStyle.setValue("Cristal Tecnológico (CyberCore)");
case "notary" -> cbIconStyle.setValue("Carimbo Notarial (Cartório)");
case "star" -> cbIconStyle.setValue("Brasão Estelar (Governamental)");
default -> cbIconStyle.setValue("Escudo de Segurança (ICP-Brasil)");
}
}
}
private CpfDisplayMode resolveCpfMode() {
if (!chkIncludeCpf.isSelected()) {
return CpfDisplayMode.NONE;
}
return rbCpfMasked.isSelected() ? CpfDisplayMode.MASKED : CpfDisplayMode.FULL;
}
private void handleInstallSeal() {
String name = txtName.getText().trim();
if (name.isBlank()) {
showAlert(Alert.AlertType.WARNING, "Nome Obrigatório", "Por favor, defina um nome para o seu selo.");
return;
}
String sealId = "seal-" + UUID.randomUUID().toString().substring(0, 8);
PdfSealTheme theme = buildThemeFromForm(sealId);
try {
PdfSealThemeRegistry.installCustomTheme(theme);
showAlert(Alert.AlertType.INFORMATION, "Selo Instalado com Sucesso!",
"O selo '" + theme.name() + "' foi instalado com sucesso e já está disponível para uso imediato em todas as assinaturas.");
if (onSealInstalled != null) {
onSealInstalled.accept(theme);
}
stage.close();
} catch (IOException e) {
showAlert(Alert.AlertType.ERROR, "Erro ao Instalar Selo", "Falha ao gravar arquivo de selo no disco: " + e.getMessage());
}
}
private void handleExportSeal() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Exportar Selo Digital (.json)");
chooser.setInitialFileName("meu-selo.json");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json"));
File file = chooser.showSaveDialog(stage);
if (file != null) {
try {
PdfSealTheme theme = buildThemeFromForm("exported-seal");
PdfSealThemeRegistry.getStorage().exportSeal(theme, file.toPath());
showAlert(Alert.AlertType.INFORMATION, "Selo Exportado", "O selo foi exportado com sucesso para:\n" + file.getAbsolutePath());
} catch (Exception ex) {
showAlert(Alert.AlertType.ERROR, "Erro na Exportação", "Falha ao exportar selo: " + ex.getMessage());
}
}
}
private void handleImportSeal() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Arquivo de Selo (.json)");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json"));
File file = chooser.showOpenDialog(stage);
if (file != null) {
try {
PdfSealTheme imported = PdfSealThemeRegistry.getStorage().importSeal(file.toPath());
PdfSealThemeRegistry.registerTheme(imported);
loadThemeIntoForm(imported);
updatePreview();
showAlert(Alert.AlertType.INFORMATION, "Selo Importado e Instalado",
"O selo '" + imported.name() + "' foi importado com sucesso e está pronto para uso!");
if (onSealInstalled != null) {
onSealInstalled.accept(imported);
}
stage.close();
} catch (Exception ex) {
showAlert(Alert.AlertType.ERROR, "Erro na Importação", "Falha ao importar selo do arquivo: " + ex.getMessage());
}
}
}
private void showAlert(Alert.AlertType type, String title, String msg) {
Alert a = new Alert(type);
a.setTitle(title);
a.setHeaderText(null);
a.setContentText(msg);
a.initOwner(stage);
ThemeManager.applyTheme(a.getDialogPane().getScene());
a.showAndWait();
}
private static String toHex(Color c) {
return String.format("#%02X%02X%02X",
(int) (c.getRed() * 255),
(int) (c.getGreen() * 255),
(int) (c.getBlue() * 255));
}
public void show() {
stage.centerOnScreen();
stage.show();
}
}
@@ -0,0 +1,498 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.config.AgentConfiguration;
import br.com.cybercore.signer.core.config.AgentConfigurationService;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Diálogo Central de Gerenciamento e Seleção de Selos Digitais.
* Exibe a lista completa de selos disponíveis (embutidos e personalizados),
* permite selecionar o selo padrão ativo de assinatura, visualizar em tempo real,
* criar novos selos, editar/clonar, excluir, importar e exportar.
*/
@SuppressWarnings("this-escape")
public final class SealManagementDialog {
private static final Logger logger = LoggerFactory.getLogger(SealManagementDialog.class);
private final Stage stage;
private final AgentConfigurationService configService;
private ListView<PdfSealTheme> listViewSeals;
private ImageView previewImageView;
private Label lblSelectedName;
private Label lblSelectedDesc;
private Label lblActiveBadge;
private Button btnSetDefault;
private Button btnEditClone;
private Button btnDelete;
private Button btnExport;
private Label lblFeedback;
private String currentDefaultThemeId;
public SealManagementDialog(Stage owner) {
this.configService = new AgentConfigurationService();
AgentConfiguration currentConfig = configService.loadConfiguration();
this.currentDefaultThemeId = currentConfig.defaultSealThemeId();
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Gerenciamento e Seleção de Selos Digitais");
this.stage.setResizable(true);
this.stage.setMinWidth(840);
this.stage.setMinHeight(520);
initLayout();
refreshSealsList();
}
private void initLayout() {
VBox root = new VBox(14);
root.setPadding(new Insets(18));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(3);
Label lblTitle = new Label("GERENCIAMENTO DE SELOS DIGITAIS");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Escolha o selo padrão ativo para assinar PDFs, crie novas identidades visuais ou personalize selos instalados.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.sealIcon(), titleBox);
// Painel Principal Dividido: Lista à esquerda, Detalhes e Preview à direita
HBox mainContent = new HBox(16);
VBox.setVgrow(mainContent, Priority.ALWAYS);
// --- Coluna Esquerda: Lista de Selos ---
VBox leftColumn = new VBox(10);
leftColumn.setPrefWidth(330);
leftColumn.setMinWidth(300);
leftColumn.getStyleClass().add("dialog-box");
HBox listHeader = new HBox(8);
listHeader.setAlignment(Pos.CENTER_LEFT);
Label lblListTitle = new Label("Selos Disponíveis no Sistema");
lblListTitle.getStyleClass().add("dialog-field-title");
listHeader.getChildren().add(lblListTitle);
listViewSeals = new ListView<>();
VBox.setVgrow(listViewSeals, Priority.ALWAYS);
listViewSeals.setStyle("-fx-background-color: transparent; -fx-background-radius: 6px;");
listViewSeals.setCellFactory(lv -> new SealListCell());
listViewSeals.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> onThemeSelected(newV));
HBox listActionRow = new HBox(8);
listActionRow.setAlignment(Pos.CENTER_LEFT);
Button btnCreateNew = new Button("+ Novo Selo");
btnCreateNew.getStyleClass().add("btn-primary");
btnCreateNew.setOnAction(e -> handleCreateNewSeal());
HBox.setHgrow(btnCreateNew, Priority.ALWAYS);
btnCreateNew.setMaxWidth(Double.MAX_VALUE);
Button btnImport = new Button("📥 Importar");
btnImport.getStyleClass().add("btn-secondary");
btnImport.setOnAction(e -> handleImportSeal());
HBox.setHgrow(btnImport, Priority.ALWAYS);
btnImport.setMaxWidth(Double.MAX_VALUE);
listActionRow.getChildren().addAll(btnCreateNew, btnImport);
leftColumn.getChildren().addAll(listHeader, listViewSeals, listActionRow);
// --- Coluna Direita: Pré-Visualização e Ações do Selo Selecionado ---
VBox rightColumn = new VBox(10);
rightColumn.setPrefWidth(430);
rightColumn.setMinWidth(380);
rightColumn.getStyleClass().add("dialog-box");
rightColumn.setAlignment(Pos.TOP_CENTER);
HBox.setHgrow(rightColumn, Priority.ALWAYS);
Label lblPreviewHeader = new Label("PRÉ-VISUALIZAÇÃO EM TEMPO REAL");
lblPreviewHeader.getStyleClass().add("dialog-field-title");
lblSelectedName = new Label("Selecione um selo para visualizar");
lblSelectedName.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
lblSelectedDesc = new Label("");
lblSelectedDesc.setStyle("-fx-font-size: 11.5px; -fx-text-fill: #64748b;");
lblSelectedDesc.setWrapText(true);
previewImageView = new ImageView();
previewImageView.setPreserveRatio(true);
previewImageView.setFitWidth(380);
HBox previewContainer = new HBox(previewImageView);
previewContainer.setAlignment(Pos.CENTER);
previewContainer.setPadding(new Insets(14, 10, 14, 10));
previewContainer.setStyle("-fx-background-color: #090d16; -fx-background-radius: 8px; -fx-border-color: #334155; -fx-border-radius: 8px;");
VBox.setVgrow(previewContainer, Priority.ALWAYS);
lblActiveBadge = new Label("");
lblActiveBadge.setStyle("-fx-font-weight: bold; -fx-font-size: 12px;");
lblFeedback = new Label("");
lblFeedback.setStyle("-fx-font-size: 12px; -fx-font-weight: bold; -fx-text-fill: #16a34a;");
// Botões de Ação do Selo Selecionado
VBox sealActions = new VBox(8);
sealActions.setAlignment(Pos.CENTER);
btnSetDefault = new Button("✔ Definir como Selo Padrão Ativo");
btnSetDefault.getStyleClass().add("btn-primary");
btnSetDefault.setMaxWidth(Double.MAX_VALUE);
btnSetDefault.setPrefHeight(34);
btnSetDefault.setOnAction(e -> handleSetDefault());
HBox secondaryActions = new HBox(8);
secondaryActions.setAlignment(Pos.CENTER);
btnEditClone = new Button("✏ Clonar");
btnEditClone.getStyleClass().add("btn-secondary");
btnEditClone.setPrefHeight(32);
HBox.setHgrow(btnEditClone, Priority.ALWAYS);
btnEditClone.setMaxWidth(Double.MAX_VALUE);
btnEditClone.setOnAction(e -> handleEditClone());
btnExport = new Button("📥 Exportar");
btnExport.getStyleClass().add("btn-secondary");
btnExport.setPrefHeight(32);
HBox.setHgrow(btnExport, Priority.ALWAYS);
btnExport.setMaxWidth(Double.MAX_VALUE);
btnExport.setOnAction(e -> handleExportSeal());
btnDelete = new Button("✖ Excluir");
btnDelete.getStyleClass().addAll("button", "btn-danger");
btnDelete.setAlignment(Pos.CENTER);
btnDelete.setPrefHeight(32);
HBox.setHgrow(btnDelete, Priority.ALWAYS);
btnDelete.setMaxWidth(Double.MAX_VALUE);
btnDelete.setOnAction(e -> handleDeleteCustomSeal());
secondaryActions.getChildren().addAll(btnEditClone, btnExport, btnDelete);
sealActions.getChildren().addAll(btnSetDefault, secondaryActions);
rightColumn.getChildren().addAll(lblPreviewHeader, lblSelectedName, lblSelectedDesc, previewContainer, lblActiveBadge, lblFeedback, sealActions);
mainContent.getChildren().addAll(leftColumn, rightColumn);
// Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
root.getChildren().addAll(header, mainContent, footer);
Scene scene = new Scene(root, 880, 560);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void refreshSealsList() {
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
listViewSeals.setItems(FXCollections.observableArrayList(themes));
// Seleciona o selo padrão ativo ou o primeiro disponível
PdfSealTheme toSelect = themes.stream()
.filter(t -> t.id().equalsIgnoreCase(currentDefaultThemeId))
.findFirst()
.orElse(themes.isEmpty() ? null : themes.get(0));
if (toSelect != null) {
listViewSeals.getSelectionModel().select(toSelect);
}
}
private void onThemeSelected(PdfSealTheme theme) {
lblFeedback.setText("");
if (theme == null) {
lblSelectedName.setText("Nenhum selo selecionado");
lblSelectedDesc.setText("");
lblActiveBadge.setText("");
btnSetDefault.setDisable(true);
btnEditClone.setDisable(true);
btnDelete.setDisable(true);
btnExport.setDisable(true);
previewImageView.setImage(null);
return;
}
lblSelectedName.setText(theme.name() + " (" + theme.id() + ")");
lblSelectedDesc.setText(theme.description() != null ? theme.description() : "Selo visual para assinatura");
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultThemeId);
if (isDefault) {
lblActiveBadge.setText("★ ESTE É O SELO PADRÃO ATIVO PARA TODAS AS ASSINATURAS");
lblActiveBadge.setStyle("-fx-text-fill: #38bdf8; -fx-font-weight: bold; -fx-font-size: 11.5px;");
btnSetDefault.setText("★ Selo Padrão Ativo no Sistema");
btnSetDefault.setDisable(true);
btnSetDefault.setStyle("-fx-opacity: 0.95; -fx-background-color: #0369a1; -fx-text-fill: #ffffff; -fx-font-weight: bold;");
} else {
lblActiveBadge.setText("Selo disponível para uso.");
lblActiveBadge.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 11.5px;");
btnSetDefault.setText("✔ Definir como Selo Padrão Ativo");
btnSetDefault.setDisable(false);
btnSetDefault.setStyle("");
}
btnEditClone.setDisable(false);
btnExport.setDisable(false);
btnDelete.setDisable(!theme.isCustom());
renderThemePreview(theme);
}
private void renderThemePreview(PdfSealTheme theme) {
Platform.runLater(() -> {
try {
PdfSealData sampleData = new PdfSealData(
"Dr. Carlos Alberto de Oliveira Santos Júnior",
"CPF: 12345678900",
null,
"AC CyberCore JUS v5",
"8A7F4B2C91D0E3F5",
"Assinatura de Documento Digital",
"São Paulo, SP",
theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL,
theme.defaultIncludeDateTime(),
theme.formatCustomField()
);
byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, sampleData);
previewImageView.setImage(new Image(new ByteArrayInputStream(pngBytes)));
} catch (Exception ex) {
logger.warn("Erro ao renderizar preview do selo: {}", ex.getMessage());
}
});
}
private void handleSetDefault() {
PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem();
if (selected == null) return;
try {
AgentConfiguration currentConfig = configService.loadConfiguration();
AgentConfiguration newConfig = new AgentConfiguration(
currentConfig.requireConfirmation(),
currentConfig.alwaysPromptPin(),
currentConfig.autoStart(),
selected.id(),
currentConfig.defaultCpfDisplayMode(),
currentConfig.defaultIncludeDateTime(),
currentConfig.serverPort()
);
configService.saveConfiguration(newConfig);
this.currentDefaultThemeId = selected.id();
lblFeedback.setText("✓ Selo '" + selected.name() + "' definido como padrão com sucesso!");
listViewSeals.refresh();
onThemeSelected(selected);
} catch (Exception ex) {
showError("Falha ao salvar configuração: " + ex.getMessage());
}
}
private void handleCreateNewSeal() {
new SealDesignerDialog(stage, createdSeal -> {
refreshSealsList();
listViewSeals.getSelectionModel().select(createdSeal);
lblFeedback.setText("✓ Novo selo '" + createdSeal.name() + "' instalado!");
}).show();
}
private void handleEditClone() {
PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem();
if (selected == null) return;
new SealDesignerDialog(stage, selected, updatedSeal -> {
refreshSealsList();
listViewSeals.getSelectionModel().select(updatedSeal);
lblFeedback.setText("✓ Selo '" + updatedSeal.name() + "' salvo com sucesso!");
}).show();
}
private void handleDeleteCustomSeal() {
PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem();
if (selected == null || !selected.isCustom()) return;
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Confirmar Exclusão");
confirm.setHeaderText(null);
confirm.setContentText("Deseja realmente remover o selo personalizado '" + selected.name() + "'?");
confirm.initOwner(stage);
ThemeManager.applyTheme(confirm.getDialogPane().getScene());
Optional<ButtonType> result = confirm.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
try {
PdfSealThemeRegistry.uninstallCustomTheme(selected.id());
if (selected.id().equalsIgnoreCase(currentDefaultThemeId)) {
currentDefaultThemeId = PdfSealThemeRegistry.DEFAULT_THEME_ID;
}
refreshSealsList();
} catch (Exception ex) {
showError("Falha ao excluir selo: " + ex.getMessage());
}
}
}
private void handleImportSeal() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Arquivo de Selo (.json)");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json"));
File file = chooser.showOpenDialog(stage);
if (file != null) {
try {
PdfSealTheme imported = PdfSealThemeRegistry.getStorage().importSeal(file.toPath());
PdfSealThemeRegistry.registerTheme(imported);
refreshSealsList();
listViewSeals.getSelectionModel().select(imported);
lblFeedback.setText("✓ Selo '" + imported.name() + "' importado e instalado com sucesso!");
} catch (Exception ex) {
showError("Falha ao importar selo: " + ex.getMessage());
}
}
}
private void handleExportSeal() {
PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem();
if (selected == null) return;
FileChooser chooser = new FileChooser();
chooser.setTitle("Exportar Selo Digital (.json)");
chooser.setInitialFileName(selected.id() + ".json");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json"));
File file = chooser.showSaveDialog(stage);
if (file != null) {
try {
PdfSealThemeRegistry.getStorage().exportSeal(selected, file.toPath());
lblFeedback.setText("✓ Selo exportado com sucesso para " + file.getName());
} catch (Exception ex) {
showError("Falha ao exportar selo: " + ex.getMessage());
}
}
}
private void showError(String msg) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setTitle("Erro");
a.setHeaderText(null);
a.setContentText(msg);
a.initOwner(stage);
ThemeManager.applyTheme(a.getDialogPane().getScene());
a.showAndWait();
}
public void show() {
stage.centerOnScreen();
stage.show();
}
public void showAndWait() {
stage.centerOnScreen();
stage.showAndWait();
}
/**
* Célula customizada para a listagem visual dos selos.
*/
private class SealListCell extends ListCell<PdfSealTheme> {
@Override
protected void updateItem(PdfSealTheme item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
setText(null);
} else {
HBox card = new HBox(10);
card.setAlignment(Pos.CENTER_LEFT);
card.setPadding(new Insets(8, 10, 8, 10));
VBox textBox = new VBox(2);
Label lblName = new Label(item.name());
lblName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px;");
lblName.maxWidthProperty().bind(listViewSeals.widthProperty().subtract(120));
lblName.setWrapText(true);
String desc = (item.description() != null && !item.description().isBlank())
? item.description() : "Identidade visual de assinatura";
Label lblDesc = new Label(desc);
lblDesc.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;");
lblDesc.maxWidthProperty().bind(listViewSeals.widthProperty().subtract(120));
lblDesc.setWrapText(true);
textBox.getChildren().addAll(lblName, lblDesc);
HBox.setHgrow(textBox, Priority.ALWAYS);
VBox badgesBox = new VBox(4);
badgesBox.setAlignment(Pos.CENTER_RIGHT);
boolean isDefault = item.id().equalsIgnoreCase(currentDefaultThemeId);
if (isDefault) {
Label badgeDefault = new Label("★ PADRÃO");
badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-font-size: 9.5px; -fx-font-weight: bold; -fx-padding: 2px 6px; -fx-background-radius: 4px;");
badgesBox.getChildren().add(badgeDefault);
}
Label badgeType = new Label(item.isCustom() ? "Personalizado" : "Sistema");
badgeType.setStyle(item.isCustom()
? "-fx-background-color: #fef3c7; -fx-text-fill: #b45309; -fx-font-size: 9.5px; -fx-padding: 2px 6px; -fx-background-radius: 4px;"
: "-fx-background-color: #e0f2fe; -fx-text-fill: #0369a1; -fx-font-size: 9.5px; -fx-padding: 2px 6px; -fx-background-radius: 4px;");
badgesBox.getChildren().add(badgeType);
card.getChildren().addAll(textBox, badgesBox);
setGraphic(card);
setText(null);
}
}
}
}
@@ -0,0 +1,296 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.config.AgentConfiguration;
import br.com.cybercore.signer.core.config.AgentConfigurationService;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import br.com.cybercore.signer.core.security.AuthorizedApplication;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.security.SmartCardSessionManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.List;
/**
* Painel de Configurações, Gerenciamento de Selos Visuais e Aplicações Autorizadas.
*/
@SuppressWarnings("this-escape")
public final class SettingsView extends VBox {
private final VBox appsContainer = new VBox(10);
private final VBox sealsContainer = new VBox(8);
private final Label saveFeedbackLabel = new Label();
private java.util.function.Consumer<String> onRevokeApplication;
public SettingsView() {
initView();
}
private void initView() {
setSpacing(16);
setPadding(new Insets(20, 24, 20, 24));
getStyleClass().add("content-pane");
AgentConfigurationService configService = AgentConfigurationService.getInstance();
AgentConfiguration config = configService.getConfig();
// Header
VBox headerBox = new VBox(6);
Label titleLabel = new Label("Configurações do Agente");
titleLabel.getStyleClass().add("brand-title");
Label subtitleLabel = new Label("Parâmetros de segurança local, identidade visual dos selos e aplicações autorizadas.");
subtitleLabel.getStyleClass().add("brand-subtitle");
saveFeedbackLabel.setStyle("-fx-text-fill: #10b981; -fx-font-size: 11.5px; -fx-font-weight: bold;");
headerBox.getChildren().addAll(titleLabel, subtitleLabel, saveFeedbackLabel);
// Seção Segurança
VBox secBox = new VBox(12);
secBox.getStyleClass().add("dialog-box");
secBox.setPadding(new Insets(14, 16, 14, 16));
Label secTitle = new Label("Segurança e Confirmação");
secTitle.getStyleClass().add("section-title");
CheckBox chkConfirmAlways = new CheckBox("Sempre exigir confirmação na tela antes de assinar (Recomendado)");
chkConfirmAlways.setSelected(config.requireConfirmation());
chkConfirmAlways.setOnAction(e -> {
boolean sel = chkConfirmAlways.isSelected();
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
sel,
current.alwaysPromptPin(),
current.autoStart(),
current.defaultSealThemeId(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
notifySave("Preferência de confirmação na tela salva.");
});
CheckBox chkAlwaysAskPin = new CheckBox("Sempre exigir digitação de PIN para Tokens A3 (Sem cache)");
chkAlwaysAskPin.setSelected(config.alwaysPromptPin());
PinSessionManager.getInstance().setAlwaysPromptPin(config.alwaysPromptPin());
chkAlwaysAskPin.setOnAction(e -> {
boolean sel = chkAlwaysAskPin.isSelected();
PinSessionManager.getInstance().setAlwaysPromptPin(sel);
if (sel) {
SmartCardSessionManager.resetCardSessions();
}
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
current.requireConfirmation(),
sel,
current.autoStart(),
current.defaultSealThemeId(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
notifySave("Política de PIN atualizada.");
});
CheckBox chkAutoStart = new CheckBox("Iniciar agente automaticamente com o sistema operacional");
chkAutoStart.setSelected(config.autoStart());
chkAutoStart.setOnAction(e -> {
boolean sel = chkAutoStart.isSelected();
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
current.requireConfirmation(),
current.alwaysPromptPin(),
sel,
current.defaultSealThemeId(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
notifySave("Preferência de inicialização salva.");
});
HBox pinActionsBox = new HBox(12);
pinActionsBox.setAlignment(Pos.CENTER_LEFT);
Button btnClearPins = new Button("Limpar PINs Salvos na Sessão");
btnClearPins.getStyleClass().add("btn-secondary");
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
Label pinStatusLabel = new Label();
pinStatusLabel.setStyle("-fx-text-fill: #34d399; -fx-font-size: 12px;");
btnClearPins.setOnAction(e -> {
PinSessionManager.getInstance().clearAll();
SmartCardSessionManager.resetCardSessions();
pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados.");
});
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
secBox.getChildren().addAll(secTitle, chkConfirmAlways, chkAlwaysAskPin, chkAutoStart, pinActionsBox);
// Seção Selos Visuais de Assinatura
VBox sealsBox = new VBox(10);
sealsBox.getStyleClass().add("dialog-box");
sealsBox.setPadding(new Insets(14, 16, 14, 16));
Label sealsTitle = new Label("Selos Visuais e Identidade Gráfica");
sealsTitle.getStyleClass().add("section-title");
HBox sealHeaderRow = new HBox(12);
sealHeaderRow.setAlignment(Pos.CENTER_LEFT);
Label lblSealsDesc = new Label("Gerencie os temas e identidades gráficas instaladas para assinaturas PDF:");
lblSealsDesc.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 12px;");
HBox.setHgrow(lblSealsDesc, Priority.ALWAYS);
Button btnCreateSeal = new Button("🎨 Criar ou Instalar Selo...");
btnCreateSeal.getStyleClass().add("btn-primary");
btnCreateSeal.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
btnCreateSeal.setOnAction(e -> openSealDesigner());
sealHeaderRow.getChildren().addAll(lblSealsDesc, btnCreateSeal);
refreshSealsList();
sealsBox.getChildren().addAll(sealsTitle, sealHeaderRow, sealsContainer);
// Seção Aplicações Autorizadas
VBox appsBox = new VBox(10);
appsBox.getStyleClass().add("dialog-box");
appsBox.setPadding(new Insets(14, 16, 14, 16));
Label appsTitle = new Label("Aplicações Web Autorizadas");
appsTitle.getStyleClass().add("section-title");
ScrollPane scrollApps = new ScrollPane(appsContainer);
scrollApps.setFitToWidth(true);
scrollApps.setPrefHeight(150);
scrollApps.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
appsBox.getChildren().addAll(appsTitle, scrollApps);
getChildren().addAll(headerBox, secBox, sealsBox, appsBox);
}
private void openSealDesigner() {
Stage ownerStage = (getScene() != null && getScene().getWindow() instanceof Stage s) ? s : null;
new SealDesignerDialog(ownerStage, newSeal -> {
refreshSealsList();
notifySave("Novo selo '" + newSeal.name() + "' instalado com sucesso!");
}).show();
}
private void refreshSealsList() {
sealsContainer.getChildren().clear();
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
for (PdfSealTheme theme : themes) {
HBox item = new HBox(10);
item.setAlignment(Pos.CENTER_LEFT);
item.getStyleClass().add("diagnostic-item");
item.setPadding(new Insets(6, 12, 6, 12));
VBox info = new VBox(2);
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
lblName.getStyleClass().add("cert-title");
String desc = theme.description() != null ? theme.description() : "Selo visual";
Label lblDesc = new Label(desc);
lblDesc.getStyleClass().add("cert-detail");
info.getChildren().addAll(lblName, lblDesc);
HBox.setHgrow(info, Priority.ALWAYS);
Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
item.getChildren().addAll(info, badge);
if (theme.isCustom()) {
Button btnDelete = new Button("Excluir");
btnDelete.getStyleClass().add("btn-secondary");
btnDelete.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px; -fx-text-fill: #f87171;");
btnDelete.setOnAction(e -> {
try {
PdfSealThemeRegistry.uninstallCustomTheme(theme.id());
refreshSealsList();
notifySave("Selo '" + theme.name() + "' removido.");
} catch (Exception ex) {
showError("Falha ao remover selo: " + ex.getMessage());
}
});
item.getChildren().add(btnDelete);
}
sealsContainer.getChildren().add(item);
}
}
private void notifySave(String text) {
saveFeedbackLabel.setText("✓ " + text);
}
private void showError(String msg) {
Alert a = new Alert(Alert.AlertType.ERROR);
a.setTitle("Erro");
a.setHeaderText(null);
a.setContentText(msg);
a.showAndWait();
}
public void setOnRevokeApplication(java.util.function.Consumer<String> onRevokeApplication) {
this.onRevokeApplication = onRevokeApplication;
}
public void updateApplications(List<AuthorizedApplication> applications) {
appsContainer.getChildren().clear();
if (applications == null || applications.isEmpty()) {
Label emptyLabel = new Label("Nenhuma aplicação autorizada registrada ainda.\nNovas aplicações solicitarão permissão no primeiro acesso.");
emptyLabel.getStyleClass().add("section-subtitle");
appsContainer.getChildren().add(emptyLabel);
return;
}
for (AuthorizedApplication app : applications) {
HBox card = new HBox(12);
card.setAlignment(Pos.CENTER_LEFT);
card.getStyleClass().add("diagnostic-item");
VBox info = new VBox(4);
Label nameLabel = new Label(app.name() + " (" + app.applicationId() + ")");
nameLabel.getStyleClass().add("cert-title");
Label originsLabel = new Label("Origens: " + String.join(", ", app.allowedOrigins()));
originsLabel.getStyleClass().add("cert-detail");
info.getChildren().addAll(nameLabel, originsLabel);
HBox.setHgrow(info, Priority.ALWAYS);
Label statusLabel = new Label(app.enabled() ? "Autorizada" : "Revogada");
statusLabel.getStyleClass().add(app.enabled() ? "diagnostic-value-ok" : "diagnostic-value-warn");
Button btnRevoke = new Button("Revogar");
btnRevoke.getStyleClass().add("btn-secondary");
btnRevoke.setStyle("-fx-padding: 4px 10px; -fx-font-size: 11px; -fx-text-fill: #f87171;");
btnRevoke.setOnAction(e -> {
if (onRevokeApplication != null) {
onRevokeApplication.accept(app.applicationId());
}
});
card.getChildren().addAll(info, statusLabel, btnRevoke);
appsContainer.getChildren().add(card);
}
}
}
@@ -0,0 +1,259 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.HexFormat;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* Diálogo modal de autorização explícita do usuário para assinatura digital,
* com gerenciamento flexível de PIN para tokens A3 (sempre pedir PIN vs salvar na sessão).
*/
public class SignaturePromptDialog {
private final Stage stage;
private final CompletableFuture<Boolean> approvalFuture = new CompletableFuture<>();
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
ManagedCertificate certificate, SigningContext context) {
this(owner, applicationName, origin, certificate, context, null, null, false);
}
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
ManagedCertificate certificate, SigningContext context,
String documentName, String documentType, boolean isAuthentication) {
stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
stage.initOwner(owner);
}
stage.setTitle(isAuthentication
? "CyberCore Signer — Solicitação de Login / Autenticação"
: "CyberCore Signer — Solicitação de Assinatura");
stage.setResizable(false);
initLayout(applicationName, origin, certificate, context, documentName, documentType, isAuthentication);
}
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
String documentName, String documentType, boolean isAuthentication) {
VBox root = new VBox(18);
root.getStyleClass().add("dialog-container");
root.setPrefWidth(540);
Label header = new Label(isAuthentication ? "🔑 SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA");
header.getStyleClass().add("dialog-header");
Label desc = new Label(isAuthentication
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
desc.getStyleClass().add("brand-subtitle");
// Caixa de Detalhes
VBox detailsBox = new VBox(12);
detailsBox.getStyleClass().add("dialog-box");
GridPane grid = new GridPane();
grid.setHgap(16);
grid.setVgap(10);
int row = 0;
addField(grid, row++, "Sistema:", appName + " (" + (origin != null ? origin : "Local") + ")");
if (isAuthentication) {
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
} else {
if (documentName != null && !documentName.isBlank()) {
String docLabel = (documentType != null && !documentType.isBlank())
? "[" + documentType.toUpperCase() + "] " + documentName
: documentName;
addField(grid, row++, "Documento:", docLabel);
}
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
}
addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias());
addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " +
(cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel()));
addField(grid, row++, "Algoritmo:", context.algorithm());
detailsBox.getChildren().add(grid);
// Caixa do Hash / Digest
VBox hashBox = new VBox(6);
Label hashLabel = new Label("Hash do Documento (SHA-256 Digest):");
hashLabel.getStyleClass().add("dialog-field-title");
String hashHex = HexFormat.of().formatHex(context.digest());
Label hashValue = new Label(hashHex);
hashValue.getStyleClass().add("dialog-hash-value");
hashValue.setWrapText(true);
hashBox.getChildren().addAll(hashLabel, hashValue);
detailsBox.getChildren().add(hashBox);
// Seção de PIN para Tokens / Smart Cards A3
VBox pinContainer = new VBox(10);
PasswordField pinField = new PasswordField();
CheckBox cbSavePin = new CheckBox("Salvar PIN durante esta sessão (não solicitar nas próximas assinaturas)");
cbSavePin.setStyle("-fx-font-size: 12px;");
boolean isA3 = cert.type() == CertificateType.A3;
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
if (isA3) {
pinContainer.getStyleClass().add("dialog-box");
Label pinSectionTitle = new Label("AUTENTICAÇÃO DO TOKEN / SMART CARD A3");
pinSectionTitle.getStyleClass().add("dialog-field-title");
pinContainer.getChildren().add(pinSectionTitle);
if (isWindowsStore) {
VBox winBox = new VBox(6);
Label winTitle = new Label("🔐 Autenticação nativa pelo Windows / Driver do Token");
winTitle.setStyle("-fx-text-fill: -fx-accent; -fx-font-weight: bold; -fx-font-size: 13px;");
Label winDesc = new Label(
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
);
winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
winDesc.setWrapText(true);
winBox.getChildren().addAll(winTitle, winDesc);
pinContainer.getChildren().add(winBox);
} else {
pinField.setPromptText("Digite o PIN do token...");
if (hasStoredPin) {
HBox rememberedBox = new HBox(12);
rememberedBox.setAlignment(Pos.CENTER_LEFT);
Label rememberedBadge = new Label("● PIN salvo nesta sessão ativa");
rememberedBadge.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 12px;");
Button btnChangePin = new Button("Alterar PIN");
btnChangePin.getStyleClass().add("btn-secondary");
btnChangePin.setStyle("-fx-padding: 4px 10px; -fx-font-size: 11px;");
VBox inputWrapper = new VBox(8);
inputWrapper.setVisible(false);
inputWrapper.setManaged(false);
inputWrapper.getChildren().addAll(new Label("Novo PIN:"), pinField, cbSavePin);
btnChangePin.setOnAction(e -> {
boolean nowVisible = !inputWrapper.isVisible();
inputWrapper.setVisible(nowVisible);
inputWrapper.setManaged(nowVisible);
btnChangePin.setText(nowVisible ? "Cancelar alteração" : "Alterar PIN");
if (nowVisible) {
cbSavePin.setSelected(true);
}
});
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
pinContainer.getChildren().addAll(rememberedBox, inputWrapper);
} else {
VBox inputWrapper = new VBox(8);
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
pinPromptLabel.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
cbSavePin.setSelected(false);
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
pinContainer.getChildren().add(inputWrapper);
}
}
detailsBox.getChildren().add(pinContainer);
}
// Label de Alerta de Validação
Label validationLabel = new Label();
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
validationLabel.setVisible(false);
// Botões de Ação
HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-cancel");
btnCancel.setOnAction(e -> {
approvalFuture.complete(false);
stage.close();
});
Button btnConfirm = new Button(isAuthentication ? "Confirmar e Entrar" : "Confirmar e Assinar");
btnConfirm.getStyleClass().add("btn-primary");
btnConfirm.setDefaultButton(true);
btnConfirm.setOnAction(e -> {
if (isA3 && !isWindowsStore) {
String typedPin = pinField.getText();
boolean pinProvided = (typedPin != null && !typedPin.isBlank());
if (!hasStoredPin && !pinProvided) {
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
validationLabel.setVisible(true);
pinField.requestFocus();
return;
}
if (pinProvided) {
char[] pinChars = typedPin.toCharArray();
if (cbSavePin.isSelected()) {
PinSessionManager.getInstance().savePin(cert.id(), pinChars);
} else {
PinSessionManager.getInstance().clearPin(cert.id());
}
}
}
approvalFuture.complete(true);
stage.close();
});
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
root.getChildren().addAll(header, desc, detailsBox, validationLabel, actionsRow);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
}
private void addField(GridPane grid, int row, String title, String value) {
Label titleLabel = new Label(title);
titleLabel.getStyleClass().add("dialog-field-title");
Label valLabel = new Label(value);
valLabel.getStyleClass().add("dialog-field-value");
grid.add(titleLabel, 0, row);
grid.add(valLabel, 1, row);
}
public CompletableFuture<Boolean> showAndAwait() {
stage.show();
stage.toFront();
return approvalFuture;
}
}
@@ -0,0 +1,395 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.validation.SignatureValidationService;
import br.com.cybercore.signer.core.validation.ValidationResult;
import br.com.cybercore.signer.core.validation.ValidationSignerInfo;
import br.com.cybercore.signer.core.validation.ValidationStatus;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
/**
* Diálogo unificado para Validação Criptográfica de Assinaturas Digitais.
* Suporta Assinatura Incorporada / Embutida (Attached / PAdES / P7M / XML)
* e Assinatura Destacada (Detached / P7S).
*/
@SuppressWarnings("this-escape")
public final class SignatureValidationDialog {
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
private final Stage stage;
private final boolean isTimestamp;
private final SignatureValidationService validationService = new SignatureValidationService();
private RadioButton rbAttached;
private RadioButton rbDetached;
private VBox docBox;
private TextField txtDocPath;
private TextField txtSigPath;
private Label lblSigPath;
private File docFile;
private File sigFile;
private VBox resultBox;
private Button btnSaveExtracted;
private byte[] lastExtractedContent;
public SignatureValidationDialog(Stage owner, boolean isTimestamp) {
this.isTimestamp = isTimestamp;
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle(isTimestamp ? "CyberCore Signer — Validação de Carimbo do Tempo" : "CyberCore Signer — Validação de Assinatura Digital");
this.stage.setResizable(true);
this.stage.setMinWidth(680);
this.stage.setMinHeight(520);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(22));
root.getStyleClass().add("dialog-container");
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
isTimestamp ? SvgIcons.validateTimestampIcon() : SvgIcons.validateIcon(),
createHeaderLabel(
isTimestamp ? "VALIDAÇÃO DE CARIMBO DO TEMPO" : "VALIDAÇÃO DE ASSINATURA DIGITAL",
isTimestamp ? "Verifique a autenticidade e conformidade RFC 3161 TSP."
: "Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
)
);
// Seletor de Tipo de Assinatura (Incorporada vs Destacada)
VBox typeBox = new VBox(8);
typeBox.getStyleClass().add("dialog-box");
Label lblTypeTitle = new Label("Tipo de Assinatura para Validação:");
lblTypeTitle.getStyleClass().add("dialog-field-title");
ToggleGroup modeGroup = new ToggleGroup();
rbAttached = new RadioButton("Assinatura Incorporada / Embutida (PDF assinado, P7M ou XML)");
rbAttached.setToggleGroup(modeGroup);
rbAttached.setSelected(true);
rbAttached.setOnAction(e -> updateModeView());
rbDetached = new RadioButton("Assinatura Destacada (Documento Original + Arquivo .p7s separado)");
rbDetached.setToggleGroup(modeGroup);
rbDetached.setOnAction(e -> updateModeView());
HBox radioRow = new HBox(18);
radioRow.getChildren().addAll(rbAttached, rbDetached);
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
if (isTimestamp) {
typeBox.setVisible(false);
typeBox.setManaged(false);
}
// 1. Arquivo de Assinatura / Arquivo Assinado
VBox sigBox = new VBox(8);
sigBox.getStyleClass().add("dialog-box");
lblSigPath = new Label("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
lblSigPath.getStyleClass().add("dialog-field-title");
HBox sigRow = new HBox(10);
txtSigPath = new TextField();
txtSigPath.setPromptText("Selecione o arquivo assinado...");
txtSigPath.setEditable(false);
HBox.setHgrow(txtSigPath, Priority.ALWAYS);
Button btnBrowseSig = new Button("Procurar...");
btnBrowseSig.getStyleClass().add("btn-secondary");
btnBrowseSig.setOnAction(e -> handleBrowseSig());
sigRow.getChildren().addAll(txtSigPath, btnBrowseSig);
sigBox.getChildren().addAll(lblSigPath, sigRow);
// 2. Documento Original (Visível apenas em modo Destacado)
docBox = new VBox(8);
docBox.getStyleClass().add("dialog-box");
Label lblDoc = new Label("Documento Original Separado:");
lblDoc.getStyleClass().add("dialog-field-title");
HBox docRow = new HBox(10);
txtDocPath = new TextField();
txtDocPath.setPromptText("Selecione o documento original...");
txtDocPath.setEditable(false);
HBox.setHgrow(txtDocPath, Priority.ALWAYS);
Button btnBrowseDoc = new Button("Procurar...");
btnBrowseDoc.getStyleClass().add("btn-secondary");
btnBrowseDoc.setOnAction(e -> handleBrowseDoc());
docRow.getChildren().addAll(txtDocPath, btnBrowseDoc);
docBox.getChildren().addAll(lblDoc, docRow);
// Painel Dinâmico de Resultados
resultBox = new VBox(10);
resultBox.setVisible(false);
resultBox.setManaged(false);
resultBox.getStyleClass().add("dialog-box");
ScrollPane scrollResult = new ScrollPane(resultBox);
scrollResult.setFitToWidth(true);
scrollResult.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scrollResult, Priority.ALWAYS);
// Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
btnSaveExtracted = new Button("Salvar Conteúdo Extraído...");
btnSaveExtracted.getStyleClass().add("btn-secondary");
btnSaveExtracted.setVisible(false);
btnSaveExtracted.setManaged(false);
btnSaveExtracted.setOnAction(e -> handleSaveExtracted());
HBox.setHgrow(btnSaveExtracted, Priority.NEVER);
Button btnClose = new Button("Fechar");
btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> stage.close());
Button btnValidate = new Button("Validar Agora");
btnValidate.getStyleClass().add("btn-primary");
btnValidate.setDefaultButton(true);
btnValidate.setOnAction(e -> handleValidate());
footer.getChildren().addAll(btnSaveExtracted, btnClose, btnValidate);
root.getChildren().addAll(header, typeBox, sigBox, docBox, scrollResult, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
updateModeView();
}
private void updateModeView() {
boolean isAttached = rbAttached.isSelected() && !isTimestamp;
docBox.setVisible(!isAttached);
docBox.setManaged(!isAttached);
if (isAttached) {
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
} else {
lblSigPath.setText(isTimestamp ? "Arquivo de Carimbo (.tsr, .tsp, .p7s):" : "Arquivo de Assinatura (.p7s, .sig):");
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
}
}
private void handleBrowseDoc() {
FileChooser ch = new FileChooser();
ch.setTitle("Selecionar Documento Original");
File f = ch.showOpenDialog(stage);
if (f != null) {
docFile = f;
txtDocPath.setText(f.getAbsolutePath());
}
}
private void handleBrowseSig() {
FileChooser ch = new FileChooser();
ch.setTitle(rbAttached.isSelected() ? "Selecionar Arquivo Assinado" : "Selecionar Arquivo de Assinatura");
if (rbAttached.isSelected()) {
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
} else {
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig, *.tsr)", "*.p7s", "*.sig", "*.tsr", "*.*"));
}
File f = ch.showOpenDialog(stage);
if (f != null) {
sigFile = f;
txtSigPath.setText(f.getAbsolutePath());
// Detecção inteligente: se selecionar .pdf no modo destacado, sugere alternar para incorporada
if (f.getName().toLowerCase().endsWith(".pdf") && rbDetached.isSelected()) {
rbAttached.setSelected(true);
updateModeView();
}
}
}
private void handleValidate() {
if (sigFile == null || !sigFile.exists()) {
showAlert(Alert.AlertType.WARNING, "Arquivo Não Selecionado", "Por favor, selecione o arquivo a ser validado.");
return;
}
boolean isDetachedMode = rbDetached.isSelected() || isTimestamp;
if (isDetachedMode && (docFile == null || !docFile.exists())) {
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
return;
}
try {
byte[] sigBytes = Files.readAllBytes(sigFile.toPath());
ValidationResult result;
if (isDetachedMode) {
byte[] docBytes = Files.readAllBytes(docFile.toPath());
result = validationService.validateDetached(docBytes, sigBytes, docFile.getName());
} else {
result = validationService.validateAttached(sigBytes, sigFile.getName());
}
displayValidationResult(result);
} catch (Exception ex) {
logger.error("Erro na validação: {}", ex.getMessage(), ex);
showAlert(Alert.AlertType.ERROR, "Erro no Processamento", "Falha ao validar arquivo: " + ex.getMessage());
}
}
private void displayValidationResult(ValidationResult result) {
resultBox.getChildren().clear();
resultBox.setVisible(true);
resultBox.setManaged(true);
// 1. Badge de Status
HBox statusBadge = new HBox(10);
statusBadge.setAlignment(Pos.CENTER_LEFT);
statusBadge.setPadding(new Insets(10, 14, 10, 14));
Label lblBadge = new Label(result.status().getDescription());
lblBadge.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
if (result.status() == ValidationStatus.VALID) {
statusBadge.setStyle("-fx-background-color: rgba(16, 185, 129, 0.15); -fx-background-radius: 6px; -fx-border-color: #10b981; -fx-border-radius: 6px;");
lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #10b981;");
} else if (result.status() == ValidationStatus.INVALID) {
statusBadge.setStyle("-fx-background-color: rgba(239, 68, 68, 0.15); -fx-background-radius: 6px; -fx-border-color: #ef4444; -fx-border-radius: 6px;");
lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #ef4444;");
} else {
statusBadge.setStyle("-fx-background-color: rgba(245, 158, 11, 0.15); -fx-background-radius: 6px; -fx-border-color: #f59e0b; -fx-border-radius: 6px;");
lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #f59e0b;");
}
Label lblType = new Label("• Formato: " + result.type().getDescription());
lblType.setStyle("-fx-font-size: 12px; -fx-text-fill: #cbd5e1;");
statusBadge.getChildren().addAll(lblBadge, lblType);
Label lblMsg = new Label(result.summaryMessage());
lblMsg.setWrapText(true);
lblMsg.setStyle("-fx-font-size: 12.5px; -fx-font-weight: bold;");
resultBox.getChildren().addAll(statusBadge, lblMsg);
// 2. Lista de Signatários
if (!result.signers().isEmpty()) {
resultBox.getChildren().add(new Separator());
Label lblSignersTitle = new Label("DETALHES DOS SIGNATÁRIOS (" + result.signers().size() + "):");
lblSignersTitle.getStyleClass().add("dialog-field-title");
resultBox.getChildren().add(lblSignersTitle);
for (ValidationSignerInfo s : result.signers()) {
VBox card = new VBox(4);
card.getStyleClass().add("diagnostic-item");
card.setPadding(new Insets(10));
Label lblSignerName = new Label("Signatário: " + s.signerName());
lblSignerName.getStyleClass().add("cert-title");
String docText = s.identifier().isBlank() ? "" : s.identifier() + " • ";
Label lblCert = new Label(docText + "Emissor: " + s.issuer());
lblCert.getStyleClass().add("cert-detail");
Label lblTime = new Label("Data/Hora: " + s.signingTime() + " • Algoritmo: " + s.algorithm());
lblTime.getStyleClass().add("cert-detail");
Label lblIntegrity = new Label(s.isValid() ? "✓ Assinatura íntegra e chave pública conferida." : "✗ Falha na integridade da assinatura.");
lblIntegrity.setStyle(s.isValid() ? "-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;" : "-fx-text-fill: #ef4444; -fx-font-weight: bold; -fx-font-size: 11.5px;");
card.getChildren().addAll(lblSignerName, lblCert, lblTime, lblIntegrity);
resultBox.getChildren().add(card);
}
}
// 3. Suporte a exportação de conteúdo extraído (caso CMS/P7M incorporado)
if (result.extractedContent() != null && result.extractedContent().length > 0) {
lastExtractedContent = result.extractedContent();
btnSaveExtracted.setVisible(true);
btnSaveExtracted.setManaged(true);
} else {
lastExtractedContent = null;
btnSaveExtracted.setVisible(false);
btnSaveExtracted.setManaged(false);
}
}
private void handleSaveExtracted() {
if (lastExtractedContent == null) return;
FileChooser chooser = new FileChooser();
chooser.setTitle("Salvar Documento Original Extraído");
chooser.setInitialFileName("documento_recuperado.bin");
File dest = chooser.showSaveDialog(stage);
if (dest != null) {
try {
Files.write(dest.toPath(), lastExtractedContent);
showAlert(Alert.AlertType.INFORMATION, "Documento Extraído", "O documento contido na assinatura foi salvo com sucesso em:\n" + dest.getAbsolutePath());
} catch (Exception ex) {
showAlert(Alert.AlertType.ERROR, "Erro ao Salvar", "Falha ao gravar arquivo: " + ex.getMessage());
}
}
}
private void showAlert(Alert.AlertType type, String title, String msg) {
Alert a = new Alert(type);
a.setTitle(title);
a.setHeaderText(null);
a.setContentText(msg);
a.initOwner(stage);
ThemeManager.applyTheme(a.getDialogPane().getScene());
a.showAndWait();
}
private VBox createHeaderLabel(String title, String subtitle) {
VBox box = new VBox(3);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
Label s = new Label(subtitle);
s.getStyleClass().add("dialog-subtitle");
box.getChildren().addAll(t, s);
return box;
}
public void show() {
stage.show();
}
}
@@ -0,0 +1,118 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.util.Objects;
/**
* Diálogo interativo com instruções e tutoriais de uso do CyberCore Signer.
*/
@SuppressWarnings("this-escape")
public final class TutorialDialog {
private final Stage stage;
public TutorialDialog(Stage owner) {
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Tutorial e Guia de Uso");
this.stage.setResizable(true);
this.stage.setMinWidth(640);
this.stage.setMinHeight(500);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container");
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
header.getChildren().addAll(
SvgIcons.tutorialIcon(),
createHeaderLabel("TUTORIAL E GUIA DE UTILIZAÇÃO", "Aprenda a assinar documentos e autorizar aplicações web com facilidade")
);
VBox content = new VBox(16);
content.getStyleClass().add("dialog-box");
addStep(content, "1. Assinatura Local Direta",
"Utilize os botões da tela inicial para assinar PDFs, XMLs, arquivos diversos ou múltiplos arquivos em lote. O agente calculará o hash SHA-256 e gerará a assinatura digital protegida.");
addStep(content, "2. Integração com Sistemas Web (CCSP/1)",
"Aplicações web corporativas conectam-se ao agente local através da porta 127.0.0.1:21325. Na primeira utilização de um site, o CyberCore solicitará sua autorização explícita.");
addStep(content, "3. Login com Certificado Digital",
"Quando um sistema web solicitar autenticação com certificado, você verá um diálogo azul destacando o desafio e o titular do certificado. Nenhuma senha ou chave privada é transmitida à rede.");
addStep(content, "4. Utilização de Tokens USB e Smart Cards (A3)",
"Ao conectar um token USB ou cartão na leitora, o CyberCore Signer detecta o dispositivo automaticamente. Ao assinar, informe o PIN no diálogo protegido.");
addStep(content, "5. Importação de Certificados A1 (.pfx / .p12)",
"Certificados A1 podem ser adicionados através do menu 'Arquivos Seguros' no painel lateral. A chave privada permanece isolada na máquina do usuário.");
ScrollPane scroll = new ScrollPane(content);
scroll.setFitToWidth(true);
scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;");
VBox.setVgrow(scroll, Priority.ALWAYS);
HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
root.getChildren().addAll(header, scroll, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
}
private void addStep(VBox container, String title, String description) {
VBox box = new VBox(4);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
t.setStyle("-fx-font-size: 13.5px;");
Label d = new Label(description);
d.setWrapText(true);
d.getStyleClass().add("dialog-field-value");
box.getChildren().addAll(t, d);
container.getChildren().add(box);
}
private VBox createHeaderLabel(String title, String subtitle) {
VBox box = new VBox(3);
Label t = new Label(title);
t.getStyleClass().add("dialog-header");
Label s = new Label(subtitle);
s.getStyleClass().add("dialog-subtitle");
box.getChildren().addAll(t, s);
return box;
}
public void show() {
stage.show();
}
}
@@ -0,0 +1,513 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
import br.com.cybercore.signer.core.xml.XmlSigningService;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Diálogo especializado para Assinatura Digital de Documentos XML segundo o padrão
* W3C XMLDSig e ICP-Brasil, com suporte a envelopes completos e elementos específicos (ex: infNFe).
*/
@SuppressWarnings("this-escape")
public final class XmlSignDialog {
private static final Logger logger = LoggerFactory.getLogger(XmlSignDialog.class);
private final Stage stage;
private final CertificateManager certManager;
private final XmlSigningService xmlSigningService;
// Controles de Documento
private TextField txtSourceXml;
private File selectedSourceFile;
// Controles de Certificado
private ComboBox<ManagedCertificate> certCombo;
private PasswordField pinField;
private Label lblPinNotice;
// Controles de Modo de Assinatura
private RadioButton rbWholeDocument;
private RadioButton rbSpecificElement;
private TextField txtElementTag;
private TextField txtElementId;
private CheckBox chkValidateAfter;
// Controles de Ação e Feedback
private ProgressIndicator progressIndicator;
private Label lblStatus;
private Button btnSign;
private Button btnCancel;
public XmlSignDialog(Stage owner, CertificateManager certManager) {
this(owner, certManager, new XmlSigningService(certManager));
}
public XmlSignDialog(Stage owner, CertificateManager certManager, XmlSigningService xmlSigningService) {
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.xmlSigningService = Objects.requireNonNull(xmlSigningService, "XmlSigningService não pode ser nulo.");
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XMLDSig)");
this.stage.setResizable(true);
this.stage.setMinWidth(720);
this.stage.setMinHeight(580);
initLayout();
}
private void initLayout() {
VBox root = new VBox(16);
root.setPadding(new Insets(20));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4);
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XMLDSIG)");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Assine digitalmente documentos XML no padrão W3C XMLDSig e ICP-Brasil (NF-e, CT-e, MDF-e, eSocial, etc.).");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
// ScrollPane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;");
VBox formContent = new VBox(16);
formContent.setPadding(new Insets(4, 8, 4, 0));
// 1. Seção: Arquivo XML
VBox fileSection = new VBox(8);
fileSection.getStyleClass().add("dialog-box");
Label lblFileTitle = new Label("ARQUIVO XML A ASSINAR");
lblFileTitle.getStyleClass().add("dialog-field-title");
HBox fileRow = new HBox(10);
fileRow.setAlignment(Pos.CENTER_LEFT);
txtSourceXml = new TextField();
txtSourceXml.setPromptText("Selecione o arquivo XML original...");
txtSourceXml.setEditable(false);
HBox.setHgrow(txtSourceXml, Priority.ALWAYS);
Button btnBrowse = new Button("Procurar XML...");
btnBrowse.getStyleClass().add("btn-secondary");
btnBrowse.setOnAction(e -> handleBrowseSourceXml());
fileRow.getChildren().addAll(txtSourceXml, btnBrowse);
fileSection.getChildren().addAll(lblFileTitle, fileRow);
// 2. Seção: Certificado Digital
VBox certSection = new VBox(8);
certSection.getStyleClass().add("dialog-box");
Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
lblCertTitle.getStyleClass().add("dialog-field-title");
certCombo = new ComboBox<>();
certCombo.setMaxWidth(Double.MAX_VALUE);
populateCertificates();
certCombo.setOnAction(e -> updatePinFieldVisibility());
certCombo.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")");
}
}
});
certCombo.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")");
}
}
});
HBox pinRow = new HBox(12);
pinRow.setAlignment(Pos.CENTER_LEFT);
Label lblPin = new Label("PIN / Senha:");
lblPin.getStyleClass().add("dialog-field-title");
pinField = new PasswordField();
pinField.setPromptText("Digite o PIN ou senha da chave privada...");
pinField.setPrefWidth(260);
lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11px;");
pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice);
certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow);
// 3. Seção: Modo de Assinatura XMLDSig
VBox xmlModeSection = new VBox(12);
xmlModeSection.getStyleClass().add("dialog-box");
Label lblModeTitle = new Label("MODO DE ASSINATURA XMLDSIG");
lblModeTitle.getStyleClass().add("dialog-field-title");
ToggleGroup modeGroup = new ToggleGroup();
rbWholeDocument = new RadioButton("Documento Completo (Enveloped W3C / URI=\"\")");
rbWholeDocument.setToggleGroup(modeGroup);
rbWholeDocument.setSelected(true);
rbSpecificElement = new RadioButton("Elemento Específico por Tag / ID (Padrão NF-e, CT-e, MDF-e, etc.)");
rbSpecificElement.setToggleGroup(modeGroup);
VBox elementConfigBox = new VBox(10);
elementConfigBox.setPadding(new Insets(6, 0, 0, 24));
GridPane elementGrid = new GridPane();
elementGrid.setHgap(12);
elementGrid.setVgap(8);
Label lblTag = new Label("Tag do Elemento:");
lblTag.getStyleClass().add("dialog-field-title");
txtElementTag = new TextField("infNFe");
txtElementTag.setPromptText("Ex: infNFe, infCte, infMDFe, SignedInfo");
Label lblId = new Label("Atributo Id (opcional):");
lblId.getStyleClass().add("dialog-field-title");
txtElementId = new TextField();
txtElementId.setPromptText("Deixe vazio para autodetectar o Id da tag");
elementGrid.add(lblTag, 0, 0);
elementGrid.add(txtElementTag, 1, 0);
elementGrid.add(lblId, 0, 1);
elementGrid.add(txtElementId, 1, 1);
elementConfigBox.getChildren().add(elementGrid);
elementConfigBox.disableProperty().bind(rbWholeDocument.selectedProperty());
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
chkValidateAfter.setSelected(true);
xmlModeSection.getChildren().addAll(lblModeTitle, rbWholeDocument, rbSpecificElement, elementConfigBox, chkValidateAfter);
formContent.getChildren().addAll(fileSection, certSection, xmlModeSection);
scrollPane.setContent(formContent);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
// Rodapé com Ações
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(22, 22);
progressIndicator.setVisible(false);
lblStatus = new Label();
lblStatus.setStyle("-fx-font-size: 12px;");
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML");
btnSign.getStyleClass().add("btn-primary");
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeXmlSignature());
footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
root.getChildren().addAll(header, scrollPane, footer);
Scene scene = new Scene(root);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
updatePinFieldVisibility();
}
private void populateCertificates() {
try {
List<ManagedCertificate> certs = certManager.getAvailableCertificates();
certCombo.setItems(FXCollections.observableArrayList(certs));
if (!certs.isEmpty()) {
certCombo.setValue(certs.get(0));
}
} catch (Exception e) {
logger.error("Erro ao listar certificados para assinatura de XML", e);
}
}
private void updatePinFieldVisibility() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
pinField.setDisable(true);
lblPinNotice.setText("");
return;
}
boolean isA3 = cert.type() == CertificateType.A3;
boolean hasPinInSession = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
if (hasPinInSession) {
pinField.setDisable(true);
pinField.clear();
lblPinNotice.setText("✓ PIN memorizado na sessão segura");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
} else if (isA3 && "provider-win-store".equalsIgnoreCase(cert.providerId())) {
pinField.setDisable(true);
pinField.clear();
lblPinNotice.setText("Autenticação nativa pelo Windows/Driver");
lblPinNotice.setStyle("-fx-text-fill: -fx-accent;");
} else {
pinField.setDisable(false);
lblPinNotice.setText(isA3 ? "Requer PIN do Smart Card" : "Requer senha do certificado A1");
lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary;");
}
}
private void handleBrowseSourceXml() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Documento XML");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos XML (*.xml)", "*.xml"));
File file = chooser.showOpenDialog(stage);
if (file != null) {
selectedSourceFile = file;
txtSourceXml.setText(file.getAbsolutePath());
// Tenta detectar automaticamente se é uma NF-e, CT-e, etc.
try {
String content = Files.readString(file.toPath());
if (content.contains("<infNFe")) {
rbSpecificElement.setSelected(true);
txtElementTag.setText("infNFe");
} else if (content.contains("<infCte")) {
rbSpecificElement.setSelected(true);
txtElementTag.setText("infCte");
} else if (content.contains("<infMDFe")) {
rbSpecificElement.setSelected(true);
txtElementTag.setText("infMDFe");
}
} catch (Exception ignored) {
}
}
}
private void executeXmlSignature() {
if (selectedSourceFile == null || !selectedSourceFile.exists()) {
showAlert(Alert.AlertType.WARNING, "Documento Não Selecionado", "Por favor, selecione um arquivo XML válido para assinar.");
return;
}
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
return;
}
FileChooser chooser = new FileChooser();
chooser.setTitle("Salvar XML Assinado Como...");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos XML (*.xml)", "*.xml"));
String originalName = selectedSourceFile.getName();
String baseName = originalName.toLowerCase().endsWith(".xml")
? originalName.substring(0, originalName.length() - 4)
: originalName;
chooser.setInitialFileName(baseName + "_assinado.xml");
if (selectedSourceFile.getParentFile() != null && selectedSourceFile.getParentFile().exists()) {
chooser.setInitialDirectory(selectedSourceFile.getParentFile());
}
File targetFile = chooser.showSaveDialog(stage);
if (targetFile == null) {
return;
}
char[] pin = null;
if (!pinField.isDisable() && !pinField.getText().isEmpty()) {
pin = pinField.getText().toCharArray();
}
XmlSignatureOptions options;
if (rbSpecificElement.isSelected()) {
String tag = txtElementTag.getText().trim();
String id = txtElementId.getText().trim();
if (tag.isBlank()) {
tag = "infNFe";
}
options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id);
} else {
options = XmlSignatureOptions.wholeDocument(cert.id(), pin);
}
setFormDisabled(true);
progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento XML (XMLDSig)...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
boolean validate = chkValidateAfter.isSelected();
Task<File> signTask = new Task<>() {
@Override
protected File call() throws Exception {
xmlSigningService.signXmlFile(selectedSourceFile, targetFile, options);
if (validate) {
byte[] signedBytes = Files.readAllBytes(targetFile.toPath());
boolean valid = xmlSigningService.validateXmlSignature(signedBytes);
if (!valid) {
throw new IllegalStateException("A assinatura foi gravada, mas a validação de integridade do XML falhou.");
}
}
return targetFile;
}
};
signTask.setOnSucceeded(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
lblStatus.setText("✓ Documento XML assinado e validado com sucesso!");
lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
showSuccessDialog(targetFile);
});
signTask.setOnFailed(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
Throwable ex = signTask.getException();
logger.error("Falha ao assinar XML", ex);
lblStatus.setText("✕ Falha na assinatura");
lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do XML",
"Não foi possível concluir a assinatura XMLDSig:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
});
Thread signThread = new Thread(signTask, "cybercore-xml-signer");
signThread.setDaemon(true);
signThread.start();
}
private void showSuccessDialog(File targetFile) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Assinatura Concluída");
alert.setHeaderText("XML Assinado com Sucesso!");
alert.setContentText("O documento XML foi digitalmente assinado no padrão W3C XMLDSig ICP-Brasil.\n\n"
+ "Arquivo gerado:\n" + targetFile.getAbsolutePath());
ButtonType btnOpen = new ButtonType("Abrir XML");
ButtonType btnFolder = new ButtonType("Abrir Pasta");
ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent()) {
if (result.get() == btnOpen) {
openFile(targetFile);
} else if (result.get() == btnFolder) {
openFolder(targetFile);
}
}
}
private void openFile(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
Desktop.getDesktop().open(file);
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage());
}
}
private void openFolder(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
File parent = file.getParentFile();
if (parent != null) {
Desktop.getDesktop().open(parent);
}
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage());
}
}
private void setFormDisabled(boolean disabled) {
btnSign.setDisable(disabled);
btnCancel.setDisable(disabled);
certCombo.setDisable(disabled);
pinField.setDisable(disabled);
rbWholeDocument.setDisable(disabled);
rbSpecificElement.setDisable(disabled);
txtElementTag.setDisable(disabled);
txtElementId.setDisable(disabled);
chkValidateAfter.setDisable(disabled);
}
private void showAlert(Alert.AlertType type, String title, String message) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
alert.showAndWait();
}
public void show() {
stage.centerOnScreen();
stage.show();
stage.toFront();
}
}
@@ -0,0 +1,75 @@
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**.
---
## 📁 Estrutura de Arquivos Criados
```
assets/
├── icons/ # Ícones vetoriais SVG reais e editáveis (W3C padrão)
│ ├── pdf.svg # Ícone Assinar PDF (documento com tag vermelha PDF)
│ ├── xml.svg # Ícone Assinar XML (documento com tag azul <XML>)
│ ├── file.svg # Ícone Assinar Arquivo (documento com caneta)
│ ├── batch.svg # Ícone Assinatura em Lote (pilha de documentos com check)
│ ├── attached.svg # Ícone Assinatura Atachada (documento com clipe de papel)
│ ├── co-sign.svg # Ícone Co-Assinar (documentos duplos com usuários)
│ ├── timestamp.svg # Ícone Carimbo do Tempo (documento com relógio TSA)
│ ├── ltv.svg # Ícone Assinatura LTV (documento com selo perpétuo)
│ ├── validate.svg # Ícone Validar Assinatura (documento com checkmark verde)
│ ├── validate-timestamp.svg # Ícone Validar Carimbo (relógio com checkmark verde)
│ ├── exit.svg # Ícone Sair do Agente (porta com seta vermelha de saída)
│ ├── logs.svg # Ícone Terminal de Logs (janela com prompt </>)
│ ├── settings.svg # Ícone Configurações (engrenagem de precisão de 8 dentes)
│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play)
│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro)
│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i')
│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais)
│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo)
│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna)
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
│ └── star.svg # Ícone de Estrela notarial / favorita
│
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
```
---
## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:**
- Estão no formato `.svg` padrão.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**.
- Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente.
2. **Imagens do Sistema:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
- Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa.
3. **Ordem de Prioridade e Hot-Reloading:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar!
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
---
## 🚀 Como Testar Suas Modificações
Para compilar e gerar o pacote `.jar` completo com todos os novos assets embutidos:
```bash
./build-jar.sh
```
Para rodar a aplicação:
```bash
java -jar ./cybercore-signer.jar
```
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="20" fill="#0288d1"/>
<circle cx="24" cy="14" r="3" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="34" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 10 4 L 34 4 L 42 12 L 42 44 L 10 44 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M 34 4 L 34 12 L 42 12 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
<!-- Linhas de texto representativas -->
<line x1="16" y1="20" x2="36" y2="20" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="26" x2="36" y2="26" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="32" x2="30" y2="32" stroke="#90caf9" stroke-width="2" stroke-linecap="round"/>
<!-- Clipe de papel metálico prendendo a folha -->
<path d="M 12 2 C 8 2 6 5 6 9 L 6 25 C 6 29 9 32 14 32 C 18 32 21 29 21 25 L 21 11 C 21 8 19 6 16 6 C 13 6 12 8 12 11 L 12 24"
fill="none" stroke="#455a64" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 976 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha traseira (3) -->
<path d="M 16 4 L 38 4 L 44 10 L 44 32 L 16 32 Z" fill="#e1f5fe" stroke="#4fc3f7" stroke-width="2" stroke-linejoin="round"/>
<!-- Folha do meio (2) -->
<path d="M 10 10 L 32 10 L 38 16 L 38 38 L 10 38 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
<!-- Folha frontal (1) -->
<path d="M 4 16 L 26 16 L 32 22 L 32 44 L 4 44 Z" fill="#ffffff" stroke="#01579b" stroke-width="2.2" stroke-linejoin="round"/>
<!-- Dobra folha frontal -->
<path d="M 26 16 L 26 22 L 32 22 Z" fill="#e1f5fe" stroke="#01579b" stroke-width="1.8"/>
<!-- Checkmark de execução/lote concluído -->
<circle cx="34" cy="34" r="10" fill="#00c853"/>
<path d="M 29 34 L 33 38 L 39 30" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 920 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Pasta de arquivos de certificados -->
<path d="M 4 10 L 16 10 L 20 14 L 44 14 L 44 40 L 4 40 Z" fill="#e1f5fe" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Aba superior da pasta -->
<path d="M 4 18 L 44 18" stroke="#0288d1" stroke-width="2"/>
<!-- Cadeado de segurança dourado/azul -->
<rect x="26" y="24" width="14" height="12" rx="3" fill="#0288d1"/>
<!-- Arco do cadeado -->
<path d="M 29 24 L 29 20 C 29 17 31 15 33 15 C 35 15 37 17 37 20 L 37 24" fill="none" stroke="#01579b" stroke-width="2.5"/>
<circle cx="33" cy="30" r="1.8" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 688 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="20" fill="#10b981"/>
<path d="M 14 24 L 21 31 L 34 17" fill="none" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha secundária -->
<path d="M 6 10 L 26 10 L 32 16 L 32 40 L 6 40 Z" fill="#ffffff" stroke="#1976d2" stroke-width="2.2" stroke-linejoin="round"/>
<!-- Folha primária -->
<path d="M 16 4 L 36 4 L 42 10 L 42 34 L 16 34 Z" fill="#e3f2fd" stroke="#0288d1" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M 36 4 L 36 10 L 42 10 Z" fill="#bbdefb" stroke="#0288d1" stroke-width="1.8"/>
<!-- Ícone de dois usuários / parceiros de assinatura -->
<circle cx="16" cy="24" r="4" fill="#0d47a1"/>
<path d="M 10 36 C 10 31 13 30 16 30 C 19 30 22 31 22 36 Z" fill="#0d47a1"/>
<circle cx="26" cy="22" r="3.5" fill="#0288d1"/>
<path d="M 21 34 C 21 30 24 29 26 29 C 29 29 32 30 32 34 Z" fill="#0288d1"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="20" fill="#ef4444"/>
<line x1="16" y1="16" x2="32" y2="32" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
<line x1="32" y1="16" x2="16" y2="32" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Porta de saída / batente -->
<path d="M 24 8 L 12 8 C 9.8 8 8 9.8 8 12 L 8 36 C 8 38.2 9.8 40 12 40 L 24 40"
fill="none" stroke="#546e7a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Seta vermelha apontando para fora -->
<line x1="18" y1="24" x2="40" y2="24" stroke="#d32f2f" stroke-width="3.2" stroke-linecap="round"/>
<polyline points="32 16, 40 24, 32 32" fill="none" stroke="#d32f2f" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="34" y2="20" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="26" x2="34" y2="26" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<!-- Caneta / Pena de assinatura eletrônica -->
<path d="M 16 38 L 30 24 L 35 29 L 21 43 L 14 44 Z" fill="#0288d1"/>
<path d="M 30 24 L 34 20 L 39 25 L 35 29 Z" fill="#01579b"/>
<circle cx="16" cy="42" r="1.5" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 873 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Janela de terminal / console -->
<rect x="4" y="6" width="40" height="36" rx="6" fill="#1e293b" stroke="#334155" stroke-width="2"/>
<!-- Barra de título do terminal com botões -->
<circle cx="10" cy="12" r="2" fill="#ef4444"/>
<circle cx="16" cy="12" r="2" fill="#f59e0b"/>
<circle cx="22" cy="12" r="2" fill="#10b981"/>
<!-- Linha divisória -->
<line x1="4" y1="18" x2="44" y2="18" stroke="#334155" stroke-width="1.5"/>
<!-- Texto e prompt de código </> -->
<path d="M 12 24 L 18 29 L 12 34" fill="none" stroke="#38bdf8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="22" y1="34" x2="32" y2="34" stroke="#94a3b8" stroke-width="2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 811 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 30 4 L 38 12 L 38 34 L 8 34 Z" fill="#ffffff" stroke="#00838f" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M 30 4 L 30 12 L 38 12 Z" fill="#b2dfdb" stroke="#00838f" stroke-width="2"/>
<!-- Linhas no documento -->
<line x1="14" y1="18" x2="30" y2="18" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="24" x2="24" y2="24" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<!-- Selo LTV com fita de proteção permanente -->
<circle cx="32" cy="32" r="12" fill="#e0f2f1" stroke="#00897b" stroke-width="2.5"/>
<text x="32" y="36.5" font-family="Segoe UI, Arial, sans-serif" font-size="9" font-weight="bold" fill="#00695c" text-anchor="middle">LTV</text>
</svg>

After

Width:  |  Height:  |  Size: 861 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Lua crescente ciano/índigo -->
<path d="M 40 26.5 A 18 18 0 1 1 21.5 8 A 14 14 0 0 0 40 26.5 Z" fill="#38bdf8"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z" fill="#ffffff" stroke="#d32f2f" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta vermelha com sigla PDF -->
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8.5" font-weight="bold" fill="#ffffff" text-anchor="middle">PDF</text>
</svg>

After

Width:  |  Height:  |  Size: 996 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Fita pendente do selo -->
<polygon points="18,30 24,44 30,30" fill="#f59e0b"/>
<polygon points="12,30 16,42 20,30" fill="#d97706"/>
<polygon points="28,30 32,42 36,30" fill="#d97706"/>
<!-- Medalha / Selo circular -->
<circle cx="24" cy="20" r="16" fill="#d97706" stroke="#fbbf24" stroke-width="2.5"/>
<circle cx="24" cy="20" r="13" fill="none" stroke="#fbbf24" stroke-width="1.2" stroke-dasharray="2,2"/>
<!-- Estrela de 5 pontas central -->
<polygon points="24,9 26.5,16.5 34,16.5 28,21 30.5,28.5 24,24 17.5,28.5 20,21 14,16.5 21.5,16.5" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Engrenagem simétrica de 8 dentes centrada em (24, 24) -->
<path d="M 21 4 L 27 4 L 28 10 C 30 10.8 31.8 11.8 33.4 13.2 L 39 10 L 43 14 L 39.8 19.6 C 41.2 21.2 42.2 23 43 25 L 49 26 L 49 32 L 43 33 C 42.2 35 41.2 36.8 39.8 38.4 L 43 44 L 39 48 L 33.4 44.8 C 31.8 46.2 30 47.2 28 48 L 27 54 L 21 54 L 20 48 C 18 47.2 16.2 46.2 14.6 44.8 L 9 48 L 5 44 L 8.2 38.4 C 6.8 36.8 5.8 35 5 33 L -1 32 L -1 26 L 5 25 C 5.8 23 6.8 21.2 8.2 19.6 L 5 14 L 9 10 L 14.6 13.2 C 16.2 11.8 18 10.8 20 10 Z" transform="scale(0.8) translate(6, 4)" fill="#0288d1"/>
<!-- Círculo externo da engrenagem -->
<circle cx="24" cy="24" r="14" fill="#0288d1"/>
<!-- Furo central vazado -->
<circle cx="24" cy="24" r="6" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 817 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Escudo CyberShield -->
<path d="M 24 4 L 8 11 L 8 24 C 8 34.5 15 42.5 24 45 C 33 42.5 40 34.5 40 24 L 40 11 Z" fill="#0284c7" stroke="#38bdf8" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Chave de segurança criptográfica no centro -->
<path d="M 24 14 C 20.7 14 18 16.7 18 20 C 18 22.4 19.5 24.5 22 25.4 L 22 34 L 26 34 L 26 30 L 28 30 L 28 27 L 26 27 L 26 25.4 C 28.5 24.5 30 22.4 30 20 C 30 16.7 27.3 14 24 14 Z M 24 18 C 25.1 18 26 18.9 26 20 C 26 21.1 25.1 22 24 22 C 22.9 22 22 21.1 22 20 C 22 18.9 22.9 18 24 18 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 649 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<polygon points="24,4 29.5,16.5 43,17.5 32.5,26.5 35.8,40 24,32.5 12.2,40 15.5,26.5 5,17.5 18.5,16.5"
fill="#f59e0b" stroke="#d97706" stroke-width="2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Núcleo solar brilhante -->
<circle cx="24" cy="24" r="10" fill="#f59e0b"/>
<!-- Raios solares -->
<g stroke="#f59e0b" stroke-width="3" stroke-linecap="round">
<line x1="24" y1="4" x2="24" y2="9"/>
<line x1="24" y1="39" x2="24" y2="44"/>
<line x1="4" y1="24" x2="9" y2="24"/>
<line x1="39" y1="24" x2="44" y2="24"/>
<line x1="9.8" y1="9.8" x2="13.3" y2="13.3"/>
<line x1="34.7" y1="34.7" x2="38.2" y2="38.2"/>
<line x1="9.8" y1="38.2" x2="13.3" y2="34.7"/>
<line x1="34.7" y1="13.3" x2="38.2" y2="9.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 30 4 L 38 12 L 38 34 L 8 34 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M 30 4 L 30 12 L 38 12 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
<!-- Linhas no documento -->
<line x1="14" y1="18" x2="30" y2="18" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="24" x2="24" y2="24" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<!-- Relógio de precisão temporal TSA -->
<circle cx="32" cy="32" r="12" fill="#e1f5fe" stroke="#0288d1" stroke-width="2.5"/>
<circle cx="32" cy="32" r="2" fill="#01579b"/>
<line x1="32" y1="24" x2="32" y2="32" stroke="#01579b" stroke-width="2.5" stroke-linecap="round"/>
<line x1="32" y1="32" x2="38" y2="32" stroke="#01579b" stroke-width="2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 958 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Monitor de computador -->
<rect x="4" y="6" width="40" height="28" rx="4" fill="#ffffff" stroke="#0288d1" stroke-width="2.5"/>
<!-- Base do monitor -->
<path d="M 16 34 L 12 42 L 36 42 L 32 34" fill="none" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Ícone de play/vídeo tutorial central -->
<polygon points="20,14 32,20 20,26" fill="#0288d1"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Círculo de relógio base -->
<circle cx="24" cy="24" r="20" fill="#f1f8e9" stroke="#558b2f" stroke-width="2.5"/>
<!-- Ponteiros do relógio -->
<line x1="24" y1="12" x2="24" y2="24" stroke="#33691e" stroke-width="2" stroke-linecap="round"/>
<line x1="24" y1="24" x2="32" y2="24" stroke="#33691e" stroke-width="2" stroke-linecap="round"/>
<!-- Checkmark verde de validação de tempo -->
<circle cx="34" cy="34" r="10" fill="#2e7d32"/>
<path d="M 29 34 L 33 38 L 39 30" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2"/>
<!-- Linhas no documento -->
<line x1="14" y1="20" x2="34" y2="20" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="26" x2="28" y2="26" stroke="#cfd8dc" stroke-width="2" stroke-linecap="round"/>
<!-- Emblema de validação com Checkmark verde -->
<circle cx="30" cy="34" r="11" fill="#e8f5e9" stroke="#2e7d32" stroke-width="2.5"/>
<path d="M 24 34 L 28 38 L 36 29" fill="none" stroke="#2e7d32" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 848 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Folha de documento com dobra -->
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z" fill="#ffffff" stroke="#0288d1" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Dobra de canto -->
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
<!-- Linhas de texto representativas -->
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
<!-- Etiqueta azul com sigla XML -->
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8" font-weight="bold" fill="#ffffff" text-anchor="middle">&lt;XML&gt;</text>
</svg>

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
package br.com.cybercore.signer.ui;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.net.URL;
import static org.assertj.core.api.Assertions.assertThat;
class CyberCoreUiConstantsTest {
@Test
@DisplayName("Deve conter constantes de UI válidas e carregar folha de estilos CSS")
void shouldHaveValidUiConstants() {
assertThat(CyberCoreUiConstants.APP_NAME).isEqualTo("CyberCore Signer");
assertThat(CyberCoreUiConstants.APP_VERSION).isNotBlank();
assertThat(CyberCoreUiConstants.MAIN_CSS_PATH).isEqualTo("/styles/theme.css");
URL cssResource = getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH);
assertThat(cssResource).isNotNull();
}
}
@@ -0,0 +1,73 @@
package br.com.cybercore.signer.ui;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import br.com.cybercore.signer.ui.view.MainAppView;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.image.PixelReader;
import javafx.scene.image.WritableImage;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
public class UiPreviewGenerator extends Application {
public static void main(String[] args) {
Application.launch(UiPreviewGenerator.class, args);
}
@Override
public void start(Stage primaryStage) {
// 1. Inicia com Tema Claro
ThemeManager.setCurrentTheme(ThemeManager.AppTheme.LIGHT);
MainAppView appView = new MainAppView(primaryStage);
appView.show();
Platform.runLater(() -> {
try {
File outputDir = new File("docs/screenshots");
outputDir.mkdirs();
// Snapshot Modo Claro
saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "01_layout_claro.png"));
// 2. Alterna para Tema Escuro
ThemeManager.setCurrentTheme(ThemeManager.AppTheme.DARK);
ThemeManager.applyTheme(primaryStage.getScene());
Platform.runLater(() -> {
try {
// Snapshot Modo Escuro
saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "02_layout_escuro.png"));
} catch (Exception e) {
e.printStackTrace();
} finally {
Platform.exit();
}
});
} catch (Exception e) {
e.printStackTrace();
Platform.exit();
}
});
}
private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception {
WritableImage image = scene.snapshot(null);
int w = (int) image.getWidth();
int h = (int) image.getHeight();
BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
PixelReader reader = image.getPixelReader();
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
bufferedImage.setRGB(x, y, reader.getArgb(x, y));
}
}
ImageIO.write(bufferedImage, "png", targetFile);
}
}
@@ -0,0 +1,9 @@
package br.com.cybercore.signer.ui;
import javafx.application.Application;
public class UiPreviewLauncher {
public static void main(String[] args) {
Application.launch(UiPreviewGenerator.class, args);
}
}