feat: initial commit - cybercore signer desktop agent

This commit is contained in:
Claudecio Martins
2026-10-11 08:20:24 -03:00
commit ccfa358f97
206 changed files with 23952 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
---
trigger: always_on
---
PROMPT MESTRE — CYBERCORE SIGNER
Você é responsável pelo desenvolvimento do CyberCore Signer, um agente desktop Java para Windows que permite aplicações web autorizadas solicitar assinaturas digitais usando certificados instalados/localmente disponíveis na máquina do usuário.
1. REGRA DE DESENVOLVIMENTO
Siga sempre: ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR.
Antes de alterar qualquer código, analise o repositório real, sua estrutura, dependências, configurações, padrões, documentação e testes. Não invente arquivos, classes ou arquitetura existentes.
Na fase de análise, não altere o projeto. Depois apresente o plano e aguarde autorização explícita para implementar.
Se durante a implementação surgir mudança relevante de escopo, pare e peça nova autorização.
Ao finalizar, informe arquivos alterados/criados, dependências, decisões arquiteturais, testes executados, limitações e pendências.
2. TECNOLOGIA
Use Java 21+ LTS, Maven ou Gradle conforme o projeto, JavaFX para a interface e JCA/JCE para criptografia.
Use X.509, KeyStore/PKCS12 e PKCS#11 conforme necessário. Use SunPKCS11 quando apropriado e Bouncy Castle somente quando houver necessidade real.
O aplicativo deve ser empacotado com runtime próprio, preferencialmente via jpackage, sem exigir Java instalado pelo usuário.
Windows 10/11 é o alvo inicial; mantenha a arquitetura preparada para macOS/Linux.
3. ARQUITETURA
Separe responsabilidades em módulos/componentes coerentes, sem criar módulos artificiais. A arquitetura deve permitir evolução e testes independentes.
Crie abstrações para descoberta e uso de certificados, por exemplo CertificateProvider, com implementações para A1, Windows Certificate Store e PKCS#11 quando aplicável.
CertificateManager deve centralizar descoberta, validade, subject, issuer, algoritmo, disponibilidade da chave privada e seleção do provider.
SigningService deve executar a assinatura por JCA/JCE e providers; nunca implemente criptografia própria.
Mantenha o núcleo independente de fabricantes.
4. A1
Suporte certificados .PFX/.P12.
A chave privada permanece exclusivamente na máquina. A senha nunca deve ser enviada ao servidor nem aparecer em logs.
Trate certificado válido, expirado, inválido, senha incorreta, ausência de chave privada e algoritmos não suportados.
Se houver persistência de credenciais, use armazenamento seguro do sistema operacional.
5. A3 / PKCS#11
Suporte tokens USB, smart cards, leitores e middleware compatíveis com PKCS#11, além de cenários Windows CSP/CNG/KSP quando necessários.
Detecte slots, tokens, certificados e chaves disponíveis.
Trate token ausente/removido, middleware/provider indisponível, PIN requerido/incorreto, timeout e falhas do dispositivo.
Nunca armazene ou envie PIN. Nunca implemente sua própria camada PKCS#11: utilize APIs/providers existentes.
Não assuma que todos os A3 funcionam da mesma forma; use adapters/providers sem acoplar o núcleo a um fabricante.
6. ASSINATURA
Priorize o modelo em que a aplicação web calcula o digest e o CyberCore assina localmente apenas esse digest.
Exemplo: documento → SHA-256 → digest → CyberCore → certificado A1/A3 → assinatura.
A chave privada nunca sai da máquina.
Valide algoritmo, tamanho/formato do digest, certificado e capacidade do provider antes da operação.
7. PROTOCOLO CCSP
Defina o CyberCore Signing Protocol (CCSP), versão inicial CCSP/1.
Toda requisição deve possuir request_id UUID, application_id, operação, algoritmo, digest, indicação opcional do certificado e expiração.
Exemplo:
{
"version":"1","request_id":"uuid","application_id":"uuid",
"operation":"sign","algorithm":"SHA256withRSA",
"digest":"base64","certificate_hint":"uuid","expires_at":"..."
}
Resposta:
{
"version":"1","request_id":"uuid","status":"signed",
"signature":"base64","certificate":"base64","algorithm":"SHA256withRSA"
}
Especifique em documentação autenticação, autorização, validação, timeouts, nonce, replay protection, erros e compatibilidade de versões.
8. COMUNICAÇÃO LOCAL
A comunicação deve ocorrer somente localmente por padrão. Avalie HTTPS localhost, WebSocket ou outro mecanismo adequado.
Nunca exponha uma API de assinatura local sem autenticação/autorização.
Não permita exposição involuntária à rede LAN.
Use registro/autorização de aplicações, origem permitida, desafio/nonce, timestamp, expiração e proteção contra replay.
Não confie exclusivamente em Origin ou User-Agent.
9. SEGURANÇA
A assinatura deve exigir confirmação explícita do usuário por padrão.
Mostre aplicação solicitante, certificado, tipo A1/A3, algoritmo e informações relevantes antes de assinar.
Nunca registre PIN, senha, chave privada ou conteúdo desnecessário do documento.
Minimize dados trafegados e armazenados.
Use UUIDs, correlação por request_id e validação rigorosa de entrada.
10. INTERFACE
Use JavaFX com aparência profissional e consistente.
Inclua, conforme o escopo: Dashboard, Certificados, Solicitação de Assinatura, Configurações, Diagnóstico e Sobre.
Suporte System Tray.
A interface deve informar claramente estado do agente, certificados disponíveis, token conectado e erros acionáveis.
11. DIAGNÓSTICO
Disponibilize diagnóstico de Java/runtime, engine criptográfica, Windows Certificate Store, A1, A3, PKCS#11, comunicação local e SDK.
Permita diagnóstico completo e teste de assinatura.
Diferencie claramente falha de configuração, middleware, token, certificado, comunicação e aplicação solicitante.
12. ERROS
Utilize erros estruturados e estáveis, incluindo:
CERTIFICATE_NOT_FOUND, CERTIFICATE_EXPIRED, CERTIFICATE_INVALID,
PRIVATE_KEY_NOT_AVAILABLE, TOKEN_NOT_FOUND, TOKEN_REMOVED,
PIN_REQUIRED, PIN_INCORRECT, PKCS11_ERROR, SIGNATURE_FAILED,
APPLICATION_NOT_AUTHORIZED, REQUEST_EXPIRED, REPLAY_DETECTED,
INVALID_ORIGIN, UNSUPPORTED_ALGORITHM, USER_CANCELLED,
PROVIDER_NOT_AVAILABLE e COMMUNICATION_ERROR.
Mensagens para o usuário devem ser claras, sem expor segredos ou detalhes internos desnecessários.
13. LOGS E AUDITORIA
Registre eventos relevantes: inicialização, descoberta de certificados, provider utilizado, aplicação, request_id, operação, sucesso/falha e remoção de token.
Nunca registre PIN, senha, chave privada ou dados sensíveis desnecessários.
Use níveis de log adequados e correlação por request_id.
14. SDK WEB
Planeje o pacote JavaScript @cybercore/signer-sdk.
API mínima:
connect(), isInstalled(), getVersion(), getStatus(),
getCertificates(), getCapabilities(), sign(), verify().
O SDK deve abstrair o CCSP e fornecer erros previsíveis para aplicações web.
15. MULTIAPLICAÇÃO E CONCORRÊNCIA
Uma única instalação deve atender múltiplas aplicações web autorizadas.
Controle concorrência e fila de operações, especialmente em A3, pois tokens/middleware podem não suportar operações simultâneas.
Nunca misture contexto ou autorização de uma aplicação com outra.
16. PDF/PAdES
Não misture a operação criptográfica básica com a montagem/finalização de PDF.
Para PAdES, avalie bibliotecas maduras como Apache PDFBox, Bouncy Castle ou DSS.
Nunca implemente manualmente mecanismos criptográficos ou estruturas complexas de assinatura PDF sem necessidade.
17. TESTES
Crie testes unitários, integração, segurança, protocolo, providers, comunicação local, SDK e concorrência.
Cubra A1 válido/expirado/inválido/senha incorreta; A3 sem token, token removido, PIN incorreto, middleware indisponível e cancelamento; assinatura válida; algoritmo incompatível; requisição expirada/repetida/não autorizada; origem inválida; payload malformado e concorrência.
Testes dependentes de hardware só podem ser declarados executados quando o hardware realmente estiver disponível.
18. MODO DESENVOLVIMENTO
Implemente mock quando útil para desenvolvimento e testes, simulando A1/A3, token removido, PIN incorreto, certificado expirado, provider indisponível, timeout e cancelamento.
O modo deve ser claramente identificado como DESENVOLVIMENTO.
Nunca apresente assinatura simulada como assinatura real.
19. INSTALADOR E ATUALIZAÇÃO
Crie instalador Windows com runtime próprio, instalação/desinstalação, atalhos e opções de inicialização quando aplicável.
Atualizações devem ser autenticadas e seguras.
Executáveis e instaladores devem ser preparados para code signing.
20. PRIVACIDADE
A chave privada, PIN e senha A1 permanecem na máquina.
Quando apenas o digest for necessário, o documento não deve ser enviado ao CyberCore.
Colete, armazene e registre somente o necessário para operação, diagnóstico e auditoria.
21. DOCUMENTAÇÃO
Mantenha documentação técnica em docs/.
Documente especialmente docs/CCSP.md, arquitetura, instalação, certificados A1/A3, PKCS#11, segurança, SDK, erros, diagnóstico, desenvolvimento e limitações.
Toda mudança relevante de protocolo ou arquitetura deve atualizar a documentação correspondente.
22. IMPLEMENTAÇÃO
Ao receber uma tarefa:
1) ANALISE o projeto real e identifique impacto, dependências e riscos.
2) PLANEJE a solução, apresentando arquivos/componentes afetados e estratégia de testes.
3) PEÇA AUTORIZAÇÃO explícita antes de modificar o código.
4) IMPLEMENTE seguindo os padrões existentes e evitando alterações fora do escopo.
5) VALIDE com build, testes, análise de erros e, quando possível, execução real.
Não marque uma tarefa como concluída apenas porque o código compila.
23. REGRA FINAL
Priorize segurança, interoperabilidade, rastreabilidade, manutenção e compatibilidade.
Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados.
Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência.
Princípio obrigatório:
ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR.
+80
View File
@@ -0,0 +1,80 @@
# ==============================================================================
# CyberCore Signer - .gitignore
# ==============================================================================
# Compilação e Artefatos Maven
target/
**/target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Binários gerados e Distribuições Locais
*.jar
*.war
*.ear
*.zip
dist/
build/
bin/
# Certificados, Chaves e Credenciais Locais (Privacidade e Segurança)
*.pfx
*.p12
*.jks
*.keystore
*.key
*.pem
*.cer
*.crt
*.der
!assets/
!docs/
# Ambientes e Variáveis Locais
.env
.env.*
!.env.example
# IDEs e Editores
.idea/
*.iml
*.ipr
*.iws
.vscode/
*.code-workspace
.project
.classpath
.settings/
.factorypath
bin/
# Logs e Auditorias de Runtime Locais
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Node / SDK de Testes
node_modules/
sdk/node_modules/
coverage/
.nyc_output/
# Sistema Operacional
.DS_Store
Thumbs.db
desktop.ini
$RECYCLE.BIN/
*.tmp
*~
# Arquivos temporários de Scratch locais
scratch/temp/
+3
View File
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
+100
View File
@@ -0,0 +1,100 @@
# CyberCore Signer
> **Agente Desktop de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.**
O **CyberCore Signer** é uma solução desktop em Java 21 LTS com JavaFX que permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e carimbos do tempo utilizando certificados digitais ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) instalados ou conectados na estação de trabalho do usuário.
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP/WebSocket localhost autenticado e seguro.
---
## 🏛 Arquitetura do Projeto
O projeto é estruturado em módulos Maven:
* **[modules/cybercore-core](modules/cybercore-core)**: Núcleo de regras criptográficas (JCA/JCE, SunPKCS11, X.509, PAdES, XMLDSig, carimbo de tempo TSA, LTV e validação de cadeias). Não possui dependência de interface gráfica.
* **[modules/cybercore-ui](modules/cybercore-ui)**: Interface gráfica com o usuário construída em JavaFX (Dashboard, Gerenciador de Certificados, Diagnóstico Criptográfico, Diálogo de Confirmação de Assinatura, PIN dialog e System Tray).
* **[modules/cybercore-app](modules/cybercore-app)**: Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida e servidor CCSP local.
* **[sdk/](sdk)**: SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração de aplicações web com o agente desktop.
* **[docs/](docs)**: Documentação técnica detalhada de arquitetura, protocolo, segurança e casos de uso.
---
## 🚀 Requisitos
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin, Corretto, etc.).
* **Maven:** 3.9+ (ou utilizar o wrapper `./mvnw` incluído no projeto).
* **Sistema Operacional Alvo:** Windows 10/11 ou Linux (x86_64).
---
## 🛠 Compilação e Empacotamento
### 1. Compilação do JAR Executável
Para compilar e gerar o JAR consolidado (com todas as dependências sombreadas e assets):
```bash
./build-jar.sh
```
Ou diretamente via Maven:
```bash
./mvnw clean package -DskipTests
```
O artefato consolidado será gerado em:
`modules/cybercore-app/target/cybercore-signer.jar`
### 2. Execução Local
Para iniciar o agente em ambiente de desenvolvimento:
```bash
# No Linux / macOS:
./run.sh
# No Windows:
run.bat
```
### 3. Empacotamento com Runtime Nativo (`jpackage`)
Para criar a distribuição com Java Runtime embutido (sem exigir JDK pré-instalado pelo usuário final):
```bash
./scripts/package-jpackage.sh
```
---
## 🔒 Princípios de Segurança e Privacidade
1. **Chave Privada Preservada:** A chave privada e os segredos (PIN de token A3 e senha de certificado A1) nunca saem da máquina do usuário nem trafegam pela rede.
2. **Assinatura por Digest:** Por padrão, a aplicação web calcula o hash/digest (ex: SHA-256) do documento e o CyberCore assina localmente apenas o digest, minimizando o tráfego de dados sensíveis.
3. **Confirmação Explícita:** Toda operação de assinatura exige consentimento e autorização visual do usuário antes da aplicação da chave.
4. **Isolamento de Origem e CCSP:** O agente atende apenas origens web autorizadas, protegendo contra abusos via CSRF ou requisições locais não autenticadas.
---
## 📚 Documentação Técnica
Consulte a pasta [docs/](docs) para especificações completas:
* [Protocolo CCSP (CyberCore Signing Protocol)](docs/CCSP.md)
* [Arquitetura e Fluxo do Sistema](docs/architecture.md)
* [Certificados A1 e A3 (PKCS#11 / MSCAPI)](docs/a1-a3-certificates.md)
* [Assinatura de Documentos PDF (PAdES / LTV / TSA)](docs/pades.md)
* [Assinatura de Documentos XML (XMLDSig)](docs/xml-xmldsig.md)
* [Autenticação e Sessão Criptográfica](docs/authentication-login.md)
* [Diagnóstico e Resolução de Problemas](docs/diagnostics.md)
* [Segurança e Tratamento de Erros](docs/security.md)
* [SDK Web JavaScript](docs/sdk.md)
* [Guia de Instalação](docs/installation.md)
---
## 📄 Licença
Propriedade de **CyberCore Systems**. Todos os direitos reservados.
+75
View File
@@ -0,0 +1,75 @@
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**.
---
## 📁 Estrutura de Arquivos Criados
```
assets/
├── icons/ # Ícones vetoriais SVG reais e editáveis (W3C padrão)
│ ├── pdf.svg # Ícone Assinar PDF (documento com tag vermelha PDF)
│ ├── xml.svg # Ícone Assinar XML (documento com tag azul <XML>)
│ ├── file.svg # Ícone Assinar Arquivo (documento com caneta)
│ ├── batch.svg # Ícone Assinatura em Lote (pilha de documentos com check)
│ ├── attached.svg # Ícone Assinatura Atachada (documento com clipe de papel)
│ ├── co-sign.svg # Ícone Co-Assinar (documentos duplos com usuários)
│ ├── timestamp.svg # Ícone Carimbo do Tempo (documento com relógio TSA)
│ ├── ltv.svg # Ícone Assinatura LTV (documento com selo perpétuo)
│ ├── validate.svg # Ícone Validar Assinatura (documento com checkmark verde)
│ ├── validate-timestamp.svg # Ícone Validar Carimbo (relógio com checkmark verde)
│ ├── exit.svg # Ícone Sair do Agente (porta com seta vermelha de saída)
│ ├── logs.svg # Ícone Terminal de Logs (janela com prompt </>)
│ ├── settings.svg # Ícone Configurações (engrenagem de precisão de 8 dentes)
│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play)
│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro)
│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i')
│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais)
│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo)
│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna)
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
│ └── star.svg # Ícone de Estrela notarial / favorita
│
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
```
---
## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:**
- Estão no formato `.svg` padrão.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**.
- Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente.
2. **Imagens do Sistema:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
- Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa.
3. **Ordem de Prioridade e Hot-Reloading:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar!
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
---
## 🚀 Como Testar Suas Modificações
Para compilar e gerar o pacote `.jar` completo com todos os novos assets embutidos:
```bash
./build-jar.sh
```
Para rodar a aplicação:
```bash
java -jar ./cybercore-signer.jar
```
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><title>info-circle-fill</title><path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.93-9.412l-1 4.705c-.07.34.029.533.304.533c.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598c-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081l.082-.381l2.29-.287zM8 5.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2"/></svg>

After

Width:  |  Height:  |  Size: 429 B

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

After

Width:  |  Height:  |  Size: 976 B

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

After

Width:  |  Height:  |  Size: 920 B

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

After

Width:  |  Height:  |  Size: 688 B

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

After

Width:  |  Height:  |  Size: 274 B

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

After

Width:  |  Height:  |  Size: 815 B

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

After

Width:  |  Height:  |  Size: 343 B

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

After

Width:  |  Height:  |  Size: 598 B

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

After

Width:  |  Height:  |  Size: 873 B

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

After

Width:  |  Height:  |  Size: 811 B

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

After

Width:  |  Height:  |  Size: 861 B

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

After

Width:  |  Height:  |  Size: 314 B

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

After

Width:  |  Height:  |  Size: 996 B

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

After

Width:  |  Height:  |  Size: 671 B

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

After

Width:  |  Height:  |  Size: 817 B

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

After

Width:  |  Height:  |  Size: 649 B

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

After

Width:  |  Height:  |  Size: 282 B

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

After

Width:  |  Height:  |  Size: 647 B

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

After

Width:  |  Height:  |  Size: 958 B

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

After

Width:  |  Height:  |  Size: 477 B

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

After

Width:  |  Height:  |  Size: 679 B

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

After

Width:  |  Height:  |  Size: 848 B

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

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Executable
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# ==============================================================================
# CyberCore Signer - Script de Automação de Build do Fat JAR Executável
# ==============================================================================
set -euo pipefail
# Garante execução no diretório raiz do projeto
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${SCRIPT_DIR}"
COLOR_RESET="\033[0m"
COLOR_BOLD="\033[1m"
COLOR_CYAN="\033[36m"
COLOR_GREEN="\033[32m"
COLOR_YELLOW="\033[33m"
COLOR_RED="\033[31m"
RUN_AFTER_BUILD=false
RUN_TESTS=false
# Interpretação de argumentos
for arg in "$@"; do
case "${arg}" in
--with-tests|-t)
RUN_TESTS=true
;;
--run|-r)
RUN_AFTER_BUILD=true
;;
--help|-h)
echo -e "${COLOR_BOLD}Uso: ./build-jar.sh [opções]${COLOR_RESET}"
echo ""
echo "Opções disponíveis:"
echo " --with-tests, -t Executa a suíte completa de testes unitários antes de empacotar."
echo " --run, -r Inicia automaticamente o CyberCore Signer após a compilação."
echo " --help, -h Exibe esta mensagem de ajuda."
echo ""
echo "Exemplo:"
echo " ./build-jar.sh # Compilação e empacotamento rápido do JAR"
echo " ./build-jar.sh --run # Compila e já abre a aplicação"
exit 0
;;
*)
echo -e "${COLOR_YELLOW}Aviso: Opção desconhecida: ${arg}. Use --help para detalhes.${COLOR_RESET}"
;;
esac
done
echo -e "${COLOR_CYAN}${COLOR_BOLD}"
echo "============================================================"
echo " CYBERCORE SIGNER - AUTOMAÇÃO DE BUILD DO JAR"
echo "============================================================"
echo -e "${COLOR_RESET}"
# 1. Verificação e Auto-Detecção do Java (JDK 21+)
if ! command -v java &>/dev/null; then
POSSIBLE_JDKS=(
"/home/claudecio/.jdks/jdk-25.0.4.1+1"
"/usr/lib/jvm/java-25-openjdk-amd64"
"/usr/lib/jvm/java-21-openjdk-amd64"
"/usr/lib/jvm/default-java"
)
for candidate in "${POSSIBLE_JDKS[@]}"; do
if [ -x "${candidate}/bin/java" ]; then
export JAVA_HOME="${candidate}"
export PATH="${JAVA_HOME}/bin:${PATH}"
break
fi
done
fi
if ! command -v java &>/dev/null; then
echo -e "${COLOR_RED}Erro: Java não foi encontrado no PATH nem nos diretórios padrão de JDK.${COLOR_RESET}"
echo -e "${COLOR_YELLOW}Dica: Não é necessário executar com 'sudo'. Tente executar normalmente: ./build-jar.sh${COLOR_RESET}"
exit 1
fi
JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | head -n 1)
echo -e "Ambiente Java detectado: ${COLOR_BOLD}${JAVA_VERSION}${COLOR_RESET} (${JAVA_HOME:-$(which java)})"
# 2. Verificação do Maven Wrapper
if [[ ! -f "./mvnw" ]]; then
echo -e "${COLOR_RED}Erro: Maven Wrapper (./mvnw) não encontrado na raiz do projeto.${COLOR_RESET}"
exit 1
fi
chmod +x ./mvnw
# 3. Execução do Build
MVN_ARGS=("clean" "package")
if [[ "${RUN_TESTS}" == "false" ]]; then
echo -e "${COLOR_YELLOW}Modo rápido ativado (pulando testes unitários). Para testar use: ./build-jar.sh --with-tests${COLOR_RESET}"
MVN_ARGS+=("-DskipTests")
else
echo -e "${COLOR_CYAN}Executando testes unitários e de integração...${COLOR_RESET}"
fi
echo -e "Iniciando compilação e geração do Fat JAR via Maven..."
./mvnw "${MVN_ARGS[@]}"
SOURCE_JAR="modules/cybercore-app/target/cybercore-signer.jar"
DEST_JAR="./cybercore-signer.jar"
if [[ ! -f "${SOURCE_JAR}" ]]; then
echo -e "${COLOR_RED}Erro: O artefato ${SOURCE_JAR} não foi gerado pelo build.${COLOR_RESET}"
exit 1
fi
# 4. Copia para a raiz para facilitar o acesso
cp -f "${SOURCE_JAR}" "${DEST_JAR}"
JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}')
echo ""
echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}"
echo -e "${COLOR_GREEN}${COLOR_BOLD}✓ JAR GERADO COM SUCESSO!${COLOR_RESET}"
echo -e "Arquivo: ${COLOR_BOLD}${DEST_JAR}${COLOR_RESET} (${JAR_SIZE})"
echo -e "Local de origem: ${SOURCE_JAR}"
echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}"
echo ""
echo -e "Para testar manualmente, execute:"
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR}${COLOR_RESET} # Interface gráfica desktop"
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --dev${COLOR_RESET} # Modo desenvolvimento (logs detalhados)"
echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --headless${COLOR_RESET} # Modo console de segundo plano (porta 21325)"
echo ""
# 5. Execução automática opcional
if [[ "${RUN_AFTER_BUILD}" == "true" ]]; then
echo -e "${COLOR_BOLD}Iniciando aplicação agora...${COLOR_RESET}"
java -jar "${DEST_JAR}"
fi
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Exclusões do SpotBugs / FindSecBugs.
Regra: toda exclusão deve ser específica (classe/padrão) e justificada em comentário.
-->
<FindBugsFilter
xmlns="https://github.com/spotbugs/filter/3.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
<!--
Records imutáveis de DTO e relatório que já realizam cópia defensiva em seus
construtores canônicos (List.copyOf / Map.copyOf) mas são apontados por heurística do SpotBugs.
-->
<Match>
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
<Or>
<Class name="br.com.cybercore.signer.core.diagnostics.DiagnosticReport"/>
<Class name="br.com.cybercore.signer.core.domain.SigningResult"/>
<Class name="br.com.cybercore.signer.core.protocol.CcspResponse"/>
</Or>
</Match>
</FindBugsFilter>
+208
View File
@@ -0,0 +1,208 @@
# CyberCore Signing Protocol (CCSP/1)
O **CyberCore Signing Protocol (CCSP)** é o protocolo padrão para comunicação local entre aplicações web rodando no navegador e o agente desktop **CyberCore Signer**.
A versão inicial descrita neste documento é a **CCSP/1**.
---
## 1. Princípios de Segurança
1. **Isolamento de Rede:** O servidor local escuta exclusivamente no endereço de loopback `127.0.0.1` (porta padrão `21325`), impedindo qualquer acesso a partir da rede local (LAN) ou externa (WAN).
2. **Controle Estrito de Origem (CORS):** O agente valida cabeçalhos `Origin` e responde apenas a pré-voos `OPTIONS` com origens autorizadas.
3. **Registro e Autorização de Aplicações:** Nenhuma aplicação desconhecida tem acesso a certificados ou assinaturas. Ao tentar conectar pela primeira vez, uma caixa de diálogo nativa solicita aprovação explícita do usuário.
4. **Confirmação Visual da Assinatura:** Toda operação de assinatura exige confirmação manual na interface gráfica, exibindo a aplicação solicitante, o hash do documento e o certificado selecionado.
5. **Proteção Contra Ataques de Replay:** Toda requisição deve conter um `nonce` UUID único com validação temporal (`expires_at` ou `timestamp`). Requisições com nonces repetidos são sumariamente rejeitadas (`REPLAY_DETECTED`).
6. **Privacidade Criptográfica:** Apenas o **digest (hash)** do documento é enviado ao agente. O documento completo nunca sai da aplicação web e a chave privada nunca sai do dispositivo/computador do usuário.
---
## 2. Endpoints do Protocolo (CCSP/1)
### 2.1 `GET /ccsp/v1/status`
Verifica se o agente está instalado, em execução e pronto para operações.
#### Resposta de Sucesso (200 OK)
```json
{
"version": "1",
"status": "ready",
"agent": "CyberCore Signer",
"agent_version": "0.1.0",
"os": "Windows 11",
"timestamp": "2026-10-08T19:40:00Z"
}
```
---
### 2.2 `GET /ccsp/v1/certificates`
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura.
#### Headers / Parâmetros
- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`).
- `Origin`: Origem web autorizada.
#### Resposta de Sucesso (200 OK)
```json
{
"version": "1",
"status": "success",
"certificates": [
{
"id": "win-4340656a",
"alias": "CLAUDIO SILVA:12345678900",
"common_name": "CLAUDIO SILVA:12345678900",
"subject_dn": "CN=CLAUDIO SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
"issuer_dn": "CN=AC SERPRO v5, O=ICP-Brasil, C=BR",
"type": "A3",
"provider_id": "provider-win-store",
"device_name": "Token / Smart Card (Windows CAPI/CNG)",
"valid_from": "2024-05-10T12:00:00Z",
"valid_to": "2027-05-10T12:00:00Z",
"serial_number": "72910482910482019482",
"can_sign": true,
"status": "AVAILABLE"
}
]
}
```
---
### 2.3 `POST /ccsp/v1/authorize`
Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário.
#### Requisição
```json
{
"version": "1",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Portal Assinaturas Jurídicas"
}
```
#### Resposta (200 OK)
```json
{
"version": "1",
"status": "authorized",
"application_id": "550e8400-e29b-41d4-a716-446655440000"
}
```
---
### 2.4 `POST /ccsp/v1/sign`
Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquivos gerais).
#### Requisição
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "sign",
"algorithm": "SHA256withRSA",
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
"certificate_hint": "win-4340656a",
"document_name": "contrato_locacao.pdf",
"document_type": "PDF",
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
"expires_at": "2026-10-08T19:45:00Z"
}
```
#### Resposta de Sucesso (200 OK)
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "signed",
"signature": "MEUCIQDq...base64...",
"certificate": "MIIFazC...base64...",
"certificate_chain": [
"MIIFazC...base64...",
"MIIEazC...base64..."
],
"algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA",
"subject_cpf_cnpj": "12345678900",
"document_name": "contrato_locacao.pdf",
"document_type": "PDF"
}
```
---
### 2.5 `POST /ccsp/v1/authenticate`
Executa o login / autenticação digital segura de um usuário em sistemas web via desafio criptográfico (Challenge-Response ICP-Brasil).
#### Requisição
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "authenticate",
"challenge": "server-session-challenge-nonce-2026",
"certificate_hint": "win-4340656a",
"purpose": "AUTHENTICATION",
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
"expires_at": "2026-10-08T19:45:00Z"
}
```
#### Resposta de Sucesso (200 OK)
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "authenticated",
"signature": "MEUCIQDq...base64...",
"certificate": "MIIFazC...base64...",
"certificate_chain": [
"MIIFazC...base64..."
],
"algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA",
"subject_cpf_cnpj": "12345678900",
"document_type": "AUTH_CHALLENGE"
}
```
#### Resposta de Erro (400/403/404)
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "error",
"error_code": "USER_CANCELLED",
"message": "Operação de assinatura cancelada pelo usuário."
}
```
---
## 3. Tabela de Códigos de Erro Estruturados
| Código | Significado |
|---|---|
| `CERTIFICATE_NOT_FOUND` | Certificado indicado no hint não foi encontrado. |
| `CERTIFICATE_EXPIRED` | O certificado selecionado está fora do período de vigência. |
| `CERTIFICATE_INVALID` | O certificado é inválido ou corrompido. |
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível ou ausente. |
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. |
| `TOKEN_REMOVED` | Dispositivo de hardware foi removido durante a assinatura. |
| `PIN_REQUIRED` | PIN obrigatório para liberar chave privada em PKCS#11. |
| `PIN_INCORRECT` | PIN digitado está incorreto. |
| `PKCS11_ERROR` | Falha no middleware PKCS#11 do fabricante. |
| `SIGNATURE_FAILED` | Erro durante a geração da assinatura digital. |
| `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou recusada pelo usuário. |
| `REQUEST_EXPIRED` | A data/hora de validade da requisição expirou. |
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. |
| `INVALID_ORIGIN` | O cabeçalho Origin não corresponde ao cadastro da aplicação. |
| `UNSUPPORTED_ALGORITHM` | Algoritmo não suportado (ex: diferente de SHA256withRSA). |
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela de confirmação. |
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário está inativo no SO. |
| `COMMUNICATION_ERROR` | Erro interno no agente de comunicação local. |
+46
View File
@@ -0,0 +1,46 @@
# Certificados Digitais A1, A3 e PKCS#11
O **CyberCore Signer** oferece suporte abrangente aos formatos e repositórios de certificados digitais padrão ICP-Brasil utilizados no Brasil.
---
## 1. Certificados Digitais A1 (Software / PKCS#12)
Os certificados A1 são arquivos com extensão `.pfx` ou `.p12` protegidos por senha.
### Fluxo de Uso
1. O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou importa via tela de Dashboard.
2. É solicitada a senha em um diálogo seguro (`A1PasswordDialog`), mantida em `char[]` temporário e sobrescrita com zeros logo após a leitura da chave.
3. O provedor `Pkcs12CertificateProvider` instancia um `KeyStore.getInstance("PKCS12")` e extrai os metadados do certificado X.509 (`subject`, `issuer`, `validade`, `algoritmo`, `tamanho da chave`).
4. Durante a assinatura, a chave privada em memória gera a assinatura digital via `Signature.getInstance("SHA256withRSA")`.
---
## 2. Certificados Digitais A3 no Windows (Windows Certificate Store)
No Windows 10/11, a maioria dos drivers de tokens USB e Smart Cards (ex: SafeSign, SafeNet Authentication Client, Serpro, Certisign) registra seus certificados diretamente no repositório de certificados do usuário (`Windows-MY`).
### Vantagens do Provider `WindowsCertificateStoreProvider`
- Acessa certificados físicos via CryptoAPI (CAPI) e Cryptography Next Generation (CNG) através do provedor JDK `SunMSCAPI`.
- A chave privada permanece dentro do chip criptográfico do token.
- Quando o token exige autenticação por PIN, a janela nativa do próprio driver do fabricante é invocada com segurança pelo sistema operacional Windows.
---
## 3. Hardware PKCS#11 Direto (Tokens USB e Smartcards)
Para ambientes onde se deseja acesso direto ao middleware ou no Linux:
- `Pkcs11CertificateProvider` pesquisa bibliotecas DLL / SO padrão:
- `eps2003csp11.dll` (SafeNet / eToken)
- `aetpkss1.dll` (SafeSign AET)
- `acospkcs11.dll` (ACOS / G&D)
- `opensc-pkcs11.dll` / `opensc-pkcs11.so` (OpenSC)
- Utiliza o provider SunPKCS11 do JDK (`jdk.crypto.cryptoki`).
---
## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`)
O componente `SmartCardWatcher` monitora os leitores PC/SC em segundo plano:
- **Ao inserir um token:** Atualiza a lista de certificados automaticamente e notifica a interface gráfica.
- **Ao remover o token:** Registra evento de auditoria (`AUDITORIA [TOKEN_REMOVED]`), limpa o cache de PIN de sessão em `PinSessionManager` e força um warm reset via `SmartCardSessionManager`.
+79
View File
@@ -0,0 +1,79 @@
# Arquitetura do CyberCore Signer
O **CyberCore Signer** é um agente desktop nativo desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica JCA/JCE pura, projetado para permitir que aplicações web autorizadas solicitem assinaturas digitais ICP-Brasil padrão **A1** e **A3** diretamente da máquina do usuário.
---
## 1. Visão Geral da Arquitetura em Módulos
O projeto adota uma arquitetura desacoplada em múltiplos módulos Maven independentes:
```
cybercore-signer/
├── modules/
│ ├── cybercore-core/ # Núcleo de criptografia, provedores, protocolo e segurança
│ ├── cybercore-ui/ # Interface gráfica JavaFX, diálogos nativos e System Tray
│ └── cybercore-app/ # Ponto de entrada (Main), bootstrap e empacotamento nativo
├── sdk/ # SDK Web em JavaScript (@cybercore/signer-sdk)
├── scripts/ # Scripts de build nativo jpackage para Windows e Linux
└── docs/ # Especificações técnicas e manuais de integração
```
### 1.1 `cybercore-core`
- **Domínio:** Modelos imutáveis como `ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType` e `CertificateStatus`.
- **Criptografia:** `SigningService` e `DigestAlgorithm` utilizando exclusivamente os provedores padrão JCA/JCE (sem reimplementação manual de algoritmos).
- **Provedores:** Abstração `CertificateProvider` com implementações:
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória protegida.
- `WindowsCertificateStoreProvider`: integração nativa com o repositório de chaves do Windows (`Windows-MY` via SunMSCAPI/CNG).
- `Pkcs11CertificateProvider`: integração com drivers de middleware e tokens USB/Smartcards via `SunPKCS11`.
- **Protocolo & Rede Local:** `CcspServer` escuta estritamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1.
- **Segurança & Configurações:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` (persistência em `~/.cybercore/config.json`).
- **Validação Criptográfica:** `SignatureValidationService` com suporte a Assinaturas Incorporadas (PAdES PDF, CMS/P7M com conteúdo embutido, XMLDSig) e Assinaturas Destacadas (Detached CMS/P7S).
- **Selos Visuais Personalizados:** `CustomSealStorage` (persistência em `~/.cybercore/seals/`), `PdfSealThemeRegistry` dinâmico e `CpfDisplayMode` (privacidade LGPD).
### 1.2 `cybercore-ui`
- Interface desenvolvida com JavaFX 25, empregando layout moderno e simétrico de grade 3x3 de operações e coluna lateral de utilitários:
- `MainAppView`: grade 3x3 otimizada:
- Linha 0: Assinar PDF, Assinar XML, Assinatura Atachada
- Linha 1: Co-Assinar Documento, Carimbo do Tempo, Gerenciar Selos
- Linha 2: Validar Assinatura, Validar Carimbo, Sair do Agente
- `SealDesignerDialog`: criador e instalador de selos digitais personalizados com pré-visualização ao vivo (Live Preview), seletores de cores, estilos de emblema, obrigatoriedade de nome completo, controle de mascaramento de CPF e estampa de data/hora.
- `SignatureValidationDialog`: diálogo de validação criptográfica real com alternância entre Assinatura Incorporada e Assinatura Destacada, extração de documento embutido e relatório de signatários.
- `PdfSignDialog`: modal de assinatura PAdES com posicionamento por clique na folha, atalho direto para criar/instalar selos e opções dinâmicas de CPF/data-hora.
- `SettingsView`: configurações gerais persistentes, política de PIN e gerenciamento de selos instalados.
### 1.3 `cybercore-app`
- Main bootstrap (`CyberCoreSignerApplication`).
- Tratamento de parâmetros de inicialização (`--dev`, `--headless`).
- Execução de diagnósticos preliminares de ambiente e engine criptográfica antes do início das threads de escuta.
- Geração de Fat JAR autocontido via `maven-shade-plugin` e preparação para distribuição `jpackage`.
---
## 2. Fluxo de Dados e Princípio de Menor Privilégio
O CyberCore Signer opera sob o **modelo de assinatura descentralizada por digest**:
1. A aplicação web calcula o hash criptográfico SHA-256 no próprio navegador utilizando a Web Crypto API nativa.
2. Apenas o hash Base64 de 32 bytes trafega localmente para o CyberCore Signer.
3. O documento completo nunca sai do navegador e a chave privada nunca sai do computador ou do hardware do usuário.
```mermaid
sequenceDiagram
autonumber
actor Usuario as Usuário
participant Web as Aplicação Web (SDK)
participant CCSP as CcspServer (127.0.0.1)
participant UI as CyberCore JavaFX
participant Crypto as SigningService & Providers
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce)
CCSP->>CCSP: Valida autorização, nonce anti-replay e digest
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
UI->>Usuario: Exibe Diálogo (Hash, Certificado, Aplicação)
Usuario->>UI: Aprova assinatura (e insere PIN se A3)
UI-->>CCSP: Confirmação concedida
CCSP->>Crypto: sign(SigningContext) [Trava FIFO]
Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign()
Crypto-->>CCSP: SigningResult (signatureBytes, certChain)
CCSP-->>Web: JSON { status: "signed", signature: "...", certificate: "..." }
```
+71
View File
@@ -0,0 +1,71 @@
# Autenticação e Login Digital via CyberCore Signer
Este documento descreve o fluxo de **Login e Autenticação de Usuários com Certificado Digital ICP-Brasil (A1 e A3)** através do agente desktop **CyberCore Signer** e o protocolo **CCSP/1**.
---
## 1. Visão Geral do Modelo de Autenticação
O login por certificado digital no CyberCore Signer adota o padrão de mercado **Challenge-Response Criptográfico**:
1. O backend da sua aplicação web gera um **desafio único (challenge / nonce)** para a sessão atual.
2. A aplicação web solicita a assinatura desse desafio ao **CyberCore Signer** através do SDK JavaScript.
3. O CyberCore Signer apresenta ao usuário na tela nativa do computador uma solicitação visual com destaque de **LOGIN / AUTENTICAÇÃO**:
- Identifica a aplicação solicitante e a origem;
- Mostra o certificado selecionado e o titular;
- Solicita aprovação expressa do usuário (e a digitação do PIN caso seja token ou smart card A3).
4. O CyberCore assina o desafio com a chave privada do certificado local e devolve:
- A **assinatura digital do desafio**;
- O **certificado X.509 completo** (DER Base64) e cadeia;
- O **nome e CPF/CNPJ** do titular extraídos do certificado.
5. O frontend web envia a resposta de volta ao backend, que:
- Valida criptograficamente a assinatura com a chave pública do certificado recebido;
- Confirma a validade e cadeia do certificado junto à ICP-Brasil;
- Localiza o usuário correspondente no banco de dados pelo CPF/CNPJ;
- Cria a sessão autenticada do usuário no sistema.
---
## 2. Exemplo de Integração Frontend com o SDK
```javascript
import { CyberCoreSigner } from '@cybercore/signer-sdk';
const signer = new CyberCoreSigner({
applicationName: 'Portal Corporativo'
});
async function realizarLoginComCertificado(certificateId) {
// 1. Obter desafio único gerado pelo seu backend
const { challenge } = await fetch('/api/auth/challenge').then(r => r.json());
// 2. Solicitar autenticação ao CyberCore Signer
const authResult = await signer.authenticate({
certificateId: certificateId,
challenge: challenge
});
// 3. Enviar assinatura e certificado ao backend para validação e abertura de sessão
const sessionResponse = await fetch('/api/auth/login-certificate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: authResult.challenge,
signature: authResult.signature,
certificate: authResult.certificate
})
});
if (sessionResponse.ok) {
console.log('Usuário autenticado com sucesso:', authResult.subjectName);
window.location.href = '/dashboard';
}
}
```
---
## 3. Segurança e Prevenção de Ataques
- **Prevenção de Ataques de Replay:** Cada desafio deve possuir tempo de expiração curto (ex: 60 a 90 segundos) e ser descartado no servidor após uma única tentativa.
- **Proteção da Chave Privada:** A chave privada e o PIN do usuário permanecem exclusivamente sob custódia do hardware/computador local do usuário.
- **Consentimento Explícito:** O CyberCore Signer nunca assina desafios em segundo plano sem a exibição do diálogo e autorização do usuário na tela nativa.
+48
View File
@@ -0,0 +1,48 @@
# Modo de Desenvolvimento e Diagnóstico Detalhado
O **CyberCore Signer** opera exclusivamente com **certificados digitais e provedores criptográficos reais** (A1 via arquivos `.pfx`/`.p12`, repositório nativo do Windows `Windows-MY` e tokens/smart cards físicos PKCS#11). Em conformidade com a exigência de segurança de produção, **não existem mocks ou certificados sintéticos no agente**.
---
## 1. Ativação do Modo de Desenvolvimento
Para iniciar o agente em modo de desenvolvimento (com logs detalhados e diagnósticos ampliados), utilize o argumento `--dev`:
```bash
java -jar cybercore-signer.jar --dev
```
Ao iniciar, uma mensagem informativa é registrada no log:
```
=========================================================
MODO DE DESENVOLVIMENTO ATIVADO (--dev)
Logs detalhados ativos. Operação com certificados reais.
=========================================================
```
---
## 2. Testando em Ambiente de Desenvolvimento
Para desenvolvedores testarem fluxos completos de assinatura de PDF, XML, arquivos em geral e login digital:
1. **Certificados A1 de Teste/Homologação:**
- Adicione um certificado A1 `.pfx` ou `.p12` emitido por uma autoridade de teste ou autoassinado.
- Pela interface desktop, clique no botão **"Importar A1 (.pfx/.p12)"** no Dashboard e informe o arquivo e a senha.
- O certificado ficará disponível imediatamente para todas as operações CCSP/1.
2. **Repositório Nativo do Windows (Windows-MY):**
- Instale o certificado no repositório de certificados pessoais do Windows (`certmgr.msc`).
- O provedor `WindowsCertificateStoreProvider` detectará o certificado automaticamente.
3. **Tokens USB / Smart Cards (PKCS#11):**
- Conecte o dispositivo físico com middleware homologado (SafeSign, GD Burti, SafeNet, OpenSC, etc.).
- O provedor `Pkcs11CertificateProvider` detectará o token e seus slots.
---
## 3. Garantia de Operação Real
1. Toda assinatura é gerada criptograficamente através dos provedores JCA/JCE reais.
2. A resposta do protocolo CCSP/1 contém assinaturas válidas e certificados X.509 autênticos.
3. Não há certificados simulados ou flags artificiais nas respostas do agente.
+34
View File
@@ -0,0 +1,34 @@
# Diagnóstico de Ambiente e Auto-Teste
O **CyberCore Signer** possui um subsistema nativo de diagnóstico integrado (`DiagnosticService`), permitindo auditoria detalhada de saúde e integridade técnica da máquina do usuário.
---
## 1. Itens Inspecionados no Diagnóstico
Ao abrir a aba **Diagnóstico** na interface ou invocar `diagnosticService.runDiagnostic()`, o agente avalia:
1. **Ambiente Java / Runtime:**
- Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS);
- Vendor do JDK (Temurin, Oracle, etc.);
- Arquitetura do processador e sistema operacional.
2. **Engine Criptográfica JCA/JCE:**
- Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.);
- Suporte ao algoritmo de hash `SHA-256`;
- Suporte ao algoritmo de assinatura `SHA256withRSA`.
3. **Repositório de Chaves do Sistema Operacional:**
- Detecção do repositório `Windows-MY` (Windows CryptoAPI / CNG).
4. **Middlewares PKCS#11 e SmartCard:**
- Verificação de bibliotecas de smartcard registradas no sistema (`eps2003csp11.dll`, `aetpkss1.dll`, `opensc-pkcs11.dll`, etc.);
- Disponibilidade do subsistema PC/SC `javax.smartcardio`.
---
## 2. Auto-Teste de Assinatura Local (Self-Test)
A aba de diagnóstico disponibiliza o botão **"Executar Auto-teste de Assinatura"**:
1. Seleciona o primeiro certificado operacional disponível na máquina.
2. Gera um hash SHA-256 interno de teste: `"Documento_Teste_CyberCore_<timestamp>"`.
3. Abre o diálogo de confirmação visual para validação do usuário.
4. Executa a assinatura real JCA/JCE e afere os bytes gerados.
5. Emite relatório de sucesso com tempo decorrido e tamanho da assinatura.
+28
View File
@@ -0,0 +1,28 @@
# Catálogo Estruturado de Erros
O **CyberCore Signer** adota um sistema unificado e determinístico de códigos de erro (`CcspErrorCode`) compartilhados entre o agente Java desktop, o protocolo CCSP/1 e o SDK Web em JavaScript.
---
## 1. Tabela de Erros Estruturados
| Código de Erro | Descrição Detalhada | Ação Recomendada para a Aplicação Web |
|---|---|---|
| `CERTIFICATE_NOT_FOUND` | O certificado indicado pelo `certificate_hint` não foi localizado. | Solicite ao usuário selecionar novamente o certificado via `getCertificates()`. |
| `CERTIFICATE_EXPIRED` | O certificado selecionado expirou. | Oriente o usuário a renovar o certificado digital junto à autoridade certificadora. |
| `CERTIFICATE_INVALID` | O certificado possui estrutura corrompida ou ainda não é válido. | Verificar a data e integridade do arquivo/dispositivo. |
| `PRIVATE_KEY_NOT_AVAILABLE` | O certificado está presente mas a chave privada não está acessível. | Em tokens A3, verifique se o middleware/driver está instalado corretamente. |
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. | Solicite ao usuário conectar o token USB ou inserir o smartcard no leitor. |
| `TOKEN_REMOVED` | O token USB ou cartão foi removido fisicamente durante a assinatura. | Peça ao usuário reconectar o token e reiniciar a operação de assinatura. |
| `PIN_REQUIRED` | O token exige PIN para liberar a chave privada e nenhum PIN foi fornecido. | Exibir campo de solicitação de PIN na tela. |
| `PIN_INCORRECT` | O PIN digitado está incorreto. | Informar o usuário e alertar sobre o risco de bloqueio de PUK após 3 tentativas. |
| `PKCS11_ERROR` | Falha no driver ou biblioteca PKCS#11 do fabricante. | Verificar a instalação do SafeSign, SafeNet ou OpenSC. |
| `SIGNATURE_FAILED` | Erro durante a geração dos bytes da assinatura digital. | Tentar novamente ou reiniciar o agente desktop. |
| `APPLICATION_NOT_AUTHORIZED` | A aplicação web teve autorização recusada pelo usuário. | Informar o usuário que é necessário aceitar a notificação na tela do agente. |
| `REQUEST_EXPIRED` | O timestamp ou `expires_at` da requisição expirou. | Gerar nova requisição de assinatura com timestamp atualizado. |
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | Sempre gerar um UUID único para cada chamada de assinatura. |
| `INVALID_ORIGIN` | O cabeçalho `Origin` não confere com o cadastro da aplicação. | Conectar-se com a URL e protocolo autorizados. |
| `UNSUPPORTED_ALGORITHM` | Algoritmo solicitado não é suportado pelo agente ou certificado. | Utilizar algoritmo padrão homologado (`SHA256withRSA`). |
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela do CyberCore. | Tratar como cancelamento voluntário sem mensagem de erro alarmante. |
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário não está ativo no sistema operacional. | Verificar a integridade do Windows Certificate Store ou Java Runtime. |
| `COMMUNICATION_ERROR` | Erro interno de socket ou timeout de rede local. | Verificar se a porta `21325` não está bloqueada por antivírus local. |
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

+43
View File
@@ -0,0 +1,43 @@
# Instalação e Empacotamento Nativo
O **CyberCore Signer** foi projetado para distribuição sem exigir que o usuário final tenha o Java ou JRE instalado em seu computador.
---
## 1. Requisitos de Sistema
- **Sistema Operacional Homologado:** Windows 10 (64-bit) ou Windows 11 (64-bit).
- **Compatibilidade Adicional:** Linux x86_64 e macOS (arquitetura preparada).
- **Hardware:** Mínimo de 4 GB RAM, 150 MB de espaço em disco.
- **Porta de Rede Local:** A porta TCP `21325` deve estar livre em `127.0.0.1`.
---
## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage`
O empacotamento utiliza a ferramenta nativa `jpackage` incluída no JDK 21/25.
### No Windows:
Execute o script em lote localizado na raiz:
```cmd
scripts\package-windows.bat
```
### O que o script realiza:
1. Compila o projeto em modo release:
```cmd
mvnw clean package -DskipTests
```
2. Invoca o `jpackage` criando o instalador Windows:
- Extrai um runtime mínimo customizado contendo apenas os módulos necessários (`java.base`, `java.desktop`, `java.smartcardio`, `jdk.crypto.cryptoki`, etc.).
- Cria atalhos na Área de Trabalho e no Menu Iniciar.
- Gera um instalador padrão MSI ou EXE em `dist/`.
---
## 3. Instalação e Inicialização com o Sistema
1. Execute o instalador `CyberCore-Signer-0.1.0.msi`.
2. Siga as instruções do assistente de instalação.
3. Ao concluir, o ícone do **CyberCore Signer** aparecerá na Área de Notificação (System Tray) ao lado do relógio do Windows.
4. O serviço local `http://127.0.0.1:21325` estará imediatamente disponível para atender as aplicações web autorizadas.
+25
View File
@@ -0,0 +1,25 @@
# Limitações Técnicas e Dependências Declaradas
Conforme estabelecido na **Regra 23** do projeto (*"Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados. Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência"*), as seguintes limitações são formalmente declaradas:
---
## 1. Dependências de Sistema Operacional e Hardware
1. **Windows Certificate Store (Windows-MY):**
- Disponível apenas em sistemas operacionais Microsoft Windows (Windows 10/11).
- Em Linux e macOS, o provedor `WindowsCertificateStoreProvider` declara-se automaticamente indisponível (`isAvailable() == false`), operando via arquivos A1 ou PKCS#11 direto.
2. **Tokens A3 e Middlewares Proprietários:**
- O reconhecimento de smartcards e tokens USB depende da instalação prévia dos drivers do fabricante do dispositivo físico (ex: SafeSign Identity Client, SafeNet Authentication Client, drivers de leitoras Gemalto/Omnikey).
- O CyberCore Signer não instala drivers de hardware nem substitui o middleware do fabricante.
3. **Bloqueio de Cartão por Tentativas de PIN:**
- A política de bloqueio por tentativas consecutivas de PIN incorreto (geralmente 3 tentativas) é governada estritamente pelo chip físico do dispositivo A3 e pelo driver do fabricante, estando fora do controle do software do agente.
---
## 2. Limitações de Rede e Concorrência
1. **Escuta Exclusiva em Loopback:**
- O agente não oferece nem oferece suporte para escuta em endereços de rede externa (IPs locais de LAN ou WAN). Essa restrição é um elemento fundamental do modelo de segurança.
2. **Operações Concorrentes no Mesmo Token Físico:**
- Drivers PKCS#11 e smartcards ICP-Brasil frequentemente operam em modo mono-thread ou canal serial exclusivo. Por essa razão, o CyberCore Signer serializa as assinaturas em fila FIFO (`signLock`) para impedir corrupção de sessão no chip do cartão.
+92
View File
@@ -0,0 +1,92 @@
![alt text](image.png)# Assinaturas PDF (PAdES) e Selos Visuais em SVG
Este documento descreve a arquitetura e o funcionamento da assinatura digital de arquivos PDF no padrão **PAdES (PDF Advanced Electronic Signatures)** no CyberCore Signer, incluindo o subsistema de **selos visuais temáticos baseados em SVG**.
---
## 1. Visão Geral da Arquitetura
O CyberCore Signer implementa assinatura PAdES em dois modos complementares:
1. **Modo Desacoplado via Protocolo CCSP/1 (Web):** A aplicação web prepara a estrutura `/ByteRange`, envia apenas o digest SHA-256 e o CyberCore assina localmente com o certificado A1/A3 sem expor a chave privada.
2. **Modo Desktop Completo (Desktop UI):** O usuário seleciona um arquivo PDF localmente, escolhe o certificado ICP-Brasil, customiza o selo visual e o `PdfSigningService` gera o PDF assinado completo com envelope CMS/PKCS#7 e estampa gráfica.
```mermaid
graph TD
A[Arquivo PDF Original] --> B[PdfSigningService]
C[Tema de Selo SVG] --> D[PdfSealRenderer Java2D]
E[Metadados Signatário] --> D
D -->|Estampa Visual 300 DPI| B
F[Chave Privada A1/A3] --> G[Bouncy Castle CMS Generator]
B -->|PAdES Incremental| H[PDF Assinado Digitalmente]
G --> H
```
---
## 2. Subsistema de Selos Visuais em SVG e Temas
O selo visual é uma representação gráfica da assinatura estipulada em conformidade com as recomendações do ITI e MP nº 2.200-2/2001.
### 2.1 Temas Pré-configurados e Ícones Vetoriais (`PdfSealThemeRegistry` & `PdfSealRenderer`)
Os brasões e ícones padrão do sistema ficam centralizados em:
- **Catálogo de Temas:** `modules/cybercore-core/.../pdf/seal/PdfSealThemeRegistry.java` (definição de cores, dimensões, textos padrão e tipo de ícone).
- **Desenho Vetorial Java2D:** `modules/cybercore-core/.../pdf/seal/PdfSealRenderer.drawThemeIcon()` (renderização matemática em Java2D dos brasões padrão: `shield`, `star`, `crystal`, `notary`, etc.).
| ID do Tema | Nome | Descrição | Ícone Vetorial |
| :--- | :--- | :--- | :--- |
| `icp-brasil` | **ICP-Brasil Oficial** | Padrão clássico com brasão e cadeado verde/azul | Escudo e cadeado |
| `cybercore-modern` | **CyberCore Modern** | Estilo tecnológico neon com cristal multifacetado | Cristal vetorial |
| `minimal-clean` | **Minimalista Clean** | Monocromático para relatórios executivos sóbrios | Carimbo notarial com check |
| `gov-br` | **Gov.br Oficial** | Identidade visual Verde-Oliva conforme MP 2.200-2/2001 | Brasão estelar estilizado |
### 2.2 Renderização Gráfica (`PdfSealRenderer`)
- O núcleo `cybercore-core` **não depende de JavaFX**, atendendo às regras estritas de arquitetura (`ArchitectureTest`).
- A renderização é efetuada através da API **Java2D (AWT)** nativa com anti-aliasing duplo (`RenderingHints.VALUE_ANTIALIAS_ON`, subpixel text, escala 2x / 300 DPI).
- Exporta tanto `BufferedImage` quanto `byte[]` PNG para inclusão no PDFBox ou pré-visualização dinâmica na interface com `ImageView`.
### 2.3 Criação, Instalação e Privacidade de Selos Personalizados
- **Foco Exclusivo no Titular (Sem Emissor/AC no Gráfico):** O selo foi desenhado para ser limpo e elegante. Dados técnicos do certificado e o emissor da Autoridade Certificadora não são exibidos no gráfico do selo, evitando poluição visual.
- **Obrigatoriedade do Nome Completo:** O renderizador calcula dinamicamente a métrica de fontes via `drawFittedText()` para garantir que o nome completo do signatário seja sempre exibido na íntegra, com destaque tipográfico (13.5pt a 14.5pt em negrito), sem cortes ou truncamentos.
- **Upload de Logotipo / Emblema Próprio:** Suporte a upload de imagens de logotipos (PNG, JPG, BMP) armazenadas de forma portável em Base64 dentro do tema, com renderização redimensionada e anti-aliasing de 300 DPI.
- **Campos Customizáveis (Matrícula, Cargo, OAB, etc.):** Permite definir rótulo e valor padrão no tema (ex: `Matrícula: 104829/SP` ou `Cargo: Auditor Fiscal`) ou informar pontualmente durante a assinatura via `customExtraInfo`. A informação é posicionada de forma harmônica na linha de identificação do signatário.
- **Privacidade do CPF (LGPD):** O modelo `CpfDisplayMode` suporta três modalidades:
- `FULL`: exibe o documento completo (`CPF: 123.456.789-00`).
- `MASKED`: mascara dígitos sensíveis (`CPF: ***.456.789-**`).
- `NONE`: oculta totalmente o CPF do gráfico do selo.
- **Data e Hora Opcional:** Parâmetro booleano para incluir ou suprimir data/hora no gráfico.
- **Instalação e Persistência Local:** Através de `CustomSealStorage`, novos selos criados ou importados são salvos em `~/.cybercore/seals/*.json` e registrados dinamicamente no `PdfSealThemeRegistry`.
### 2.4 Gerenciamento Centralizado de Selos (`SealManagementDialog`)
- **Lista Completa:** Exibição unificada de selos embutidos do sistema e selos personalizados instalados pelo usuário.
- **Selo Padrão Ativo:** Permite definir qual selo é o padrão ativo para todas as assinaturas do usuário, gravado em `AgentConfiguration` (`defaultSealThemeId`).
- **Pré-visualização em Tempo Real:** Renderização fiel do selo com dados de exemplo ao navegar pela lista.
- **Clonagem e Edição:** Permite duplicar qualquer selo (inclusive os embutidos) e abri-lo no `SealDesignerDialog` para customização.
- **Importação e Exportação:** Exportação de temas em arquivos `.json` e importação com um clique para compartilhamento entre máquinas e equipes.
---
## 3. Posicionamento e Pré-Visualização Interativa
A posição e a página da estampa são parametrizáveis via `PdfSignatureOptions`:
- **Posições Automáticas:** `BOTTOM_RIGHT`, `BOTTOM_LEFT`, `TOP_RIGHT`, `TOP_LEFT`.
- **Posição Interativa Personalizada (`CUSTOM`):** O usuário clica ou arrasta o selo diretamente sobre a página do PDF na interface gráfica. As coordenadas na tela são matematicamente mapeadas para as coordenadas de mídia do PDFBox:
$$X_{pdf} = \frac{X_{clique}}{Largura_{view}} \times Largura_{pdf}$$
$$Y_{pdf} = Altura_{pdf} - \left(\frac{Y_{clique}}{Altura_{view}} \times Altura_{pdf}\right) - Altura_{selo}$$
- **Páginas:** `LAST_PAGE` (recomendado), `FIRST_PAGE`, `ALL_PAGES`, ou página específica numérica (`targetPageNumber`).
- **Dimensões padrão:** 200 pt de largura por 55 pt de altura, com margens de segurança e clamping contra extravasamento.
- **Renderização da Folha:** O `PdfSigningService` fornece `renderPagePngBytes(pdfBytes, pageIndex, dpi)` via PDFBox `PDFRenderer`, permitindo que o diálogo da UI JavaFX renderize a folha de papel em background thread com paginação e sobreposição visual do selo.
---
## 4. Estrutura Criptográfica PAdES
- **Motor:** Apache PDFBox 3.0.4 + Bouncy Castle 1.80 (`bcpkix-jdk18on`).
- **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`.
- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256.
- **Salva Incremental:** Preserva a integridade de assinaturas anteriores e do documento original sem quebrar certificações prévias.
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+117
View File
@@ -0,0 +1,117 @@
# SDK Web JavaScript (@cybercore/signer-sdk)
O `@cybercore/signer-sdk` é a biblioteca oficial cliente em JavaScript para integrar aplicações web modernas com o agente desktop **CyberCore Signer** via protocolo **CCSP/1**.
---
## 1. Instalação e Importação
### Via Módulo ES6 (Browser ou Bundler Vite/Webpack/Next.js)
```javascript
import { CyberCoreSigner } from '@cybercore/signer-sdk';
const signer = new CyberCoreSigner({
applicationName: 'Portal Meu Negócio'
});
```
---
## 2. Métodos da API
### `isInstalled(timeoutMs = 2000): Promise<boolean>`
Testa rapidamente se o agente desktop está ativo na porta local `21325`.
### `connect(): Promise<boolean>`
Realiza o handshake inicial de autorização. Se for a primeira vez que a aplicação tenta se conectar, abre uma caixa de diálogo nativa no desktop do usuário para concessão de acesso.
### `getCertificates(): Promise<Array<Object>>`
Retorna todos os certificados A1 e A3 disponíveis e operacionais na máquina do usuário.
```javascript
const certs = await signer.getCertificates();
certs.forEach(cert => {
console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`);
});
```
### `sign(options): Promise<Object>`
Solicita a assinatura digital local de um digest ou conteúdo bruto.
### `signPdf(pdfFile, options): Promise<Object>`
Assina um documento PDF (PAdES / Assinatura desacoplada). Recebe `File`, `Blob`, `ArrayBuffer` ou `Uint8Array`.
Calcula o SHA-256 no próprio navegador e transmite metadados do documento (`PDF`) ao agente para exibição clara na janela de confirmação.
```javascript
const pdfResult = await signer.signPdf(fileInput.files[0], {
certificateId: certs[0].id
});
```
### `signXml(xmlContent, options): Promise<Object>`
Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`.
```javascript
const xmlResult = await signer.signXml(xmlString, {
certificateId: certs[0].id,
documentName: 'nfe-3524.xml'
});
```
### `signFile(file, options): Promise<Object>`
Assina qualquer arquivo (ZIP, DOCX, imagens, binários, executáveis) gerando pacote de assinatura detached / CAdES.
```javascript
const fileResult = await signer.signFile(fileInput.files[0], {
certificateId: certs[0].id
});
```
### `authenticate(options): Promise<Object>` (ou `login(options)`)
Executa login seguro com certificado digital via desafio criptográfico (Challenge-Response).
```javascript
const auth = await signer.authenticate({
certificateId: certs[0].id,
challenge: 'server-nonce-12345'
});
console.log('Bem-vindo,', auth.subjectName);
console.log('CPF/CNPJ:', auth.subjectCpfCnpj);
console.log('Assinatura do Desafio:', auth.signature);
```
### `extractSubjectInfo(certificateBase64): { name, cpfCnpj }`
Decodifica o certificado X.509 e extrai os dados do titular ICP-Brasil (Nome, CPF ou CNPJ).
### `verify(options): Promise<boolean>`
Validação sintática e criptográfica da integridade da assinatura gerada.
---
## 3. Tratamento Estruturado de Erros
O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`:
```javascript
import {
UserCancelledError,
CertificateNotFoundError,
PinIncorrectError,
TokenNotFoundError
} from '@cybercore/signer-sdk/errors';
try {
await signer.sign({ certificateId: 'cert-1', data: 'Contrato' });
} catch (err) {
if (err instanceof UserCancelledError) {
alert('Você cancelou a assinatura no CyberCore Signer.');
} else if (err instanceof PinIncorrectError) {
alert('O PIN do token informado está incorreto.');
} else if (err instanceof TokenNotFoundError) {
alert('Conecte o seu token USB na máquina para assinar.');
} else {
console.error('Erro na assinatura:', err);
}
}
```
+46
View File
@@ -0,0 +1,46 @@
# Modelo de Segurança e Privacidade
O **CyberCore Signer** foi projetado seguindo as diretrizes rigorosas da ICP-Brasil e os princípios de *Security by Design* e *Least Privilege*.
---
## 1. Isolamento de Rede
1. **Loopback Estrito:** O socket do servidor HTTP local vincula-se exclusivamente ao endereço IP `127.0.0.1`. Nenhuma interface de rede externa (LAN ou WAN) escuta a porta de serviço `21325`.
2. **Prevenção contra Exposição Acidental:** Testes unitários e de arquitetura garantem que conexões fora de `127.0.0.1` sejam bloqueadas imediatamente no nível de bind do socket.
---
## 2. Autenticação e Autorização de Aplicações
Nenhuma aplicação desconhecida tem autorização para obter certificados ou assinar documentos sem consentimento expresso:
1. **Handshake de Autorização (`POST /ccsp/v1/authorize`):**
- A aplicação web identifica-se enviando seu `application_id` (UUID estável) e seu nome amigável.
- O CyberCore Signer suspende a requisição e abre uma janela modal nativa na tela principal do sistema operacional perguntando se o usuário autoriza aquele domínio web.
2. **Armazenamento Seguro das Autorizações:**
- As aplicações autorizadas são gravadas em `~/.cybercore/authorized-apps.json`.
- O usuário pode visualizar e **revogar a qualquer momento** qualquer permissão através da aba **Configurações** na interface gráfica.
---
## 3. Proteção Contra Ataques de Replay
Cada solicitação de assinatura enviada pelo SDK deve incluir:
- Um `nonce` (UUID criptograficamente aleatório);
- Um campo `expires_at` (ou `timestamp`).
O componente `NonceManager`:
- Armazena os nonces recentes em memória protegida com expiração temporal (TTL);
- Rejeita com código estruturado `REPLAY_DETECTED` caso o mesmo nonce seja submetido mais de uma vez;
- Rejeita com código `REQUEST_EXPIRED` requisições cuja data/hora de validade já tenha sido ultrapassada.
---
## 4. Confirmação Visual Obrigatória e Proteção de PIN/Chaves
1. **Confirmação Manual do Usuário:** Toda e qualquer assinatura exige intervenção física e visual do usuário. Não é possível assinar "silenciosamente" por chamadas de API em segundo plano.
2. **Inviolabilidade da Chave Privada:** A chave privada nunca é exportada nem manipulada em forma de bytes puros. Todas as operações utilizam handles JCA/JCE ou comandos de hardware nativo.
3. **Proteção de PIN e Senhas:**
- O PIN de smartcards e a senha de arquivos A1 nunca são transmitidos para a aplicação web e nunca aparecem em logs de auditoria.
- Em caso de remoção do hardware, o `SmartCardWatcher` limpa e zera os buffers de memória de PIN imediatamente.
+82
View File
@@ -0,0 +1,82 @@
# Layout CyberCore Signer — Design Dual-Theme & Assinatura em Lote
O **CyberCore Signer** adota uma interface gráfica que une a ergonomia de hub consolidada no mercado nacional com uma identidade visual moderna, autoral e com suporte dinâmico a **Tema Claro (Light)** e **Tema Escuro (Dark)**.
---
## 1. Identidade Visual e Filosofia de Design
Em conformidade com os requisitos do projeto, a interface **não é uma cópia datada**, mas sim uma evolução:
1. **Identidade CyberCore:** Logotipo central em cristal tridimensional e escudo criptográfico vetorial (`SvgIcons`), subtítulo "Agente Criptográfico Local • ICP-Brasil" e indicador de segurança em tempo real.
2. **Badge de Status Ativo:** Exibe o status da porta local (`● Agente Ativo • 127.0.0.1:21325`) com verde esmeralda no modo claro e verde neon no modo escuro.
3. **Alternador Dinâmico de Temas:** Botão pílula localizado no cabeçalho superior que permite ao usuário alternar instantaneamente entre **Modo Claro** e **Modo Escuro**:
- Ícone do Sol ☀️ para comutar para o tema Claro.
- Ícone da Lua 🌙 para comutar para o tema Escuro.
- Classe gerenciadora centralizada: [`ThemeManager.java`](file:///var/www/cybercore/cybercore-signer/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java), que replica a escolha de tema para todos os diálogos secundários.
---
## 2. Paleta de Cores dos Temas
| Componente | Tema Claro (Light) | Tema Escuro (Dark) |
|---|---|---|
| **Fundo da Janela** | `#f1f5f9` (Slate suave) | `#0b0f17` (Dark profundo) |
| **Header Superior** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` |
| **Cartões de Ação** | `#ffffff`, borda `#e2e8f0`, hover `#0284c7` | `#131c2e`, borda `#1e293b`, hover `#38bdf8` |
| **Texto dos Botões** | `#1e293b` (Cinza escuro de alto contraste) | `#f8fafc` (Branco gelo nítido) |
| **Painel Lateral** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` |
| **Badge de Status** | Fundo `#ecfdf5`, texto `#047857`, borda `#10b981` | Fundo `rgba(16, 185, 129, 0.15)`, texto `#34d399` |
| **Alternador de Tema** | Fundo `#ffffff`, borda `#cbd5e1`, hover `#0284c7` | Fundo `#1e293b`, borda `#334155`, hover `#38bdf8` |
---
## 3. Grade Central de Ações (Hub 4x3)
A interface principal organiza as operações em 12 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos:
| 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. |
| (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. |
| (3, 0) | **Assinar em Lote** | Processamento simultâneo de múltiplos arquivos com barra de progresso em tempo real. |
| (0, 1) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). |
| (1, 1) | **Co-Assinar Documento** | Adição de nova assinatura a um documento previamente assinado por outro titular. |
| (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. |
---
## 4. Painel Lateral de Utilitários
Na coluna lateral direita, o usuário dispõe de atalhos rápidos para operações do sistema:
1. **`</> Logs`:** Visualizador de auditoria em tempo real, monitorando requisições locais CCSP/1 e eventos criptográficos.
2. **`⚙ Configurações`:** Gerenciador de origens web autorizadas, revogação de acessos e opções de PIN.
3. **`💻 Tutorial`:** Guia passo a passo interativo de uso do assinador e integração web.
4. **`🗂 Certificados`:** Lista de certificados ICP-Brasil instalados (Windows-MY, A1 e A3) e ferramenta para importação de certificados A1 (`.pfx` / `.p12`).
5. **`ℹ Sobre`:** Informações de versão, engine JCA/JCE e garantias de privacidade local.
---
## 5. Assinatura Digital em Lote (`BatchSigningDialog`)
A funcionalidade de assinatura em lote permite:
- Seleção de múltiplos arquivos simultâneos via diálogo de arquivos ou pastas.
- Visualização de tabela com nome, tamanho e status de cada item em tempo real.
- Seleção do certificado digital ICP-Brasil desejado (com exibição de titular, CPF/CNPJ, tipo A1/A3 e validade).
- Escolha da pasta de destino para os arquivos assinados (salvando arquivos `.p7s` correspondentes).
- Execução assíncrona em thread isolada para manter a interface responsiva durante todo o lote.
- Compatibilidade automática com o tema ativo (Claro ou Escuro).
---
## 6. Ícones Vetoriais SVG Nativos (`SvgIcons`)
Todos os ícones e o logotipo central em cristal foram construídos via `SVGPath` e primitivas vetoriais JavaFX nativas:
- **Resolução Infinita:** Perfeita nitidez em monitores Full HD, 2K, 4K e configurações de escala de DPI do Windows (100%, 125%, 150%, 200%).
- **Sem Dependência de Imagens Rasterizadas:** Sem risco de imagens ausentes ou corrompidas no runtime empacotado.
+56
View File
@@ -0,0 +1,56 @@
# Assinaturas XML (XMLDSig e ICP-Brasil)
Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer.
---
## 1. Visão Geral
O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`).
### 1.1 Casos de Uso Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML.
- **Assinatura de Elementos Específicos por ID:**
- **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">`
- **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">`
- **MDF-e (Manifesto Eletrônico de Documentos Fiscais):** Assinatura da tag `<infMDFe Id="MDFe...">`
- **eSocial / EFD-Reinf:** Assinatura de eventos com tags `<evt... Id="...">`
---
## 2. Padrões Criptográficos
- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários).
- **Transformações:**
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest).
- C14N padrão.
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`).
- **Algoritmo de Assinatura:** RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`).
- **KeyInfo:** Informações do certificado X.509 embutidas em `<X509Data><X509Certificate>`.
---
## 3. Segurança e Mitigação contra XXE
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
```java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
```
---
## 4. Validação Pós-Assinatura
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando:
1. A integridade criptográfica da assinatura (`SignedInfo`).
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`).
3. O certificado X.509 público extraído da chave.
+162
View File
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.com.cybercore.signer</groupId>
<artifactId>cybercore-signer-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>cybercore-app</artifactId>
<packaging>jar</packaging>
<name>CyberCore Signer :: Application</name>
<description>Módulo executável desktop, ciclo de vida da aplicação, shutdown hooks e empacotamento nativo.</description>
<dependencies>
<dependency>
<groupId>br.com.cybercore.signer</groupId>
<artifactId>cybercore-core</artifactId>
</dependency>
<dependency>
<groupId>br.com.cybercore.signer</groupId>
<artifactId>cybercore-ui</artifactId>
</dependency>
<!-- JavaFX Neutro -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
</dependency>
<!-- JavaFX Nativos Windows (para execução direta em ambiente Windows) -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
<classifier>win</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
<classifier>win</classifier>
</dependency>
<!-- JavaFX Nativos Linux (para execução direta em ambiente Linux) -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
<classifier>linux</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
<classifier>linux</classifier>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
<classifier>linux</classifier>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<!-- Testes de Arquitetura -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<configuration>
<mainClass>br.com.cybercore.signer.app.CyberCoreSignerApplication</mainClass>
</configuration>
</plugin>
<!-- Geração de Fat JAR executável autocontido -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>cybercore-signer</finalName>
<shadedArtifactAttached>false</shadedArtifactAttached>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>br.com.cybercore.signer.app.CyberCoreSignerApplication</mainClass>
<manifestEntries>
<Implementation-Title>CyberCore Signer</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-Vendor>CyberCore</Implementation-Vendor>
</manifestEntries>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>module-info.class</exclude>
<exclude>META-INF/versions/**/module-info.class</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,144 @@
package br.com.cybercore.signer.app;
import br.com.cybercore.signer.core.diagnostics.DiagnosticReport;
import br.com.cybercore.signer.core.diagnostics.DiagnosticService;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.protocol.CcspServer;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.ui.CyberCoreDesktopApplication;
import javafx.application.Application;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.GraphicsEnvironment;
/**
* Ponto de entrada (Main Class) e bootstrap do agente CyberCore Signer.
*/
public class CyberCoreSignerApplication {
private static final Logger logger = LoggerFactory.getLogger(CyberCoreSignerApplication.class);
private static boolean developmentMode = false;
private static boolean headlessMode = false;
private static boolean noWaitMode = false;
private static volatile CcspServer runningServer = null;
public static CcspServer getRunningServer() {
return runningServer;
}
public static void stopServer() {
if (runningServer != null) {
runningServer.stop();
runningServer = null;
}
}
public static void main(String[] args) {
parseArguments(args);
printBanner();
logger.info("Inicializando CyberCore Signer v0.1.0...");
if (developmentMode) {
logger.warn("=========================================================");
logger.warn(" MODO DE DESENVOLVIMENTO ATIVADO (--dev)");
logger.warn(" Logs detalhados ativos. Operação com certificados reais.");
logger.warn("=========================================================");
}
DiagnosticService diagnosticService = new DiagnosticService();
DiagnosticReport report = diagnosticService.runDiagnostic();
logger.info("Ambiente verificado: OS={} {} ({}), Java={} ({})",
report.osName(), report.osVersion(), report.osArch(),
report.javaVersion(), report.javaVendor());
if (!report.cryptoEngineOk()) {
logger.error("ALERTA CRÍTICO: Engine criptográfica JCA/JCE não está totalmente operacional!");
}
// Se o ambiente tiver suporte gráfico e não for headless forçado, abre a interface JavaFX
boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless();
if (isHeadless) {
logger.info("Executando em modo console / headless (sem interface gráfica).");
startHeadlessServer();
} else {
try {
logger.info("Iniciando interface gráfica desktop...");
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
Application.launch(CyberCoreDesktopApplication.class, args);
} catch (Throwable t) {
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
startHeadlessServer();
}
}
logger.info("CyberCore Signer finalizado.");
}
private static void startHeadlessServer() {
try {
CertificateManager certManager = CertificateManager.createDefault();
if (developmentMode) {
logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema).");
}
SigningService signingService = new SigningService(certManager);
runningServer = new CcspServer(certManager, signingService, null);
runningServer.start();
logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort());
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Encerrando servidor local CCSP/1...");
stopServer();
}));
// Mantém processo vivo para atender requisições web, a menos que em modo no-wait
if (!noWaitMode) {
Thread.currentThread().join();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e);
}
}
private static void parseArguments(String[] args) {
if (args != null) {
for (String arg : args) {
if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) {
developmentMode = true;
} else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg)
|| "--cli".equalsIgnoreCase(arg)) {
headlessMode = true;
} else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg)
|| "--test".equalsIgnoreCase(arg)) {
noWaitMode = true;
}
}
}
}
public static boolean isDevelopmentMode() {
return developmentMode;
}
public static boolean isHeadlessMode() {
return headlessMode;
}
private static void printBanner() {
System.out.println("""
============================================================
____ _ ____
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___|
|___/
SIGNER AGENT v0.1.0
============================================================
""");
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<logger name="br.com.cybercore.signer" level="DEBUG" />
</configuration>
@@ -0,0 +1,37 @@
package br.com.cybercore.signer.app;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
class ArchitectureTest {
private final JavaClasses importedClasses = new ClassFileImporter()
.importPackages("br.com.cybercore.signer");
@Test
@DisplayName("O módulo Core não deve depender de classes dos pacotes UI ou App")
void coreShouldNotDependOnUiOrApp() {
ArchRule rule = noClasses()
.that().resideInAPackage("br.com.cybercore.signer.core..")
.should().dependOnClassesThat()
.resideInAnyPackage("br.com.cybercore.signer.ui..", "br.com.cybercore.signer.app..");
rule.check(importedClasses);
}
@Test
@DisplayName("O módulo Core não deve conter dependências de bibliotecas JavaFX")
void coreShouldNotDependOnJavaFx() {
ArchRule rule = noClasses()
.that().resideInAPackage("br.com.cybercore.signer.core..")
.should().dependOnClassesThat()
.resideInAPackage("javafx..");
rule.check(importedClasses);
}
}
@@ -0,0 +1,20 @@
package br.com.cybercore.signer.app;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatCode;
class CyberCoreSignerApplicationTest {
@Test
@DisplayName("Deve inicializar a aplicação com argumentos sem erros")
void shouldBootstrapWithoutErrors() {
try {
assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"}))
.doesNotThrowAnyException();
} finally {
CyberCoreSignerApplication.stopServer();
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.com.cybercore.signer</groupId>
<artifactId>cybercore-signer-parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>cybercore-core</artifactId>
<packaging>jar</packaging>
<name>CyberCore Signer :: Core</name>
<description>Núcleo de domínio, abstrações criptográficas, provedores A1/A3, protocolo CCSP e servidor local.</description>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- PDF (PAdES) e Criptografia CMS/PKCS#7 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
</dependency>
<!-- Testes -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,43 @@
package br.com.cybercore.signer.core.config;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* Modelo de configurações gerais e de segurança do CyberCore Signer.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record AgentConfiguration(
boolean requireConfirmation,
boolean alwaysPromptPin,
boolean autoStart,
String defaultSealThemeId,
CpfDisplayMode defaultCpfDisplayMode,
boolean defaultIncludeDateTime,
int serverPort
) {
public AgentConfiguration {
if (defaultSealThemeId == null || defaultSealThemeId.isBlank()) {
defaultSealThemeId = "icp-brasil";
}
if (defaultCpfDisplayMode == null) {
defaultCpfDisplayMode = CpfDisplayMode.FULL;
}
if (serverPort <= 0) {
serverPort = 18420;
}
}
public static AgentConfiguration defaultConfiguration() {
return new AgentConfiguration(
true,
false,
false,
"icp-brasil",
CpfDisplayMode.FULL,
true,
18420
);
}
}
@@ -0,0 +1,94 @@
package br.com.cybercore.signer.core.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
/**
* Serviço de gerenciamento e persistência das configurações do CyberCore Signer.
* Salva e recupera as preferências do usuário no arquivo ~/.cybercore/config.json.
*/
public final class AgentConfigurationService {
private static final Logger logger = LoggerFactory.getLogger(AgentConfigurationService.class);
private static final String CONFIG_FILE_NAME = "config.json";
private static final AgentConfigurationService INSTANCE = new AgentConfigurationService();
private final Path configPath;
private final ObjectMapper objectMapper;
private AgentConfiguration currentConfig;
public static AgentConfigurationService getInstance() {
return INSTANCE;
}
public AgentConfigurationService() {
this(resolveDefaultConfigPath());
}
public AgentConfigurationService(Path configPath) {
this.configPath = Objects.requireNonNull(configPath);
this.objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
this.currentConfig = load();
}
private static Path resolveDefaultConfigPath() {
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore", CONFIG_FILE_NAME);
}
public synchronized AgentConfiguration getConfig() {
return currentConfig;
}
public synchronized AgentConfiguration loadConfiguration() {
return getConfig();
}
public synchronized void updateConfig(AgentConfiguration newConfig) {
Objects.requireNonNull(newConfig, "Configuração não pode ser nula.");
this.currentConfig = newConfig;
save();
}
public synchronized void saveConfiguration(AgentConfiguration newConfig) {
updateConfig(newConfig);
}
private AgentConfiguration load() {
try {
if (Files.exists(configPath)) {
AgentConfiguration cfg = objectMapper.readValue(configPath.toFile(), AgentConfiguration.class);
logger.info("Configurações do agente carregadas de: {}", configPath);
return cfg;
}
} catch (Exception e) {
logger.warn("Falha ao ler {}. Usando configurações padrão: {}", configPath, e.getMessage());
}
return AgentConfiguration.defaultConfiguration();
}
private void save() {
try {
if (configPath.getParent() != null && !Files.exists(configPath.getParent())) {
Files.createDirectories(configPath.getParent());
}
objectMapper.writeValue(configPath.toFile(), currentConfig);
logger.info("Configurações do agente salvas com sucesso em: {}", configPath);
} catch (IOException e) {
logger.error("Erro ao salvar configurações em {}: {}", configPath, e.getMessage(), e);
}
}
public Path getConfigPath() {
return configPath;
}
}
@@ -0,0 +1,66 @@
package br.com.cybercore.signer.core.crypto;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import br.com.cybercore.signer.core.provider.CertificateProviderException;
/**
* Algoritmos criptográficos de assinatura suportados pelo CyberCore Signer.
*/
public enum DigestAlgorithm {
SHA256_WITH_RSA("SHA256withRSA", "SHA-256", 32),
SHA384_WITH_RSA("SHA384withRSA", "SHA-384", 48),
SHA512_WITH_RSA("SHA512withRSA", "SHA-512", 64),
SHA256_WITH_ECDSA("SHA256withECDSA", "SHA-256", 32),
SHA384_WITH_ECDSA("SHA384withECDSA", "SHA-384", 48),
SHA512_WITH_ECDSA("SHA512withECDSA", "SHA-512", 64);
private final String jcaName;
private final String digestName;
private final int expectedDigestLengthBytes;
DigestAlgorithm(String jcaName, String digestName, int expectedDigestLengthBytes) {
this.jcaName = jcaName;
this.digestName = digestName;
this.expectedDigestLengthBytes = expectedDigestLengthBytes;
}
public String getJcaName() {
return jcaName;
}
public String getDigestName() {
return digestName;
}
public int getExpectedDigestLengthBytes() {
return expectedDigestLengthBytes;
}
public static DigestAlgorithm fromName(String name) throws CertificateProviderException {
if (name == null || name.isBlank()) {
throw new CertificateProviderException(CcspErrorCode.UNSUPPORTED_ALGORITHM, "Algoritmo não especificado.");
}
for (DigestAlgorithm alg : values()) {
if (alg.jcaName.equalsIgnoreCase(name.trim())) {
return alg;
}
}
throw new CertificateProviderException(
CcspErrorCode.UNSUPPORTED_ALGORITHM,
"Algoritmo não suportado pelo CyberCore Signer: " + name
);
}
public void validateDigestLength(byte[] digest) throws CertificateProviderException {
if (digest == null) {
throw new CertificateProviderException(CcspErrorCode.SIGNATURE_FAILED, "Digest não pode ser nulo.");
}
if (digest.length != expectedDigestLengthBytes) {
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
String.format("Tamanho inválido para digest %s: esperado %d bytes, recebido %d bytes.",
digestName, expectedDigestLengthBytes, digest.length)
);
}
}
}
@@ -0,0 +1,169 @@
package br.com.cybercore.signer.core.crypto;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.CertificateProvider;
import br.com.cybercore.signer.core.provider.CertificateProviderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Serviço central de assinatura digital do CyberCore Signer.
* Orquestra validações rigorosas de protocolo e algoritmos, resolução de certificados
* e delegação aos provedores criptográficos JCA/JCE (Windows-MY, A1, PKCS#11).
* Nunca implementa algoritmos criptográficos manuais e nunca expõe a chave privada.
*/
public class SigningService {
private static final Logger logger = LoggerFactory.getLogger(SigningService.class);
private final CertificateManager certificateManager;
public SigningService(CertificateManager certificateManager) {
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager must not be null");
}
/**
* Executa a assinatura digital local de um digest fornecido por uma aplicação autorizada.
*
* @param context Contexto da requisição contendo digest, algoritmo, ID da requisição e certificado indicado.
* @return Resultado imutável da assinatura com os bytes assinados e certificados em Base64.
* @throws CertificateProviderException Se qualquer validação, dispositivo ou operação criptográfica falhar.
*/
public SigningResult sign(SigningContext context) throws CertificateProviderException {
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
if (context.requestId() == null || context.requestId().isBlank()) {
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"O campo 'request_id' é obrigatório para correlação de assinatura."
);
}
// 1. Validação de algoritmo suportado e tamanho do digest
DigestAlgorithm algorithm = DigestAlgorithm.fromName(context.algorithm());
algorithm.validateDigestLength(context.digest());
// 2. Resolução do certificado alvo
ManagedCertificate targetCertificate = resolveCertificate(context.certificateHint());
// 3. Validação do ciclo de vida e disponibilidade de chave privada
validateCertificateState(targetCertificate);
// 4. Obtenção do provedor responsável
CertificateProvider provider = certificateManager.getProviderForCertificate(targetCertificate.id());
if (provider == null || !provider.isAvailable()) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"O provedor para o certificado '" + targetCertificate.alias() + "' não está disponível no sistema."
);
}
logger.info("Assinando digest para requestId='{}', appId='{}', cert='{}', provider='{}', alg='{}'",
context.requestId(), context.applicationId(), targetCertificate.alias(),
provider.getProviderId(), algorithm.getJcaName());
// Se o contexto não tiver hint explícito, utiliza o ID do certificado resolvido
SigningContext effectiveContext = (context.certificateHint() == null || context.certificateHint().isBlank())
? new SigningContext(context.requestId(), context.applicationId(), context.digest(),
context.algorithm(), targetCertificate.id(), context.requestedAt())
: context;
// 5. Execução no provedor JCA/JCE
SigningResult result = provider.sign(effectiveContext);
if (result == null || result.signatureBytes() == null || result.signatureBytes().length == 0) {
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"O provedor retornou uma assinatura nula ou vazia."
);
}
logger.info("Assinatura digital concluída com sucesso para requestId='{}'. Tamanho assinatura: {} bytes",
context.requestId(), result.signatureBytes().length);
return result;
}
private ManagedCertificate resolveCertificate(String hint) throws CertificateProviderException {
if (hint != null && !hint.isBlank()) {
Optional<ManagedCertificate> found = certificateManager.findByHint(hint);
if (found.isEmpty()) {
// Tenta rediscovery caso um token tenha acabado de ser conectado
certificateManager.discoverCertificates();
found = certificateManager.findByHint(hint);
}
if (found.isPresent()) {
return found.get();
}
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Nenhum certificado encontrado para o identificador ou hint: " + hint
);
}
// Sem hint: seleciona automaticamente se houver apenas um certificado operacional disponível
List<ManagedCertificate> available = certificateManager.getAvailableCertificates();
if (available.isEmpty()) {
// Tenta redescobrir
certificateManager.discoverCertificates();
available = certificateManager.getAvailableCertificates();
}
if (available.isEmpty()) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Nenhum certificado com chave privada disponível foi encontrado na máquina."
);
}
if (available.size() == 1) {
return available.get(0);
}
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Existem múltiplos certificados disponíveis (" + available.size() +
"). Especifique o 'certificate_hint' para selecionar o certificado desejado."
);
}
private void validateCertificateState(ManagedCertificate cert) throws CertificateProviderException {
if (cert.status() == CertificateStatus.EXPIRED) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_EXPIRED,
"O certificado '" + cert.alias() + "' está expirado desde " + cert.notAfter()
);
}
if (cert.status() == CertificateStatus.NOT_YET_VALID) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_INVALID,
"O certificado '" + cert.alias() + "' ainda não é válido. Início da validade: " + cert.notBefore()
);
}
if (cert.status() == CertificateStatus.NO_PRIVATE_KEY) {
throw new CertificateProviderException(
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
"O certificado '" + cert.alias() + "' não possui chave privada acessível para assinatura."
);
}
if (cert.status() == CertificateStatus.REVOKED) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_INVALID,
"O certificado '" + cert.alias() + "' consta como revogado."
);
}
}
public CertificateManager getCertificateManager() {
return certificateManager;
}
}
@@ -0,0 +1,230 @@
package br.com.cybercore.signer.core.crypto;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Collection;
import java.util.HexFormat;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utilitários para parsing, inspeção e extração de metadados de certificados X.509
* com suporte nativo a extensões da Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil).
*/
public final class X509CertificateUtils {
private static final Logger logger = LoggerFactory.getLogger(X509CertificateUtils.class);
private static final Pattern CN_PATTERN = Pattern.compile("CN=([^,]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern CPF_PATTERN = Pattern.compile("(\\d{11})");
private static final Pattern CNPJ_PATTERN = Pattern.compile("(\\d{14})");
private X509CertificateUtils() {
// Utilitário estático
}
/**
* Decodifica um certificado X.509 a partir de bytes codificados (DER ou PEM).
*/
public static X509Certificate parseCertificate(byte[] encoded) throws CertificateException {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(encoded));
}
/**
* Converte um X509Certificate em um ManagedCertificate imutável de domínio.
*/
public static ManagedCertificate toManagedCertificate(
String id,
String alias,
X509Certificate cert,
CertificateType type,
boolean hasPrivateKey,
String providerId,
String hardwareDeviceName
) {
String subject = cert.getSubjectX500Principal().getName();
String issuer = cert.getIssuerX500Principal().getName();
String serial = cert.getSerialNumber().toString(16).toUpperCase();
Instant notBefore = cert.getNotBefore().toInstant();
Instant notAfter = cert.getNotAfter().toInstant();
Instant now = Instant.now();
CertificateStatus status;
if (!hasPrivateKey) {
status = CertificateStatus.NO_PRIVATE_KEY;
} else if (now.isBefore(notBefore)) {
status = CertificateStatus.NOT_YET_VALID;
} else if (now.isAfter(notAfter)) {
status = CertificateStatus.EXPIRED;
} else {
status = CertificateStatus.AVAILABLE;
}
String cn = extractCommonName(subject);
String sha1 = calculateFingerprint(cert, "SHA-1");
String sha256 = calculateFingerprint(cert, "SHA-256");
String cpf = extractCpf(cert, cn);
String cnpj = extractCnpj(cert, cn);
int keySize = extractKeySize(cert);
String alg = cert.getPublicKey().getAlgorithm();
return new ManagedCertificate(
id,
alias,
type,
status,
subject,
issuer,
serial,
sha1,
sha256,
notBefore,
notAfter,
alg,
keySize,
cn,
cpf,
cnpj,
providerId,
hardwareDeviceName
);
}
public static String extractCommonName(String dn) {
if (dn == null) return "Desconhecido";
Matcher matcher = CN_PATTERN.matcher(dn);
if (matcher.find()) {
return matcher.group(1).trim();
}
return dn;
}
public static String calculateFingerprint(X509Certificate cert, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(cert.getEncoded());
return HexFormat.ofDelimiter(":").formatHex(digest).toUpperCase();
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
logger.debug("Não foi possível calcular fingerprint {} do certificado: {}", algorithm, e.getMessage());
return "";
}
}
private static int extractKeySize(X509Certificate cert) {
try {
String alg = cert.getPublicKey().getAlgorithm();
if ("RSA".equalsIgnoreCase(alg)) {
java.security.interfaces.RSAPublicKey rsa = (java.security.interfaces.RSAPublicKey) cert.getPublicKey();
return rsa.getModulus().bitLength();
} else if ("EC".equalsIgnoreCase(alg) || "ECDSA".equalsIgnoreCase(alg)) {
java.security.interfaces.ECPublicKey ec = (java.security.interfaces.ECPublicKey) cert.getPublicKey();
return ec.getParams().getCurve().getField().getFieldSize();
}
} catch (Exception ignored) {
// Se falhar em extrair tamanho específico da chave, retorna 0
}
return 0;
}
/**
* Tenta extrair CPF do certificado (via ICP-Brasil SAN ou padrão comum no CN: "NOME:12345678901").
*/
public static String extractCpf(X509Certificate cert, String commonName) {
// Tenta padrão CN ICP-Brasil comum ("FULANO DE TAL:00011122233")
if (commonName != null && commonName.contains(":")) {
String[] parts = commonName.split(":");
if (parts.length > 1) {
String candidate = parts[parts.length - 1].trim();
if (CPF_PATTERN.matcher(candidate).matches()) {
return candidate;
}
}
}
// Tenta SubjectAlternativeNames
try {
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
if (sans != null) {
for (List<?> item : sans) {
if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) {
String content = new String(bytes, StandardCharsets.ISO_8859_1);
Matcher m = CPF_PATTERN.matcher(content);
if (m.find()) {
return m.group(1);
}
}
}
}
} catch (Exception ignored) {
// SAN não disponível ou inacessível
}
return null;
}
/**
* Tenta extrair CNPJ do certificado para pessoas jurídicas.
*/
public static String extractCnpj(X509Certificate cert, String commonName) {
if (commonName != null && commonName.contains(":")) {
String[] parts = commonName.split(":");
if (parts.length > 1) {
String candidate = parts[parts.length - 1].trim();
if (CNPJ_PATTERN.matcher(candidate).matches()) {
return candidate;
}
}
}
try {
Collection<List<?>> sans = cert.getSubjectAlternativeNames();
if (sans != null) {
for (List<?> item : sans) {
if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) {
String content = new String(bytes, StandardCharsets.ISO_8859_1);
Matcher m = CNPJ_PATTERN.matcher(content);
if (m.find()) {
return m.group(1);
}
}
}
}
} catch (Exception ignored) {
// SAN não disponível ou inacessível
}
return null;
}
/**
* Extrai o nome amigável do titular (removendo sufixo :CPF ou :CNPJ comum em ICP-Brasil).
*/
public static String extractHolderName(String commonName) {
if (commonName == null || commonName.isBlank()) {
return "Titular do Certificado";
}
if (commonName.contains(":")) {
String[] parts = commonName.split(":");
if (parts.length > 0 && !parts[0].isBlank()) {
return parts[0].trim();
}
}
return commonName.trim();
}
}
@@ -0,0 +1,51 @@
package br.com.cybercore.signer.core.diagnostics;
import java.io.Serializable;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Relatório consolidado de integridade e diagnósticos do CyberCore Signer.
*/
public record DiagnosticReport(
Instant generatedAt,
String osName,
String osVersion,
String osArch,
String javaVersion,
String javaVendor,
String javaRuntime,
boolean isWindows,
boolean cryptoEngineOk,
List<String> registeredSecurityProviders,
boolean windowsStoreSupported,
boolean pkcs11ModuleAvailable,
int totalCertificatesFound,
int a1CertificatesCount,
int a3CertificatesCount,
boolean localServerRunning,
int localServerPort,
Map<String, String> additionalDetails
) implements Serializable {
public DiagnosticReport {
registeredSecurityProviders = registeredSecurityProviders != null
? List.copyOf(registeredSecurityProviders)
: Collections.emptyList();
additionalDetails = additionalDetails != null
? Map.copyOf(additionalDetails)
: Collections.emptyMap();
}
@Override
public List<String> registeredSecurityProviders() {
return Collections.unmodifiableList(registeredSecurityProviders);
}
@Override
public Map<String, String> additionalDetails() {
return Collections.unmodifiableMap(additionalDetails);
}
}
@@ -0,0 +1,87 @@
package br.com.cybercore.signer.core.diagnostics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Serviço responsável por verificar as capacidades do sistema, JVM e engines criptográficas.
*/
public class DiagnosticService {
private static final Logger logger = LoggerFactory.getLogger(DiagnosticService.class);
public DiagnosticReport runDiagnostic() {
logger.info("Iniciando diagnóstico do ambiente e engine criptográfica...");
String osName = System.getProperty("os.name", "Desconhecido");
String osVersion = System.getProperty("os.version", "Desconhecido");
String osArch = System.getProperty("os.arch", "Desconhecido");
boolean isWindows = osName.toLowerCase().contains("win");
String javaVersion = System.getProperty("java.version", "Desconhecido");
String javaVendor = System.getProperty("java.vendor", "Desconhecido");
String javaRuntime = System.getProperty("java.runtime.name", "Desconhecido");
List<String> providersList = new ArrayList<>();
boolean windowsStoreSupported = false;
boolean pkcs11ModuleAvailable = false;
for (Provider p : Security.getProviders()) {
providersList.add(p.getName() + " v" + p.getVersionStr());
if ("SunMSCAPI".equalsIgnoreCase(p.getName())) {
windowsStoreSupported = true;
}
if ("SunPKCS11".equalsIgnoreCase(p.getName())) {
pkcs11ModuleAvailable = true;
}
}
boolean cryptoEngineOk = verifyCryptoAlgorithms();
Map<String, String> details = new HashMap<>();
details.put("VirtualThreadsSupported", "true");
details.put("DefaultEncoding", System.getProperty("file.encoding", "UTF-8"));
return new DiagnosticReport(
Instant.now(),
osName,
osVersion,
osArch,
javaVersion,
javaVendor,
javaRuntime,
isWindows,
cryptoEngineOk,
providersList,
windowsStoreSupported,
pkcs11ModuleAvailable,
0,
0,
0,
false,
39741,
details
);
}
private boolean verifyCryptoAlgorithms() {
try {
Signature.getInstance("SHA256withRSA");
Signature.getInstance("SHA384withRSA");
Signature.getInstance("SHA512withRSA");
return true;
} catch (Exception e) {
logger.error("Falha ao inicializar algoritmos padrão JCA/JCE: {}", e.getMessage());
return false;
}
}
}
@@ -0,0 +1,30 @@
package br.com.cybercore.signer.core.domain;
/**
* Estado de validade e disponibilidade de um certificado.
*/
public enum CertificateStatus {
AVAILABLE("Disponível", true),
EXPIRED("Expirado", false),
NOT_YET_VALID("Ainda não válido", false),
NO_PRIVATE_KEY("Sem chave privada", false),
TOKEN_DISCONNECTED("Token desconectado", false),
REVOKED("Revogado", false),
UNKNOWN("Desconhecido", false);
private final String description;
private final boolean usableForSigning;
CertificateStatus(String description, boolean usableForSigning) {
this.description = description;
this.usableForSigning = usableForSigning;
}
public String getDescription() {
return description;
}
public boolean isUsableForSigning() {
return usableForSigning;
}
}
@@ -0,0 +1,32 @@
package br.com.cybercore.signer.core.domain;
/**
* Tipo de certificado digital suportado pelo CyberCore Signer.
*/
public enum CertificateType {
/**
* Certificado em software (.pfx / .p12).
*/
A1("A1", "Certificado Local"),
/**
* Certificado em hardware (Smart Card / Token USB / PKCS#11 / Windows Store).
*/
A3("A3", "Token / Smart Card");
private final String code;
private final String label;
CertificateType(String code, String label) {
this.code = code;
this.label = label;
}
public String getCode() {
return code;
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,56 @@
package br.com.cybercore.signer.core.domain;
import java.io.Serializable;
import java.time.Instant;
import java.util.Objects;
/**
* Representação imutável de um certificado digital gerenciado pelo CyberCore Signer.
* Contém apenas metadados públicos e nunca expõe referências diretas de chave privada.
*/
public record ManagedCertificate(
String id,
String alias,
CertificateType type,
CertificateStatus status,
String subjectDn,
String issuerDn,
String serialNumber,
String sha1Fingerprint,
String sha256Fingerprint,
Instant notBefore,
Instant notAfter,
String algorithm,
int keySize,
String commonName,
String cpf,
String cnpj,
String providerId,
String hardwareDeviceName
) implements Serializable {
public ManagedCertificate {
Objects.requireNonNull(id, "id must not be null");
Objects.requireNonNull(alias, "alias must not be null");
Objects.requireNonNull(type, "type must not be null");
Objects.requireNonNull(status, "status must not be null");
Objects.requireNonNull(subjectDn, "subjectDn must not be null");
Objects.requireNonNull(issuerDn, "issuerDn must not be null");
}
/**
* Verifica se o certificado está atualmente dentro do período de validade temporal.
*/
public boolean isCurrentlyValid() {
Instant now = Instant.now();
return (notBefore == null || !now.isBefore(notBefore))
&& (notAfter == null || !now.isAfter(notAfter));
}
/**
* Verifica se o certificado está apto a assinar.
*/
public boolean canSign() {
return status.isUsableForSigning() && isCurrentlyValid();
}
}
@@ -0,0 +1,55 @@
package br.com.cybercore.signer.core.domain;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Contexto da operação de assinatura a ser processada pelo SigningService / CertificateProvider.
*/
public record SigningContext(
String requestId,
String applicationId,
byte[] digest,
String algorithm,
String certificateHint,
Instant requestedAt
) {
public SigningContext {
Objects.requireNonNull(requestId, "requestId must not be null");
Objects.requireNonNull(applicationId, "applicationId must not be null");
Objects.requireNonNull(digest, "digest must not be null");
Objects.requireNonNull(algorithm, "algorithm must not be null");
digest = digest.clone();
if (requestedAt == null) {
requestedAt = Instant.now();
}
}
@Override
public byte[] digest() {
return digest.clone();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SigningContext that = (SigningContext) o;
return Objects.equals(requestId, that.requestId)
&& Objects.equals(applicationId, that.applicationId)
&& Arrays.equals(digest, that.digest)
&& Objects.equals(algorithm, that.algorithm)
&& Objects.equals(certificateHint, that.certificateHint)
&& Objects.equals(requestedAt, that.requestedAt);
}
@Override
public int hashCode() {
int result = Objects.hash(requestId, applicationId, algorithm, certificateHint, requestedAt);
result = 31 * result + Arrays.hashCode(digest);
return result;
}
}
@@ -0,0 +1,38 @@
package br.com.cybercore.signer.core.domain;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Objects;
/**
* Contêiner seguro para material criptográfico de assinatura (chave privada e cadeia X.509).
* Permite que serviços de alto nível (PDF/PAdES e XMLDSig) realizem assinaturas estruturadas.
* A chave privada e a cadeia permanecem estritamente na máquina local do usuário.
*/
public record SigningKeyMaterial(
PrivateKey privateKey,
X509Certificate[] certificateChain,
Provider jcaProvider
) {
public SigningKeyMaterial {
Objects.requireNonNull(privateKey, "Chave privada não pode ser nula.");
Objects.requireNonNull(certificateChain, "Cadeia de certificados não pode ser nula.");
if (certificateChain.length == 0) {
throw new IllegalArgumentException("A cadeia de certificados deve conter ao menos um certificado.");
}
// Cópia defensiva
certificateChain = Arrays.copyOf(certificateChain, certificateChain.length);
}
public X509Certificate signingCertificate() {
return certificateChain[0];
}
@Override
public X509Certificate[] certificateChain() {
return Arrays.copyOf(certificateChain, certificateChain.length);
}
}
@@ -0,0 +1,63 @@
package br.com.cybercore.signer.core.domain;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Resultado da operação de assinatura digital.
*/
public record SigningResult(
String requestId,
byte[] signatureBytes,
String certificateBase64,
List<String> certificateChainBase64,
String algorithm,
Instant signedAt
) {
public SigningResult {
Objects.requireNonNull(requestId, "requestId must not be null");
Objects.requireNonNull(signatureBytes, "signatureBytes must not be null");
Objects.requireNonNull(certificateBase64, "certificateBase64 must not be null");
Objects.requireNonNull(algorithm, "algorithm must not be null");
signatureBytes = signatureBytes.clone();
certificateChainBase64 = certificateChainBase64 != null
? List.copyOf(certificateChainBase64)
: Collections.singletonList(certificateBase64);
if (signedAt == null) {
signedAt = Instant.now();
}
}
@Override
public byte[] signatureBytes() {
return signatureBytes.clone();
}
@Override
public List<String> certificateChainBase64() {
return Collections.unmodifiableList(certificateChainBase64);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SigningResult that = (SigningResult) o;
return Objects.equals(requestId, that.requestId)
&& Arrays.equals(signatureBytes, that.signatureBytes)
&& Objects.equals(certificateBase64, that.certificateBase64)
&& Objects.equals(certificateChainBase64, that.certificateChainBase64)
&& Objects.equals(algorithm, that.algorithm)
&& Objects.equals(signedAt, that.signedAt);
}
@Override
public int hashCode() {
int result = Objects.hash(requestId, certificateBase64, certificateChainBase64, algorithm, signedAt);
result = 31 * result + Arrays.hashCode(signatureBytes);
return result;
}
}
@@ -0,0 +1,20 @@
package br.com.cybercore.signer.core.pdf;
/**
* Seleção da página do PDF na qual o selo visual da assinatura digital será carimbado.
*/
public enum PdfSealPageChoice {
LAST_PAGE("Última Página (Recomendado)"),
FIRST_PAGE("Primeira Página"),
ALL_PAGES("Todas as Páginas");
private final String description;
PdfSealPageChoice(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@@ -0,0 +1,22 @@
package br.com.cybercore.signer.core.pdf;
/**
* Posição do selo visual de assinatura na página do documento PDF.
*/
public enum PdfSealPosition {
BOTTOM_RIGHT("Canto Inferior Direito"),
BOTTOM_LEFT("Canto Inferior Esquerdo"),
TOP_RIGHT("Canto Superior Direito"),
TOP_LEFT("Canto Superior Esquerdo"),
CUSTOM("Posição Personalizada por Clique na Página");
private final String description;
PdfSealPosition(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@@ -0,0 +1,186 @@
package br.com.cybercore.signer.core.pdf;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
import java.util.Objects;
/**
* Parâmetros de configuração para a operação de assinatura digital PAdES com selo visual em PDF,
* incluindo suporte a posicionamento predefinido ou coordenadas personalizadas na página,
* e preferências granulares para exibição de CPF e data/hora.
*/
public record PdfSignatureOptions(
String certificateId,
char[] pinOrPassword,
boolean visibleSeal,
PdfSealTheme sealTheme,
PdfSealPosition position,
PdfSealPageChoice pageChoice,
String reason,
String location,
Float customX,
Float customY,
Integer targetPageNumber,
CpfDisplayMode cpfDisplayMode,
Boolean includeDateTime,
String customExtraInfo
) {
public PdfSignatureOptions {
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
if (sealTheme == null) {
sealTheme = PdfSealThemeRegistry.getDefaultTheme();
}
if (position == null) {
position = (customX != null && customY != null) ? PdfSealPosition.CUSTOM : PdfSealPosition.BOTTOM_RIGHT;
}
if (pageChoice == null) {
pageChoice = PdfSealPageChoice.LAST_PAGE;
}
if (reason == null || reason.isBlank()) {
reason = "Assinatura Digital de Documento";
}
if (location == null || location.isBlank()) {
location = "Brasil";
}
if (cpfDisplayMode == null) {
cpfDisplayMode = (sealTheme.defaultCpfMode() != null) ? sealTheme.defaultCpfMode() : CpfDisplayMode.FULL;
}
if (includeDateTime == null) {
includeDateTime = sealTheme.defaultIncludeDateTime();
}
}
/**
* Construtor para compatibilidade com assinaturas anteriores a customExtraInfo.
*/
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null);
}
/**
* Construtor de compatibilidade para assinatura com posições pré-fixadas por enum.
*/
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null);
}
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location, Float customX, Float customY, Integer targetPageNumber) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, customX, customY, targetPageNumber, null, null, null);
}
public static Builder builder(String certificateId) {
return new Builder(certificateId);
}
public static final class Builder {
private final String certificateId;
private char[] pinOrPassword;
private boolean visibleSeal = true;
private PdfSealTheme sealTheme;
private PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT;
private PdfSealPageChoice pageChoice = PdfSealPageChoice.LAST_PAGE;
private String reason = "Assinatura Digital ICP-Brasil";
private String location = "Brasil";
private Float customX;
private Float customY;
private Integer targetPageNumber;
private CpfDisplayMode cpfDisplayMode;
private Boolean includeDateTime;
public Builder(String certificateId) {
this.certificateId = Objects.requireNonNull(certificateId);
}
public Builder pinOrPassword(char[] pin) {
this.pinOrPassword = pin;
return this;
}
public Builder visibleSeal(boolean visible) {
this.visibleSeal = visible;
return this;
}
public Builder sealTheme(PdfSealTheme theme) {
this.sealTheme = theme;
return this;
}
public Builder position(PdfSealPosition pos) {
this.position = pos;
return this;
}
public Builder pageChoice(PdfSealPageChoice choice) {
this.pageChoice = choice;
return this;
}
public Builder reason(String reason) {
this.reason = reason;
return this;
}
public Builder location(String location) {
this.location = location;
return this;
}
public Builder cpfDisplayMode(CpfDisplayMode mode) {
this.cpfDisplayMode = mode;
return this;
}
private String customExtraInfo;
public Builder includeDateTime(boolean include) {
this.includeDateTime = include;
return this;
}
public Builder customExtraInfo(String info) {
this.customExtraInfo = info;
return this;
}
public Builder customPosition(Float x, Float y, Integer pageNumber) {
this.customX = x;
this.customY = y;
this.targetPageNumber = pageNumber;
if (x != null && y != null) {
this.position = PdfSealPosition.CUSTOM;
}
return this;
}
public PdfSignatureOptions build() {
return new PdfSignatureOptions(
certificateId,
pinOrPassword,
visibleSeal,
sealTheme,
position,
pageChoice,
reason,
location,
customX,
customY,
targetPageNumber,
cpfDisplayMode,
includeDateTime,
customExtraInfo
);
}
}
}
@@ -0,0 +1,351 @@
package br.com.cybercore.signer.core.pdf;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.CertificateProviderException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Serviço de alto nível para Assinatura Digital de Documentos PDF no padrão PAdES (PDF Advanced Electronic Signatures),
* com suporte à aplicação de Selos Visuais estilizados por Temas SVG, posicionamento livre por clique na página e renderização de previews.
*/
public class PdfSigningService {
private static final Logger logger = LoggerFactory.getLogger(PdfSigningService.class);
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
.withZone(ZoneId.systemDefault());
private final CertificateManager certificateManager;
public PdfSigningService(CertificateManager certificateManager) {
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
}
/**
* Retorna a quantidade total de páginas contidas no documento PDF.
*/
public static int getPageCount(byte[] pdfBytes) throws IOException {
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
return doc.getNumberOfPages();
}
}
/**
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
*/
public static BufferedImage renderPageImage(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException {
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
}
PDFRenderer renderer = new PDFRenderer(doc);
return renderer.renderImageWithDPI(pageIndex0Based, dpi);
}
}
/**
* Renderiza uma página específica do PDF diretamente para bytes de imagem PNG.
*/
public static byte[] renderPagePngBytes(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException {
BufferedImage image = renderPageImage(pdfBytes, pageIndex0Based, dpi);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
return baos.toByteArray();
}
/**
* Assina digitalmente um arquivo PDF do sistema de arquivos e grava o resultado no arquivo de destino.
*/
public void signPdfFile(File sourcePdf, File targetPdf, PdfSignatureOptions options) throws Exception {
Objects.requireNonNull(sourcePdf, "Arquivo PDF de origem não pode ser nulo.");
Objects.requireNonNull(targetPdf, "Arquivo PDF de destino não pode ser nulo.");
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
byte[] inputBytes = Files.readAllBytes(sourcePdf.toPath());
byte[] signedBytes = signPdf(inputBytes, options);
if (targetPdf.getParentFile() != null) {
targetPdf.getParentFile().mkdirs();
}
try (FileOutputStream fos = new FileOutputStream(targetPdf)) {
fos.write(signedBytes);
}
logger.info("PDF assinado com sucesso e salvo em: {}", targetPdf.getAbsolutePath());
}
/**
* Assina digitalmente os bytes de um documento PDF, aplicando o selo visual configurado
* e retornando o PDF assinado completo com envelope PAdES.
*/
public byte[] signPdf(byte[] pdfBytes, PdfSignatureOptions options) throws Exception {
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas.");
// 1. Localiza o certificado e o material criptográfico (chave e cadeia)
Optional<ManagedCertificate> certOpt = certificateManager.findById(options.certificateId());
if (certOpt.isEmpty()) {
certOpt = certificateManager.findByHint(options.certificateId());
}
if (certOpt.isEmpty()) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Certificado não encontrado no gerenciador para ID: " + options.certificateId()
);
}
ManagedCertificate cert = certOpt.get();
Optional<SigningKeyMaterial> keyMaterialOpt = certificateManager.getKeyMaterial(cert.id(), options.pinOrPassword());
if (keyMaterialOpt.isEmpty()) {
throw new CertificateProviderException(
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
"Chave privada inacessível para o certificado '" + cert.alias() + "'."
);
}
SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
// 2. Prepara dados do signatário e resumo para o selo visual
String signerDoc = cert.cpf() != null ? "CPF: " + cert.cpf() : (cert.cnpj() != null ? "CNPJ: " + cert.cnpj() : "");
String issuerCn = extractIssuerCn(cert.issuerDn());
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] pdfDigest = sha256.digest(pdfBytes);
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
PdfSealTheme theme = options.sealTheme();
String effectiveExtraInfo = (options.customExtraInfo() != null && !options.customExtraInfo().isBlank())
? options.customExtraInfo().trim()
: theme.formatCustomField();
// 3. Monta dados do selo visual
PdfSealData sealData = new PdfSealData(
cert.commonName() != null ? cert.commonName() : cert.alias(),
signerDoc,
signingTimeStr,
issuerCn,
hashShort,
options.reason(),
options.location(),
options.cpfDisplayMode(),
options.includeDateTime(),
effectiveExtraInfo
);
// 4. Carrega o PDF e estampa o selo se solicitado
byte[] intermediatePdf;
if (options.visibleSeal()) {
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
} else {
intermediatePdf = pdfBytes;
}
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(cert.commonName());
signature.setLocation(options.location());
signature.setReason(options.reason());
signature.setSignDate(Calendar.getInstance());
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
doc.addSignature(signature, signer);
doc.saveIncremental(finalOut);
}
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
return finalOut.toByteArray();
}
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
PdfSignatureOptions options) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
int totalPages = doc.getNumberOfPages();
if (totalPages == 0) {
return pdfBytes;
}
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
float sealWidth = theme.width() * 0.72f;
float sealHeight = theme.height() * 0.72f;
float margin = 20f;
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
if (options.customX() != null && options.customY() != null) {
int targetPageIdx = totalPages - 1;
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
targetPageIdx = options.targetPageNumber() - 1;
}
PDPage page = doc.getPage(targetPageIdx);
PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight();
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
doc.save(baos);
return baos.toByteArray();
}
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
int startPage = 0;
int endPage = totalPages - 1;
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
endPage = 0;
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
startPage = totalPages - 1;
}
for (int i = startPage; i <= endPage; i++) {
PDPage page = doc.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight();
float x;
float y;
switch (options.position()) {
case BOTTOM_LEFT -> {
x = margin;
y = margin;
}
case TOP_RIGHT -> {
x = pageW - sealWidth - margin;
y = pageH - sealHeight - margin;
}
case TOP_LEFT -> {
x = margin;
y = pageH - sealHeight - margin;
}
default -> { // BOTTOM_RIGHT
x = pageW - sealWidth - margin;
y = margin;
}
}
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
}
doc.save(baos);
}
return baos.toByteArray();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static String extractIssuerCn(String dn) {
if (dn == null) return "ICP-Brasil";
int idx = dn.indexOf("CN=");
if (idx >= 0) {
int end = dn.indexOf(',', idx);
return end > idx ? dn.substring(idx + 3, end).trim() : dn.substring(idx + 3).trim();
}
return dn;
}
/**
* Assinador PAdES CMS/PKCS#7 desacoplado via Bouncy Castle.
*/
private static class PadesBouncyCastleSigner implements SignatureInterface {
private final SigningKeyMaterial keyMaterial;
public PadesBouncyCastleSigner(SigningKeyMaterial keyMaterial) {
this.keyMaterial = Objects.requireNonNull(keyMaterial);
}
@Override
public byte[] sign(InputStream content) throws IOException {
try {
byte[] messageBytes = content.readAllBytes();
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
.build(keyMaterial.privateKey());
X509Certificate userCert = keyMaterial.certificateChain()[0];
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build())
.build(contentSigner, userCert));
gen.addCertificates(new JcaCertStore(Arrays.asList(keyMaterial.certificateChain())));
CMSTypedData msg = new CMSProcessableByteArray(messageBytes);
CMSSignedData signedData = gen.generate(msg, false);
return signedData.getEncoded();
} catch (Exception e) {
logger.error("Falha ao gerar assinatura CMS/PKCS#7 para PDF", e);
throw new IOException("Falha criptográfica no Bouncy Castle: " + e.getMessage(), e);
}
}
}
}
@@ -0,0 +1,64 @@
package br.com.cybercore.signer.core.pdf.seal;
/**
* Modo de exibição do CPF do signatário no selo visual de assinatura digital.
* Permite balancear autenticidade documental e privacidade de dados (LGPD).
*/
public enum CpfDisplayMode {
/**
* O CPF não é exibido no selo visual.
*/
NONE("Ocultar CPF"),
/**
* O CPF é exibido parcialmente mascarado (ex: ***.456.789-**).
*/
MASKED("Mascarado (***.456.789-**)"),
/**
* O CPF é exibido integralmente (ex: 123.456.789-00).
*/
FULL("Completo (123.456.789-00)");
private final String description;
CpfDisplayMode(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
/**
* Formata um CPF bruto ou previamente formatado de acordo com este modo.
*
* @param rawCpf CPF a ser formatado (com ou sem pontuação)
* @return String formatada conforme o modo escolhido, ou vazio se não houver CPF
*/
public String formatCpf(String rawCpf) {
if (this == NONE || rawCpf == null || rawCpf.isBlank()) {
return "";
}
String digits = rawCpf.replaceAll("\\D", "");
if (digits.length() != 11) {
// Se não for um CPF de 11 dígitos, retorna mascarado ou o próprio texto
return this == MASKED ? "***.***.***-**" : rawCpf;
}
if (this == MASKED) {
// Mascara os 3 primeiros e os 2 dígitos verificadores: ***.456.789-**
String part2 = digits.substring(3, 6);
String part3 = digits.substring(6, 9);
return "***." + part2 + "." + part3 + "-**";
}
// FULL: 123.456.789-00
return digits.substring(0, 3) + "." +
digits.substring(3, 6) + "." +
digits.substring(6, 9) + "-" +
digits.substring(9, 11);
}
}
@@ -0,0 +1,175 @@
package br.com.cybercore.signer.core.pdf.seal;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Gerenciador de armazenamento em disco para Selos Visuais Personalizados criados ou instalados pelo usuário.
* Persiste cada selo como um arquivo JSON no diretório ~/.cybercore/seals/.
*/
public final class CustomSealStorage {
private static final Logger logger = LoggerFactory.getLogger(CustomSealStorage.class);
private static final String SEALS_DIR_NAME = "seals";
private final Path storageDir;
private final ObjectMapper objectMapper;
public CustomSealStorage() {
this(resolveDefaultStorageDir());
}
public CustomSealStorage(Path storageDir) {
this.storageDir = Objects.requireNonNull(storageDir);
this.objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
ensureDirectoryExists();
}
private static Path resolveDefaultStorageDir() {
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore", SEALS_DIR_NAME);
}
private void ensureDirectoryExists() {
try {
if (!Files.exists(storageDir)) {
Files.createDirectories(storageDir);
}
} catch (IOException e) {
logger.error("Falha ao criar diretório de selos personalizados: {}", storageDir, e);
}
}
public Path getStorageDir() {
return storageDir;
}
/**
* Salva ou atualiza um selo personalizado em disco.
*/
public void saveSeal(PdfSealTheme seal) throws IOException {
Objects.requireNonNull(seal, "Selo não pode ser nulo.");
ensureDirectoryExists();
String safeFileName = sanitizeFileName(seal.id()) + ".json";
Path targetPath = storageDir.resolve(safeFileName);
// Marca como personalizado
PdfSealTheme customSeal = new PdfSealTheme(
seal.id(),
seal.name(),
seal.description(),
seal.width(),
seal.height(),
seal.primaryColor(),
seal.secondaryColor(),
seal.backgroundColor(),
seal.borderColor(),
seal.textColor(),
seal.svgTemplate(),
seal.customHeader(),
seal.customFooter(),
seal.iconStyle(),
seal.defaultCpfMode(),
seal.defaultIncludeDateTime(),
true,
seal.customLogoBase64(),
seal.customFieldLabel(),
seal.customFieldValue()
);
objectMapper.writeValue(targetPath.toFile(), customSeal);
logger.info("Selo personalizado '{}' instalado com sucesso em: {}", seal.id(), targetPath);
}
/**
* Carrega todos os selos personalizados instalados no diretório do usuário.
*/
public List<PdfSealTheme> loadAllCustomSeals() {
List<PdfSealTheme> seals = new ArrayList<>();
if (!Files.exists(storageDir)) {
return seals;
}
try (Stream<Path> stream = Files.list(storageDir)) {
stream.filter(p -> p.toString().toLowerCase().endsWith(".json"))
.forEach(file -> {
try {
PdfSealTheme seal = objectMapper.readValue(file.toFile(), PdfSealTheme.class);
seals.add(seal);
} catch (Exception e) {
logger.warn("Falha ao carregar arquivo de selo {}: {}", file.getFileName(), e.getMessage());
}
});
} catch (IOException e) {
logger.error("Erro ao listar diretório de selos {}", storageDir, e);
}
return seals;
}
/**
* Remove um selo personalizado do disco.
*/
public boolean deleteSeal(String sealId) {
if (sealId == null || sealId.isBlank()) return false;
String safeFileName = sanitizeFileName(sealId) + ".json";
Path targetPath = storageDir.resolve(safeFileName);
try {
boolean deleted = Files.deleteIfExists(targetPath);
if (deleted) {
logger.info("Selo personalizado '{}' removido do disco.", sealId);
}
return deleted;
} catch (IOException e) {
logger.error("Erro ao excluir selo {}: {}", sealId, e.getMessage());
return false;
}
}
/**
* Importa um selo a partir de um arquivo externo (.json) e o instala no diretório do usuário.
*/
public PdfSealTheme importSeal(Path externalFile) throws IOException {
Objects.requireNonNull(externalFile, "Arquivo de importação não pode ser nulo.");
if (!Files.exists(externalFile)) {
throw new IllegalArgumentException("O arquivo de selo especificado não existe: " + externalFile);
}
PdfSealTheme seal = objectMapper.readValue(externalFile.toFile(), PdfSealTheme.class);
saveSeal(seal);
return seal;
}
/**
* Exporta um selo para um arquivo externo de destino.
*/
public void exportSeal(PdfSealTheme seal, Path destinationFile) throws IOException {
Objects.requireNonNull(seal, "Selo não pode ser nulo.");
Objects.requireNonNull(destinationFile, "Destino não pode ser nulo.");
if (destinationFile.getParent() != null && !Files.exists(destinationFile.getParent())) {
Files.createDirectories(destinationFile.getParent());
}
objectMapper.writeValue(destinationFile.toFile(), seal);
logger.info("Selo '{}' exportado com sucesso para: {}", seal.id(), destinationFile);
}
private static String sanitizeFileName(String id) {
return id.replaceAll("[^a-zA-Z0-9._-]", "_");
}
}
@@ -0,0 +1,64 @@
package br.com.cybercore.signer.core.pdf.seal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* Metadados e dados do signatário a serem gravados e representados no selo visual da assinatura PDF.
*/
public record PdfSealData(
String signerName,
String identifier,
String signingDate,
String issuer,
String hashSummary,
String reason,
String location,
CpfDisplayMode cpfDisplayMode,
boolean includeDateTime,
String customExtraInfo
) {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss (z)")
.withZone(ZoneId.systemDefault());
public PdfSealData {
signerName = (signerName != null && !signerName.isBlank()) ? signerName : "Signatário ICP-Brasil";
identifier = (identifier != null && !identifier.isBlank()) ? identifier : "";
signingDate = (signingDate != null && !signingDate.isBlank()) ? signingDate : FORMATTER.format(Instant.now());
issuer = (issuer != null && !issuer.isBlank()) ? issuer : "Autoridade Certificadora ICP-Brasil";
hashSummary = (hashSummary != null && !hashSummary.isBlank()) ? hashSummary : "";
reason = (reason != null && !reason.isBlank()) ? reason : "Assinatura Digital de Documento";
location = (location != null && !location.isBlank()) ? location : "Brasil";
if (cpfDisplayMode == null) {
cpfDisplayMode = CpfDisplayMode.FULL;
}
}
/**
* Construtor para compatibilidade com assinaturas anteriores a customExtraInfo.
*/
public PdfSealData(String signerName, String identifier, String signingDate, String issuer,
String hashSummary, String reason, String location,
CpfDisplayMode cpfDisplayMode, boolean includeDateTime) {
this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, cpfDisplayMode, includeDateTime, null);
}
/**
* Construtor de compatibilidade para código que não especifica cpfDisplayMode e includeDateTime.
*/
public PdfSealData(String signerName, String identifier, String signingDate, String issuer,
String hashSummary, String reason, String location) {
this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, CpfDisplayMode.FULL, true, null);
}
public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary) {
return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, CpfDisplayMode.FULL, true);
}
public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary,
CpfDisplayMode cpfDisplayMode, boolean includeDateTime) {
return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, cpfDisplayMode, includeDateTime);
}
}
@@ -0,0 +1,411 @@
package br.com.cybercore.signer.core.pdf.seal;
import javax.imageio.ImageIO;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Objects;
/**
* Renderizador de alta fidelidade para Selos Visuais de Assinatura PDF.
* Gera imagens nítidas (300 DPI equivalente) baseadas nos Temas e vetores SVG,
* utilizando exclusivamente a API gráfica padrão Java2D (AWT), sem dependência de JavaFX.
*
* Garante que o nome completo do signatário seja sempre exibido na íntegra,
* além de respeitar as opções de CPF (completo, mascarado ou omitido) e data/hora.
*/
public final class PdfSealRenderer {
private PdfSealRenderer() {
}
/**
* Renderiza o selo visual baseado no tema e dados fornecidos para um BufferedImage em escala 2x (alta nitidez).
*/
public static BufferedImage renderToImage(PdfSealTheme theme, PdfSealData data) {
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
Objects.requireNonNull(data, "Dados da assinatura não podem ser nulos.");
int baseWidth = theme.width();
int baseHeight = theme.height();
// Escala 2x para renderização super-nítida no documento PDF e na interface
int scale = 2;
int imgWidth = baseWidth * scale;
int imgHeight = baseHeight * scale;
BufferedImage image = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
try {
// Configuração de anti-aliasing e renderização de alta qualidade
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2d.scale(scale, scale);
Color primaryColor = parseColor(theme.primaryColor(), new Color(13, 71, 161));
Color secondaryColor = parseColor(theme.secondaryColor(), new Color(2, 132, 199));
Color bgColor = parseColor(theme.backgroundColor(), Color.WHITE);
Color borderColor = parseColor(theme.borderColor(), new Color(203, 213, 225));
Color textColor = parseColor(theme.textColor(), new Color(15, 23, 42));
// 1. Fundo do Selo com cantos arredondados
g2d.setColor(bgColor);
g2d.fill(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8));
// 2. Borda externa
g2d.setColor(borderColor);
g2d.setStroke(new BasicStroke(1.2f));
g2d.draw(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8));
// 3. Tarja/Acento lateral do tema
g2d.setColor(primaryColor);
g2d.fill(new RoundRectangle2D.Float(1, 1, 5, baseHeight - 2, 3, 3));
if ("gov-br".equalsIgnoreCase(theme.id()) || "star".equalsIgnoreCase(theme.iconStyle())) {
g2d.setColor(secondaryColor);
g2d.fillRect(6, 1, 3, baseHeight - 2);
}
// 4. Emblema à Esquerda (Logo Customizada em Imagem ou Ícone Vetorial)
if (theme.customLogoBase64() != null && !theme.customLogoBase64().isBlank()) {
drawCustomLogo(g2d, theme.customLogoBase64(), 10, 14, 36, baseHeight - 28);
} else {
String effectiveIcon = (theme.iconStyle() != null && !theme.iconStyle().isBlank()) ? theme.iconStyle() : theme.id();
drawThemeIcon(g2d, effectiveIcon, primaryColor, secondaryColor);
}
// 5. Informações Textuais da Assinatura (Focado no Titular, sem dados técnicos/emissor)
int textX = 52;
int textWidthAvailable = baseWidth - textX - 12;
String formattedDoc = formatSignerDoc(data);
String customInfo = (data.customExtraInfo() != null && !data.customExtraInfo().isBlank())
? data.customExtraInfo().trim()
: theme.formatCustomField();
String identityLine;
if (!formattedDoc.isBlank() && !customInfo.isBlank()) {
identityLine = formattedDoc + " • " + customInfo;
} else if (!formattedDoc.isBlank()) {
identityLine = formattedDoc;
} else {
identityLine = customInfo;
}
boolean hasIdentity = !identityLine.isBlank();
boolean hasDate = data.includeDateTime();
String fullName = data.signerName() != null ? data.signerName().trim() : "Signatário ICP-Brasil";
String headerText = resolveHeaderText(theme);
String footerText = resolveFooterText(theme, null);
if (hasIdentity && hasDate) {
// Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé
g2d.setColor(primaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
g2d.drawString(headerText, textX, 16);
g2d.setColor(textColor);
drawFullSignerName(g2d, fullName, textX, 33, textWidthAvailable, 13.0f);
g2d.setColor(new Color(51, 65, 85));
g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
drawFittedText(g2d, identityLine, textX, 47, textWidthAvailable, 10.0f);
g2d.drawString("Data: " + data.signingDate(), textX, 60);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
g2d.drawString(footerText, textX, 73);
} else if (hasIdentity || hasDate) {
// Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé
g2d.setColor(primaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10));
g2d.drawString(headerText, textX, 17);
g2d.setColor(textColor);
drawFullSignerName(g2d, fullName, textX, 36, textWidthAvailable, 13.5f);
g2d.setColor(new Color(51, 65, 85));
g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10));
String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate());
drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
g2d.drawString(footerText, textX, 71);
} else {
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
g2d.setColor(primaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10));
g2d.drawString(headerText, textX, 19);
g2d.setColor(textColor);
drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f);
g2d.setColor(secondaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9));
g2d.drawString(footerText, textX, 68);
}
} finally {
g2d.dispose();
}
return image;
}
/**
* Desenha o logotipo personalizado fornecido em Base64, preservando o aspecto proporcional
* e aplicando anti-aliasing e interpolação bilinear de alta fidelidade.
*/
private static void drawCustomLogo(Graphics2D g2d, String base64Data, int x, int y, int maxWidth, int maxHeight) {
try {
String cleanBase64 = base64Data;
int commaIdx = cleanBase64.indexOf(",");
if (commaIdx >= 0 && cleanBase64.substring(0, commaIdx).contains("base64")) {
cleanBase64 = cleanBase64.substring(commaIdx + 1);
}
byte[] bytes = java.util.Base64.getDecoder().decode(cleanBase64.trim());
BufferedImage logoImg = ImageIO.read(new java.io.ByteArrayInputStream(bytes));
if (logoImg != null) {
int originalW = logoImg.getWidth();
int originalH = logoImg.getHeight();
if (originalW > 0 && originalH > 0) {
double scaleW = (double) maxWidth / originalW;
double scaleH = (double) maxHeight / originalH;
double scale = Math.min(scaleW, scaleH);
int targetW = Math.max(1, (int) Math.round(originalW * scale));
int targetH = Math.max(1, (int) Math.round(originalH * scale));
int posX = x + (maxWidth - targetW) / 2;
int posY = y + (maxHeight - targetH) / 2;
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(logoImg, posX, posY, targetW, targetH, null);
return;
}
}
} catch (Exception ignored) {
// Se houver falha na leitura do logo, recorre ao escudo padrão
}
drawThemeIcon(g2d, "shield", new Color(13, 71, 161), new Color(2, 132, 199));
}
/**
* Desenha o nome completo do signatário sem cortes ou truncamentos.
* Ajusta a fonte gradativamente para caber perfeitamente na largura do selo.
*/
private static void drawFullSignerName(Graphics2D g2d, String fullName, int x, int y, int maxWidth, float preferredSize) {
float fontSize = preferredSize;
Font font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize));
FontMetrics fm = g2d.getFontMetrics(font);
// Se o nome for longo, reduz o tamanho da fonte até caber perfeitamente
while (fm.stringWidth(fullName) > maxWidth && fontSize > 8.0f) {
fontSize -= 0.5f;
font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize));
fm = g2d.getFontMetrics(font);
}
g2d.setFont(font);
g2d.drawString(fullName, x, y);
}
/**
* Desenha texto informativo secundário ajustando suavemente a fonte caso exceda a largura.
*/
private static void drawFittedText(Graphics2D g2d, String text, int x, int y, int maxWidth, float baseSize) {
if (text == null || text.isBlank()) return;
float fontSize = baseSize;
Font font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize));
FontMetrics fm = g2d.getFontMetrics(font);
while (fm.stringWidth(text) > maxWidth && fontSize > 7.5f) {
fontSize -= 0.5f;
font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize));
fm = g2d.getFontMetrics(font);
}
g2d.setFont(font);
g2d.drawString(text, x, y);
}
/**
* Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado.
*/
private static String formatSignerDoc(PdfSealData data) {
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL;
String rawDoc = data.identifier() != null ? data.identifier() : "";
if (mode == CpfDisplayMode.NONE) {
// Se for CPF, oculta. Se for CNPJ, pode manter ou formatar.
if (rawDoc.toUpperCase().contains("CPF")) {
return "";
}
return rawDoc;
}
if (rawDoc.toUpperCase().contains("CPF")) {
String digits = rawDoc.replaceAll("\\D", "");
String formatted = mode.formatCpf(digits);
return formatted.isBlank() ? "" : "CPF: " + formatted;
}
return rawDoc;
}
/**
* Renderiza o selo visual e o exporta diretamente como array de bytes PNG.
*/
public static byte[] renderToPngBytes(PdfSealTheme theme, PdfSealData data) throws IOException {
BufferedImage img = renderToImage(theme, data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "PNG", baos);
return baos.toByteArray();
}
private static void drawThemeIcon(Graphics2D g2d, String iconStyleOrId, Color primary, Color secondary) {
g2d.translate(14, 18);
String style = (iconStyleOrId != null) ? iconStyleOrId.toLowerCase() : "shield";
if ("cybercore-modern".equals(style) || "crystal".equals(style)) {
// Cristal CyberCore Vetorial
Path2D.Float crystal = new Path2D.Float();
crystal.moveTo(16, 2);
crystal.lineTo(28, 9);
crystal.lineTo(28, 25);
crystal.lineTo(16, 32);
crystal.lineTo(4, 25);
crystal.lineTo(4, 9);
crystal.closePath();
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35));
g2d.fill(crystal);
g2d.setColor(primary);
g2d.setStroke(new BasicStroke(1.5f));
g2d.draw(crystal);
g2d.setColor(secondary);
g2d.drawLine(16, 2, 16, 32);
g2d.setColor(primary);
g2d.drawLine(4, 17, 28, 17);
} else if ("minimal-clean".equals(style) || "notary".equals(style) || "cartorio".equals(style)) {
// Carimbo notarial sóbrio
g2d.setColor(primary);
g2d.setStroke(new BasicStroke(1.5f));
g2d.draw(new Ellipse2D.Float(2, 2, 28, 28));
g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f));
g2d.draw(new Ellipse2D.Float(5, 5, 22, 22));
// Checkmark
g2d.setStroke(new BasicStroke(1.8f));
g2d.drawLine(10, 16, 14, 20);
g2d.drawLine(14, 20, 22, 12);
} else if ("gov-br".equals(style) || "star".equals(style) || "governo".equals(style)) {
// Brasão estelar estilizado
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 30));
g2d.fill(new Ellipse2D.Float(2, 2, 28, 28));
g2d.setColor(primary);
g2d.setStroke(new BasicStroke(1.4f));
g2d.draw(new Ellipse2D.Float(2, 2, 28, 28));
// Estrela interna
Path2D.Float star = new Path2D.Float();
star.moveTo(16, 6);
star.lineTo(19, 13);
star.lineTo(26, 13);
star.lineTo(20, 17);
star.lineTo(22, 24);
star.lineTo(16, 20);
star.lineTo(10, 24);
star.lineTo(12, 17);
star.lineTo(6, 13);
star.lineTo(13, 13);
star.closePath();
g2d.setColor(secondary);
g2d.fill(star);
} else {
// Padrão: Escudo Criptográfico Oficial com Cadeado
Path2D.Float shield = new Path2D.Float();
shield.moveTo(16, 2);
shield.lineTo(28, 6);
shield.lineTo(28, 16);
shield.curveTo(28, 25, 22, 31, 16, 34);
shield.curveTo(10, 31, 4, 25, 4, 16);
shield.lineTo(4, 6);
shield.closePath();
g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35));
g2d.fill(shield);
g2d.setColor(primary);
g2d.setStroke(new BasicStroke(1.5f));
g2d.draw(shield);
// Cadeado
g2d.setColor(primary);
g2d.fillRect(11, 15, 10, 8);
g2d.setStroke(new BasicStroke(1.3f));
g2d.drawArc(12, 9, 8, 8, 0, 180);
g2d.setColor(Color.WHITE);
g2d.fillOval(15, 17, 2, 2);
}
g2d.translate(-14, -18);
}
private static String resolveHeaderText(PdfSealTheme theme) {
if (theme.customHeader() != null && !theme.customHeader().isBlank()) {
return theme.customHeader().toUpperCase();
}
return switch (theme.id().toLowerCase()) {
case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL";
case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE";
case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001";
default -> "ASSINADO DIGITALMENTE • ICP-BRASIL";
};
}
private static String resolveFooterText(PdfSealTheme theme, String hash) {
if (theme.customFooter() != null && !theme.customFooter().isBlank()) {
return theme.customFooter();
}
if (hash != null && !hash.isBlank()) {
return "Hash Criptográfico: " + hash;
}
return switch (theme.id().toLowerCase()) {
case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local";
case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil";
case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001";
default -> "Conformidade ICP-Brasil e validade jurídica plena";
};
}
private static Color parseColor(String hex, Color def) {
if (hex == null || !hex.startsWith("#")) return def;
try {
return Color.decode(hex);
} catch (Exception e) {
return def;
}
}
}
@@ -0,0 +1,135 @@
package br.com.cybercore.signer.core.pdf.seal;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.Objects;
/**
* Modelo de configuração e identidade visual de um Tema de Selo de Assinatura PDF.
* Permite definir paleta de cores, dimensões, templates, textos personalizados e preferências de exibição.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record PdfSealTheme(
String id,
String name,
String description,
int width,
int height,
String primaryColor,
String secondaryColor,
String backgroundColor,
String borderColor,
String textColor,
String svgTemplate,
String customHeader,
String customFooter,
String iconStyle,
CpfDisplayMode defaultCpfMode,
boolean defaultIncludeDateTime,
boolean isCustom,
String customLogoBase64,
String customFieldLabel,
String customFieldValue
) {
public PdfSealTheme {
Objects.requireNonNull(id, "ID do tema não pode ser nulo.");
Objects.requireNonNull(name, "Nome do tema não pode ser nulo.");
if (width <= 0) width = 320;
if (height <= 0) height = 82;
if (primaryColor == null) primaryColor = "#0d47a1";
if (secondaryColor == null) secondaryColor = "#0284c7";
if (backgroundColor == null) backgroundColor = "#ffffff";
if (borderColor == null) borderColor = "#cbd5e1";
if (textColor == null) textColor = "#0f172a";
if (iconStyle == null || iconStyle.isBlank()) iconStyle = "shield";
if (defaultCpfMode == null) defaultCpfMode = CpfDisplayMode.FULL;
}
/**
* Construtor para temas customizados com logotipo Base64, sem campos extras.
*/
public PdfSealTheme(String id, String name, String description, int width, int height,
String primaryColor, String secondaryColor, String backgroundColor,
String borderColor, String textColor, String svgTemplate,
String customHeader, String customFooter, String iconStyle,
CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom,
String customLogoBase64) {
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle,
defaultCpfMode, defaultIncludeDateTime, isCustom, customLogoBase64, null, null);
}
/**
* Construtor para temas customizados sem logo personalizada em imagem.
*/
public PdfSealTheme(String id, String name, String description, int width, int height,
String primaryColor, String secondaryColor, String backgroundColor,
String borderColor, String textColor, String svgTemplate,
String customHeader, String customFooter, String iconStyle,
CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom) {
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle,
defaultCpfMode, defaultIncludeDateTime, isCustom, null, null, null);
}
/**
* Construtor legado para os temas embutidos do sistema.
*/
public PdfSealTheme(String id, String name, String description, int width, int height,
String primaryColor, String secondaryColor, String backgroundColor,
String borderColor, String textColor, String svgTemplate) {
this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor,
borderColor, textColor, svgTemplate, null, null, "shield", CpfDisplayMode.FULL, true, false, null, null, null);
}
/**
* Formata o campo customizado (ex: "Matrícula: 104829" ou "Cargo: Auditor Fiscal").
*/
public String formatCustomField() {
boolean hasLabel = customFieldLabel != null && !customFieldLabel.isBlank();
boolean hasValue = customFieldValue != null && !customFieldValue.isBlank();
if (hasLabel && hasValue) {
String label = customFieldLabel.trim();
if (!label.endsWith(":")) label += ":";
return label + " " + customFieldValue.trim();
} else if (hasValue) {
return customFieldValue.trim();
} else if (hasLabel) {
return customFieldLabel.trim();
}
return "";
}
/**
* Gera o código SVG final substituindo os marcadores pelos dados reais da assinatura.
*/
public String renderSvg(PdfSealData data) {
if (svgTemplate == null || svgTemplate.isBlank()) {
return "";
}
return svgTemplate
.replace("${THEME_PRIMARY}", primaryColor)
.replace("${THEME_SECONDARY}", secondaryColor)
.replace("${THEME_BG}", backgroundColor)
.replace("${THEME_BORDER}", borderColor)
.replace("${THEME_TEXT}", textColor)
.replace("${SIGNER_NAME}", escapeXml(data.signerName()))
.replace("${SIGNER_DOC}", escapeXml(data.identifier()))
.replace("${SIGNING_DATE}", escapeXml(data.signingDate()))
.replace("${CERT_ISSUER}", escapeXml(data.issuer()))
.replace("${HASH_SUMMARY}", escapeXml(data.hashSummary()))
.replace("${REASON}", escapeXml(data.reason()))
.replace("${LOCATION}", escapeXml(data.location()));
}
private static String escapeXml(String text) {
if (text == null) return "";
return text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
}
@@ -0,0 +1,240 @@
package br.com.cybercore.signer.core.pdf.seal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Registro central de temas de selos visuais para assinatura PDF.
* Inclui os temas padrão do sistema e permite instalar, gerenciar e persistir dinamicamente
* temas personalizados de selo criados ou importados pelo usuário.
*/
public final class PdfSealThemeRegistry {
private static final Logger logger = LoggerFactory.getLogger(PdfSealThemeRegistry.class);
private static final Map<String, PdfSealTheme> THEMES = new ConcurrentHashMap<>();
private static final CustomSealStorage STORAGE = new CustomSealStorage();
public static final String DEFAULT_THEME_ID = "icp-brasil";
static {
registerBuiltInThemes();
loadCustomThemes();
}
private PdfSealThemeRegistry() {
}
private static void registerBuiltInThemes() {
// 1. Tema ICP-Brasil Institucional
registerTheme(new PdfSealTheme(
"icp-brasil",
"ICP-Brasil Institucional",
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
320,
82,
"#0d47a1",
"#0284c7",
"#ffffff",
"#93c5fd",
"#0f172a",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="6" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="3" fill="${THEME_PRIMARY}"/>
<!-- Escudo Criptográfico Vetorial -->
<g transform="translate(14, 18)">
<path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="${THEME_PRIMARY}" opacity="0.12"/>
<path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="none" stroke="${THEME_PRIMARY}" stroke-width="2"/>
<!-- Cadeado -->
<rect x="12" y="17" width="12" height="10" rx="2" fill="${THEME_PRIMARY}"/>
<path d="M14 17 V13 C14 10.8 15.8 9 18 9 C20.2 9 22 10.8 22 13 V17" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.8"/>
<circle cx="18" cy="22" r="1.5" fill="#ffffff"/>
</g>
<!-- Dados da Assinatura -->
<text x="56" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${THEME_PRIMARY}" letter-spacing="0.5">ASSINADO DIGITALMENTE • ICP-BRASIL</text>
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Data/Hora: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="${THEME_SECONDARY}">Verificação criptográfica válida e com fé pública</text>
</svg>
"""
));
// 2. Tema CyberCore Moderno
registerTheme(new PdfSealTheme(
"cybercore-modern",
"CyberCore Moderno",
"Identidade tecnológica com cristal vetorial CyberCore e visual ciano/esmeralda",
320,
82,
"#0284c7",
"#10b981",
"#ffffff",
"#38bdf8",
"#0f172a",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="8" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="5" height="80" rx="2.5" fill="${THEME_SECONDARY}"/>
<!-- Cristal CyberCore Vetorial -->
<g transform="translate(14, 20)">
<polygon points="18,2 32,10 32,28 18,36 4,28 4,10" fill="${THEME_PRIMARY}" opacity="0.15"/>
<polygon points="18,2 32,10 32,28 18,36 4,28 4,10" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.8"/>
<line x1="18" y1="2" x2="18" y2="36" stroke="${THEME_SECONDARY}" stroke-width="1.2"/>
<line x1="4" y1="19" x2="32" y2="19" stroke="${THEME_PRIMARY}" stroke-width="1.2"/>
</g>
<!-- Dados da Assinatura -->
<text x="56" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${THEME_PRIMARY}">CYBERCORE SIGNER • ASSINATURA ELETRÔNICA</text>
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">${SIGNER_DOC} • ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Autenticado em: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="monospace" font-size="7.5" fill="${THEME_SECONDARY}">Hash: ${HASH_SUMMARY}</text>
</svg>
"""
));
// 3. Tema Minimalista Clássico
registerTheme(new PdfSealTheme(
"minimal-clean",
"Minimalista Clássico",
"Design monocromático sóbrio em tons de grafite, ideal para contratos e certidões",
300,
76,
"#334155",
"#64748b",
"#fafafa",
"#cbd5e1",
"#1e293b",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 76" width="300" height="76">
<rect x="1" y="1" width="298" height="74" rx="4" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1"/>
<!-- Ícone de Carimbo Notarial -->
<g transform="translate(12, 18)">
<circle cx="18" cy="18" r="16" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.5"/>
<circle cx="18" cy="18" r="13" fill="none" stroke="${THEME_PRIMARY}" stroke-width="0.8" stroke-dasharray="2,2"/>
<path d="M12 18 L16 22 L24 14" fill="none" stroke="${THEME_PRIMARY}" stroke-width="2"/>
</g>
<!-- Dados da Assinatura -->
<text x="52" y="17" font-family="Georgia, serif" font-size="9" font-weight="bold" fill="${THEME_PRIMARY}">Assinado Digitalmente</text>
<text x="52" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="11.5" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="52" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="8" fill="#475569">${SIGNER_DOC} • ${CERT_ISSUER}</text>
<text x="52" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="8" fill="#475569">${SIGNING_DATE}</text>
<text x="52" y="69" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="7.5" fill="#64748b">Conforme MP 2.200-2/2001 e ICP-Brasil</text>
</svg>
"""
));
// 4. Tema Governamental (Gov.br / ITI)
registerTheme(new PdfSealTheme(
"gov-br",
"Governamental (Padrão Gov.br / ITI)",
"Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001",
320,
82,
"#047857",
"#eab308",
"#ffffff",
"#a7f3d0",
"#064e3b",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="4" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="2" fill="${THEME_PRIMARY}"/>
<rect x="7" y="1" width="3" height="80" fill="${THEME_SECONDARY}"/>
<!-- Brasão Estilizado -->
<g transform="translate(16, 20)">
<circle cx="17" cy="18" r="15" fill="${THEME_PRIMARY}" opacity="0.1"/>
<circle cx="17" cy="18" r="15" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.5"/>
<path d="M17 6 L20 14 L28 14 L22 19 L24 27 L17 22 L10 27 L12 19 L6 14 L14 14 Z" fill="${THEME_SECONDARY}"/>
</g>
<!-- Dados da Assinatura -->
<text x="56" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${THEME_PRIMARY}">GOV.BR • DOCUMENTO ASSINADO DIGITALMENTE</text>
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#065f46">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Data/Hora: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#047857">Validade jurídica assegurada pela MP nº 2.200-2/2001</text>
</svg>
"""
));
}
/**
* Carrega os temas personalizados armazenados no disco.
*/
public static void loadCustomThemes() {
try {
List<PdfSealTheme> customs = STORAGE.loadAllCustomSeals();
for (PdfSealTheme theme : customs) {
THEMES.put(theme.id().toLowerCase(), theme);
}
logger.info("Carregados {} temas de selos personalizados do disco.", customs.size());
} catch (Exception e) {
logger.warn("Não foi possível carregar temas personalizados: {}", e.getMessage());
}
}
public static void registerTheme(PdfSealTheme theme) {
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
THEMES.put(theme.id().toLowerCase(), theme);
}
/**
* Instala um tema personalizado, salvando-o no disco e registrando-o para uso imediato.
*/
public static void installCustomTheme(PdfSealTheme theme) throws IOException {
Objects.requireNonNull(theme, "Tema não pode ser nulo.");
STORAGE.saveSeal(theme);
registerTheme(theme);
}
/**
* Remove um tema personalizado do disco e do registro.
* Não permite remover temas padrão do sistema.
*/
public static boolean uninstallCustomTheme(String themeId) {
if (themeId == null) return false;
String idLower = themeId.toLowerCase();
// Protege temas padrão
if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") ||
idLower.equals("minimal-clean") || idLower.equals("gov-br")) {
throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema.");
}
boolean deleted = STORAGE.deleteSeal(idLower);
THEMES.remove(idLower);
return deleted;
}
public static Optional<PdfSealTheme> findById(String id) {
if (id == null) return Optional.empty();
return Optional.ofNullable(THEMES.get(id.toLowerCase()));
}
public static Optional<PdfSealTheme> getTheme(String id) {
return findById(id);
}
public static PdfSealTheme getDefaultTheme() {
return THEMES.getOrDefault(DEFAULT_THEME_ID, THEMES.values().iterator().next());
}
public static List<PdfSealTheme> listThemes() {
List<PdfSealTheme> list = new ArrayList<>(THEMES.values());
return Collections.unmodifiableList(list);
}
public static CustomSealStorage getStorage() {
return STORAGE;
}
}
@@ -0,0 +1,35 @@
package br.com.cybercore.signer.core.protocol;
/**
* Códigos de erro padronizados do protocolo CyberCore Signing Protocol (CCSP/1).
*/
public enum CcspErrorCode {
CERTIFICATE_NOT_FOUND("Certificado especificado não foi encontrado."),
CERTIFICATE_EXPIRED("O certificado selecionado está expirado."),
CERTIFICATE_INVALID("O certificado é inválido ou está corrompido."),
PRIVATE_KEY_NOT_AVAILABLE("Chave privada inacessível ou não disponível para a operação."),
TOKEN_NOT_FOUND("Nenhum token ou leitor de smart card compatível foi encontrado."),
TOKEN_REMOVED("O token ou smart card foi desconectado durante a operação."),
PIN_REQUIRED("PIN obrigatório para desbloqueio do dispositivo."),
PIN_INCORRECT("PIN informado é incorreto."),
PKCS11_ERROR("Falha na comunicação com o módulo PKCS#11 do dispositivo."),
SIGNATURE_FAILED("Falha ao gerar a assinatura digital."),
APPLICATION_NOT_AUTHORIZED("Aplicação web não possui autorização para solicitar assinaturas."),
REQUEST_EXPIRED("A requisição de assinatura expirou."),
REPLAY_DETECTED("Requisição duplicada detectada (replay attack prevenido)."),
INVALID_ORIGIN("Origem da requisição não é permitida para esta aplicação."),
UNSUPPORTED_ALGORITHM("Algoritmo de assinatura não suportado."),
USER_CANCELLED("Operação cancelada pelo usuário."),
PROVIDER_NOT_AVAILABLE("O provedor criptográfico necessário não está disponível."),
COMMUNICATION_ERROR("Erro interno de comunicação no agente local.");
private final String defaultMessage;
CcspErrorCode(String defaultMessage) {
this.defaultMessage = defaultMessage;
}
public String getDefaultMessage() {
return defaultMessage;
}
}
@@ -0,0 +1,39 @@
package br.com.cybercore.signer.core.protocol;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Operações suportadas pelo protocolo CCSP.
*/
public enum CcspOperation {
SIGN("sign"),
AUTHENTICATE("authenticate"),
LIST_CERTIFICATES("list_certificates"),
STATUS("status"),
PING("ping");
private final String value;
CcspOperation(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@JsonCreator
public static CcspOperation fromValue(String value) {
if (value == null) {
return null;
}
for (CcspOperation op : values()) {
if (op.value.equalsIgnoreCase(value.trim())) {
return op;
}
}
throw new IllegalArgumentException("Operação CCSP desconhecida: " + value);
}
}
@@ -0,0 +1,96 @@
package br.com.cybercore.signer.core.protocol;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.time.Instant;
/**
* Mensagem de requisição do CyberCore Signing Protocol (CCSP/1).
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public record CcspRequest(
@JsonProperty("version")
String version,
@JsonProperty("request_id")
String requestId,
@JsonProperty("application_id")
String applicationId,
@JsonProperty("operation")
String operation,
@JsonProperty("algorithm")
String algorithm,
@JsonProperty("digest")
String digest,
@JsonProperty("certificate_hint")
String certificateHint,
@JsonProperty("expires_at")
String expiresAt,
@JsonProperty("timestamp")
Long timestamp,
@JsonProperty("nonce")
String nonce,
@JsonProperty("signature_type")
String signatureType,
@JsonProperty("document_name")
String documentName,
@JsonProperty("document_type")
String documentType,
@JsonProperty("purpose")
String purpose,
@JsonProperty("challenge")
String challenge
) implements Serializable {
public static final String PROTOCOL_VERSION = "1";
public CcspRequest(
String version, String requestId, String applicationId, String operation,
String algorithm, String digest, String certificateHint, String expiresAt,
Long timestamp, String nonce, String signatureType
) {
this(version, requestId, applicationId, operation, algorithm, digest,
certificateHint, expiresAt, timestamp, nonce, signatureType,
null, null, null, null);
}
public boolean isVersionSupported() {
return PROTOCOL_VERSION.equals(version);
}
public boolean isAuthentication() {
return "authenticate".equalsIgnoreCase(operation)
|| "AUTHENTICATION".equalsIgnoreCase(purpose)
|| "AUTH_CHALLENGE".equalsIgnoreCase(documentType)
|| (challenge != null && !challenge.isBlank());
}
public boolean isExpired() {
if (expiresAt == null || expiresAt.isBlank()) {
return false;
}
try {
Instant expiry = Instant.parse(expiresAt);
return Instant.now().isAfter(expiry);
} catch (Exception e) {
return false;
}
}
}
@@ -0,0 +1,136 @@
package br.com.cybercore.signer.core.protocol;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
/**
* Mensagem de resposta do CyberCore Signing Protocol (CCSP/1).
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record CcspResponse(
@JsonProperty("version")
String version,
@JsonProperty("request_id")
String requestId,
@JsonProperty("status")
String status,
@JsonProperty("signature")
String signature,
@JsonProperty("certificate")
String certificate,
@JsonProperty("certificate_chain")
List<String> certificateChain,
@JsonProperty("algorithm")
String algorithm,
@JsonProperty("error_code")
String errorCode,
@JsonProperty("message")
String message,
@JsonProperty("subject_name")
String subjectName,
@JsonProperty("subject_cpf_cnpj")
String subjectCpfCnpj,
@JsonProperty("document_name")
String documentName,
@JsonProperty("document_type")
String documentType
) implements Serializable {
public CcspResponse {
certificateChain = certificateChain != null ? List.copyOf(certificateChain) : null;
}
@Override
public List<String> certificateChain() {
return certificateChain != null ? Collections.unmodifiableList(certificateChain) : null;
}
public static final String STATUS_SIGNED = "signed";
public static final String STATUS_AUTHENTICATED = "authenticated";
public static final String STATUS_SUCCESS = "success";
public static final String STATUS_ERROR = "error";
public static CcspResponse success(String requestId, String signatureBase64, String certBase64,
List<String> chain, String algorithm) {
return success(requestId, signatureBase64, certBase64, chain, algorithm, null, null, null, null);
}
public static CcspResponse success(String requestId, String signatureBase64, String certBase64,
List<String> chain, String algorithm,
String subjectName, String subjectCpfCnpj,
String documentName, String documentType) {
return new CcspResponse(
CcspRequest.PROTOCOL_VERSION,
requestId,
STATUS_SIGNED,
signatureBase64,
certBase64,
chain,
algorithm,
null,
null,
subjectName,
subjectCpfCnpj,
documentName,
documentType
);
}
public static CcspResponse authenticated(String requestId, String signatureBase64, String certBase64,
List<String> chain, String algorithm,
String subjectName, String subjectCpfCnpj) {
return new CcspResponse(
CcspRequest.PROTOCOL_VERSION,
requestId,
STATUS_AUTHENTICATED,
signatureBase64,
certBase64,
chain,
algorithm,
null,
null,
subjectName,
subjectCpfCnpj,
null,
"AUTH_CHALLENGE"
);
}
public static CcspResponse error(String requestId, CcspErrorCode errorCode, String message) {
return new CcspResponse(
CcspRequest.PROTOCOL_VERSION,
requestId,
STATUS_ERROR,
null,
null,
null,
null,
errorCode.name(),
message != null ? message : errorCode.getDefaultMessage(),
null,
null,
null,
null
);
}
public static CcspResponse error(String requestId, CcspErrorCode errorCode) {
return error(requestId, errorCode, errorCode.getDefaultMessage());
}
}
@@ -0,0 +1,550 @@
package br.com.cybercore.signer.core.protocol;
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import java.security.MessageDigest;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.provider.CertificateProviderException;
import br.com.cybercore.signer.core.security.ApplicationRegistry;
import br.com.cybercore.signer.core.security.AuthorizedApplication;
import br.com.cybercore.signer.core.security.NonceManager;
import br.com.cybercore.signer.core.security.OriginValidator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Servidor HTTP local do CyberCore Signing Protocol (CCSP/1).
* Vinculado exclusivamente ao loopback 127.0.0.1 para comunicação segura com navegadores web.
*/
public class CcspServer {
private static final Logger logger = LoggerFactory.getLogger(CcspServer.class);
public static final int DEFAULT_PORT = 21325;
public static final String BIND_HOST = "127.0.0.1";
private final int port;
private final CertificateManager certificateManager;
private final SigningService signingService;
private final ApplicationRegistry applicationRegistry;
private final NonceManager nonceManager;
private final OriginValidator originValidator;
private final CcspUserPromptListener promptListener;
private final ObjectMapper objectMapper;
private final ReentrantLock signLock = new ReentrantLock(true); // Concorrência FIFO
private HttpServer httpServer;
private ExecutorService executor;
private volatile boolean running = false;
public CcspServer(int port,
CertificateManager certificateManager,
SigningService signingService,
ApplicationRegistry applicationRegistry,
CcspUserPromptListener promptListener) {
this.port = port;
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
this.promptListener = promptListener;
this.nonceManager = new NonceManager();
this.originValidator = new OriginValidator();
this.objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public CcspServer(CertificateManager certificateManager,
SigningService signingService,
CcspUserPromptListener promptListener) {
this(DEFAULT_PORT, certificateManager, signingService, ApplicationRegistry.getInstance(), promptListener);
}
/**
* Inicia o servidor local na porta configurada.
*/
public synchronized void start() throws IOException {
if (running) {
return;
}
InetSocketAddress address = new InetSocketAddress(BIND_HOST, port);
httpServer = HttpServer.create(address, 50);
executor = Executors.newFixedThreadPool(8, r -> {
Thread t = new Thread(r, "ccsp-http-worker");
t.setDaemon(true);
return t;
});
httpServer.setExecutor(executor);
// Registra endpoints do protocolo CCSP/1
httpServer.createContext("/ccsp/v1/status", new StatusHandler());
httpServer.createContext("/ccsp/v1/certificates", new CertificatesHandler());
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
httpServer.start();
running = true;
logger.info("Servidor local CCSP/1 iniciado com sucesso em http://{}:{}", BIND_HOST, getPort());
}
/**
* Encerra a execução do servidor local.
*/
public synchronized void stop() {
if (!running) {
return;
}
if (httpServer != null) {
httpServer.stop(1);
httpServer = null;
}
if (executor != null) {
executor.shutdownNow();
executor = null;
}
running = false;
logger.info("Servidor local CCSP/1 finalizado.");
}
public boolean isRunning() {
return running;
}
public int getPort() {
if (httpServer != null) {
return httpServer.getAddress().getPort();
}
return port;
}
// =========================================================================
// Handlers HTTP
// =========================================================================
private class StatusHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (handleCors(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas GET é suportado.");
return;
}
Map<String, Object> resp = new HashMap<>();
resp.put("version", CcspRequest.PROTOCOL_VERSION);
resp.put("status", "ready");
resp.put("agent", "CyberCore Signer");
resp.put("agent_version", "0.1.0");
resp.put("os", System.getProperty("os.name"));
resp.put("timestamp", Instant.now().toString());
sendJsonResponse(exchange, 200, resp);
}
}
private class CertificatesHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (handleCors(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas GET é suportado.");
return;
}
String origin = getOrigin(exchange);
String appId = exchange.getRequestHeaders().getFirst("X-CyberCore-AppId");
if (appId == null || appId.isBlank()) {
URI uri = exchange.getRequestURI();
appId = getQueryParam(uri, "application_id");
}
// Verifica autorização se app_id e origin forem informados
if (appId != null && !appId.isBlank()) {
boolean authorized = applicationRegistry.isAuthorized(appId, origin);
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;
}
}
}
List<ManagedCertificate> certs = certificateManager.getAvailableCertificates();
if (certs.isEmpty()) {
certs = certificateManager.discoverCertificates();
}
List<Map<String, Object>> certDtoList = certs.stream().map(c -> {
Map<String, Object> dto = new HashMap<>();
dto.put("id", c.id());
dto.put("alias", c.alias());
dto.put("common_name", c.commonName());
dto.put("subject_dn", c.subjectDn());
dto.put("issuer_dn", c.issuerDn());
dto.put("type", c.type().name());
dto.put("provider_id", c.providerId());
dto.put("device_name", c.hardwareDeviceName());
dto.put("valid_from", c.notBefore() != null ? c.notBefore().toString() : null);
dto.put("valid_to", c.notAfter() != null ? c.notAfter().toString() : null);
dto.put("serial_number", c.serialNumber());
dto.put("can_sign", c.canSign());
dto.put("status", c.status().name());
return dto;
}).toList();
Map<String, Object> resp = new HashMap<>();
resp.put("version", CcspRequest.PROTOCOL_VERSION);
resp.put("status", "success");
resp.put("certificates", certDtoList);
sendJsonResponse(exchange, 200, resp);
}
}
private class AuthorizeHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (handleCors(exchange)) return;
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado.");
return;
}
String origin = getOrigin(exchange);
try (InputStream is = exchange.getRequestBody()) {
@SuppressWarnings("unchecked")
Map<String, String> body = objectMapper.readValue(is, Map.class);
String appId = body.get("application_id");
String appName = body.getOrDefault("name", "Aplicação Web");
if (appId == null || appId.isBlank()) {
sendErrorResponse(exchange, 400, "BAD_REQUEST", "application_id é obrigatório.");
return;
}
if (applicationRegistry.isAuthorized(appId, origin)) {
Map<String, Object> resp = Map.of(
"version", CcspRequest.PROTOCOL_VERSION,
"status", "authorized",
"application_id", appId
);
sendJsonResponse(exchange, 200, resp);
return;
}
boolean approved = promptApplicationAuthorization(origin, appName, appId);
if (approved) {
Map<String, Object> resp = Map.of(
"version", CcspRequest.PROTOCOL_VERSION,
"status", "authorized",
"application_id", appId
);
sendJsonResponse(exchange, 200, resp);
} else {
sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(),
"Autorização negada pelo usuário.");
}
} catch (Exception e) {
sendErrorResponse(exchange, 400, "BAD_REQUEST", "Payload JSON inválido: " + e.getMessage());
}
}
}
private class SignHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (handleCors(exchange)) return;
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado.");
return;
}
String origin = getOrigin(exchange);
CcspRequest request;
try (InputStream is = exchange.getRequestBody()) {
request = objectMapper.readValue(is, CcspRequest.class);
} catch (Exception e) {
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
return;
}
// 1. Validação de versão
if (!request.isVersionSupported()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
"Versão do protocolo não suportada: " + request.version());
return;
}
// 2. Validação de expiração
if (request.isExpired()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
"A requisição de assinatura expirou.");
return;
}
// 3. Validação anti-replay (Nonce)
if (request.nonce() != null && !request.nonce().isBlank()) {
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
String appId = request.applicationId();
if (appId == null || appId.isBlank()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
"application_id é obrigatório.");
return;
}
if (!applicationRegistry.isAuthorized(appId, origin)) {
boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
if (!approved) {
sendCcspError(exchange, 403, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
"Aplicação não autorizada pelo usuário.");
return;
}
}
// 5. Validação de digest / challenge
byte[] digestBytes;
if (request.digest() != null && !request.digest().isBlank()) {
try {
digestBytes = Base64.getDecoder().decode(request.digest());
} catch (Exception e) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
"Digest em Base64 inválido.");
return;
}
} else if (request.challenge() != null && !request.challenge().isBlank()) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
digestBytes = md.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
"Falha ao gerar digest do desafio: " + e.getMessage());
return;
}
} else {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
"É obrigatório informar 'digest' ou 'challenge' para a operação.");
return;
}
// 6. Resolução do certificado
Optional<ManagedCertificate> certOpt = certificateManager.findByHint(request.certificateHint());
if (certOpt.isEmpty()) {
sendCcspError(exchange, 404, request.requestId(), CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Nenhum certificado correspondente encontrado.");
return;
}
ManagedCertificate cert = certOpt.get();
// 7. Confirmação do usuário na UI (se listener configurado)
if (promptListener != null) {
try {
CompletableFuture<Boolean> approvalFuture = promptListener.requestSignatureConfirmation(request, cert);
Boolean confirmed = approvalFuture.get(60, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(confirmed)) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
"Operação de assinatura cancelada pelo usuário.");
return;
}
} catch (Exception e) {
logger.warn("Falha ou timeout na confirmação visual de assinatura: {}", e.getMessage());
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
"Confirmação da assinatura expirou ou foi cancelada.");
return;
}
}
// 8. Assinatura com controle sequencial FIFO
signLock.lock();
try {
SigningContext ctx = new SigningContext(
request.requestId(),
request.applicationId(),
digestBytes,
request.algorithm() != null ? request.algorithm() : "SHA256withRSA",
cert.id(),
Instant.now()
);
SigningResult result = signingService.sign(ctx);
String sigBase64 = Base64.getEncoder().encodeToString(result.signatureBytes());
String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
boolean isAuth = request.isAuthentication() || exchange.getRequestURI().getPath().endsWith("/authenticate");
CcspResponse response = isAuth
? CcspResponse.authenticated(
request.requestId(),
sigBase64,
result.certificateBase64(),
result.certificateChainBase64(),
result.algorithm(),
subjectName,
cpfCnpj
)
: CcspResponse.success(
request.requestId(),
sigBase64,
result.certificateBase64(),
result.certificateChainBase64(),
result.algorithm(),
subjectName,
cpfCnpj,
request.documentName(),
request.documentType()
);
sendJsonResponse(exchange, 200, response);
} catch (CertificateProviderException cpe) {
sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
} catch (Exception e) {
logger.error("Erro inesperado na assinatura: {}", e.getMessage(), e);
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
"Erro interno: " + e.getMessage());
} finally {
signLock.unlock();
}
}
}
// =========================================================================
// Utilitários de Segurança e HTTP
// =========================================================================
private boolean promptApplicationAuthorization(String origin, String appName, String appId) {
if (promptListener == null) {
// Em testes unitários sem UI, aprova e registra
applicationRegistry.register(new AuthorizedApplication(
appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now()
));
return true;
}
try {
CompletableFuture<Boolean> future = promptListener.requestApplicationAuthorization(origin, appName, appId);
Boolean approved = future.get(30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(approved)) {
applicationRegistry.register(new AuthorizedApplication(
appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now()
));
return true;
}
} catch (Exception e) {
logger.warn("Timeout ou erro na autorização da aplicação: {}", e.getMessage());
}
return false;
}
private boolean handleCors(HttpExchange exchange) throws IOException {
String origin = getOrigin(exchange);
if (origin == null || origin.isBlank()) {
origin = "*";
}
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");
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
exchange.close();
return true;
}
return false;
}
private String getOrigin(HttpExchange exchange) {
String origin = exchange.getRequestHeaders().getFirst("Origin");
if (origin == null || origin.isBlank()) {
origin = exchange.getRequestHeaders().getFirst("Referer");
}
return origin;
}
private String getQueryParam(URI uri, String key) {
if (uri.getQuery() == null) return null;
for (String param : uri.getQuery().split("&")) {
String[] pair = param.split("=");
if (pair.length == 2 && pair[0].equalsIgnoreCase(key)) {
return pair[1];
}
}
return null;
}
private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException {
byte[] bytes = objectMapper.writeValueAsBytes(data);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException {
Map<String, Object> err = Map.of(
"version", CcspRequest.PROTOCOL_VERSION,
"status", "error",
"error_code", code,
"message", message
);
sendJsonResponse(exchange, httpStatus, err);
}
private void sendCcspError(HttpExchange exchange, int httpStatus, String reqId, CcspErrorCode code, String msg) throws IOException {
CcspResponse err = CcspResponse.error(reqId, code, msg != null ? msg : code.getDefaultMessage());
sendJsonResponse(exchange, httpStatus, err);
}
}
@@ -0,0 +1,31 @@
package br.com.cybercore.signer.core.protocol;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import java.util.concurrent.CompletableFuture;
/**
* Interface de interação com o usuário para autorização de aplicações e confirmação de assinaturas.
* Permite desacoplar o servidor HTTP de comunicação da interface gráfica desktop (JavaFX).
*/
public interface CcspUserPromptListener {
/**
* Solicita ao usuário autorização para que uma nova aplicação web se comunique com o agente.
*
* @param origin Origem HTTP/HTTPS da aplicação (ex: https://sistema.exemplo.com.br).
* @param appName Nome amigável da aplicação solicitante.
* @param applicationId ID único da aplicação.
* @return Future completado com {@code true} se o usuário autorizar; {@code false} caso recuse.
*/
CompletableFuture<Boolean> requestApplicationAuthorization(String origin, String appName, String applicationId);
/**
* Solicita ao usuário confirmação visual explícita antes de realizar uma assinatura digital.
*
* @param request Requisição CCSP de assinatura recebida.
* @param certificate Certificado digital selecionado para a operação.
* @return Future completado com {@code true} se o usuário confirmar; {@code false} caso cancele.
*/
CompletableFuture<Boolean> requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate);
}
@@ -0,0 +1,194 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Gerenciador central de certificados digitais do CyberCore Signer.
* Responsável pela descoberta em múltiplos provedores (Windows Certificate Store, A1, PKCS#11),
* validação de integridade, mapeamento de provedores e seleção segura para operações de assinatura.
*/
public class CertificateManager {
private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class);
private final List<CertificateProvider> providers = new CopyOnWriteArrayList<>();
private final Map<String, CertificateProvider> certificateToProviderMap = new ConcurrentHashMap<>();
private final Map<String, ManagedCertificate> cachedCertificates = new ConcurrentHashMap<>();
public CertificateManager() {
}
/**
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
*/
public static CertificateManager createDefault() {
CertificateManager manager = new CertificateManager();
manager.registerProvider(new WindowsCertificateStoreProvider());
manager.registerProvider(new Pkcs12CertificateProvider());
manager.registerProvider(new Pkcs11CertificateProvider());
return manager;
}
/**
* Registra um provedor de certificados no ecossistema do agente.
*/
public void registerProvider(CertificateProvider provider) {
Objects.requireNonNull(provider, "Provedor não pode ser nulo.");
// Evita duplicidade por providerId
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId()));
providers.add(provider);
logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})",
provider.getDisplayName(), provider.getProviderId(), provider.isAvailable());
}
/**
* Remove um provedor previamente registrado.
*/
public void unregisterProvider(String providerId) {
if (providerId != null) {
providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId));
}
}
public List<CertificateProvider> getProviders() {
return Collections.unmodifiableList(providers);
}
/**
* Executa a descoberta de certificados em todos os provedores operacionais.
* Consolida os resultados, atualiza o mapeamento de provedores e ordena por relevância/disponibilidade.
*/
public synchronized List<ManagedCertificate> discoverCertificates() {
logger.info("Iniciando descoberta de certificados digitais nos provedores ativos...");
certificateToProviderMap.clear();
cachedCertificates.clear();
List<ManagedCertificate> discovered = new ArrayList<>();
for (CertificateProvider provider : providers) {
if (!provider.isAvailable()) {
logger.debug("Provedor '{}' não disponível no momento. Ignorando na descoberta.", provider.getProviderId());
continue;
}
try {
List<ManagedCertificate> certs = provider.listCertificates();
logger.info("Provedor '{}' retornou {} certificado(s).", provider.getProviderId(), certs.size());
for (ManagedCertificate cert : certs) {
certificateToProviderMap.put(cert.id(), provider);
cachedCertificates.put(cert.id(), cert);
discovered.add(cert);
}
} catch (Exception e) {
logger.error("Erro ao listar certificados do provedor '{}': {}", provider.getProviderId(), e.getMessage());
}
}
// Ordenação: certificados disponíveis primeiro, depois por nome do titular
discovered.sort(Comparator
.comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1)
.thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER));
logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size());
return Collections.unmodifiableList(discovered);
}
/**
* Retorna o provedor responsável pelo certificado com o ID informado.
*/
public CertificateProvider getProviderForCertificate(String certId) {
if (certId == null) {
return null;
}
return certificateToProviderMap.get(certId);
}
/**
* Obtém o material criptográfico (chave privada e cadeia X.509) do certificado especificado.
*/
public Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certId, char[] pinOrPassword) throws CertificateProviderException {
CertificateProvider provider = getProviderForCertificate(certId);
if (provider == null) {
return Optional.empty();
}
return provider.getKeyMaterial(certId, pinOrPassword);
}
/**
* Busca um certificado previamente descoberto pelo seu identificador único.
*/
public Optional<ManagedCertificate> findById(String certId) {
if (certId == null) {
return Optional.empty();
}
return Optional.ofNullable(cachedCertificates.get(certId));
}
/**
* Localiza um certificado usando hints flexíveis: id, alias, CPF, CNPJ ou impressões digitais SHA-1/SHA-256.
*/
public Optional<ManagedCertificate> findByHint(String hint) {
if (hint == null || hint.isBlank()) {
return Optional.empty();
}
String cleanHint = hint.trim();
// 1. Busca por ID direto
ManagedCertificate direct = cachedCertificates.get(cleanHint);
if (direct != null) {
return Optional.of(direct);
}
// 2. Busca por alias, CPF, CNPJ ou fingerprints
for (ManagedCertificate cert : cachedCertificates.values()) {
if (cleanHint.equalsIgnoreCase(cert.alias())
|| cleanHint.equalsIgnoreCase(cert.cpf())
|| cleanHint.equalsIgnoreCase(cert.cnpj())
|| cleanHint.equalsIgnoreCase(cert.sha1Fingerprint())
|| cleanHint.equalsIgnoreCase(cert.sha256Fingerprint())
|| cleanHint.equalsIgnoreCase(cert.serialNumber())) {
return Optional.of(cert);
}
}
return Optional.empty();
}
/**
* Retorna a lista dos certificados em cache que possuem status AVAILABLE (com chave privada e vigentes).
*/
public List<ManagedCertificate> getAvailableCertificates() {
return cachedCertificates.values().stream()
.filter(c -> c.status() == CertificateStatus.AVAILABLE)
.toList();
}
/**
* Solicita recarregamento de todos os provedores cadastrados.
*/
public void refreshAll() {
for (CertificateProvider provider : providers) {
try {
provider.refresh();
} catch (Exception e) {
logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage());
}
}
discoverCertificates();
}
}
@@ -0,0 +1,61 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import java.util.List;
/**
* Contrato base para provedores de descoberta e assinatura com certificados digitais.
* Permite plugar diferentes backends (A1 em arquivo, Windows-MY, PKCS#11, Mock)
* sem acoplar o núcleo do aplicativo a fabricantes ou dispositivos específicos.
*/
public interface CertificateProvider {
/**
* Identificador único do provedor (ex: "provider-a1", "provider-win-store", "provider-pkcs11").
*/
String getProviderId();
/**
* Nome legível para apresentação na interface e diagnósticos.
*/
String getDisplayName();
/**
* Tipo do provedor.
*/
ProviderType getType();
/**
* Indica se o provedor está disponível e operacional no sistema atual.
*/
boolean isAvailable();
/**
* Lista os certificados disponíveis atualmente através deste provedor.
*/
List<ManagedCertificate> listCertificates() throws CertificateProviderException;
/**
* Executa a assinatura do digest contido no contexto.
* A chave privada NUNCA deve sair do provedor/dispositivo.
*/
SigningResult sign(SigningContext context) throws CertificateProviderException;
/**
* Retorna o material criptográfico (chave privada e cadeia) do certificado indicado,
* permitindo que operações que montam envelopes como PAdES ou XMLDSig operem localmente.
*/
default java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
return java.util.Optional.empty();
}
/**
* Solicita recarga ou reescaneamento de certificados e dispositivos.
*/
default void refresh() {
// Implementação padrão opcional
}
}
@@ -0,0 +1,32 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
/**
* Exceção disparada por falhas nos provedores de certificado.
* Carrega obrigatoriamente um código de erro padronizado do CCSP.
*/
public class CertificateProviderException extends Exception {
private static final long serialVersionUID = 1L;
private final CcspErrorCode errorCode;
public CertificateProviderException(CcspErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED;
}
public CertificateProviderException(CcspErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED;
}
public CertificateProviderException(CcspErrorCode errorCode) {
this(errorCode, errorCode != null ? errorCode.getDefaultMessage() : CcspErrorCode.SIGNATURE_FAILED.getDefaultMessage());
}
public CcspErrorCode getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,247 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
/**
* Provedor de certificados A3 via interface padrão PKCS#11 (tokens USB, smart cards e leitores).
* Utiliza o provider SunPKCS11 integrado à JVM para comunicação com middlewares dos fabricantes.
* Nunca persiste PIN em disco e nunca o transmite para servidores web.
*/
public class Pkcs11CertificateProvider implements CertificateProvider {
private static final Logger logger = LoggerFactory.getLogger(Pkcs11CertificateProvider.class);
private static final String PROVIDER_ID = "provider-pkcs11";
/**
* Bibliotecas PKCS#11 comuns de middleware para tokens e smartcards no Windows e Linux.
*/
private static final String[] WELL_KNOWN_LIBRARIES = {
// Windows
"C:/Windows/System32/eps2003csp11.dll", // SafeNet / eToken
"C:/Windows/System32/aetpkss1.dll", // SafeSign AET
"C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient
"C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows
// Linux
"/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"/usr/lib/opensc-pkcs11.so",
"/usr/lib/libaetpkss.so"
};
private Provider sunPkcs11Provider;
private KeyStore keyStore;
private String detectedLibraryPath;
public Pkcs11CertificateProvider() {
detectAndInitialize();
}
private synchronized void detectAndInitialize() {
for (String libPath : WELL_KNOWN_LIBRARIES) {
File f = new File(libPath);
if (f.exists() && f.canRead()) {
detectedLibraryPath = libPath;
logger.info("Middleware PKCS#11 detectado no sistema: {}", libPath);
break;
}
}
}
/**
* Permite registrar explicitamente um provider PKCS#11 já inicializado (ex: via SunPKCS11 com PIN do usuário).
*/
public synchronized void setProvider(Provider provider, KeyStore keyStore) {
this.sunPkcs11Provider = provider;
this.keyStore = keyStore;
}
@Override
public String getProviderId() {
return PROVIDER_ID;
}
@Override
public String getDisplayName() {
return "Hardware PKCS#11 (Smart Card / Token USB)";
}
@Override
public ProviderType getType() {
return ProviderType.A3_PKCS11;
}
@Override
public synchronized boolean isAvailable() {
return (detectedLibraryPath != null) || (keyStore != null);
}
public synchronized String getDetectedLibraryPath() {
return detectedLibraryPath;
}
@Override
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
if (keyStore == null) {
// Se o middleware estiver detectado mas o token ainda não estiver autenticado via PIN
return Collections.emptyList();
}
try {
List<ManagedCertificate> certificates = new ArrayList<>();
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
boolean isKeyEntry = keyStore.isKeyEntry(alias);
Certificate cert = keyStore.getCertificate(alias);
if (!(cert instanceof X509Certificate x509)) {
continue;
}
String certId = "p11-" + Integer.toHexString(alias.hashCode());
ManagedCertificate mc = X509CertificateUtils.toManagedCertificate(
certId,
alias,
x509,
CertificateType.A3,
isKeyEntry,
PROVIDER_ID,
"Token USB / Smart Card (PKCS#11)"
);
certificates.add(mc);
}
return Collections.unmodifiableList(certificates);
} catch (KeyStoreException e) {
throw new CertificateProviderException(
CcspErrorCode.PKCS11_ERROR,
"Falha ao ler certificados do dispositivo PKCS#11: " + e.getMessage(),
e
);
}
}
@Override
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
if (keyStore == null) {
throw new CertificateProviderException(
CcspErrorCode.TOKEN_NOT_FOUND,
"Dispositivo PKCS#11 não conectado ou não autenticado com PIN."
);
}
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
alg.validateDigestLength(context.digest());
String targetAlias = resolveAlias(context.certificateHint());
if (targetAlias == null) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Certificado não encontrado no dispositivo PKCS#11."
);
}
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
if (privateKey == null) {
throw new CertificateProviderException(
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
"Chave privada inacessível no dispositivo PKCS#11."
);
}
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
if (chain == null || chain.length == 0) {
Certificate singleCert = keyStore.getCertificate(targetAlias);
if (singleCert != null) {
chain = new Certificate[]{singleCert};
} else {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Cadeia do certificado não encontrada no dispositivo PKCS#11."
);
}
}
logger.info("Executando assinatura digital no hardware PKCS#11 [alg={}]", alg.getJcaName());
Signature signature = (sunPkcs11Provider != null)
? Signature.getInstance(alg.getJcaName(), sunPkcs11Provider)
: Signature.getInstance(alg.getJcaName());
signature.initSign(privateKey);
signature.update(context.digest());
byte[] signatureBytes = signature.sign();
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
List<String> chainBase64 = new ArrayList<>();
for (Certificate c : chain) {
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
}
return new SigningResult(
context.requestId(),
signatureBytes,
certBase64,
chainBase64,
alg.getJcaName(),
Instant.now()
);
} catch (CertificateProviderException cpe) {
throw cpe;
} catch (Exception e) {
logger.error("Erro na assinatura via dispositivo PKCS#11: {}", e.getMessage(), e);
throw new CertificateProviderException(
CcspErrorCode.PKCS11_ERROR,
"Erro ao assinar com o dispositivo PKCS#11: " + e.getMessage(),
e
);
}
}
private String resolveAlias(String hint) {
try {
Enumeration<String> aliases = keyStore.aliases();
List<String> list = Collections.list(aliases);
if (hint == null || hint.isBlank()) {
return list.isEmpty() ? null : list.get(0);
}
for (String alias : list) {
String certId = "p11-" + Integer.toHexString(alias.hashCode());
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
return alias;
}
}
} catch (Exception ignored) {
}
return null;
}
}
@@ -0,0 +1,318 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
/**
* Provedor de certificados A1 em formato PKCS#12 (.pfx / .p12).
* Mantém chaves e senhas isoladas na memória local do agente sem persistência insegura ou vazamento em logs.
*/
public class Pkcs12CertificateProvider implements CertificateProvider {
private static final Logger logger = LoggerFactory.getLogger(Pkcs12CertificateProvider.class);
private static final String PROVIDER_ID = "provider-a1";
private final String providerId;
private final String displayName;
private KeyStore keyStore;
private char[] currentPassword;
public Pkcs12CertificateProvider() {
this(PROVIDER_ID, "Certificados A1 (PKCS#12)");
}
public Pkcs12CertificateProvider(String providerId, String displayName) {
this.providerId = Objects.requireNonNull(providerId);
this.displayName = Objects.requireNonNull(displayName);
}
/**
* Carrega um KeyStore PKCS#12 diretamente a partir de bytes.
* A senha é utilizada exclusivamente para decifrar a chave em memória e nunca é gravada em logs.
*/
public synchronized void loadFromBytes(byte[] pfxData, char[] password) throws CertificateProviderException {
Objects.requireNonNull(pfxData, "Dados do certificado não podem ser nulos.");
try (ByteArrayInputStream bais = new ByteArrayInputStream(pfxData)) {
loadFromStream(bais, password);
} catch (CertificateProviderException cpe) {
throw cpe;
} catch (Exception e) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_INVALID,
"Falha ao processar arquivo PKCS#12: " + e.getMessage(),
e
);
}
}
/**
* Carrega um KeyStore PKCS#12 a partir de um InputStream.
*/
public synchronized void loadFromStream(InputStream stream, char[] password) throws CertificateProviderException {
try {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(stream, password);
this.keyStore = ks;
this.currentPassword = password != null ? password.clone() : new char[0];
logger.info("KeyStore PKCS#12 carregado com sucesso no provedor '{}'", providerId);
} catch (Exception e) {
String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
if (msg.contains("password") || msg.contains("keystore password was incorrect") || msg.contains("mac")) {
throw new CertificateProviderException(
CcspErrorCode.PIN_INCORRECT,
"Senha do certificado A1 incorreta.",
e
);
}
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_INVALID,
"Não foi possível carregar o arquivo PKCS#12: " + e.getMessage(),
e
);
}
}
/**
* Define diretamente uma instância de KeyStore PKCS#12 pré-carregada (útil em testes ou integrações).
*/
public synchronized void setKeyStore(KeyStore keyStore, char[] password) {
this.keyStore = keyStore;
this.currentPassword = password != null ? password.clone() : new char[0];
}
public synchronized void clear() {
this.keyStore = null;
if (this.currentPassword != null) {
java.util.Arrays.fill(this.currentPassword, ' ');
this.currentPassword = null;
}
}
@Override
public String getProviderId() {
return providerId;
}
@Override
public String getDisplayName() {
return displayName;
}
@Override
public ProviderType getType() {
return ProviderType.A1_FILE;
}
@Override
public synchronized boolean isAvailable() {
return keyStore != null;
}
@Override
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
if (!isAvailable()) {
return Collections.emptyList();
}
try {
List<ManagedCertificate> certificates = new ArrayList<>();
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
boolean isKeyEntry = keyStore.isKeyEntry(alias);
Certificate cert = keyStore.getCertificate(alias);
if (!(cert instanceof X509Certificate x509)) {
continue;
}
String certId = "a1-" + Integer.toHexString(alias.hashCode());
ManagedCertificate mc = X509CertificateUtils.toManagedCertificate(
certId,
alias,
x509,
CertificateType.A1,
isKeyEntry,
providerId,
"Arquivo PKCS#12 (.pfx/.p12)"
);
certificates.add(mc);
}
return Collections.unmodifiableList(certificates);
} catch (KeyStoreException e) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"Erro ao consultar certificados do KeyStore PKCS#12: " + e.getMessage(),
e
);
}
}
@Override
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
if (!isAvailable()) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"Nenhum certificado A1 carregado neste provedor."
);
}
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
alg.validateDigestLength(context.digest());
String targetAlias = resolveAlias(context.certificateHint());
if (targetAlias == null) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Certificado A1 não encontrado para a requisição: " + context.certificateHint()
);
}
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, currentPassword);
if (privateKey == null) {
throw new CertificateProviderException(
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
"Chave privada não encontrada para o certificado: " + targetAlias
);
}
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
if (chain == null || chain.length == 0) {
Certificate singleCert = keyStore.getCertificate(targetAlias);
if (singleCert != null) {
chain = new Certificate[]{singleCert};
} else {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Cadeia de certificados ausente para o alias: " + targetAlias
);
}
}
logger.info("Executando assinatura com certificado A1 [alg={}]", alg.getJcaName());
Signature signature = Signature.getInstance(alg.getJcaName());
signature.initSign(privateKey);
signature.update(context.digest());
byte[] signatureBytes = signature.sign();
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
List<String> chainBase64 = new ArrayList<>();
for (Certificate c : chain) {
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
}
return new SigningResult(
context.requestId(),
signatureBytes,
certBase64,
chainBase64,
alg.getJcaName(),
Instant.now()
);
} catch (CertificateProviderException cpe) {
throw cpe;
} catch (Exception e) {
logger.error("Erro ao realizar assinatura com certificado A1: {}", e.getMessage(), e);
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"Erro ao assinar com certificado A1: " + e.getMessage(),
e
);
}
}
@Override
public synchronized java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
if (!isAvailable()) {
return java.util.Optional.empty();
}
String targetAlias = resolveAlias(certificateId);
if (targetAlias == null) {
return java.util.Optional.empty();
}
char[] passToUse = (currentPassword != null && currentPassword.length > 0) ? currentPassword : pinOrPassword;
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, passToUse);
if (privateKey == null) {
return java.util.Optional.empty();
}
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
if (chain == null || chain.length == 0) {
Certificate single = keyStore.getCertificate(targetAlias);
if (single != null) {
chain = new Certificate[]{single};
}
}
if (chain == null || chain.length == 0) {
return java.util.Optional.empty();
}
X509Certificate[] x509Chain = new X509Certificate[chain.length];
for (int i = 0; i < chain.length; i++) {
x509Chain[i] = (X509Certificate) chain[i];
}
return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, null));
} catch (Exception e) {
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"Falha ao obter chave privada e certificados A1: " + e.getMessage(),
e
);
}
}
private String resolveAlias(String hint) {
try {
Enumeration<String> aliases = keyStore.aliases();
List<String> list = Collections.list(aliases);
if (hint == null || hint.isBlank()) {
return list.isEmpty() ? null : list.get(0);
}
for (String alias : list) {
String certId = "a1-" + Integer.toHexString(alias.hashCode());
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
return alias;
}
Certificate c = keyStore.getCertificate(alias);
if (c instanceof X509Certificate x509) {
String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1");
String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256");
if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) {
return alias;
}
}
}
} catch (Exception ignored) {
}
return null;
}
}
@@ -0,0 +1,21 @@
package br.com.cybercore.signer.core.provider;
/**
* Classificação da tecnologia ou repositório do provedor de certificados.
*/
public enum ProviderType {
A1_FILE("Arquivo PKCS#12 (.pfx/.p12)"),
A3_WINDOWS_STORE("Windows Certificate Store (CAPI/CNG)"),
A3_PKCS11("Hardware PKCS#11 (Smart Card / Token USB)"),
MOCK("Simulador de Desenvolvimento");
private final String description;
ProviderType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@@ -0,0 +1,307 @@
package br.com.cybercore.signer.core.provider;
import br.com.cybercore.signer.core.crypto.DigestAlgorithm;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.CertificateStatus;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.domain.SigningResult;
import br.com.cybercore.signer.core.protocol.CcspErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
/**
* Provedor de certificados integrado nativamente ao repositório de certificados do Windows (Windows-MY).
* Utiliza o subsistema SunMSCAPI/CAPI/CNG para gerenciar chaves privadas e certificados A1/A3 sem expor credenciais.
*/
public class WindowsCertificateStoreProvider implements CertificateProvider {
private static final Logger logger = LoggerFactory.getLogger(WindowsCertificateStoreProvider.class);
private static final String PROVIDER_ID = "provider-win-store";
private static final String KEYSTORE_TYPE = "Windows-MY";
private KeyStore keyStore;
private final boolean isWindows;
public WindowsCertificateStoreProvider() {
String osName = System.getProperty("os.name", "").toLowerCase();
this.isWindows = osName.contains("win");
if (this.isWindows) {
initKeyStore();
}
}
private synchronized void initKeyStore() {
try {
this.keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
this.keyStore.load(null, null);
logger.info("Windows Certificate Store (Windows-MY) inicializado com sucesso.");
} catch (Throwable t) {
logger.warn("Não foi possível carregar o repositório Windows-MY: {}", t.getMessage());
this.keyStore = null;
}
}
@Override
public String getProviderId() {
return PROVIDER_ID;
}
@Override
public String getDisplayName() {
return "Windows Certificate Store (Windows-MY)";
}
@Override
public ProviderType getType() {
return ProviderType.A3_WINDOWS_STORE;
}
@Override
public boolean isAvailable() {
return isWindows && keyStore != null;
}
@Override
public synchronized List<ManagedCertificate> listCertificates() throws CertificateProviderException {
if (!isAvailable()) {
return Collections.emptyList();
}
try {
// Recarrega o repositório para detectar tokens recém-inseridos
this.keyStore.load(null, null);
List<ManagedCertificate> certificates = new ArrayList<>();
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
try {
if (!keyStore.isKeyEntry(alias)) {
continue;
}
Certificate cert = keyStore.getCertificate(alias);
if (!(cert instanceof X509Certificate x509)) {
continue;
}
// Heurística de tipo A1 vs A3:
// ICP-Brasil A3 tem tipicamente 3 anos (validade > ~400 dias).
// A1 tem validade de até 1 ano (<= 370 dias).
long validityDays = ChronoUnit.DAYS.between(x509.getNotBefore().toInstant(), x509.getNotAfter().toInstant());
CertificateType certType = validityDays > 400 ? CertificateType.A3 : CertificateType.A1;
String deviceName = certType == CertificateType.A3
? "Token / Smart Card (Windows CAPI/CNG)"
: "Repositório do Windows (Software A1)";
String certId = "win-" + Integer.toHexString(alias.hashCode());
ManagedCertificate managedCert = X509CertificateUtils.toManagedCertificate(
certId,
alias,
x509,
certType,
true,
PROVIDER_ID,
deviceName
);
certificates.add(managedCert);
} catch (Exception e) {
logger.warn("Ignorando alias '{}' devido a erro na leitura: {}", alias, e.getMessage());
}
}
return Collections.unmodifiableList(certificates);
} catch (Exception e) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"Falha ao enumerar certificados do Windows Certificate Store: " + e.getMessage(),
e
);
}
}
@Override
public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException {
Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo.");
if (!isAvailable()) {
throw new CertificateProviderException(
CcspErrorCode.PROVIDER_NOT_AVAILABLE,
"Repositório Windows Certificate Store não está disponível neste sistema operacional."
);
}
// Valida algoritmo e tamanho de digest
DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm());
alg.validateDigestLength(context.digest());
String targetAlias = resolveAlias(context.certificateHint());
if (targetAlias == null) {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Nenhum certificado correspondente encontrado no Windows Certificate Store para o hint: " + context.certificateHint()
);
}
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
if (privateKey == null) {
throw new CertificateProviderException(
CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE,
"Chave privada não acessível para o certificado: " + targetAlias
);
}
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
if (chain == null || chain.length == 0) {
Certificate singleCert = keyStore.getCertificate(targetAlias);
if (singleCert != null) {
chain = new Certificate[]{singleCert};
} else {
throw new CertificateProviderException(
CcspErrorCode.CERTIFICATE_NOT_FOUND,
"Cadeia de certificação não encontrada para o alias: " + targetAlias
);
}
}
logger.info("Executando assinatura digital com algoritmo {} via Windows-MY para alias '{}'",
alg.getJcaName(), targetAlias);
Signature signature = Signature.getInstance(alg.getJcaName());
signature.initSign(privateKey);
signature.update(context.digest());
byte[] signatureBytes = signature.sign();
String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded());
List<String> chainBase64 = new ArrayList<>();
for (Certificate c : chain) {
chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded()));
}
return new SigningResult(
context.requestId(),
signatureBytes,
certBase64,
chainBase64,
alg.getJcaName(),
Instant.now()
);
} catch (CertificateProviderException cpe) {
throw cpe;
} catch (Exception e) {
logger.error("Erro durante a assinatura via Windows-MY: {}", e.getMessage(), e);
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"Erro ao assinar com o Windows Certificate Store: " + e.getMessage(),
e
);
} finally {
try {
// Força liberação de handles no SunMSCAPI para que o driver de SmartCard/Token
// possa reavaliar o estado de autenticação de PIN
if (keyStore != null) {
keyStore.load(null, null);
}
} catch (Exception ignored) {
}
// Se configurado para sempre exigir PIN (sem cache), forçamos o reset da sessão no Smart Card
// para que o Windows BaseCSP e o SafeSign invalidem o cache de processo e peçam o PIN na próxima assinatura
if (br.com.cybercore.signer.core.security.PinSessionManager.getInstance().isAlwaysPromptPin()) {
br.com.cybercore.signer.core.security.SmartCardSessionManager.resetCardSessions();
}
}
}
private String resolveAlias(String hint) {
if (hint == null || hint.isBlank()) {
return null;
}
try {
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
String certId = "win-" + Integer.toHexString(alias.hashCode());
if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) {
return alias;
}
Certificate c = keyStore.getCertificate(alias);
if (c instanceof X509Certificate x509) {
String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1");
String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256");
if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) {
return alias;
}
}
}
} catch (Exception ignored) {
}
return null;
}
@Override
public synchronized java.util.Optional<br.com.cybercore.signer.core.domain.SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException {
if (!isAvailable()) {
return java.util.Optional.empty();
}
String targetAlias = resolveAlias(certificateId);
if (targetAlias == null) {
return java.util.Optional.empty();
}
try {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null);
if (privateKey == null) {
return java.util.Optional.empty();
}
Certificate[] chain = keyStore.getCertificateChain(targetAlias);
if (chain == null || chain.length == 0) {
Certificate single = keyStore.getCertificate(targetAlias);
if (single != null) {
chain = new Certificate[]{single};
}
}
if (chain == null || chain.length == 0) {
return java.util.Optional.empty();
}
X509Certificate[] x509Chain = new X509Certificate[chain.length];
for (int i = 0; i < chain.length; i++) {
x509Chain[i] = (X509Certificate) chain[i];
}
return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, keyStore.getProvider()));
} catch (Exception e) {
throw new CertificateProviderException(
CcspErrorCode.SIGNATURE_FAILED,
"Falha ao obter chave privada e certificados no Windows Store: " + e.getMessage(),
e
);
}
}
@Override
public synchronized void refresh() {
if (isWindows) {
initKeyStore();
}
}
}
@@ -0,0 +1,162 @@
package br.com.cybercore.signer.core.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Registro de aplicações web autorizadas a se comunicar com o agente CyberCore Signer.
* Suporta persistência segura no diretório do usuário (~/.cybercore/authorized-apps.json).
*/
public final class ApplicationRegistry {
private static final Logger logger = LoggerFactory.getLogger(ApplicationRegistry.class);
private static final String DEFAULT_FILE_NAME = "authorized-apps.json";
private static final ApplicationRegistry INSTANCE = new ApplicationRegistry();
private final Path storagePath;
private final Map<String, AuthorizedApplication> applications = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public static ApplicationRegistry getInstance() {
return INSTANCE;
}
public ApplicationRegistry() {
this(resolveDefaultStoragePath());
}
public ApplicationRegistry(Path storagePath) {
this.storagePath = storagePath;
this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
load();
}
private static Path resolveDefaultStoragePath() {
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME);
}
/**
* Busca uma aplicação pelo seu ID único.
*/
public Optional<AuthorizedApplication> findById(String applicationId) {
if (applicationId == null || applicationId.isBlank()) {
return Optional.empty();
}
return Optional.ofNullable(applications.get(applicationId));
}
/**
* Busca uma aplicação que contenha a origem especificada autorizada.
*/
public Optional<AuthorizedApplication> findByOrigin(String origin) {
if (origin == null || origin.isBlank()) {
return Optional.empty();
}
return applications.values().stream()
.filter(app -> app.isOriginAllowed(origin))
.findFirst();
}
/**
* Verifica se uma aplicação e sua respectiva origem estão autorizadas e ativas.
*/
public boolean isAuthorized(String applicationId, String origin) {
Optional<AuthorizedApplication> appOpt = findById(applicationId);
return appOpt.map(app -> app.isOriginAllowed(origin)).orElse(false);
}
/**
* Registra ou atualiza uma aplicação autorizada e persiste a alteração.
*/
public synchronized void register(AuthorizedApplication app) {
Objects.requireNonNull(app, "Aplicação não pode ser nula");
applications.put(app.applicationId(), app);
logger.info("Aplicação registrada: '{}' [id={}] com origens: {}", app.name(), app.applicationId(), app.allowedOrigins());
save();
}
/**
* Revoga a autorização de uma aplicação.
*/
public synchronized void revoke(String applicationId) {
if (applicationId != null && applications.remove(applicationId) != null) {
logger.info("Autorização revogada para a aplicação [id={}]", applicationId);
save();
}
}
/**
* Retorna a lista imutável de todas as aplicações cadastradas.
*/
public List<AuthorizedApplication> listAll() {
return Collections.unmodifiableList(new ArrayList<>(applications.values()));
}
/**
* Carrega as aplicações persistidas em disco.
*/
public synchronized void load() {
if (storagePath == null || !Files.exists(storagePath)) {
logger.debug("Nenhum arquivo de aplicações persistidas encontrado em {}", storagePath);
return;
}
try {
List<AuthorizedApplication> list = objectMapper.readValue(
storagePath.toFile(),
new TypeReference<List<AuthorizedApplication>>() {}
);
applications.clear();
for (AuthorizedApplication app : list) {
applications.put(app.applicationId(), app);
}
logger.info("Carregadas {} aplicação(ões) autorizada(s) de {}", applications.size(), storagePath);
} catch (Exception e) {
logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage());
}
}
/**
* Salva as aplicações atuais em disco.
*/
public synchronized void save() {
if (storagePath == null) {
return;
}
try {
Path parent = storagePath.getParent();
if (parent != null && !Files.exists(parent)) {
Files.createDirectories(parent);
}
objectMapper.writerWithDefaultPrettyPrinter().writeValue(
storagePath.toFile(),
new ArrayList<>(applications.values())
);
logger.debug("Aplicações autorizadas salvas com sucesso em {}", storagePath);
} catch (Exception e) {
logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage());
}
}
public synchronized void clear() {
applications.clear();
save();
}
}
@@ -0,0 +1,44 @@
package br.com.cybercore.signer.core.security;
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Representa uma aplicação web autorizada a solicitar assinaturas ao CyberCore Signer.
*/
public record AuthorizedApplication(
String applicationId,
String name,
Set<String> allowedOrigins,
boolean enabled,
Instant registeredAt
) implements Serializable {
public AuthorizedApplication {
Objects.requireNonNull(applicationId, "applicationId must not be null");
Objects.requireNonNull(name, "name must not be null");
allowedOrigins = allowedOrigins != null ? Set.copyOf(allowedOrigins) : Set.of();
if (registeredAt == null) {
registeredAt = Instant.now();
}
}
/**
* Verifica se uma determinada Origin web está autorizada para esta aplicação.
*/
public boolean isOriginAllowed(String origin) {
if (!enabled || origin == null || origin.isBlank()) {
return false;
}
String normalized = origin.trim().toLowerCase();
for (String allowed : allowedOrigins) {
if (allowed.trim().toLowerCase().equals(normalized)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,53 @@
package br.com.cybercore.signer.core.security;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
/**
* Gerenciador de nonces em memória para proteção contra ataques de replay (CCSP/1).
* Garante que uma requisição com o mesmo nonce e correlation_id não seja reprocessada.
*/
public class NonceManager {
private final Duration windowDuration;
private final ConcurrentHashMap<String, Instant> usedNonces = new ConcurrentHashMap<>();
public NonceManager(Duration windowDuration) {
this.windowDuration = windowDuration != null ? windowDuration : Duration.ofSeconds(120);
}
public NonceManager() {
this(Duration.ofSeconds(120));
}
/**
* Tenta registrar um nonce.
*
* @param nonce Nonce gerado pelo cliente/requisição.
* @return {@code true} se o nonce for novo e válido; {@code false} se já tiver sido utilizado (replay).
*/
public boolean registerNonce(String nonce) {
if (nonce == null || nonce.isBlank()) {
return false;
}
cleanupExpired();
Instant now = Instant.now();
Instant existing = usedNonces.putIfAbsent(nonce, now);
return existing == null;
}
/**
* Remove nonces expirados além da janela máxima de aceitação.
*/
public void cleanupExpired() {
Instant cutoff = Instant.now().minus(windowDuration);
usedNonces.entrySet().removeIf(entry -> entry.getValue().isBefore(cutoff));
}
public int activeNonceCount() {
return usedNonces.size();
}
}

Some files were not shown because too many files have changed in this diff Show More