Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c997afbfd4 | ||
|
|
7f6d8a4a08 |
+17
@@ -66,6 +66,8 @@ public class PdfSigningService {
|
|||||||
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
|
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.
|
* 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.
|
* 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);
|
mainAppView = new MainAppView(primaryStage);
|
||||||
|
|
||||||
// Ações da Grade de Assinatura e Validação
|
// 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.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show());
|
||||||
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
|
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, 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) {
|
private void openSettingsDialog(Stage owner) {
|
||||||
Stage dlg = new Stage();
|
Stage dlg = new Stage();
|
||||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||||
@@ -178,8 +191,9 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||||
|
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-secondary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> dlg.close());
|
btnClose.setOnAction(e -> dlg.close());
|
||||||
|
|
||||||
HBox footer = new HBox(btnClose);
|
HBox footer = new HBox(btnClose);
|
||||||
@@ -206,8 +220,9 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
|
|
||||||
AboutView aboutView = new AboutView();
|
AboutView aboutView = new AboutView();
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-primary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> dlg.close());
|
btnClose.setOnAction(e -> dlg.close());
|
||||||
|
|
||||||
HBox footer = new HBox(btnClose);
|
HBox footer = new HBox(btnClose);
|
||||||
|
|||||||
+99
@@ -729,4 +729,103 @@ public final class SvgIcons {
|
|||||||
box.getChildren().add(cross);
|
box.getChildren().add(cross);
|
||||||
return box;
|
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);
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-secondary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
|
|
||||||
actionsRow.getChildren().add(btnClose);
|
actionsRow.getChildren().add(btnClose);
|
||||||
|
|||||||
+6
-3
@@ -63,12 +63,15 @@ public final class LogsViewerDialog {
|
|||||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
Button btnRefresh = new Button("Atualizar");
|
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());
|
btnRefresh.setOnAction(e -> loadInitialLogs());
|
||||||
|
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-primary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
|
|
||||||
footer.getChildren().addAll(btnRefresh, btnClose);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+581
-683
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);
|
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-secondary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
|
|
||||||
footer.getChildren().add(btnClose);
|
footer.getChildren().add(btnClose);
|
||||||
|
|||||||
+34
-5
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
|
|||||||
HBox pinActionsBox = new HBox(12);
|
HBox pinActionsBox = new HBox(12);
|
||||||
pinActionsBox.setAlignment(Pos.CENTER_LEFT);
|
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.getStyleClass().add("btn-secondary");
|
||||||
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
|
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
|
||||||
|
|
||||||
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
|
|||||||
|
|
||||||
btnClearPins.setOnAction(e -> {
|
btnClearPins.setOnAction(e -> {
|
||||||
PinSessionManager.getInstance().clearAll();
|
PinSessionManager.getInstance().clearAll();
|
||||||
|
br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
|
||||||
SmartCardSessionManager.resetCardSessions();
|
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);
|
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
|
||||||
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
|
|||||||
private void refreshSealsList() {
|
private void refreshSealsList() {
|
||||||
sealsContainer.getChildren().clear();
|
sealsContainer.getChildren().clear();
|
||||||
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
|
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
|
||||||
|
AgentConfigurationService configService = AgentConfigurationService.getInstance();
|
||||||
|
String currentDefaultId = configService.getConfig().defaultSealThemeId();
|
||||||
|
|
||||||
for (PdfSealTheme theme : themes) {
|
for (PdfSealTheme theme : themes) {
|
||||||
HBox item = new HBox(10);
|
HBox item = new HBox(10);
|
||||||
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
|
|||||||
item.getStyleClass().add("diagnostic-item");
|
item.getStyleClass().add("diagnostic-item");
|
||||||
item.setPadding(new Insets(6, 12, 6, 12));
|
item.setPadding(new Insets(6, 12, 6, 12));
|
||||||
|
|
||||||
|
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
|
||||||
|
|
||||||
VBox info = new VBox(2);
|
VBox info = new VBox(2);
|
||||||
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
|
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
|
||||||
lblName.getStyleClass().add("cert-title");
|
lblName.getStyleClass().add("cert-title");
|
||||||
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
|
|||||||
info.getChildren().addAll(lblName, lblDesc);
|
info.getChildren().addAll(lblName, lblDesc);
|
||||||
HBox.setHgrow(info, Priority.ALWAYS);
|
HBox.setHgrow(info, Priority.ALWAYS);
|
||||||
|
|
||||||
Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
|
Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
|
||||||
badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
|
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()) {
|
if (theme.isCustom()) {
|
||||||
Button btnDelete = new Button("Excluir");
|
Button btnDelete = new Button("Excluir");
|
||||||
|
|||||||
+3
-2
@@ -77,8 +77,9 @@ public final class TutorialDialog {
|
|||||||
HBox footer = new HBox();
|
HBox footer = new HBox();
|
||||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-primary");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(95);
|
btnClose.setPrefWidth(110);
|
||||||
|
btnClose.setMinWidth(110);
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
footer.getChildren().add(btnClose);
|
footer.getChildren().add(btnClose);
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -27,9 +27,11 @@ import javafx.scene.control.RadioButton;
|
|||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.control.ToggleGroup;
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
@@ -214,6 +216,16 @@ public final class XmlSignDialog {
|
|||||||
elementGrid.setHgap(12);
|
elementGrid.setHgap(12);
|
||||||
elementGrid.setVgap(8);
|
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:");
|
Label lblTag = new Label("Tag do Elemento:");
|
||||||
lblTag.getStyleClass().add("dialog-field-title");
|
lblTag.getStyleClass().add("dialog-field-title");
|
||||||
txtElementTag = new TextField("infNFe");
|
txtElementTag = new TextField("infNFe");
|
||||||
@@ -254,11 +266,12 @@ public final class XmlSignDialog {
|
|||||||
HBox.setHgrow(lblStatus, Priority.ALWAYS);
|
HBox.setHgrow(lblStatus, Priority.ALWAYS);
|
||||||
|
|
||||||
btnCancel = new Button("Cancelar");
|
btnCancel = new Button("Cancelar");
|
||||||
btnCancel.getStyleClass().add("btn-secondary");
|
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnCancel.setOnAction(e -> stage.close());
|
btnCancel.setOnAction(e -> stage.close());
|
||||||
|
|
||||||
btnSign = new Button("Assinar Documento XML");
|
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.setDefaultButton(true);
|
||||||
btnSign.setOnAction(e -> executeXmlSignature());
|
btnSign.setOnAction(e -> executeXmlSignature());
|
||||||
|
|
||||||
|
|||||||
@@ -853,6 +853,11 @@
|
|||||||
3. BOTÕES E COMPONENTES COMUNS (DUAL THEME)
|
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 {
|
.btn-primary {
|
||||||
-fx-background-color: #0284c7;
|
-fx-background-color: #0284c7;
|
||||||
-fx-text-fill: #ffffff;
|
-fx-text-fill: #ffffff;
|
||||||
@@ -860,6 +865,8 @@
|
|||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
-fx-cursor: hand;
|
-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);
|
-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-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
|
-fx-min-width: -fx-pref-size;
|
||||||
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,6 +910,8 @@
|
|||||||
-fx-text-fill: #475569;
|
-fx-text-fill: #475569;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
|
-fx-min-width: -fx-pref-size;
|
||||||
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,6 +930,29 @@
|
|||||||
-fx-text-fill: #f87171;
|
-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 */
|
/* Botão de Perigo / Exclusão / Remoção */
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
-fx-background-color: #fee2e2;
|
-fx-background-color: #fee2e2;
|
||||||
@@ -931,6 +965,7 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 6px 14px;
|
-fx-padding: 6px 14px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
|
-fx-min-width: -fx-pref-size;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,3 +1098,131 @@
|
|||||||
-fx-background-color: #0284c7;
|
-fx-background-color: #0284c7;
|
||||||
-fx-background-radius: 4px;
|
-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