feat(pdf): centraliza configuracao de selo padrao e adiciona armazenamento seguro de PIN

This commit is contained in:
Claudecio Martins
2026-10-11 10:00:46 -03:00
parent ccfa358f97
commit 7f6d8a4a08
14 changed files with 1766 additions and 684 deletions
@@ -104,7 +104,7 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView = new MainAppView(primaryStage);
// Ações da Grade de Assinatura e Validação
mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignPdf(() -> handleStartPdfSigning(primaryStage));
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());
@@ -155,6 +155,19 @@ public class CyberCoreDesktopApplication extends Application {
}
}
private void handleStartPdfSigning(Stage ownerStage) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Documento PDF para Assinar");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"),
new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
);
File selectedFile = chooser.showOpenDialog(ownerStage);
if (selectedFile != null && selectedFile.exists()) {
new PdfSignDialog(ownerStage, certificateManager, selectedFile).show();
}
}
private void openSettingsDialog(Stage owner) {
Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL);
@@ -178,8 +191,9 @@ public class CyberCoreDesktopApplication extends Application {
VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
@@ -206,8 +220,9 @@ public class CyberCoreDesktopApplication extends Application {
AboutView aboutView = new AboutView();
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose);
@@ -729,4 +729,103 @@ public final class SvgIcons {
box.getChildren().add(cross);
return box;
}
/**
* Ícone vetorial de Seta para a Direita / Avançar.
*/
public static Node arrowRightIcon(double size) {
return arrowRightIcon(size, "#ffffff");
}
public static Node arrowRightIcon(double size, String colorHex) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath arrow = new SVGPath();
arrow.setContent("M 5 3 L 10.5 8 L 5 13");
arrow.setStroke(Color.web(colorHex));
arrow.setStrokeWidth(2.2);
arrow.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
arrow.setStrokeLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND);
arrow.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
arrow.setScaleX(scale);
arrow.setScaleY(scale);
box.getChildren().add(arrow);
return box;
}
/**
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
*/
public static Node zoomInIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5 M 6.5 4.5 L 6.5 8.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomOutIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomFitIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 2 5 L 2 2 L 5 2 M 11 2 L 14 2 L 14 5 M 14 11 L 14 14 L 11 14 M 5 14 L 2 14 L 2 11");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
}
@@ -129,8 +129,9 @@ public class CertificateDetailsDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
actionsRow.getChildren().add(btnClose);
@@ -63,12 +63,15 @@ public final class LogsViewerDialog {
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnRefresh = new Button("Atualizar");
btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnRefresh.setPrefWidth(110);
btnRefresh.setMinWidth(110);
btnRefresh.setOnAction(e -> loadInitialLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose);
@@ -0,0 +1,534 @@
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.pdf.PdfSignatureOptions;
import br.com.cybercore.signer.core.pdf.PdfSigningService;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.security.SecurePinStorage;
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.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.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.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.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Diálogo dedicado e compacto para Seleção de Certificado Digital ICP-Brasil, inserção de PIN/senha,
* definição do arquivo de destino e conclusão da Assinatura Digital PAdES.
*/
@SuppressWarnings("this-escape")
public final class PdfCertificateSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
public record SealConfig(
boolean visibleSeal,
PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode,
boolean includeDateTime,
String customExtraInfo,
PdfSignDialog.PositionOption positionOption,
PdfSignDialog.PageOption pageOption,
boolean isCustomPosition,
Float customX,
Float customY,
Integer customPageNumber,
String reason,
String location
) {}
private final Stage stage;
private final CertificateManager certManager;
private final PdfSigningService pdfSigningService;
private final File sourcePdfFile;
private final SealConfig sealConfig;
private final String sealSummaryText;
private ComboBox<ManagedCertificate> certCombo;
private Label lblPinTitle;
private PasswordField pinField;
private Button btnClearRememberedPin;
private Label lblPinNotice;
private CheckBox chkRememberPin;
private ProgressIndicator progressIndicator;
private Label lblStatus;
private Button btnCancel;
private Button btnSign;
public PdfCertificateSignDialog(Stage owner,
CertificateManager certManager,
PdfSigningService pdfSigningService,
File sourcePdfFile,
SealConfig sealConfig,
String sealSummaryText) {
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo.");
this.sourcePdfFile = Objects.requireNonNull(sourcePdfFile, "sourcePdfFile não pode ser nulo.");
this.sealConfig = Objects.requireNonNull(sealConfig, "sealConfig não pode ser nulo.");
this.sealSummaryText = sealSummaryText != null ? sealSummaryText : "Selo Visual PAdES";
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setResizable(true);
this.stage.setMinWidth(620);
this.stage.setMinHeight(460);
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
initLayout();
}
private void initLayout() {
VBox root = new VBox(14);
root.setPadding(new Insets(18, 20, 18, 20));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2);
Label lblTitle = new Label("SELEÇÃO DE CERTIFICADO DIGITAL");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Escolha o certificado ICP-Brasil e informe o PIN para concluir a assinatura digital.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.certificatesIcon(), titleBox);
// 1. Resumo do Documento e do Selo
VBox summaryBox = new VBox(8);
summaryBox.getStyleClass().addAll("dialog-box", "document-summary-box");
summaryBox.setPadding(new Insets(12, 14, 12, 14));
Label lblSummaryHeader = new Label("DOCUMENTO CONFIGURADO");
lblSummaryHeader.getStyleClass().add("dialog-field-title");
GridPane summaryGrid = new GridPane();
summaryGrid.setHgap(12);
summaryGrid.setVgap(6);
Label lblDocTitle = new Label("Arquivo:");
lblDocTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblDocValue = new Label(sourcePdfFile.getName() + " (" + (sourcePdfFile.length() / 1024) + " KB)");
lblDocValue.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-primary; -fx-font-size: 11.5px;");
Label lblSealTitle = new Label("Selo Visual:");
lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblSealValue = new Label(sealSummaryText);
lblSealValue.setStyle("-fx-text-fill: #0284c7; -fx-font-weight: bold; -fx-font-size: 11.5px;");
summaryGrid.add(lblDocTitle, 0, 0);
summaryGrid.add(lblDocValue, 1, 0);
summaryGrid.add(lblSealTitle, 0, 1);
summaryGrid.add(lblSealValue, 1, 1);
summaryBox.getChildren().addAll(lblSummaryHeader, summaryGrid);
// 2. Card de Certificado ICP-Brasil
VBox certSection = new VBox(10);
certSection.getStyleClass().add("dialog-box");
certSection.setPadding(new Insets(14));
VBox.setVgrow(certSection, Priority.ALWAYS);
HBox certHeader = new HBox(10);
certHeader.setAlignment(Pos.CENTER_LEFT);
Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
lblCertTitle.getStyleClass().add("dialog-field-title");
HBox.setHgrow(lblCertTitle, Priority.ALWAYS);
Button btnRefresh = new Button("↻ Atualizar Lista");
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-compact");
btnRefresh.setOnAction(e -> {
populateCertificates();
updatePinFieldVisibility();
});
certHeader.getChildren().addAll(lblCertTitle, btnRefresh);
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(10);
pinRow.setAlignment(Pos.CENTER_LEFT);
lblPinTitle = new Label("PIN / Senha:");
lblPinTitle.getStyleClass().add("dialog-field-title");
pinField = new PasswordField();
pinField.setPromptText("PIN ou senha do certificado...");
pinField.setPrefWidth(220);
btnClearRememberedPin = new Button("Alterar PIN");
btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
btnClearRememberedPin.setVisible(false);
btnClearRememberedPin.setOnAction(e -> {
ManagedCertificate sel = certCombo.getValue();
if (sel != null) {
SecurePinStorage.getInstance().removePin(sel.id());
updatePinFieldVisibility();
}
});
lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;");
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;");
chkRememberPin.setSelected(true);
VBox tipBox = new VBox(2);
tipBox.setPadding(new Insets(8, 10, 8, 10));
tipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px;");
Label lblTip = new Label("🔒 Segurança: O PIN é criptografado localmente com AES-256-GCM vinculado à máquina e nunca é transmitido na rede.");
lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
tipBox.getChildren().add(lblTip);
certSection.getChildren().addAll(certHeader, certCombo, pinRow, chkRememberPin, tipBox);
// 3. Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
footer.setPadding(new Insets(4, 0, 0, 0));
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().addAll("btn-secondary", "btn-dialog");
btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento PDF");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
btnSign.setMinWidth(220);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> handleSign());
footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
root.getChildren().addAll(header, summaryBox, certSection, footer);
Scene scene = new Scene(root, 650, 480);
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", e);
}
}
private void updatePinFieldVisibility() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
pinField.setDisable(true);
lblPinNotice.setText("");
chkRememberPin.setVisible(false);
btnClearRememberedPin.setVisible(false);
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
if (isWindowsStore) {
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
pinField.setVisible(false);
lblPinTitle.setVisible(false);
chkRememberPin.setVisible(false);
btnClearRememberedPin.setVisible(false);
lblPinNotice.setText("✓ Certificado instalado no computador — Gerenciado pelo Windows (PIN não necessário)");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
return;
}
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
pinField.setVisible(true);
lblPinTitle.setVisible(true);
char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id());
if (storedPin != null && storedPin.length > 0) {
pinField.setText(new String(storedPin));
pinField.setDisable(true);
chkRememberPin.setVisible(false);
btnClearRememberedPin.setVisible(true);
lblPinNotice.setText("✓ PIN lembrado com segurança neste computador");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
} else {
pinField.clear();
pinField.setDisable(false);
chkRememberPin.setVisible(true);
btnClearRememberedPin.setVisible(false);
boolean isA3 = cert.type() == CertificateType.A3;
lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado");
lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
}
}
private void handleSign() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
char[] storedPin = !isWindowsStore ? SecurePinStorage.getInstance().getPin(cert.id()) : null;
if (!isWindowsStore && (storedPin == null || storedPin.length == 0) && (pinField.getText() == null || pinField.getText().trim().isEmpty())) {
showAlert(Alert.AlertType.WARNING, "PIN / Senha Obrigatório",
cert.type() == CertificateType.A3
? "Por favor, informe o PIN do seu dispositivo A3 para autorizar a assinatura."
: "Por favor, informe a senha do certificado A1 para autorizar a assinatura.");
pinField.requestFocus();
return;
}
FileChooser chooser = new FileChooser();
chooser.setTitle("Salvar PDF Assinado Como...");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
String originalName = sourcePdfFile.getName();
String baseName = originalName.toLowerCase().endsWith(".pdf")
? originalName.substring(0, originalName.length() - 4)
: originalName;
chooser.setInitialFileName(baseName + "_assinado.pdf");
if (sourcePdfFile.getParentFile() != null && sourcePdfFile.getParentFile().exists()) {
chooser.setInitialDirectory(sourcePdfFile.getParentFile());
}
File targetFile = chooser.showSaveDialog(stage);
if (targetFile == null) {
return;
}
char[] pinToUse = null;
if (!isWindowsStore) {
if (storedPin != null && storedPin.length > 0) {
pinToUse = storedPin;
} else if (!pinField.getText().isEmpty()) {
pinToUse = pinField.getText().toCharArray();
}
}
final char[] enteredPin = (!isWindowsStore && pinField.getText() != null && !pinField.getText().isEmpty())
? pinField.getText().toCharArray()
: null;
PdfSignatureOptions.Builder b = PdfSignatureOptions.builder(cert.id())
.pinOrPassword(pinToUse)
.visibleSeal(sealConfig.visibleSeal())
.sealTheme(sealConfig.sealTheme())
.cpfDisplayMode(sealConfig.cpfDisplayMode())
.includeDateTime(sealConfig.includeDateTime())
.customExtraInfo(sealConfig.customExtraInfo())
.reason(sealConfig.reason())
.location(sealConfig.location());
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
} else if (sealConfig.positionOption() != null) {
b.position(sealConfig.positionOption().toPosition());
if (sealConfig.pageOption() != null) {
b.pageChoice(sealConfig.pageOption().toPageChoice());
}
}
PdfSignatureOptions options = b.build();
setFormDisabled(true);
progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento PDF com envelope PAdES...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
Task<File> signTask = new Task<>() {
@Override
protected File call() throws Exception {
pdfSigningService.signPdfFile(sourcePdfFile, targetFile, options);
return targetFile;
}
};
signTask.setOnSucceeded(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
lblStatus.setText("✓ Documento assinado com sucesso!");
lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
// Salva o PIN de forma segura se solicitado pelo usuário
if (!isWindowsStore && chkRememberPin.isSelected() && enteredPin != null && enteredPin.length > 0) {
SecurePinStorage.getInstance().savePin(cert.id(), enteredPin);
}
showSuccessDialog(targetFile);
stage.close();
});
signTask.setOnFailed(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
Throwable ex = signTask.getException();
logger.error("Falha ao assinar PDF", ex);
// Se o PIN estava incorreto, remove do armazenamento seguro
String msg = ex != null ? ex.getMessage() : "";
if (msg.contains("PIN_INCORRECT") || msg.contains("senha") || msg.contains("PIN")) {
SecurePinStorage.getInstance().removePin(cert.id());
updatePinFieldVisibility();
}
lblStatus.setText("✕ Falha na assinatura");
lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF",
"Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
});
Thread signThread = new Thread(signTask, "cybercore-pdf-cert-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("PDF Assinado com Sucesso!");
alert.setContentText("O documento foi digitalmente assinado no padrão PAdES ICP-Brasil e salvo no destino solicitado.\n\n"
+ "Arquivo gerado:\n" + targetFile.getAbsolutePath());
ButtonType btnOpen = new ButtonType("Abrir PDF");
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);
}
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();
}
}
File diff suppressed because it is too large Load Diff
@@ -222,8 +222,9 @@ public final class SealManagementDialog {
footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary");
btnClose.setPrefWidth(95);
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
HBox pinActionsBox = new HBox(12);
pinActionsBox.setAlignment(Pos.CENTER_LEFT);
Button btnClearPins = new Button("Limpar PINs Salvos na Sessão");
Button btnClearPins = new Button("Limpar PINs Salvos (Sessão e Computador)");
btnClearPins.getStyleClass().add("btn-secondary");
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
btnClearPins.setOnAction(e -> {
PinSessionManager.getInstance().clearAll();
br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
SmartCardSessionManager.resetCardSessions();
pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados.");
pinStatusLabel.setText("✓ Todos os PINs salvos em memória e neste computador foram zerados.");
});
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
private void refreshSealsList() {
sealsContainer.getChildren().clear();
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
AgentConfigurationService configService = AgentConfigurationService.getInstance();
String currentDefaultId = configService.getConfig().defaultSealThemeId();
for (PdfSealTheme theme : themes) {
HBox item = new HBox(10);
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
item.getStyleClass().add("diagnostic-item");
item.setPadding(new Insets(6, 12, 6, 12));
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
VBox info = new VBox(2);
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
lblName.getStyleClass().add("cert-title");
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
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");
Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
badgeType.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
item.getChildren().addAll(info, badgeType);
item.getChildren().addAll(info, badge);
if (isDefault) {
Label badgeDefault = new Label("★ PADRÃO");
badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-padding: 3px 8px; -fx-background-radius: 4px; -fx-font-weight: bold; -fx-font-size: 11px;");
item.getChildren().add(badgeDefault);
} else {
Button btnSetDefault = new Button("★ Definir Padrão");
btnSetDefault.getStyleClass().addAll("btn-secondary", "btn-compact");
btnSetDefault.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px;");
btnSetDefault.setOnAction(e -> {
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
current.requireConfirmation(),
current.alwaysPromptPin(),
current.autoStart(),
theme.id(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
refreshSealsList();
notifySave("Selo '" + theme.name() + "' definido como o padrão para assinaturas.");
});
item.getChildren().add(btnSetDefault);
}
if (theme.isCustom()) {
Button btnDelete = new Button("Excluir");
@@ -77,8 +77,9 @@ public final class TutorialDialog {
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.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose);
@@ -27,9 +27,11 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
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.stage.FileChooser;
import javafx.stage.Modality;
@@ -214,6 +216,16 @@ public final class XmlSignDialog {
elementGrid.setHgap(12);
elementGrid.setVgap(8);
ColumnConstraints colTagLabel = new ColumnConstraints();
colTagLabel.setMinWidth(140);
colTagLabel.setPrefWidth(145);
colTagLabel.setHgrow(Priority.NEVER);
ColumnConstraints colTagField = new ColumnConstraints();
colTagField.setHgrow(Priority.ALWAYS);
elementGrid.getColumnConstraints().addAll(colTagLabel, colTagField);
Label lblTag = new Label("Tag do Elemento:");
lblTag.getStyleClass().add("dialog-field-title");
txtElementTag = new TextField("infNFe");
@@ -254,11 +266,12 @@ public final class XmlSignDialog {
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML");
btnSign.getStyleClass().add("btn-primary");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
btnSign.setMinWidth(Region.USE_PREF_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeXmlSignature());
@@ -853,6 +853,11 @@
3. BOTÕES E COMPONENTES COMUNS (DUAL THEME)
========================================================================== */
/* Botões Base e Proteção contra Truncamento com Reticências */
.button {
-fx-min-width: -fx-pref-size;
}
.btn-primary {
-fx-background-color: #0284c7;
-fx-text-fill: #ffffff;
@@ -860,6 +865,8 @@
-fx-padding: 8px 18px;
-fx-background-radius: 6px;
-fx-cursor: hand;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
}
@@ -873,6 +880,8 @@
-fx-font-weight: bold;
-fx-padding: 8px 18px;
-fx-background-radius: 6px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-cursor: hand;
}
@@ -901,6 +910,8 @@
-fx-text-fill: #475569;
-fx-font-weight: bold;
-fx-padding: 8px 18px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-cursor: hand;
}
@@ -919,6 +930,29 @@
-fx-text-fill: #f87171;
}
/* Botões Padronizados para Diálogos e Modais */
.btn-dialog {
-fx-min-width: 110px;
-fx-pref-width: 110px;
-fx-pref-height: 34px;
-fx-alignment: center;
}
.btn-dialog-wide {
-fx-min-width: 230px;
-fx-pref-width: 230px;
-fx-pref-height: 36px;
-fx-alignment: center;
}
/* Botões Compactos (barras de ferramentas e atalhos em linha) */
.btn-compact {
-fx-padding: 5px 12px;
-fx-font-size: 11.5px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
}
/* Botão de Perigo / Exclusão / Remoção */
.btn-danger {
-fx-background-color: #fee2e2;
@@ -931,6 +965,7 @@
-fx-font-weight: bold;
-fx-padding: 6px 14px;
-fx-alignment: center;
-fx-min-width: -fx-pref-size;
-fx-cursor: hand;
}
@@ -1063,3 +1098,131 @@
-fx-background-color: #0284c7;
-fx-background-radius: 4px;
}
/* =========================================================================
WIZARD STEPPER (FLUXO MULTI-ETAPAS)
========================================================================= */
.wizard-stepper {
-fx-alignment: center-left;
-fx-padding: 2px 0 6px 0;
}
.wizard-step-pill {
-fx-alignment: center;
-fx-padding: 4px 12px;
-fx-background-radius: 14px;
-fx-border-radius: 14px;
-fx-border-width: 1px;
}
.theme-light .wizard-step-pill {
-fx-background-color: #f1f5f9;
-fx-border-color: #cbd5e1;
}
.theme-dark .wizard-step-pill {
-fx-background-color: #1e293b;
-fx-border-color: #334155;
}
.theme-light .wizard-step-pill-active {
-fx-background-color: #e0f2fe;
-fx-border-color: #0284c7;
}
.theme-dark .wizard-step-pill-active {
-fx-background-color: #0c4a6e;
-fx-border-color: #38bdf8;
}
.wizard-step-badge {
-fx-font-size: 11px;
-fx-font-weight: bold;
-fx-alignment: center;
-fx-min-width: 20px;
-fx-min-height: 20px;
-fx-background-radius: 10px;
-fx-text-fill: #ffffff;
}
.theme-light .wizard-step-badge-active {
-fx-background-color: #0284c7;
-fx-text-fill: #ffffff;
}
.theme-dark .wizard-step-badge-active {
-fx-background-color: #38bdf8;
-fx-text-fill: #0f172a;
}
.theme-light .wizard-step-badge-inactive {
-fx-background-color: #94a3b8;
-fx-text-fill: #ffffff;
}
.theme-dark .wizard-step-badge-inactive {
-fx-background-color: #475569;
-fx-text-fill: #e2e8f0;
}
.wizard-step-text {
-fx-font-size: 11.5px;
-fx-font-weight: bold;
}
.theme-light .wizard-step-text-active {
-fx-text-fill: #0369a1;
}
.theme-dark .wizard-step-text-active {
-fx-text-fill: #38bdf8;
}
.theme-light .wizard-step-text-inactive {
-fx-text-fill: #64748b;
}
.theme-dark .wizard-step-text-inactive {
-fx-text-fill: #94a3b8;
}
.wizard-step-separator {
-fx-pref-width: 28px;
-fx-min-width: 20px;
-fx-pref-height: 2px;
}
.theme-light .wizard-step-separator {
-fx-background-color: #cbd5e1;
}
.theme-dark .wizard-step-separator {
-fx-background-color: #334155;
}
.theme-light .wizard-step-separator-active {
-fx-background-color: #0284c7;
}
.theme-dark .wizard-step-separator-active {
-fx-background-color: #38bdf8;
}
.document-summary-box {
-fx-padding: 10px 14px;
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-border-width: 1px;
-fx-alignment: center-left;
}
.theme-light .document-summary-box {
-fx-background-color: #f8fafc;
-fx-border-color: #e2e8f0;
}
.theme-dark .document-summary-box {
-fx-background-color: #0f172a;
-fx-border-color: #1e293b;
}