Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539d5e4c33 | ||
|
|
5551d9e667 | ||
|
|
ef44d34944 | ||
|
|
edccfff7ff | ||
|
|
16212702cb | ||
|
|
6f191c6a69 | ||
|
|
d1cfab95c2 | ||
|
|
64ca13b3a1 |
@@ -56,6 +56,7 @@ assets/
|
|||||||
|
|
||||||
1. **Ícones:**
|
1. **Ícones:**
|
||||||
- Todos estão no formato `.svg` padrão da W3C.
|
- Todos estão no formato `.svg` padrão da W3C.
|
||||||
|
- Consulte o guia completo de especificações em [docs/svg-design-guide.md](../docs/svg-design-guide.md) para tokens de cores, dimensões e boas práticas ao exportar do **Inkscape/Figma**.
|
||||||
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
||||||
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- Dobra de canto -->
|
||||||
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
|
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
|
||||||
<!-- Linhas de texto representativas -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta vermelha com sigla PDF -->
|
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
|
||||||
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
|
||||||
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8.5" font-weight="bold" fill="#ffffff" text-anchor="middle">PDF</text>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 996 B After Width: | Height: | Size: 1.0 KiB |
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- Dobra de canto -->
|
||||||
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
|
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
|
||||||
<!-- Linhas de texto representativas -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta azul com sigla XML -->
|
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
|
||||||
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#0288d1"/>
|
||||||
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8" font-weight="bold" fill="#ffffff" text-anchor="middle"><XML></text>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central"><XML></text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.0 KiB |
@@ -55,6 +55,7 @@ echo -e "${COLOR_RESET}"
|
|||||||
# 1. Verificação e Auto-Detecção do Java (JDK 21+)
|
# 1. Verificação e Auto-Detecção do Java (JDK 21+)
|
||||||
if ! command -v java &>/dev/null; then
|
if ! command -v java &>/dev/null; then
|
||||||
POSSIBLE_JDKS=(
|
POSSIBLE_JDKS=(
|
||||||
|
"${HOME}/.jdks/jdk-25.0.4.1+1"
|
||||||
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
||||||
"/usr/lib/jvm/java-25-openjdk-amd64"
|
"/usr/lib/jvm/java-25-openjdk-amd64"
|
||||||
"/usr/lib/jvm/java-21-openjdk-amd64"
|
"/usr/lib/jvm/java-21-openjdk-amd64"
|
||||||
@@ -105,9 +106,15 @@ if [[ ! -f "${SOURCE_JAR}" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Copia para a raiz para facilitar o acesso
|
# 4. Copia para a raiz e para a pasta sdk/ para manter o ambiente sincronizado
|
||||||
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
|
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
|
||||||
|
|
||||||
|
if [[ -d "sdk" ]]; then
|
||||||
|
SDK_JAR="sdk/cybercore-signer.jar"
|
||||||
|
cp -f "${SOURCE_JAR}" "${SDK_JAR}"
|
||||||
|
echo -e "Cópia sincronizada para o SDK: ${COLOR_BOLD}${SDK_JAR}${COLOR_RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
|
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Diagnóstico: Janela Principal no Windows 11 (Bisseção e Monitoramento)
|
||||||
|
|
||||||
|
Este documento descreve o procedimento de diagnóstico para isolar a causa de ocultamento nativo da janela principal no Windows 11 ao passar o mouse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Monitor Nativo Win32/DWM (`scripts/diag-window.ps1`)
|
||||||
|
|
||||||
|
O script [scripts/diag-window.ps1](file:///var/www/cybercore/cybercore-signer/scripts/diag-window.ps1) inspeciona a cada 300ms a janela nativa via P/Invoke da Win32 API e DWM:
|
||||||
|
- `IsWindowVisible`: se a janela está visível perante o subsistema Win32;
|
||||||
|
- `DWMWA_CLOAKED`: se o gerenciador gráfico do Windows 11 ocultou a composição;
|
||||||
|
- `IsIconic`: se foi minimizada;
|
||||||
|
- `GetWindowRect`: coordenadas reais (detecta se foi deslocada para fora da tela).
|
||||||
|
|
||||||
|
### Como executar o monitor:
|
||||||
|
Em uma janela separada do PowerShell:
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Roteiro de Bisseção (Fase 2)
|
||||||
|
|
||||||
|
Execute cada um dos comandos abaixo individualmente (garantindo antes que instâncias anteriores foram encerradas com `Stop-Process -Name java -Force` se necessário):
|
||||||
|
|
||||||
|
### Teste A: Desativar Middleware PKCS#11 / SafeSign (`aetpkss1.dll`)
|
||||||
|
Isola hooks nativos do SafeSign e middlewares A3:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-pkcs11=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é o hook nativo do middleware PKCS#11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste B: Plain Stage (Janela livre sem `setResizable(false)` e sem limites min/max)
|
||||||
|
Isola o bug de composição do DWM / Glass no Windows 11 com janelas de tamanho fixo:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.plain-stage=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é a restrição fixa de tamanho no Windows 11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste C: Desativar Efeitos de Sombra no Mouse (`-fx-effect`)
|
||||||
|
Isola recálculo de dropshadow e troca de cursor:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-effects=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste D: Desativar Monitor Dinâmico de Smart Cards (PC/SC)
|
||||||
|
Isola threads em segundo plano de `javax.smartcardio`:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-smartcard-watcher=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste E: Desativar Windows Certificate Store (Windows-MY)
|
||||||
|
Isola inicialização de CryptoAPI / SunMSCAPI:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-wincert=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
@@ -35,7 +35,7 @@ O módulo de núcleo é totalmente independente de JavaFX ou bibliotecas visuais
|
|||||||
|
|
||||||
### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
|
### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
|
||||||
Responsável por toda a interação com o usuário:
|
Responsável por toda a interação com o usuário:
|
||||||
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade 3x3 de operações rápidas (PDF, XML, Arquivo Genérico, Assinatura em Lote, Assinatura Atachada, Co-Assinatura, Carimbo de Tempo, Designer de Selos, Validador) e coluna lateral de utilitários (Logs em tempo real, Configurações e Diagnóstico).
|
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade de operações rápidas (PDF, XML, Assinatura Atachada, Designer de Selos, Validar Assinatura, Sair do Agente) e coluna lateral de utilitários (Logs em tempo real, Configurações, Tutorial, Certificados e Sobre).
|
||||||
* **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
|
* **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
|
||||||
* **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
|
* **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
|
||||||
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
|
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
|
||||||
|
|||||||
@@ -30,24 +30,18 @@ Em conformidade com os requisitos do projeto, a interface **não é uma cópia d
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Grade Central de Ações (Hub 4x3)
|
## 3. Grade Central de Ações (Hub 3x2)
|
||||||
|
|
||||||
A interface principal organiza as operações em 12 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
|
A interface principal organiza as operações em 6 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
|
||||||
|
|
||||||
| Posição | Operação | Descrição |
|
| Posição | Operação | Descrição |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF com cálculo de hash SHA-256 e gravação de assinatura. |
|
| (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF com cálculo de hash SHA-256 e gravação de assinatura. |
|
||||||
| (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). |
|
| (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). |
|
||||||
| (2, 0) | **Assinar Arquivo** | Assinatura genérica de qualquer arquivo ou binário do sistema. |
|
| (2, 0) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
|
||||||
| (3, 0) | **Assinar em Lote** | Processamento simultâneo de múltiplos arquivos com barra de progresso em tempo real. |
|
| (0, 1) | **Gerenciar Selos** | Abertura do Designer e Gerenciador de Selos Digitais visuais para PAdES. |
|
||||||
| (0, 1) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
|
| (1, 1) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. |
|
||||||
| (1, 1) | **Co-Assinar Documento** | Adição de nova assinatura a um documento previamente assinado por outro titular. |
|
| (2, 1) | **Sair do Agente** | Encerramento seguro da sessão do agente. |
|
||||||
| (2, 1) | **Carimbo do Tempo** | Integração com protocolo RFC 3161 TSP para prova de anterioridade temporal. |
|
|
||||||
| (3, 1) | **Assinatura LTV** | Perfil de Longa Duração com inclusão de cadeia de certificados e carimbos para validade permanente. |
|
|
||||||
| (0, 2) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. |
|
|
||||||
| (1, 2) | **Validar Carimbo** | Verificação da autenticidade e validade temporal de tokens TSR/TSP. |
|
|
||||||
| (2, 2) | *(Reservado)* | Espaço reservado para expansões futuras do agente. |
|
|
||||||
| (3, 2) | **Sair do Agente** | Encerramento seguro da sessão do agente. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Guia de Padrão Visual e Design System SVG — CyberCore Signer
|
||||||
|
|
||||||
|
Este documento estabelece a especificação oficial de design, geometria, tokens de cores e regras de compatibilidade para todos os ícones e elementos gráficos vetoriais (**SVG**) do ecossistema **CyberCore Signer**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Princípios de Design
|
||||||
|
|
||||||
|
1. **Clareza e Precisão:** Cada ícone deve comunicar imediatamente sua função técnica (PDF, XML, assinatura embutida, validação, credenciais).
|
||||||
|
2. **Minimalismo e Geometria Pura:** Evitar ruídos visuais, nós desnecessários, filtros proprietários ou imagens bitmap embutidas (`<image>`).
|
||||||
|
3. **Consistência de Traço e Escala:** Espessuras de contorno previsíveis e alinhadas ao grid de 48x48 pixels.
|
||||||
|
4. **Interoperabilidade:** Totalmente compatível com editores vetoriais modernos (**Inkscape, Figma, Adobe Illustrator**) e com o parser nativo de JavaFX em `AssetManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Grid e Canvas Canônico
|
||||||
|
|
||||||
|
| Propriedade | Valor Padrão | Descrição |
|
||||||
|
|---|---|---|
|
||||||
|
| **ViewBox** | `0 0 48 48` | Padrão obrigatório para ícones da grade de ações e ferramentas laterais. |
|
||||||
|
| **Dimensões Base** | `width="48" height="48"` | Dimensões nominais 1:1. |
|
||||||
|
| **Área Segura** | `4px` de margem interna | O conteúdo vetorial deve se concentrar entre `(4, 4)` e `(44, 44)`. |
|
||||||
|
| **Raio de Canto Padrão** | `rx="3"` ou `rx="6"` | Arredondamentos harmônicos em cantos de janelas e etiquetas. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Paleta de Cores e Tokens Visuais
|
||||||
|
|
||||||
|
Os ícones utilizam uma paleta de cores corporativa baseada em Material Design e Tailwind Modern, com alto contraste sobre fundos claros e escuros:
|
||||||
|
|
||||||
|
| Token | Hexadecimal | Papel / Significado |
|
||||||
|
|---|---|---|
|
||||||
|
| **`color-brand-primary`** | `#0288d1` | Azul CyberCore. Traço padrão de folhas, botões principais e XML. |
|
||||||
|
| **`color-brand-deep`** | `#01579b` | Azul escuro para dobras e acentos de profundidade. |
|
||||||
|
| **`color-paper-fold`** | `#b3e5fc` | Preenchimento suave da dobra superior do documento padrão. |
|
||||||
|
| **`color-pdf-accent`** | `#d32f2f` | Vermelho institucional do formato Adobe PDF. |
|
||||||
|
| **`color-pdf-fold`** | `#ffcdd2` | Preenchimento suave da dobra do documento PDF. |
|
||||||
|
| **`color-success`** | `#10b981` / `#2e7d32` | Verde esmeralda para assinaturas válidas e verificações. |
|
||||||
|
| **`color-gold-seal`** | `#f59e0b` / `#d97706` | Dourado e âmbar para selos notariais e certificação digital. |
|
||||||
|
| **`color-paper-lines`** | `#b0bec5` / `#cfd8dc` | Linhas de texto representativas e divisórias neutras. |
|
||||||
|
| **`color-console-bg`** | `#1e293b` | Fundo de janelas de terminal (Logs). |
|
||||||
|
| **`color-danger-exit`** | `#d32f2f` | Seta e alertas de encerramento do agente. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Anatomia dos Ícones de Documentos
|
||||||
|
|
||||||
|
Para documentos com dobra (como PDF, XML, Atachada e Validador):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<!-- 1. Folha de documento com dobra -->
|
||||||
|
<path d="M 8 4 L 32 4 L 42 14 L 42 44 L 8 44 Z"
|
||||||
|
fill="#ffffff" stroke="[COR_BORDA]" stroke-width="2.5" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- 2. Dobra de canto superior direito -->
|
||||||
|
<path d="M 32 4 L 32 14 L 42 14 Z"
|
||||||
|
fill="[COR_FUNDO_DOBRA]" stroke="[COR_BORDA]" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- 3. Linhas de texto representativas -->
|
||||||
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- 4. Elemento distintivo inferior (Etiqueta, Caneta, Clipe ou Checkmark) -->
|
||||||
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="[COR_BADGE]"/>
|
||||||
|
<text x="25" y="39" font-family="'Segoe UI', Arial, sans-serif"
|
||||||
|
font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle">[TEXTO]</text>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boas Práticas para Exportação no Inkscape / Figma
|
||||||
|
|
||||||
|
Ao criar ou editar ícones nas ferramentas vetoriais externas:
|
||||||
|
|
||||||
|
1. **Escolha a opção "SVG Simples" (Plain SVG) no Inkscape:**
|
||||||
|
- No Inkscape: `Arquivo → Salvar Como... → SVG Simples (*.svg)`.
|
||||||
|
- Isso elimina metadados de sessão (`sodipodi:namedview`, guias, zoom e histórico).
|
||||||
|
2. **Nunca incorpore imagens bitmap (`<image>` base64):**
|
||||||
|
- O aplicativo desktop deve conter exclusivamente geometria vetorial limpa (`path`, `rect`, `circle`, `line`, `polygon`, `polyline`, `text`).
|
||||||
|
3. **Suporte a `style="..."` inline:**
|
||||||
|
- O parser do CyberCore Signer (`AssetManager`) suporta tanto atributos XML puros (`fill="#..."`) quanto estilos inline em CSS (`style="fill:#...;stroke:#..."`).
|
||||||
|
4. **Mantenha o ViewBox Quadrado:**
|
||||||
|
- Preserve sempre `viewBox="0 0 48 48"` para garantir alinhamento perfeito nos botões e painéis da interface.
|
||||||
@@ -36,6 +36,17 @@ public class CyberCoreSignerApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Previne conflitos nativos de UI Automation/Acessibilidade do Glass no Windows 11
|
||||||
|
if (System.getProperty("javafx.accessibility") == null) {
|
||||||
|
System.setProperty("javafx.accessibility", "false");
|
||||||
|
}
|
||||||
|
if (System.getProperty("glass.accessible.force") == null) {
|
||||||
|
System.setProperty("glass.accessible.force", "false");
|
||||||
|
}
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
logger.error("Exceção não tratada na thread '{}': {}", thread.getName(), throwable.getMessage(), throwable);
|
||||||
|
});
|
||||||
|
|
||||||
parseArguments(args);
|
parseArguments(args);
|
||||||
printBanner();
|
printBanner();
|
||||||
|
|
||||||
@@ -67,6 +78,9 @@ public class CyberCoreSignerApplication {
|
|||||||
try {
|
try {
|
||||||
logger.info("Iniciando interface gráfica desktop...");
|
logger.info("Iniciando interface gráfica desktop...");
|
||||||
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() ->
|
||||||
|
logger.warn("JVM em encerramento (sinal externo, System.exit ou fim do aplicativo)."),
|
||||||
|
"cybercore-shutdown-diagnostic"));
|
||||||
Application.launch(CyberCoreDesktopApplication.class, args);
|
Application.launch(CyberCoreDesktopApplication.class, args);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
|
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
|
||||||
|
|||||||
@@ -218,16 +218,20 @@ public class CcspServer {
|
|||||||
appId = getQueryParam(uri, "application_id");
|
appId = getQueryParam(uri, "application_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica autorização se app_id e origin forem informados
|
// Exige obrigatoriamente a identificação da aplicação para listagem de certificados
|
||||||
if (appId != null && !appId.isBlank()) {
|
if (appId == null || appId.isBlank()) {
|
||||||
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
sendErrorResponse(exchange, 401, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||||
if (!authorized) {
|
"application_id (via query param ou cabeçalho X-CyberCore-AppId) é obrigatório.");
|
||||||
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
return;
|
||||||
if (!approved) {
|
}
|
||||||
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
|
||||||
"Aplicação não autorizada pelo usuário.");
|
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
|
||||||
return;
|
if (!authorized) {
|
||||||
}
|
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
|
||||||
|
if (!approved) {
|
||||||
|
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
|
||||||
|
"Aplicação não autorizada pelo usuário.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,14 +354,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validação anti-replay (Nonce)
|
// 3. Validação anti-replay (Nonce obrigatório)
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
if (!accepted) {
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
return;
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
}
|
||||||
return;
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
}
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Validação de autorização da aplicação
|
// 4. Validação de autorização da aplicação
|
||||||
@@ -520,13 +527,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
// Validação anti-replay (Nonce obrigatório)
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
if (!accepted) {
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String appId = request.applicationId();
|
String appId = request.applicationId();
|
||||||
@@ -735,13 +746,17 @@ public class CcspServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.nonce() != null && !request.nonce().isBlank()) {
|
// Validação anti-replay (Nonce obrigatório)
|
||||||
boolean accepted = nonceManager.registerNonce(request.nonce());
|
if (request.nonce() == null || request.nonce().isBlank()) {
|
||||||
if (!accepted) {
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
"O parâmetro 'nonce' é obrigatório para proteção contra replay attack.");
|
||||||
"Requisição duplicada detectada (replay prevenido).");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
boolean accepted = nonceManager.registerNonce(request.nonce());
|
||||||
|
if (!accepted) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
|
||||||
|
"Requisição duplicada detectada (replay prevenido).");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String appId = request.applicationId();
|
String appId = request.applicationId();
|
||||||
@@ -1032,25 +1047,53 @@ public class CcspServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean handleCors(HttpExchange exchange) throws IOException {
|
private boolean handleCors(HttpExchange exchange) throws IOException {
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
String origin = getOrigin(exchange);
|
String origin = getOrigin(exchange);
|
||||||
|
|
||||||
if (origin == null || origin.isBlank()) {
|
if (origin == null || origin.isBlank()) {
|
||||||
origin = "*";
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
exchange.sendResponseHeaders(204, -1);
|
||||||
|
exchange.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
// Determina se a origem ou rota é elegível para CORS
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
boolean isHandshakePath = path.endsWith("/status") || path.endsWith("/authorize");
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
boolean isAuthorizedOrigin = applicationRegistry.findByOrigin(origin).isPresent() || isLocalOrigin(origin);
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
if (isHandshakePath || isAuthorizedOrigin) {
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
||||||
|
}
|
||||||
|
|
||||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
exchange.sendResponseHeaders(204, -1);
|
if (isHandshakePath || isAuthorizedOrigin) {
|
||||||
|
exchange.sendResponseHeaders(204, -1);
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(403, -1);
|
||||||
|
}
|
||||||
exchange.close();
|
exchange.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isLocalOrigin(String origin) {
|
||||||
|
if (origin == null || origin.isBlank()) return false;
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(origin);
|
||||||
|
String host = uri.getHost();
|
||||||
|
return "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getOrigin(HttpExchange exchange) {
|
private String getOrigin(HttpExchange exchange) {
|
||||||
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
String origin = exchange.getRequestHeaders().getFirst("Origin");
|
||||||
if (origin == null || origin.isBlank()) {
|
if (origin == null || origin.isBlank()) {
|
||||||
|
|||||||
@@ -33,12 +33,27 @@ public class CertificateManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
||||||
|
* Suporta chaves de diagnóstico para isolamento de problemas de middleware:
|
||||||
|
* -Dcybercore.diag.no-wincert=true (não inicializa Windows Certificate Store)
|
||||||
|
* -Dcybercore.diag.no-pkcs11=true (não inicializa PKCS#11 / DLLs nativas)
|
||||||
*/
|
*/
|
||||||
public static CertificateManager createDefault() {
|
public static CertificateManager createDefault() {
|
||||||
CertificateManager manager = new CertificateManager();
|
CertificateManager manager = new CertificateManager();
|
||||||
manager.registerProvider(new WindowsCertificateStoreProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-wincert")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor Windows Certificate Store DESATIVADO via -Dcybercore.diag.no-wincert=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new WindowsCertificateStoreProvider());
|
||||||
|
}
|
||||||
|
|
||||||
manager.registerProvider(new Pkcs12CertificateProvider());
|
manager.registerProvider(new Pkcs12CertificateProvider());
|
||||||
manager.registerProvider(new Pkcs11CertificateProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-pkcs11")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor PKCS#11 DESATIVADO via -Dcybercore.diag.no-pkcs11=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new Pkcs11CertificateProvider());
|
||||||
|
}
|
||||||
|
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package br.com.cybercore.signer.core.security;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -12,15 +14,19 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
|
* Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer.
|
||||||
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
|
* Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3
|
||||||
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
|
* exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão".
|
||||||
|
* Os PINs são escopados por aplicação web (applicationId + certId) e possuem expiração por inatividade (TTL de 15 min).
|
||||||
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
|
* Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte.
|
||||||
*/
|
*/
|
||||||
public class PinSessionManager {
|
public class PinSessionManager {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class);
|
||||||
|
private static final long SESSION_TTL_MINUTES = 15;
|
||||||
|
|
||||||
private static final PinSessionManager INSTANCE = new PinSessionManager();
|
private static final PinSessionManager INSTANCE = new PinSessionManager();
|
||||||
|
|
||||||
private final Map<String, char[]> sessionPins = new ConcurrentHashMap<>();
|
private record PinEntry(char[] pin, Instant lastAccessedAt) {}
|
||||||
|
|
||||||
|
private final Map<String, PinEntry> sessionPins = new ConcurrentHashMap<>();
|
||||||
private volatile boolean alwaysPromptPin = false;
|
private volatile boolean alwaysPromptPin = false;
|
||||||
|
|
||||||
public PinSessionManager() {
|
public PinSessionManager() {
|
||||||
@@ -30,63 +36,107 @@ public class PinSessionManager {
|
|||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildKey(String applicationId, String certId) {
|
||||||
|
String appId = (applicationId != null && !applicationId.isBlank()) ? applicationId.trim() : "default";
|
||||||
|
return appId + "::" + certId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Salva o PIN de um certificado/token para a sessão atual em memória volátil.
|
* Salva o PIN associado a uma aplicação específica e certificado.
|
||||||
*/
|
*/
|
||||||
public synchronized void savePin(String certId, char[] pin) {
|
public synchronized void savePin(String applicationId, String certId, char[] pin) {
|
||||||
Objects.requireNonNull(certId, "certId must not be null");
|
Objects.requireNonNull(certId, "certId must not be null");
|
||||||
|
String key = buildKey(applicationId, certId);
|
||||||
if (pin == null || pin.length == 0) {
|
if (pin == null || pin.length == 0) {
|
||||||
clearPin(certId);
|
clearPin(applicationId, certId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpa PIN anterior caso exista
|
clearPin(applicationId, certId);
|
||||||
clearPin(certId);
|
sessionPins.put(key, new PinEntry(pin.clone(), Instant.now()));
|
||||||
|
logger.debug("PIN salvo em sessão para app '{}' e cert '{}'. Tamanho: {} dígitos", applicationId, certId, pin.length);
|
||||||
sessionPins.put(certId, pin.clone());
|
|
||||||
logger.debug("PIN salvo em memória de sessão para o certificado '{}'. Tamanho: {} dígitos", certId, pin.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera o PIN salvo em sessão para o certificado informado.
|
* Salva o PIN no escopo padrão (compatibilidade).
|
||||||
*/
|
*/
|
||||||
public synchronized char[] getPin(String certId) {
|
public synchronized void savePin(String certId, char[] pin) {
|
||||||
|
savePin("default", certId, pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera o PIN salvo em sessão para a aplicação e certificado especificados.
|
||||||
|
*/
|
||||||
|
public synchronized char[] getPin(String applicationId, String certId) {
|
||||||
if (certId == null || alwaysPromptPin) {
|
if (certId == null || alwaysPromptPin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
char[] stored = sessionPins.get(certId);
|
String key = buildKey(applicationId, certId);
|
||||||
return stored != null ? stored.clone() : null;
|
PinEntry entry = sessionPins.get(key);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica expiração por inatividade (TTL)
|
||||||
|
if (Instant.now().isAfter(entry.lastAccessedAt().plus(SESSION_TTL_MINUTES, ChronoUnit.MINUTES))) {
|
||||||
|
clearPin(applicationId, certId);
|
||||||
|
logger.info("PIN da sessão expirou por inatividade (TTL {} min) para app '{}' e cert '{}'.", SESSION_TTL_MINUTES, applicationId, certId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza timestamp de último acesso
|
||||||
|
sessionPins.put(key, new PinEntry(entry.pin(), Instant.now()));
|
||||||
|
return entry.pin().clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica se existe um PIN válido salvo nesta sessão para o certificado.
|
* Recupera o PIN no escopo padrão (compatibilidade).
|
||||||
|
*/
|
||||||
|
public synchronized char[] getPin(String certId) {
|
||||||
|
return getPin("default", certId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se existe um PIN válido salvo nesta sessão para a aplicação e certificado.
|
||||||
|
*/
|
||||||
|
public synchronized boolean hasPin(String applicationId, String certId) {
|
||||||
|
return getPin(applicationId, certId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se existe um PIN no escopo padrão (compatibilidade).
|
||||||
*/
|
*/
|
||||||
public synchronized boolean hasPin(String certId) {
|
public synchronized boolean hasPin(String certId) {
|
||||||
if (certId == null || alwaysPromptPin) {
|
return hasPin("default", certId);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return sessionPins.containsKey(certId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove o PIN salvo para um certificado específico, zerando a memória.
|
* Remove o PIN salvo para uma aplicação e certificado específicos, zerando a memória.
|
||||||
*/
|
*/
|
||||||
public synchronized void clearPin(String certId) {
|
public synchronized void clearPin(String applicationId, String certId) {
|
||||||
if (certId != null) {
|
if (certId != null) {
|
||||||
char[] old = sessionPins.remove(certId);
|
String key = buildKey(applicationId, certId);
|
||||||
if (old != null) {
|
PinEntry old = sessionPins.remove(key);
|
||||||
Arrays.fill(old, ' ');
|
if (old != null && old.pin() != null) {
|
||||||
|
Arrays.fill(old.pin(), ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove o PIN no escopo padrão (compatibilidade).
|
||||||
|
*/
|
||||||
|
public synchronized void clearPin(String certId) {
|
||||||
|
clearPin("default", certId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
|
* Limpa imediatamente todos os PINs em memória e zera seus caracteres.
|
||||||
*/
|
*/
|
||||||
public synchronized void clearAll() {
|
public synchronized void clearAll() {
|
||||||
for (char[] pin : sessionPins.values()) {
|
for (PinEntry entry : sessionPins.values()) {
|
||||||
if (pin != null) {
|
if (entry != null && entry.pin() != null) {
|
||||||
Arrays.fill(pin, ' ');
|
Arrays.fill(entry.pin(), ' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionPins.clear();
|
sessionPins.clear();
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import javax.crypto.SecretKeyFactory;
|
|||||||
import javax.crypto.spec.GCMParameterSpec;
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.PosixFileAttributeView;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.spec.KeySpec;
|
import java.security.spec.KeySpec;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -24,13 +24,14 @@ import java.util.Base64;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
|
||||||
* <p>
|
* <p>
|
||||||
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
|
||||||
* a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256).
|
* a partir de semente aleatória protegida no disco local do usuário e atributos estáveis da máquina (PBKDF2WithHmacSHA256).
|
||||||
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
|
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
|
||||||
*/
|
*/
|
||||||
public final class SecurePinStorage {
|
public final class SecurePinStorage {
|
||||||
@@ -39,6 +40,7 @@ public final class SecurePinStorage {
|
|||||||
|
|
||||||
private static final String FILE_NAME = "secure-pins.dat";
|
private static final String FILE_NAME = "secure-pins.dat";
|
||||||
private static final String SALT_FILE_NAME = "secure-pins.salt";
|
private static final String SALT_FILE_NAME = "secure-pins.salt";
|
||||||
|
private static final String SEED_FILE_NAME = "secure-pins.seed";
|
||||||
private static final int GCM_IV_LENGTH = 12;
|
private static final int GCM_IV_LENGTH = 12;
|
||||||
private static final int GCM_TAG_LENGTH = 128;
|
private static final int GCM_TAG_LENGTH = 128;
|
||||||
private static final int PBKDF2_ITERATIONS = 65536;
|
private static final int PBKDF2_ITERATIONS = 65536;
|
||||||
@@ -49,6 +51,7 @@ public final class SecurePinStorage {
|
|||||||
private final Path storageDirectory;
|
private final Path storageDirectory;
|
||||||
private final Path dataFile;
|
private final Path dataFile;
|
||||||
private final Path saltFile;
|
private final Path saltFile;
|
||||||
|
private final Path seedFile;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ public final class SecurePinStorage {
|
|||||||
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
this.storageDirectory = Objects.requireNonNull(storageDirectory);
|
||||||
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
this.dataFile = storageDirectory.resolve(FILE_NAME);
|
||||||
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
|
||||||
|
this.seedFile = storageDirectory.resolve(SEED_FILE_NAME);
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
loadFromDisk();
|
loadFromDisk();
|
||||||
}
|
}
|
||||||
@@ -200,6 +204,7 @@ public final class SecurePinStorage {
|
|||||||
fos.write(iv);
|
fos.write(iv);
|
||||||
fos.write(ciphertext);
|
fos.write(ciphertext);
|
||||||
}
|
}
|
||||||
|
restrictFilePermissions(dataFile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
|
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -248,22 +253,56 @@ public final class SecurePinStorage {
|
|||||||
byte[] salt = new byte[32];
|
byte[] salt = new byte[32];
|
||||||
new SecureRandom().nextBytes(salt);
|
new SecureRandom().nextBytes(salt);
|
||||||
Files.write(saltFile, salt);
|
Files.write(saltFile, salt);
|
||||||
|
restrictFilePermissions(saltFile);
|
||||||
return salt;
|
return salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getOrCreateSeed() throws Exception {
|
||||||
|
if (Files.exists(seedFile)) {
|
||||||
|
return Files.readAllBytes(seedFile);
|
||||||
|
}
|
||||||
|
byte[] seed = new byte[32];
|
||||||
|
new SecureRandom().nextBytes(seed);
|
||||||
|
Files.write(seedFile, seed);
|
||||||
|
restrictFilePermissions(seedFile);
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restrictFilePermissions(Path path) {
|
||||||
|
try {
|
||||||
|
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
|
||||||
|
if (view != null) {
|
||||||
|
Files.setPosixFilePermissions(path, Set.of(
|
||||||
|
PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_WRITE
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Em sistemas não-POSIX como Windows, as permissões são herdadas da pasta de perfil do usuário (~/.cybercore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SecretKey deriveKey(byte[] salt) throws Exception {
|
private SecretKey deriveKey(byte[] salt) throws Exception {
|
||||||
// Combina identificadores estáveis exclusivos da máquina e usuário
|
byte[] seed = getOrCreateSeed();
|
||||||
String hostIdentity = String.join(":",
|
String hostIdentity = String.join(":",
|
||||||
System.getProperty("user.name", "unknown"),
|
System.getProperty("user.name", "unknown"),
|
||||||
System.getProperty("user.home", "unknown"),
|
System.getProperty("user.home", "unknown"),
|
||||||
System.getProperty("os.name", "unknown"),
|
System.getProperty("os.name", "unknown"),
|
||||||
System.getProperty("os.arch", "unknown"),
|
System.getProperty("os.arch", "unknown")
|
||||||
"cybercore-signer-hardware-pin-binding"
|
|
||||||
);
|
);
|
||||||
|
byte[] hostBytes = hostIdentity.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] combinedMaterial = new byte[seed.length + hostBytes.length];
|
||||||
|
System.arraycopy(seed, 0, combinedMaterial, 0, seed.length);
|
||||||
|
System.arraycopy(hostBytes, 0, combinedMaterial, seed.length, hostBytes.length);
|
||||||
|
|
||||||
KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
|
char[] materialChars = Base64.getEncoder().encodeToString(combinedMaterial).toCharArray();
|
||||||
|
KeySpec spec = new PBEKeySpec(materialChars, salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
|
||||||
|
|
||||||
|
Arrays.fill(materialChars, ' ');
|
||||||
|
Arrays.fill(combinedMaterial, (byte) 0);
|
||||||
|
|
||||||
return new SecretKeySpec(keyBytes, "AES");
|
return new SecretKeySpec(keyBytes, "AES");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,6 +198,63 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("Teste ICP-Brasil");
|
assertThat(resp.body()).contains("Teste ICP-Brasil");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /certificates deve rejeitar com 401 requisições sem application_id")
|
||||||
|
void testGetCertificatesShouldRejectWhenAppIdMissing() throws Exception {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/certificates"))
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(401);
|
||||||
|
assertThat(resp.body()).contains("APPLICATION_NOT_AUTHORIZED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Requisição OPTIONS com origem desconhecida deve ser rejeitada com 403")
|
||||||
|
void testCorsPreflightShouldRejectUnauthorizedOrigin() throws Exception {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.method("OPTIONS", HttpRequest.BodyPublishers.noBody())
|
||||||
|
.header("Origin", "https://malicious-attacker.com")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<Void> resp = httpClient.send(req, HttpResponse.BodyHandlers.discarding());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign deve rejeitar requisição sem nonce")
|
||||||
|
void testSignShouldRejectWhenNonceIsMissing() throws Exception {
|
||||||
|
byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Documento".getBytes());
|
||||||
|
String hashBase64 = Base64.getEncoder().encodeToString(docHash);
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign",
|
||||||
|
"algorithm": "SHA256withRSA",
|
||||||
|
"digest": "%s",
|
||||||
|
"certificate_hint": "cert-1"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), hashBase64);
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(400);
|
||||||
|
assertThat(resp.body()).contains("REPLAY_DETECTED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Requisição OPTIONS deve retornar headers CORS válidos")
|
@DisplayName("Requisição OPTIONS deve retornar headers CORS válidos")
|
||||||
void testCorsPreflight() throws Exception {
|
void testCorsPreflight() throws Exception {
|
||||||
@@ -398,7 +455,7 @@ class CcspServerTest {
|
|||||||
assertThat(resp.body()).contains("\"status\":\"success\"");
|
assertThat(resp.body()).contains("\"status\":\"success\"");
|
||||||
assertThat(resp.body()).contains("\"seals\":");
|
assertThat(resp.body()).contains("\"seals\":");
|
||||||
assertThat(resp.body()).contains("icp-brasil");
|
assertThat(resp.body()).contains("icp-brasil");
|
||||||
assertThat(resp.body()).contains("gov-br");
|
assertThat(resp.body()).contains("ICP-Brasil Institucional");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -63,4 +63,34 @@ class CertificateManagerTest {
|
|||||||
assertThat(byCnpj).isPresent();
|
assertThat(byCnpj).isPresent();
|
||||||
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider PKCS#11 quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoPkcs11() {
|
||||||
|
System.setProperty("cybercore.diag.no-pkcs11", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-pkcs11")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-pkcs11");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider Windows Certificate Store quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoWinCert() {
|
||||||
|
System.setProperty("cybercore.diag.no-wincert", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-win-store")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-wincert");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,17 @@ class PinSessionManagerTest {
|
|||||||
assertThat(manager.hasPin("cert-a")).isFalse();
|
assertThat(manager.hasPin("cert-a")).isFalse();
|
||||||
assertThat(manager.hasPin("cert-b")).isFalse();
|
assertThat(manager.hasPin("cert-b")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve isolar PINs entre diferentes aplicações para o mesmo certificado")
|
||||||
|
void shouldIsolatePinByApplicationId() {
|
||||||
|
manager.savePin("app-1", "cert-x", "pin-app-1".toCharArray());
|
||||||
|
|
||||||
|
assertThat(manager.hasPin("app-1", "cert-x")).isTrue();
|
||||||
|
assertThat(manager.getPin("app-1", "cert-x")).isEqualTo("pin-app-1".toCharArray());
|
||||||
|
|
||||||
|
// Aplicação 2 não deve ter acesso ao PIN da Aplicação 1
|
||||||
|
assertThat(manager.hasPin("app-2", "cert-x")).isFalse();
|
||||||
|
assertThat(manager.getPin("app-2", "cert-x")).isNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,17 +110,18 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
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());
|
||||||
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
|
mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnCoSign(() -> new FileSignDialog(primaryStage, "Co-Assinar Documento", "*.*", SvgIcons.coSignIcon(), certificateManager, signingService).show());
|
|
||||||
mainAppView.setOnTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
|
||||||
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
|
mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show());
|
||||||
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage, false).show());
|
mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage).show());
|
||||||
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
|
||||||
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
||||||
mainAppView.setOnExit(() -> {
|
mainAppView.setOnExit(() -> {
|
||||||
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
|
logger.info("Ação 'Sair' acionada na interface gráfica.");
|
||||||
primaryStage.close();
|
if (systemTrayManager != null) {
|
||||||
Platform.exit();
|
systemTrayManager.handleWindowCloseRequest();
|
||||||
System.exit(0);
|
} else {
|
||||||
|
primaryStage.close();
|
||||||
|
Platform.exit();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ações do Painel Lateral
|
// Ações do Painel Lateral
|
||||||
@@ -130,21 +131,44 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
||||||
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
||||||
|
|
||||||
|
// Observabilidade do ciclo de vida da janela
|
||||||
|
primaryStage.showingProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Visibilidade da janela principal JavaFX alterada: visível={}", newVal));
|
||||||
|
primaryStage.iconifiedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Estado de minimização da janela JavaFX alterado: minimizada={}", newVal));
|
||||||
|
primaryStage.focusedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Foco da janela principal alterado: focada={}", newVal));
|
||||||
|
primaryStage.xProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", newVal, primaryStage.getY()));
|
||||||
|
primaryStage.yProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", primaryStage.getX(), newVal));
|
||||||
|
primaryStage.widthProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", newVal, primaryStage.getHeight()));
|
||||||
|
primaryStage.heightProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", primaryStage.getWidth(), newVal));
|
||||||
|
|
||||||
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
||||||
initCcspServer(primaryStage);
|
initCcspServer(primaryStage);
|
||||||
|
|
||||||
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
||||||
initSmartCardWatcher();
|
initSmartCardWatcher();
|
||||||
|
|
||||||
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
|
|
||||||
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
||||||
this.systemTrayManager.setup();
|
|
||||||
|
|
||||||
// Carrega os certificados instalados na máquina do usuário
|
// Carrega os certificados instalados na máquina do usuário de forma estável
|
||||||
refreshCertificates();
|
refreshCertificates();
|
||||||
|
|
||||||
|
// Exibe a janela principal em primeiro plano após os serviços estarem prontos
|
||||||
mainAppView.show();
|
mainAppView.show();
|
||||||
logger.info("Janela principal JavaFX exibida.");
|
logger.info("Janela principal JavaFX exibida.");
|
||||||
|
|
||||||
|
// Configura a bandeja (System Tray) somente depois que a janela JavaFX está visível,
|
||||||
|
// para não inicializar o toolkit AWT concorrentemente com a criação da janela.
|
||||||
|
if (Boolean.getBoolean("cybercore.no.tray")) {
|
||||||
|
logger.warn("System Tray desativado via -Dcybercore.no.tray=true (diagnóstico).");
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> this.systemTrayManager.setup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshCertificates() {
|
private void refreshCertificates() {
|
||||||
@@ -472,12 +496,23 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
|
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
|
||||||
this.ccspServer.start();
|
this.ccspServer.start();
|
||||||
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
||||||
|
} catch (java.net.BindException be) {
|
||||||
|
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
|
||||||
|
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
|
||||||
|
} else {
|
||||||
|
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSmartCardWatcher() {
|
private void initSmartCardWatcher() {
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
|
||||||
|
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.smartCardWatcher = new SmartCardWatcher();
|
this.smartCardWatcher = new SmartCardWatcher();
|
||||||
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
||||||
|
|||||||
@@ -519,16 +519,38 @@ public final class AssetManager {
|
|||||||
double x = parseDouble(el.getAttribute("x"), 0);
|
double x = parseDouble(el.getAttribute("x"), 0);
|
||||||
double y = parseDouble(el.getAttribute("y"), 0);
|
double y = parseDouble(el.getAttribute("y"), 0);
|
||||||
String content = el.getTextContent();
|
String content = el.getTextContent();
|
||||||
Text t = new Text(x, y, content != null ? content.trim() : "");
|
Text t = new Text(content != null ? content.trim() : "");
|
||||||
double fs = parseDouble(el.getAttribute("font-size"), 12);
|
double fs = parseDouble(el.getAttribute("font-size"), 12);
|
||||||
String fw = el.getAttribute("font-weight");
|
String fw = el.getAttribute("font-weight");
|
||||||
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
||||||
if (el.hasAttribute("fill")) {
|
|
||||||
t.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.BLACK));
|
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
|
||||||
|
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
|
||||||
|
if (fillVal != null) {
|
||||||
|
t.setFill(parsePaint(fillVal, defsMap, Color.BLACK));
|
||||||
}
|
}
|
||||||
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
|
|
||||||
|
String anchor = inlineStyle.getOrDefault("text-anchor", el.getAttribute("text-anchor"));
|
||||||
|
String baseline = inlineStyle.getOrDefault("dominant-baseline", el.getAttribute("dominant-baseline"));
|
||||||
|
|
||||||
|
boolean isCenterBaseline = "central".equalsIgnoreCase(baseline) || "middle".equalsIgnoreCase(baseline);
|
||||||
|
if (isCenterBaseline) {
|
||||||
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
||||||
|
} else {
|
||||||
|
t.setTextOrigin(javafx.geometry.VPos.BASELINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double textWidth = t.getLayoutBounds().getWidth();
|
||||||
|
double finalX = x;
|
||||||
|
if ("middle".equalsIgnoreCase(anchor)) {
|
||||||
|
finalX = x - (textWidth / 2.0);
|
||||||
|
} else if ("end".equalsIgnoreCase(anchor)) {
|
||||||
|
finalX = x - textWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.setX(finalX);
|
||||||
|
t.setY(y);
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
case "g" -> {
|
case "g" -> {
|
||||||
@@ -558,19 +580,46 @@ public final class AssetManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseStyleAttribute(String styleAttr) {
|
||||||
|
Map<String, String> styleMap = new HashMap<>();
|
||||||
|
if (styleAttr == null || styleAttr.isBlank()) {
|
||||||
|
return styleMap;
|
||||||
|
}
|
||||||
|
String[] declarations = styleAttr.split(";");
|
||||||
|
for (String decl : declarations) {
|
||||||
|
int colonIdx = decl.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
String prop = decl.substring(0, colonIdx).trim().toLowerCase();
|
||||||
|
String val = decl.substring(colonIdx + 1).trim();
|
||||||
|
styleMap.put(prop, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styleMap;
|
||||||
|
}
|
||||||
|
|
||||||
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
|
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
|
||||||
if (el.hasAttribute("fill")) {
|
Map<String, String> inlineStyle = el.hasAttribute("style") ? parseStyleAttribute(el.getAttribute("style")) : Map.of();
|
||||||
shape.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.TRANSPARENT));
|
|
||||||
|
String fillVal = inlineStyle.getOrDefault("fill", el.hasAttribute("fill") ? el.getAttribute("fill") : null);
|
||||||
|
if (fillVal != null) {
|
||||||
|
shape.setFill(parsePaint(fillVal, defsMap, Color.TRANSPARENT));
|
||||||
} else if (shape instanceof Line) {
|
} else if (shape instanceof Line) {
|
||||||
shape.setFill(Color.TRANSPARENT);
|
shape.setFill(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke")) {
|
String strokeVal = inlineStyle.getOrDefault("stroke", el.hasAttribute("stroke") ? el.getAttribute("stroke") : null);
|
||||||
shape.setStroke(parsePaint(el.getAttribute("stroke"), defsMap, Color.TRANSPARENT));
|
if (strokeVal != null) {
|
||||||
|
shape.setStroke(parsePaint(strokeVal, defsMap, Color.TRANSPARENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke-width")) {
|
String strokeWidthVal = inlineStyle.getOrDefault("stroke-width", el.hasAttribute("stroke-width") ? el.getAttribute("stroke-width") : null);
|
||||||
shape.setStrokeWidth(parseDouble(el.getAttribute("stroke-width"), 1.0));
|
if (strokeWidthVal != null) {
|
||||||
|
shape.setStrokeWidth(parseDouble(strokeWidthVal, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
String opacityVal = inlineStyle.getOrDefault("opacity", el.hasAttribute("opacity") ? el.getAttribute("opacity") : null);
|
||||||
|
if (opacityVal != null) {
|
||||||
|
shape.setOpacity(parseDouble(opacityVal, 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
|
if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package br.com.cybercore.signer.ui.tray;
|
package br.com.cybercore.signer.ui.tray;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -40,12 +41,19 @@ public class SystemTrayManager {
|
|||||||
/**
|
/**
|
||||||
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
||||||
*/
|
*/
|
||||||
public synchronized boolean setup() {
|
public boolean setup() {
|
||||||
if (!SystemTray.isSupported()) {
|
if (!SystemTray.isSupported()) {
|
||||||
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// O TrayIcon AWT é criado na thread de eventos do AWT, nunca na thread do JavaFX,
|
||||||
|
// para evitar conflito entre os dois loops de mensagens nativos no Windows.
|
||||||
|
java.awt.EventQueue.invokeLater(this::setupOnAwtThread);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setupOnAwtThread() {
|
||||||
try {
|
try {
|
||||||
SystemTray tray = SystemTray.getSystemTray();
|
SystemTray tray = SystemTray.getSystemTray();
|
||||||
Image iconImage = createTrayIconImage();
|
Image iconImage = createTrayIconImage();
|
||||||
@@ -76,38 +84,78 @@ public class SystemTrayManager {
|
|||||||
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
||||||
this.trayIcon.setImageAutoSize(true);
|
this.trayIcon.setImageAutoSize(true);
|
||||||
|
|
||||||
// Clique no ícone restaura a janela
|
// Duplo clique (ActionListener) e clique simples (MouseListener) restauram a janela
|
||||||
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
|
this.trayIcon.addActionListener(e -> {
|
||||||
|
logger.info("Ícone da bandeja acionado (duplo clique).");
|
||||||
|
Platform.runLater(this::showStage);
|
||||||
|
});
|
||||||
|
this.trayIcon.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(java.awt.event.MouseEvent e) {
|
||||||
|
if (e.getButton() == java.awt.event.MouseEvent.BUTTON1) {
|
||||||
|
logger.info("Ícone da bandeja acionado (clique simples).");
|
||||||
|
Platform.runLater(SystemTrayManager.this::showStage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tray.add(this.trayIcon);
|
tray.add(this.trayIcon);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
// Intercepta fechamento da janela para ocultar em vez de encerrar
|
// Intercepta fechamento da janela (botão X) para confirmar ação com o usuário
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stage.setOnCloseRequest(event -> {
|
stage.setOnCloseRequest(event -> {
|
||||||
event.consume();
|
event.consume();
|
||||||
stage.hide();
|
handleWindowCloseRequest();
|
||||||
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("System Tray configurado com sucesso.");
|
logger.info("System Tray configurado com sucesso.");
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (AWTException e) {
|
} catch (AWTException e) {
|
||||||
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
||||||
return false;
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
|
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage(), t);
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exibe o diálogo de confirmação ao clicar no 'X' da janela principal,
|
||||||
|
* permitindo minimizar para a bandeja, encerrar definitivamente ou cancelar.
|
||||||
|
*/
|
||||||
|
public void handleWindowCloseRequest() {
|
||||||
|
try {
|
||||||
|
ExitConfirmationDialog dialog = new ExitConfirmationDialog(stage);
|
||||||
|
ExitConfirmationDialog.ExitChoice choice = dialog.showAndGetChoice();
|
||||||
|
|
||||||
|
switch (choice) {
|
||||||
|
case MINIMIZE_TO_TRAY -> {
|
||||||
|
logger.info("Usuário optou por minimizar o CyberCore Signer para a bandeja.");
|
||||||
|
stage.hide();
|
||||||
|
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
||||||
|
}
|
||||||
|
case EXIT_COMPLETELY -> {
|
||||||
|
logger.info("Usuário optou por encerrar definitivamente o CyberCore Signer.");
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
case CANCEL -> {
|
||||||
|
logger.debug("Fechamento da janela cancelado pelo usuário.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Erro ao exibir diálogo de fechamento, minimizando para bandeja por segurança: {}", t.getMessage());
|
||||||
|
stage.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showStage() {
|
public void showStage() {
|
||||||
if (stage != null) {
|
if (stage != null) {
|
||||||
|
logger.info("Restaurando janela principal (visível={}, minimizada={}).",
|
||||||
|
stage.isShowing(), stage.isIconified());
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.setIconified(false);
|
stage.setIconified(false);
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
|
stage.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package br.com.cybercore.signer.ui.view;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
|
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diálogo modal de confirmação ao fechar a janela principal do CyberCore Signer.
|
||||||
|
* Permite ao usuário escolher entre manter o agente ativo na bandeja do sistema
|
||||||
|
* ou encerrar completamente a aplicação.
|
||||||
|
*/
|
||||||
|
public final class ExitConfirmationDialog {
|
||||||
|
|
||||||
|
public enum ExitChoice {
|
||||||
|
MINIMIZE_TO_TRAY,
|
||||||
|
EXIT_COMPLETELY,
|
||||||
|
CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Stage stage;
|
||||||
|
private ExitChoice chosenAction = ExitChoice.CANCEL;
|
||||||
|
|
||||||
|
public ExitConfirmationDialog(Stage owner) {
|
||||||
|
this.stage = new Stage();
|
||||||
|
this.stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
if (owner != null) {
|
||||||
|
this.stage.initOwner(owner);
|
||||||
|
}
|
||||||
|
this.stage.setTitle("CyberCore Signer — Encerrar ou Manter na Bandeja");
|
||||||
|
this.stage.setResizable(false);
|
||||||
|
this.stage.setMinWidth(520);
|
||||||
|
this.stage.setMinHeight(190);
|
||||||
|
|
||||||
|
initLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initLayout() {
|
||||||
|
VBox root = new VBox(22);
|
||||||
|
root.setPadding(new Insets(24));
|
||||||
|
root.getStyleClass().add("dialog-container");
|
||||||
|
|
||||||
|
// Cabeçalho com ícone e texto explicativo
|
||||||
|
HBox header = new HBox(16);
|
||||||
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
VBox titleBox = new VBox(4);
|
||||||
|
Label title = new Label("Como você deseja proceder?");
|
||||||
|
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label subtitle = new Label(
|
||||||
|
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
|
||||||
|
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
|
||||||
|
);
|
||||||
|
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
||||||
|
|
||||||
|
titleBox.getChildren().addAll(title, subtitle);
|
||||||
|
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
|
||||||
|
|
||||||
|
// Barra de Ações com os botões
|
||||||
|
HBox actionsRow = new HBox(10);
|
||||||
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
Button btnCancel = new Button("Cancelar");
|
||||||
|
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
|
||||||
|
btnCancel.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.CANCEL;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox spacer = new HBox();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button btnExit = new Button("Encerrar Definitivamente");
|
||||||
|
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
|
||||||
|
btnExit.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.EXIT_COMPLETELY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button btnMinimize = new Button("Minimizar para a Bandeja");
|
||||||
|
btnMinimize.getStyleClass().addAll("btn-primary", "btn-dialog");
|
||||||
|
btnMinimize.setDefaultButton(true);
|
||||||
|
btnMinimize.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.MINIMIZE_TO_TRAY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
actionsRow.getChildren().addAll(btnCancel, spacer, btnExit, btnMinimize);
|
||||||
|
|
||||||
|
root.getChildren().addAll(header, actionsRow);
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
ThemeManager.applyTheme(scene);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExitChoice showAndGetChoice() {
|
||||||
|
stage.showAndWait();
|
||||||
|
return chosenAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import javafx.scene.layout.Region;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -27,21 +29,19 @@ import java.util.Objects;
|
|||||||
@SuppressWarnings("this-escape")
|
@SuppressWarnings("this-escape")
|
||||||
public final class MainAppView {
|
public final class MainAppView {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MainAppView.class);
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final VBox root = new VBox(16);
|
private final VBox root = new VBox(16);
|
||||||
private final Button themeToggleBtn = new Button();
|
private final Button themeToggleBtn = new Button();
|
||||||
|
|
||||||
// Callbacks das Ações
|
|
||||||
private Runnable onSignPdf;
|
private Runnable onSignPdf;
|
||||||
private Runnable onSignXml;
|
private Runnable onSignXml;
|
||||||
private Runnable onSignFile;
|
private Runnable onSignFile;
|
||||||
private Runnable onBatchSign;
|
private Runnable onBatchSign;
|
||||||
private Runnable onAttachedSign;
|
private Runnable onAttachedSign;
|
||||||
private Runnable onCoSign;
|
|
||||||
private Runnable onTimestamp;
|
|
||||||
private Runnable onLtvSign;
|
private Runnable onLtvSign;
|
||||||
private Runnable onValidateSignature;
|
private Runnable onValidateSignature;
|
||||||
private Runnable onValidateTimestamp;
|
|
||||||
private Runnable onManageSeals;
|
private Runnable onManageSeals;
|
||||||
private Runnable onExit;
|
private Runnable onExit;
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public final class MainAppView {
|
|||||||
mainContent.setAlignment(Pos.CENTER);
|
mainContent.setAlignment(Pos.CENTER);
|
||||||
VBox.setVgrow(mainContent, Priority.ALWAYS);
|
VBox.setVgrow(mainContent, Priority.ALWAYS);
|
||||||
|
|
||||||
// --- Hub Central de Ações (Grid 3 colunas x 3 linhas) ---
|
// --- Hub Central de Ações (Grid 3 colunas x 2 linhas) ---
|
||||||
GridPane actionGrid = new GridPane();
|
GridPane actionGrid = new GridPane();
|
||||||
actionGrid.setHgap(12);
|
actionGrid.setHgap(12);
|
||||||
actionGrid.setVgap(12);
|
actionGrid.setVgap(12);
|
||||||
@@ -81,15 +81,10 @@ public final class MainAppView {
|
|||||||
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
|
actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0);
|
||||||
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
|
actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0);
|
||||||
|
|
||||||
// Linha 1: Formatos Avançados, Temporais e Identidade Visual
|
// Linha 1: Identidade Visual, Auditoria e Saída
|
||||||
actionGrid.add(createActionButton(SvgIcons.coSignIcon(), "Co-Assinar\nDocumento", () -> trigger(onCoSign)), 0, 1);
|
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 0, 1);
|
||||||
actionGrid.add(createActionButton(SvgIcons.timestampIcon(), "Carimbo\ndo Tempo", () -> trigger(onTimestamp)), 1, 1);
|
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 1, 1);
|
||||||
actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 2, 1);
|
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 1);
|
||||||
|
|
||||||
// Linha 2: Auditoria, Validação e Saída
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 0, 2);
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.validateTimestampIcon(), "Validar\nCarimbo", () -> trigger(onValidateTimestamp)), 1, 2);
|
|
||||||
actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 2);
|
|
||||||
|
|
||||||
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
|
// --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) ---
|
||||||
VBox sideColumn = new VBox(10);
|
VBox sideColumn = new VBox(10);
|
||||||
@@ -136,6 +131,14 @@ public final class MainAppView {
|
|||||||
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
||||||
).toExternalForm());
|
).toExternalForm());
|
||||||
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-effects")) {
|
||||||
|
logger.warn("Diagnóstico: Efeitos/sombras visuais DESATIVADOS via -Dcybercore.diag.no-effects=true");
|
||||||
|
var noEffectsUrl = getClass().getResource("/styles/no-effects.css");
|
||||||
|
if (noEffectsUrl != null) {
|
||||||
|
scene.getStylesheets().add(noEffectsUrl.toExternalForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configuração e inicialização do Tema
|
// Configuração e inicialização do Tema
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
updateThemeToggleButton();
|
updateThemeToggleButton();
|
||||||
@@ -144,11 +147,17 @@ public final class MainAppView {
|
|||||||
|
|
||||||
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setResizable(false);
|
|
||||||
stage.setMinWidth(780);
|
if (Boolean.getBoolean("cybercore.diag.plain-stage")) {
|
||||||
stage.setMinHeight(480);
|
logger.warn("Diagnóstico: Plain Stage ATIVADO via -Dcybercore.diag.plain-stage=true (janela redimensionável, sem restrições min/max)");
|
||||||
stage.setMaxWidth(820);
|
stage.setResizable(true);
|
||||||
stage.setMaxHeight(520);
|
} else {
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.setMinWidth(780);
|
||||||
|
stage.setMinHeight(480);
|
||||||
|
stage.setMaxWidth(820);
|
||||||
|
stage.setMaxHeight(520);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createHeaderBar() {
|
private HBox createHeaderBar() {
|
||||||
@@ -319,11 +328,8 @@ public final class MainAppView {
|
|||||||
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
|
public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; }
|
||||||
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
|
public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; }
|
||||||
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
|
public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; }
|
||||||
public void setOnCoSign(Runnable onCoSign) { this.onCoSign = onCoSign; }
|
|
||||||
public void setOnTimestamp(Runnable onTimestamp) { this.onTimestamp = onTimestamp; }
|
|
||||||
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
|
public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; }
|
||||||
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
|
public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; }
|
||||||
public void setOnValidateTimestamp(Runnable onValidateTimestamp) { this.onValidateTimestamp = onValidateTimestamp; }
|
|
||||||
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
|
public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; }
|
||||||
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
|
public void setOnExit(Runnable onExit) { this.onExit = onExit; }
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
boolean isA3 = cert.type() == CertificateType.A3;
|
boolean isA3 = cert.type() == CertificateType.A3;
|
||||||
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
|
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
|
||||||
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id());
|
boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(context != null ? context.applicationId() : null, cert.id());
|
||||||
|
|
||||||
if (isA3) {
|
if (isA3) {
|
||||||
pinContainer.getStyleClass().add("dialog-box");
|
pinContainer.getStyleClass().add("dialog-box");
|
||||||
@@ -255,9 +255,9 @@ public class SignaturePromptDialog {
|
|||||||
if (pinProvided) {
|
if (pinProvided) {
|
||||||
char[] pinChars = typedPin.toCharArray();
|
char[] pinChars = typedPin.toCharArray();
|
||||||
if (cbSavePin.isSelected()) {
|
if (cbSavePin.isSelected()) {
|
||||||
PinSessionManager.getInstance().savePin(cert.id(), pinChars);
|
PinSessionManager.getInstance().savePin(context != null ? context.applicationId() : null, cert.id(), pinChars);
|
||||||
} else {
|
} else {
|
||||||
PinSessionManager.getInstance().clearPin(cert.id());
|
PinSessionManager.getInstance().clearPin(context != null ? context.applicationId() : null, cert.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ public final class SignatureValidationDialog {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class);
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final boolean isTimestamp;
|
|
||||||
private final SignatureValidationService validationService = new SignatureValidationService();
|
private final SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
|
||||||
private RadioButton rbAttached;
|
private RadioButton rbAttached;
|
||||||
@@ -58,15 +57,13 @@ public final class SignatureValidationDialog {
|
|||||||
private Button btnSaveExtracted;
|
private Button btnSaveExtracted;
|
||||||
private byte[] lastExtractedContent;
|
private byte[] lastExtractedContent;
|
||||||
|
|
||||||
public SignatureValidationDialog(Stage owner, boolean isTimestamp) {
|
public SignatureValidationDialog(Stage owner) {
|
||||||
this.isTimestamp = isTimestamp;
|
|
||||||
|
|
||||||
this.stage = new Stage();
|
this.stage = new Stage();
|
||||||
this.stage.initModality(Modality.APPLICATION_MODAL);
|
this.stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
this.stage.initOwner(owner);
|
this.stage.initOwner(owner);
|
||||||
}
|
}
|
||||||
this.stage.setTitle(isTimestamp ? "CyberCore Signer — Validação de Carimbo do Tempo" : "CyberCore Signer — Validação de Assinatura Digital");
|
this.stage.setTitle("CyberCore Signer — Validação de Assinatura Digital");
|
||||||
this.stage.setResizable(true);
|
this.stage.setResizable(true);
|
||||||
this.stage.setMinWidth(680);
|
this.stage.setMinWidth(680);
|
||||||
this.stage.setMinHeight(520);
|
this.stage.setMinHeight(520);
|
||||||
@@ -82,11 +79,10 @@ public final class SignatureValidationDialog {
|
|||||||
HBox header = new HBox(12);
|
HBox header = new HBox(12);
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
header.getChildren().addAll(
|
header.getChildren().addAll(
|
||||||
isTimestamp ? SvgIcons.validateTimestampIcon() : SvgIcons.validateIcon(),
|
SvgIcons.validateIcon(),
|
||||||
createHeaderLabel(
|
createHeaderLabel(
|
||||||
isTimestamp ? "VALIDAÇÃO DE CARIMBO DO TEMPO" : "VALIDAÇÃO DE ASSINATURA DIGITAL",
|
"VALIDAÇÃO DE ASSINATURA DIGITAL",
|
||||||
isTimestamp ? "Verifique a autenticidade e conformidade RFC 3161 TSP."
|
"Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
|
||||||
: "Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas."
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,11 +106,6 @@ public final class SignatureValidationDialog {
|
|||||||
radioRow.getChildren().addAll(rbAttached, rbDetached);
|
radioRow.getChildren().addAll(rbAttached, rbDetached);
|
||||||
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
|
typeBox.getChildren().addAll(lblTypeTitle, radioRow);
|
||||||
|
|
||||||
if (isTimestamp) {
|
|
||||||
typeBox.setVisible(false);
|
|
||||||
typeBox.setManaged(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Arquivo de Assinatura / Arquivo Assinado
|
// 1. Arquivo de Assinatura / Arquivo Assinado
|
||||||
VBox sigBox = new VBox(8);
|
VBox sigBox = new VBox(8);
|
||||||
sigBox.getStyleClass().add("dialog-box");
|
sigBox.getStyleClass().add("dialog-box");
|
||||||
@@ -197,7 +188,7 @@ public final class SignatureValidationDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateModeView() {
|
private void updateModeView() {
|
||||||
boolean isAttached = rbAttached.isSelected() && !isTimestamp;
|
boolean isAttached = rbAttached.isSelected();
|
||||||
docBox.setVisible(!isAttached);
|
docBox.setVisible(!isAttached);
|
||||||
docBox.setManaged(!isAttached);
|
docBox.setManaged(!isAttached);
|
||||||
|
|
||||||
@@ -205,7 +196,7 @@ public final class SignatureValidationDialog {
|
|||||||
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
|
lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):");
|
||||||
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
|
txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos...");
|
||||||
} else {
|
} else {
|
||||||
lblSigPath.setText(isTimestamp ? "Arquivo de Carimbo (.tsr, .tsp, .p7s):" : "Arquivo de Assinatura (.p7s, .sig):");
|
lblSigPath.setText("Arquivo de Assinatura (.p7s, .sig):");
|
||||||
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
|
txtSigPath.setPromptText("Selecione o arquivo de assinatura digital...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +218,7 @@ public final class SignatureValidationDialog {
|
|||||||
if (rbAttached.isSelected()) {
|
if (rbAttached.isSelected()) {
|
||||||
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
|
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*"));
|
||||||
} else {
|
} else {
|
||||||
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig, *.tsr)", "*.p7s", "*.sig", "*.tsr", "*.*"));
|
ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig)", "*.p7s", "*.sig", "*.*"));
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = ch.showOpenDialog(stage);
|
File f = ch.showOpenDialog(stage);
|
||||||
@@ -249,7 +240,7 @@ public final class SignatureValidationDialog {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isDetachedMode = rbDetached.isSelected() || isTimestamp;
|
boolean isDetachedMode = rbDetached.isSelected();
|
||||||
if (isDetachedMode && (docFile == null || !docFile.exists())) {
|
if (isDetachedMode && (docFile == null || !docFile.exists())) {
|
||||||
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
|
showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado",
|
||||||
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
|
"Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura.");
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- Dobra de canto -->
|
||||||
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
|
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#ffcdd2" stroke="#d32f2f" stroke-width="2" stroke-linejoin="round"/>
|
||||||
<!-- Linhas de texto representativas -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta vermelha com sigla PDF -->
|
<!-- Etiqueta vermelha com sigla PDF perfeitamente centralizada -->
|
||||||
<rect x="12" y="33" width="24" height="11" rx="3" fill="#e53935"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#d32f2f"/>
|
||||||
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8.5" font-weight="bold" fill="#ffffff" text-anchor="middle">PDF</text>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8.5px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central">PDF</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 996 B After Width: | Height: | Size: 1.0 KiB |
@@ -4,10 +4,10 @@
|
|||||||
<!-- Dobra de canto -->
|
<!-- Dobra de canto -->
|
||||||
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
|
<path d="M 32 4 L 32 14 L 42 14 Z" fill="#b3e5fc" stroke="#0288d1" stroke-width="2" stroke-linejoin="round"/>
|
||||||
<!-- Linhas de texto representativas -->
|
<!-- Linhas de texto representativas -->
|
||||||
<line x1="14" y1="20" x2="28" y2="20" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="18" x2="27" y2="18" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="25" x2="36" y2="25" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="22" x2="35" y2="22" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<line x1="14" y1="30" x2="36" y2="30" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
<line x1="13" y1="26" x2="35" y2="26" stroke="#b0bec5" stroke-width="2" stroke-linecap="round"/>
|
||||||
<!-- Etiqueta azul com sigla XML -->
|
<!-- Etiqueta azul com sigla XML perfeitamente centralizada -->
|
||||||
<rect x="10" y="33" width="28" height="11" rx="3" fill="#0288d1"/>
|
<rect x="11" y="31" width="28" height="11" rx="3" fill="#0288d1"/>
|
||||||
<text x="24" y="41.5" font-family="Segoe UI, Arial, sans-serif" font-size="8" font-weight="bold" fill="#ffffff" text-anchor="middle"><XML></text>
|
<text x="25" y="36.5" font-family="'Segoe UI', Arial, sans-serif" font-size="8px" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="central"><XML></text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
/* CyberCore Signer - Modo Diagnóstico: Remoção de sombras/efeitos visuais dinâmicos */
|
||||||
|
|
||||||
|
.cyber-action-card,
|
||||||
|
.serpro-action-btn,
|
||||||
|
.cyber-side-card,
|
||||||
|
.serpro-side-btn,
|
||||||
|
.cyber-header-bar,
|
||||||
|
.theme-toggle-btn,
|
||||||
|
.cyber-status-badge {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .cyber-action-card:hover,
|
||||||
|
.theme-light .serpro-action-btn:hover,
|
||||||
|
.theme-dark .cyber-action-card:hover,
|
||||||
|
.theme-dark .serpro-action-btn:hover,
|
||||||
|
.cyber-side-card:hover,
|
||||||
|
.serpro-side-btn:hover,
|
||||||
|
.theme-toggle-btn:hover {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
@@ -55,4 +55,22 @@ class AssetManagerTest {
|
|||||||
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
|
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
|
||||||
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
|
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve interpretar atributos style inline no padrão de exportação do Inkscape/Figma")
|
||||||
|
void shouldParseSvgWithInlineStyles() {
|
||||||
|
String svgWithStyle = """
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<rect x="10" y="10" width="28" height="28" style="fill:#d32f2f;stroke:#0288d1;stroke-width:2.5;opacity:0.9"/>
|
||||||
|
<text x="24" y="24" style="fill:#ffffff;font-size:10px">OK</text>
|
||||||
|
</svg>
|
||||||
|
""";
|
||||||
|
try (var in = new java.io.ByteArrayInputStream(svgWithStyle.getBytes(java.nio.charset.StandardCharsets.UTF_8))) {
|
||||||
|
Node node = AssetManager.parseSvgToNode(in, 48, 48);
|
||||||
|
assertThat(node).isNotNull();
|
||||||
|
assertThat(node).isInstanceOf(javafx.scene.layout.Pane.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
org.junit.jupiter.api.Assertions.fail("Falha ao analisar SVG com estilo inline: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package br.com.cybercore.signer.ui.tray;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.awt.SystemTray;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("Testes do Gerenciador de Bandeja (SystemTrayManager)")
|
||||||
|
class SystemTrayManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve rejeitar Stage nulo no construtor")
|
||||||
|
void shouldRejectNullStage() {
|
||||||
|
assertThatThrownBy(() -> new SystemTrayManager(null, () -> {}))
|
||||||
|
.isInstanceOf(NullPointerException.class)
|
||||||
|
.hasMessageContaining("stage must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve reportar suporte do ambiente ou lidar graciosamente sem UI nativa")
|
||||||
|
void shouldReportSupportOrHandleGracefully() {
|
||||||
|
boolean supported = SystemTray.isSupported();
|
||||||
|
// Verifica que chamada não lança exceção fatal
|
||||||
|
assertThat(supported).isIn(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# CyberCore Signer - Diagnóstico e Monitor de Janela Win32 / DWM
|
||||||
|
# Suporta matar instâncias anteriores órfãs e monitorar a janela real.
|
||||||
|
# Uso simples:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
# Ou iniciando o app automaticamente junto com o monitor:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1 -AutoLaunch
|
||||||
|
# ==============================================================================
|
||||||
|
param (
|
||||||
|
[switch]$AutoLaunch,
|
||||||
|
[string]$ExtraArgs = "--dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " CyberCore Signer - Monitor de Janela Win32 / DWM" -ForegroundColor Cyan
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. Encerra processos java zumbis anteriores que possam estar travando a porta 21325 ou a bandeja
|
||||||
|
$oldProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
if ($oldProcs) {
|
||||||
|
Write-Host "[INFO] Encerrando $($oldProcs.Count) processo(s) Java em segundo plano para limpar o ambiente..." -ForegroundColor Yellow
|
||||||
|
$oldProcs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Milliseconds 800
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = @"
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public class Win32WindowWatcher {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT {
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsIconic(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll")]
|
||||||
|
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
||||||
|
|
||||||
|
public const int DWMWA_CLOAKED = 14;
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
Add-Type -TypeDefinition $signature -PassThru | Out-Null
|
||||||
|
|
||||||
|
if ($AutoLaunch) {
|
||||||
|
Write-Host "[LAUNCH] Iniciando cybercore-signer.jar com argumentos: $ExtraArgs" -ForegroundColor Green
|
||||||
|
$jarPath = ".\cybercore-signer.jar"
|
||||||
|
if (-not (Test-Path $jarPath)) {
|
||||||
|
$jarPath = ".\modules\cybercore-app\target\cybercore-signer.jar"
|
||||||
|
}
|
||||||
|
Start-Process -FilePath "java" -ArgumentList "-jar", $jarPath, $ExtraArgs -PassThru | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Aguardando janela principal abrir (verificando processos e Win32)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$lastState = ""
|
||||||
|
$found = $false
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
# Procura via FindWindow pelo título ou pelo MainWindowHandle do processo Java
|
||||||
|
$hWnd = [Win32WindowWatcher]::FindWindow($null, "CyberCore Signer")
|
||||||
|
|
||||||
|
if ($hWnd -eq [IntPtr]::Zero) {
|
||||||
|
$javaProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
foreach ($p in $javaProcs) {
|
||||||
|
if ($p.MainWindowHandle -ne [IntPtr]::Zero) {
|
||||||
|
$hWnd = $p.MainWindowHandle
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hWnd -ne [IntPtr]::Zero -and [Win32WindowWatcher]::IsWindow($hWnd)) {
|
||||||
|
if (-not $found -or $hWnd -ne $monitoredHwnd) {
|
||||||
|
$found = $true
|
||||||
|
$monitoredHwnd = $hWnd
|
||||||
|
Write-Host "[OK] JANELA DETECTADA! HWND: $hWnd. Monitorando em tempo real a cada 200ms..." -ForegroundColor Green
|
||||||
|
Write-Host ">>> Passe o mouse sobre a janela agora para observar o que o Windows faz. <<<" -ForegroundColor Magenta
|
||||||
|
}
|
||||||
|
|
||||||
|
$visible = [Win32WindowWatcher]::IsWindowVisible($hWnd)
|
||||||
|
$iconic = [Win32WindowWatcher]::IsIconic($hWnd)
|
||||||
|
$foreground = ([Win32WindowWatcher]::GetForegroundWindow() -eq $hWnd)
|
||||||
|
|
||||||
|
$rect = New-Object Win32WindowWatcher+RECT
|
||||||
|
[Win32WindowWatcher]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
||||||
|
$w = $rect.Right - $rect.Left
|
||||||
|
$h = $rect.Bottom - $rect.Top
|
||||||
|
|
||||||
|
$cloakedVal = 0
|
||||||
|
[Win32WindowWatcher]::DwmGetWindowAttribute($hWnd, [Win32WindowWatcher]::DWMWA_CLOAKED, [ref]$cloakedVal, 4) | Out-Null
|
||||||
|
$cloaked = ($cloakedVal -ne 0)
|
||||||
|
|
||||||
|
$currentState = "Visible=$visible | Cloaked(DWM)=$cloaked | Minimized=$iconic | Focused=$foreground | Rect=($($rect.Left),$($rect.Top)) ${w}x${h}"
|
||||||
|
|
||||||
|
if ($currentState -ne $lastState) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
if (-not $visible -or $cloaked -or $w -le 0 -or $h -le 0) {
|
||||||
|
Write-Host "[$timestamp] [ALERTA-JANELA-SUMIU] $currentState" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "[$timestamp] [STATUS] $currentState" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
$lastState = $currentState
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($found) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
Write-Host "[$timestamp] [DESTRUÍDA/FECHADA] O handle da janela não existe mais no Windows!" -ForegroundColor Red
|
||||||
|
$found = $false
|
||||||
|
$lastState = ""
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ cd "${PROJECT_ROOT}"
|
|||||||
if [ -n "${JAVA_HOME:-}" ]; then
|
if [ -n "${JAVA_HOME:-}" ]; then
|
||||||
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
||||||
else
|
else
|
||||||
export JAVA_HOME="/home/claudecio/.jdks/jdk-25.0.4.1+1"
|
export JAVA_HOME="${JAVA_HOME:-${HOME}/.jdks/jdk-25.0.4.1+1}"
|
||||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||||
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
MVN_CMD="${PROJECT_ROOT}/mvnw"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -4,6 +4,25 @@
|
|||||||
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
|
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function escapeSvgText(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLogoUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export class PdfSealDesigner {
|
export class PdfSealDesigner {
|
||||||
/**
|
/**
|
||||||
* @param {Object} options Configurações iniciais do designer.
|
* @param {Object} options Configurações iniciais do designer.
|
||||||
@@ -592,8 +611,9 @@ export class PdfSealDesigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let iconGraphic = '';
|
let iconGraphic = '';
|
||||||
if (t.customLogoBase64) {
|
const safeLogo = sanitizeLogoUrl(t.customLogoBase64);
|
||||||
iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
if (safeLogo) {
|
||||||
|
iconGraphic = `<image xlink:href="${safeLogo}" href="${safeLogo}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
} else {
|
} else {
|
||||||
switch (t.iconStyle) {
|
switch (t.iconStyle) {
|
||||||
case 'star':
|
case 'star':
|
||||||
@@ -640,11 +660,11 @@ export class PdfSealDesigner {
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Tipografia do Selo -->
|
<!-- Tipografia do Selo -->
|
||||||
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${escapeSvgText(t.primaryColor)}" letter-spacing="0.3">${escapeSvgText(t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE')}</text>
|
||||||
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${escapeSvgText(t.textColor)}">MARIA DA SILVA</text>
|
||||||
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText([docText, extraText].filter(Boolean).join(' • '))}</text>
|
||||||
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText(dateText ? `Data: ${dateText}` : '')}</text>
|
||||||
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${escapeSvgText(t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001')}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,25 @@
|
|||||||
* e geração do payload de assinatura em tempo real.
|
* e geração do payload de assinatura em tempo real.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function escapeSvgText(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLogoUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return '';
|
||||||
|
const trimmed = url.trim();
|
||||||
|
if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export class PdfStampPicker {
|
export class PdfStampPicker {
|
||||||
/**
|
/**
|
||||||
* @param {Object} options Configurações do componente.
|
* @param {Object} options Configurações do componente.
|
||||||
@@ -949,8 +968,9 @@ export class PdfStampPicker {
|
|||||||
else if (name.length > 26) nameFontSize = 10.0;
|
else if (name.length > 26) nameFontSize = 10.0;
|
||||||
|
|
||||||
let iconGraphic = '';
|
let iconGraphic = '';
|
||||||
if (theme.customLogoBase64) {
|
const safeLogo = sanitizeLogoUrl(theme.customLogoBase64);
|
||||||
iconGraphic = `<image xlink:href="${theme.customLogoBase64}" href="${theme.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
if (safeLogo) {
|
||||||
|
iconGraphic = `<image xlink:href="${safeLogo}" href="${safeLogo}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
} else {
|
} else {
|
||||||
switch (theme.iconStyle || theme.id) {
|
switch (theme.iconStyle || theme.id) {
|
||||||
case 'star':
|
case 'star':
|
||||||
@@ -1004,11 +1024,11 @@ export class PdfStampPicker {
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Tipografia do Selo -->
|
<!-- Tipografia do Selo -->
|
||||||
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${theme.primaryColor || '#0d47a1'}" letter-spacing="0.3">${header}</text>
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${escapeSvgText(theme.primaryColor || '#0d47a1')}" letter-spacing="0.3">${escapeSvgText(header)}</text>
|
||||||
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${theme.textColor || '#0f172a'}">${name}</text>
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${escapeSvgText(theme.textColor || '#0f172a')}">${escapeSvgText(name)}</text>
|
||||||
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${doc}</text>
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${escapeSvgText(doc)}</text>
|
||||||
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${dateText}</text>
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${escapeSvgText(dateText)}</text>
|
||||||
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${footer}</text>
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${escapeSvgText(footer)}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ export class CyberCoreSigner {
|
|||||||
* @returns {Promise<Array<Object>>} Lista de certificados com metadados públicos.
|
* @returns {Promise<Array<Object>>} Lista de certificados com metadados públicos.
|
||||||
*/
|
*/
|
||||||
async getCertificates() {
|
async getCertificates() {
|
||||||
const resp = await this._fetch('/ccsp/v1/certificates', {
|
const url = `/ccsp/v1/certificates?application_id=${encodeURIComponent(this.applicationId)}`;
|
||||||
|
const resp = await this._fetch(url, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||