Compare commits
2
Commits
main
..
c997afbfd4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c997afbfd4 | ||
|
|
7f6d8a4a08 |
+17
@@ -66,6 +66,8 @@ public class PdfSigningService {
|
||||
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
|
||||
}
|
||||
|
||||
public record PageDimensions(float width, float height) {}
|
||||
|
||||
/**
|
||||
* Retorna a quantidade total de páginas contidas no documento PDF.
|
||||
*/
|
||||
@@ -76,6 +78,21 @@ public class PdfSigningService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna as dimensões em pontos (pt) da página informada do documento PDF.
|
||||
*/
|
||||
public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
|
||||
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
|
||||
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
|
||||
}
|
||||
PDPage page = doc.getPage(pageIndex0Based);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
return new PageDimensions(mediaBox.getWidth(), mediaBox.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
|
||||
*/
|
||||
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
package br.com.cybercore.signer.core.security;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
||||
* <p>
|
||||
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
||||
* a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256).
|
||||
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
|
||||
*/
|
||||
public final class SecurePinStorage {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SecurePinStorage.class);
|
||||
|
||||
private static final String FILE_NAME = "secure-pins.dat";
|
||||
private static final String SALT_FILE_NAME = "secure-pins.salt";
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final int PBKDF2_ITERATIONS = 65536;
|
||||
private static final int KEY_LENGTH_BITS = 256;
|
||||
|
||||
private static final SecurePinStorage INSTANCE = new SecurePinStorage();
|
||||
|
||||
private final Path storageDirectory;
|
||||
private final Path dataFile;
|
||||
private final Path saltFile;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
||||
|
||||
public static SecurePinStorage getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public SecurePinStorage() {
|
||||
this(resolveDefaultDirectory());
|
||||
}
|
||||
|
||||
public SecurePinStorage(Path storageDirectory) {
|
||||
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
||||
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
||||
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
||||
this.objectMapper = new ObjectMapper();
|
||||
loadFromDisk();
|
||||
}
|
||||
|
||||
private static Path resolveDefaultDirectory() {
|
||||
String userHome = System.getProperty("user.home", ".");
|
||||
return Path.of(userHome, ".cybercore");
|
||||
}
|
||||
|
||||
/**
|
||||
* Armazena de forma persistente e criptografada o PIN associado ao ID do certificado.
|
||||
*/
|
||||
public synchronized void savePin(String certId, char[] pin) {
|
||||
Objects.requireNonNull(certId, "certId não pode ser nulo");
|
||||
if (pin == null || pin.length == 0) {
|
||||
removePin(certId);
|
||||
return;
|
||||
}
|
||||
|
||||
memoryCache.put(certId, pin.clone());
|
||||
PinSessionManager.getInstance().savePin(certId, pin);
|
||||
persistToDisk();
|
||||
logger.info("PIN do certificado '{}' armazenado com segurança neste computador.", certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna o PIN armazenado para o certificado ou {@code null} se não houver.
|
||||
*/
|
||||
public synchronized char[] getPin(String certId) {
|
||||
if (certId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Tenta cache em memória
|
||||
char[] cached = memoryCache.get(certId);
|
||||
if (cached != null) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
// 2. Tenta carregar do PinSessionManager
|
||||
char[] sessionPin = PinSessionManager.getInstance().getPin(certId);
|
||||
if (sessionPin != null) {
|
||||
memoryCache.put(certId, sessionPin.clone());
|
||||
return sessionPin.clone();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se existe um PIN armazenado para o certificado.
|
||||
*/
|
||||
public synchronized boolean hasPin(String certId) {
|
||||
if (certId == null) {
|
||||
return false;
|
||||
}
|
||||
return memoryCache.containsKey(certId) || PinSessionManager.getInstance().hasPin(certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove o PIN salvo para o certificado e zera a memória.
|
||||
*/
|
||||
public synchronized void removePin(String certId) {
|
||||
if (certId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
char[] old = memoryCache.remove(certId);
|
||||
if (old != null) {
|
||||
Arrays.fill(old, ' ');
|
||||
}
|
||||
PinSessionManager.getInstance().clearPin(certId);
|
||||
persistToDisk();
|
||||
logger.info("PIN do certificado '{}' removido do armazenamento seguro.", certId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove todos os PINs armazenados em disco e zera todas as memórias.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
for (char[] pin : memoryCache.values()) {
|
||||
if (pin != null) {
|
||||
Arrays.fill(pin, ' ');
|
||||
}
|
||||
}
|
||||
memoryCache.clear();
|
||||
PinSessionManager.getInstance().clearAll();
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(dataFile);
|
||||
logger.info("Arquivo de credenciais criptografadas removido com sucesso.");
|
||||
} catch (Exception e) {
|
||||
logger.warn("Falha ao remover arquivo de credenciais: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int getStoredCount() {
|
||||
return memoryCache.size();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRIPTOGRAFIA AES-256-GCM E DERIVAÇÃO DE CHAVE
|
||||
// =========================================================================
|
||||
|
||||
private void persistToDisk() {
|
||||
try {
|
||||
Files.createDirectories(storageDirectory);
|
||||
|
||||
if (memoryCache.isEmpty()) {
|
||||
Files.deleteIfExists(dataFile);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> plainMap = new HashMap<>();
|
||||
for (Map.Entry<String, char[]> entry : memoryCache.entrySet()) {
|
||||
plainMap.put(entry.getKey(), new String(entry.getValue()));
|
||||
}
|
||||
|
||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(plainMap);
|
||||
byte[] salt = getOrCreateSalt();
|
||||
SecretKey secretKey = deriveKey(salt);
|
||||
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
|
||||
|
||||
byte[] ciphertext = cipher.doFinal(jsonBytes);
|
||||
|
||||
// Grava: [12 bytes IV] + [ciphertext + tag]
|
||||
try (FileOutputStream fos = new FileOutputStream(dataFile.toFile())) {
|
||||
fos.write(iv);
|
||||
fos.write(ciphertext);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromDisk() {
|
||||
if (!Files.exists(dataFile) || !Files.exists(saltFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] allBytes = Files.readAllBytes(dataFile);
|
||||
if (allBytes.length <= GCM_IV_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] iv = Arrays.copyOfRange(allBytes, 0, GCM_IV_LENGTH);
|
||||
byte[] ciphertext = Arrays.copyOfRange(allBytes, GCM_IV_LENGTH, allBytes.length);
|
||||
|
||||
byte[] salt = Files.readAllBytes(saltFile);
|
||||
SecretKey secretKey = deriveKey(salt);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
|
||||
|
||||
byte[] plainBytes = cipher.doFinal(ciphertext);
|
||||
Map<String, String> plainMap = objectMapper.readValue(plainBytes, new TypeReference<>() {});
|
||||
|
||||
memoryCache.clear();
|
||||
for (Map.Entry<String, String> entry : plainMap.entrySet()) {
|
||||
char[] pin = entry.getValue().toCharArray();
|
||||
memoryCache.put(entry.getKey(), pin);
|
||||
PinSessionManager.getInstance().savePin(entry.getKey(), pin);
|
||||
}
|
||||
logger.info("Carregados {} PIN(s) do armazenamento seguro local.", memoryCache.size());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Não foi possível carregar credenciais salvas em disco: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getOrCreateSalt() throws Exception {
|
||||
if (Files.exists(saltFile)) {
|
||||
return Files.readAllBytes(saltFile);
|
||||
}
|
||||
byte[] salt = new byte[32];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
Files.write(saltFile, salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
private SecretKey deriveKey(byte[] salt) throws Exception {
|
||||
// Combina identificadores estáveis exclusivos da máquina e usuário
|
||||
String hostIdentity = String.join(":",
|
||||
System.getProperty("user.name", "unknown"),
|
||||
System.getProperty("user.home", "unknown"),
|
||||
System.getProperty("os.name", "unknown"),
|
||||
System.getProperty("os.arch", "unknown"),
|
||||
"cybercore-signer-hardware-pin-binding"
|
||||
);
|
||||
|
||||
KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
||||
return new SecretKeySpec(keyBytes, "AES");
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package br.com.cybercore.signer.core.security;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SecurePinStorageTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private SecurePinStorage storage;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
storage = new SecurePinStorage(tempDir);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
storage.clearAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndRetrievePinCorrectly() {
|
||||
String certId = "cert-test-123";
|
||||
char[] pin = "123456".toCharArray();
|
||||
|
||||
storage.savePin(certId, pin);
|
||||
|
||||
assertThat(storage.hasPin(certId)).isTrue();
|
||||
assertThat(storage.getPin(certId)).isEqualTo(pin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistAndReloadFromDisk() {
|
||||
String certId = "token-a3-999";
|
||||
char[] pin = "senhaForte987".toCharArray();
|
||||
|
||||
storage.savePin(certId, pin);
|
||||
|
||||
// Cria nova instância apontando para o mesmo diretório
|
||||
SecurePinStorage reloaded = new SecurePinStorage(tempDir);
|
||||
|
||||
assertThat(reloaded.hasPin(certId)).isTrue();
|
||||
assertThat(reloaded.getPin(certId)).isEqualTo(pin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveSpecificPin() {
|
||||
storage.savePin("cert-1", "1111".toCharArray());
|
||||
storage.savePin("cert-2", "2222".toCharArray());
|
||||
|
||||
storage.removePin("cert-1");
|
||||
|
||||
assertThat(storage.hasPin("cert-1")).isFalse();
|
||||
assertThat(storage.getPin("cert-1")).isNull();
|
||||
assertThat(storage.hasPin("cert-2")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClearAllPins() {
|
||||
storage.savePin("cert-1", "1111".toCharArray());
|
||||
storage.savePin("cert-2", "2222".toCharArray());
|
||||
|
||||
storage.clearAll();
|
||||
|
||||
assertThat(storage.hasPin("cert-1")).isFalse();
|
||||
assertThat(storage.hasPin("cert-2")).isFalse();
|
||||
assertThat(storage.getStoredCount()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -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);
|
||||
|
||||
+99
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+6
-3
@@ -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);
|
||||
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
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(700);
|
||||
this.stage.setMinHeight(490);
|
||||
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
|
||||
|
||||
initLayout();
|
||||
}
|
||||
|
||||
private HBox windowsStoreBox;
|
||||
private HBox pinRow;
|
||||
|
||||
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;");
|
||||
lblDocValue.setWrapText(true);
|
||||
|
||||
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;");
|
||||
lblSealValue.setWrapText(true);
|
||||
|
||||
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() + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Banner informativo para certificados da Loja do Windows (sem PIN)
|
||||
windowsStoreBox = new HBox(8);
|
||||
windowsStoreBox.setAlignment(Pos.CENTER_LEFT);
|
||||
windowsStoreBox.setPadding(new Insets(10, 12, 10, 12));
|
||||
windowsStoreBox.setStyle("-fx-background-color: #ecfdf5; -fx-border-color: #a7f3d0; -fx-border-radius: 6px; -fx-background-radius: 6px;");
|
||||
Label lblWinNotice = new Label("✓ Certificado instalado no computador — Gerenciado nativamente pelo Windows (PIN não necessário)");
|
||||
lblWinNotice.setStyle("-fx-text-fill: #065f46; -fx-font-weight: bold; -fx-font-size: 11.5px;");
|
||||
lblWinNotice.setWrapText(true);
|
||||
HBox.setHgrow(lblWinNotice, Priority.ALWAYS);
|
||||
windowsStoreBox.getChildren().add(lblWinNotice);
|
||||
|
||||
// Linha de PIN / Senha para Tokens A3 e Arquivos A1
|
||||
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;");
|
||||
lblPinNotice.setWrapText(true);
|
||||
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
|
||||
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; -fx-background-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;");
|
||||
lblTip.setWrapText(true);
|
||||
lblTip.setMaxWidth(Double.MAX_VALUE);
|
||||
tipBox.getChildren().add(lblTip);
|
||||
|
||||
certSection.getChildren().addAll(certHeader, certCombo, windowsStoreBox, 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-wide");
|
||||
btnSign.setMinWidth(220);
|
||||
btnSign.setPrefWidth(Region.USE_COMPUTED_SIZE);
|
||||
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, 720, 500);
|
||||
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);
|
||||
chkRememberPin.setManaged(false);
|
||||
btnClearRememberedPin.setVisible(false);
|
||||
windowsStoreBox.setVisible(false);
|
||||
windowsStoreBox.setManaged(false);
|
||||
pinRow.setVisible(true);
|
||||
pinRow.setManaged(true);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
|
||||
if (isWindowsStore) {
|
||||
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
|
||||
windowsStoreBox.setVisible(true);
|
||||
windowsStoreBox.setManaged(true);
|
||||
pinRow.setVisible(false);
|
||||
pinRow.setManaged(false);
|
||||
chkRememberPin.setVisible(false);
|
||||
chkRememberPin.setManaged(false);
|
||||
btnClearRememberedPin.setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
|
||||
windowsStoreBox.setVisible(false);
|
||||
windowsStoreBox.setManaged(false);
|
||||
pinRow.setVisible(true);
|
||||
pinRow.setManaged(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);
|
||||
chkRememberPin.setManaged(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);
|
||||
chkRememberPin.setManaged(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();
|
||||
}
|
||||
}
|
||||
+583
-685
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -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);
|
||||
|
||||
+34
-5
@@ -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");
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
|
||||
+15
-2
@@ -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: -fx-pref-size;
|
||||
-fx-pref-height: 34px;
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.btn-dialog-wide {
|
||||
-fx-min-width: 220px;
|
||||
-fx-pref-width: -fx-pref-size;
|
||||
-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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user