Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4adc99a01 | ||
|
|
5f4a212786 | ||
|
|
2a50704a7f | ||
|
|
994a27886e | ||
|
|
b46d8b665f | ||
|
|
d8d2e6ee46 | ||
|
|
176acf6f40 | ||
|
|
3fab68c118 | ||
|
|
0cd4025f3c | ||
|
|
5360caaddf | ||
|
|
4c30649598 |
@@ -1,100 +1,137 @@
|
|||||||
# CyberCore Signer
|
# CyberCore Signer
|
||||||
|
|
||||||
> **Agente Desktop de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.**
|
> **Agente Desktop Nativo 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.
|
[](https://openjdk.org/)
|
||||||
|
[](https://openjfx.io/)
|
||||||
|
[](docs/CCSP.md)
|
||||||
|
[](https://www.gov.br/iti/pt-br)
|
||||||
|
[](#licença)
|
||||||
|
|
||||||
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.
|
O **CyberCore Signer** é uma solução desktop desenvolvida em Java 21+ LTS (homologada em Java 25 LTS) com interface gráfica moderna JavaFX e segurança criptográfica estrita JCA/JCE. Permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e validação de documentos utilizando certificados digitais padrão ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) conectados ou instalados na máquina do usuário.
|
||||||
|
|
||||||
|
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP localhost (`127.0.0.1:21325`), garantindo isolamento total de rede, consentimento visual explícito e preservação integral das chaves privadas na máquina local.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Quick Start
|
||||||
|
|
||||||
|
### 🌐 Para Aplicações Web (via SDK JavaScript)
|
||||||
|
|
||||||
|
Instale e utilize o SDK oficial `@cybercore/signer-sdk` para conectar sua aplicação web ao agente local em poucos passos:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
// 1. Inicializar o cliente do assinador
|
||||||
|
const signer = new CyberCoreSigner({
|
||||||
|
applicationName: 'Portal Meu Negócio'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Obter certificados disponíveis na máquina do usuário
|
||||||
|
const certificates = await signer.getCertificates();
|
||||||
|
const cert = certificates[0]; // Seleciona o primeiro certificado disponível
|
||||||
|
|
||||||
|
// 3. Assinar o hash SHA-256 de um documento (a chave nunca sai da máquina!)
|
||||||
|
const result = await signer.sign({
|
||||||
|
certificateId: cert.id,
|
||||||
|
digest: '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
|
||||||
|
documentName: 'contrato_prestacao_servicos.pdf'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Assinatura gerada:', result.signature);
|
||||||
|
console.log('Certificado titular:', result.subject_name);
|
||||||
|
```
|
||||||
|
|
||||||
|
> Para o manual completo com exemplos de login digital, assinatura de PDF e XML, consulte o [Guia do SDK](docs/sdk.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ☕ Para Desenvolvedores Desktop
|
||||||
|
|
||||||
|
#### Requisitos
|
||||||
|
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin ou Corretto).
|
||||||
|
* **Maven:** 3.9+ (ou utilizar o `./mvnw` embutido).
|
||||||
|
* **Sistema Operacional:** Windows 10/11 ou Linux x86_64.
|
||||||
|
|
||||||
|
#### Compilação e Execução
|
||||||
|
```bash
|
||||||
|
# Compilar e gerar o JAR consolidado (Fat JAR sombreado):
|
||||||
|
./mvnw clean package -DskipTests
|
||||||
|
|
||||||
|
# Executar o agente no Linux / macOS:
|
||||||
|
./run.sh
|
||||||
|
|
||||||
|
# Executar o agente no Windows:
|
||||||
|
run.bat
|
||||||
|
|
||||||
|
# Executar com logs detalhados de diagnóstico:
|
||||||
|
java -jar modules/cybercore-app/target/cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏛 Arquitetura do Projeto
|
## 🏛 Arquitetura do Projeto
|
||||||
|
|
||||||
O projeto é estruturado em módulos Maven:
|
O projeto é estruturado em módulos Maven desacoplados, garantindo isolamento entre regras criptográficas de baixo nível e a camada de interface gráfica:
|
||||||
|
|
||||||
* **[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-core`](modules/cybercore-core):** Núcleo de regras criptográficas (JCA/JCE, SunPKCS11, X.509, PAdES com Apache PDFBox e Bouncy Castle, XAdES-BES/XMLDSig, carimbo de tempo TSA, LTV, fila FIFO de concorrência e validação de cadeias). **Agnóstico de UI**, sem nenhuma dependência de JavaFX.
|
||||||
* **[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-ui`](modules/cybercore-ui):** Interface gráfica com o usuário construída em JavaFX 25, apresentando design moderno com alternância dinâmica Claro/Escuro (Dual-Theme), Dashboard, Gerenciador de Certificados, Designer de Selos Visuais SVG com Live Preview, Diálogos 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.
|
* **[`modules/cybercore-app`](modules/cybercore-app):** Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida, diagnóstico pré-inicialização e servidor CCSP local.
|
||||||
* **[sdk/](sdk)**: SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração de aplicações web com o agente desktop.
|
* **[`sdk/`](sdk):** SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração simplificada de aplicações web modernas com o agente desktop.
|
||||||
* **[docs/](docs)**: Documentação técnica detalhada de arquitetura, protocolo, segurança e casos de uso.
|
* **[`docs/`](docs):** Documentação técnica abrangente contendo especificações do protocolo, arquitetura, segurança e manuais de integração.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Requisitos
|
## 📊 Matriz de Recursos e Conformidade
|
||||||
|
|
||||||
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin, Corretto, etc.).
|
| Recurso | Padrão / Tecnologia | Descrição |
|
||||||
* **Maven:** 3.9+ (ou utilizar o wrapper `./mvnw` incluído no projeto).
|
|---|---|---|
|
||||||
* **Sistema Operacional Alvo:** Windows 10/11 ou Linux (x86_64).
|
| **Certificados A1** | PKCS#12 / `.pfx` / `.p12` | Leitura em memória volátil com destruição segura de senhas |
|
||||||
|
| **Certificados A3** | Windows-MY (CryptoAPI/CNG) & PKCS#11 | Acesso nativo a tokens USB e smartcards (SafeSign, SafeNet, GD Burti, OpenSC) |
|
||||||
|
| **Assinatura PDF** | PAdES (ISO 32000-1 / ETSI TS 102 778) | Envelope CMS/PKCS#7, suporte a salvamento incremental e carimbo TSA |
|
||||||
|
| **Selos Visuais PDF** | Renderização Java2D de Alta Resolução (300 DPI) | Brasões vetoriais, upload de logos, controle de sigilo de CPF (LGPD) e pré-visualização interativa |
|
||||||
|
| **Assinatura XML** | XAdES-BES (ETSI TS 101 903 / DOC-ICP-15.03) | Suporte completo a NF-e, CT-e, MDF-e, eSocial e documentos envelopados |
|
||||||
|
| **Assinatura Genérica** | CMS / CAdES / P7S / P7M | Modalidades atachada (enveloped) e destacada (detached) |
|
||||||
|
| **Login Digital** | Challenge-Response ICP-Brasil | Autenticação web segura sem senhas tradicionais |
|
||||||
|
| **Validação** | Verificação Criptográfica Local | Auditoria de integridade de hashes, validade de certificados e signatários |
|
||||||
|
| **Interface Desktop** | JavaFX 25 com Dual-Theme | Modo Claro e Modo Escuro com comutação instantânea e atalhos na bandeja |
|
||||||
|
| **Protocolo Local** | CCSP/1 REST (`127.0.0.1:21325`) | Bind estrito em loopback, proteção anti-replay via nonce e autorização de origens |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠 Compilação e Empacotamento
|
## 🔒 Princípios Fundamentais de Segurança
|
||||||
|
|
||||||
### 1. Compilação do JAR Executável
|
1. **A Chave Privada Permanece na Estação de Trabalho:** A chave privada e os segredos (PIN do token A3 ou senha do arquivo A1) nunca saem da máquina do usuário nem trafegam pela rede ou saem em logs.
|
||||||
|
2. **Assinatura por Digest Descentralizada:** Por padrão, a aplicação web calcula o hash SHA-256 do documento localmente e o CyberCore assina apenas o digest de 32 bytes, eliminando o tráfego desnecessário de arquivos inteiros.
|
||||||
Para compilar e gerar o JAR consolidado (com todas as dependências sombreadas e assets):
|
3. **Consentimento Visual Explícito:** Nenhuma operação de assinatura ocorre silenciosamente. Toda requisição exige autorização física do usuário em diálogo nativo do sistema operacional.
|
||||||
|
4. **Isolamento de Origem e Anti-Replay:** Cada requisição web deve apresentar identificador de aplicação aprovado, nonce único com validação temporal (`expires_at`) e cabeçalho de origem verificado.
|
||||||
```bash
|
5. **Fila FIFO de Operações:** O acesso a tokens físicos A3 é protegido por trava de concorrência (`signLock`) serializada, prevenindo travamento ou corrupção de sessão nos chips criptográficos.
|
||||||
./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
|
## 📚 Documentação Técnica Detalhada
|
||||||
|
|
||||||
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.
|
Para detalhes completos sobre arquitetura, protocolos e manuais técnicos, acesse o [Portal de Documentação](docs/README.md) ou consulte diretamente os links abaixo:
|
||||||
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.
|
* 🌐 **[Protocolo CCSP/1](docs/CCSP.md):** Contrato REST, endpoints, códigos de status e anti-replay.
|
||||||
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.
|
* 📦 **[SDK Web JavaScript](docs/sdk.md):** Guia de integração e métodos da biblioteca oficial.
|
||||||
|
* 🏛 **[Arquitetura do Sistema](docs/architecture.md):** Módulos, threading e fluxo sequencial.
|
||||||
|
* 💳 **[Certificados A1, A3 e PKCS#11](docs/a1-a3-certificates.md):** Suporte a hardware, drivers e OIDs ICP-Brasil.
|
||||||
|
* 📄 **[Assinaturas PDF e Selos SVG](docs/pades.md):** PAdES, renderização Java2D e selos visuais.
|
||||||
|
* 📑 **[Assinaturas XML (XAdES-BES)](docs/xml-xmldsig.md):** Padrão DOC-ICP-15.03 e notas fiscais.
|
||||||
|
* 🔑 **[Autenticação e Login Digital](docs/authentication-login.md):** Fluxo Challenge-Response via ICP-Brasil.
|
||||||
|
* ⚠️ **[Catálogo Estruturado de Erros](docs/errors.md):** Tabela de erros `CcspErrorCode` e códigos HTTP.
|
||||||
|
* 🛡️ **[Modelo de Segurança e Privacidade](docs/security.md):** Diretrizes de isolamento, memória e LGPD.
|
||||||
|
* 🩺 **[Diagnóstico e Troubleshooting](docs/diagnostics.md):** Auto-teste e guia de resolução de problemas.
|
||||||
|
* 💿 **[Instalação e Empacotamento Nativo](docs/installation.md):** Distribuição via `jpackage` sem JRE pré-instalado.
|
||||||
|
* ⚖️ **[Limitações Técnicas](docs/limitations.md):** Declaração de dependências conforme a Regra 23.
|
||||||
|
* 🛠️ **[Modo de Desenvolvimento](docs/development-mode.md):** Parâmetro `--dev` e certificados de teste.
|
||||||
|
* 🎨 **[Layout Dual-Theme e Assinatura em Lote](docs/serpro-layout-and-batch.md):** Design da UI e operações em lote.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Documentação Técnica
|
## 📄 Licença e Propriedade
|
||||||
|
|
||||||
Consulte a pasta [docs/](docs) para especificações completas:
|
Copyright © 2026 **CyberCore Systems**. Todos os direitos reservados.
|
||||||
|
Este software é proprietário e confidencial. O uso, distribuição ou modificação não autorizados são estritamente proibidos.
|
||||||
* [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.
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer
|
# Recursos Visuais (Ícones e Imagens em SVG) — 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**.
|
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente todos os **ícones em SVG** e as **imagens em SVG** do **CyberCore Signer**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,13 +31,23 @@ assets/
|
|||||||
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
|
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
|
||||||
│ ├── check.svg # Ícone Checkmark de sucesso e validação
|
│ ├── check.svg # Ícone Checkmark de sucesso e validação
|
||||||
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
|
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
|
||||||
│ └── star.svg # Ícone de Estrela notarial / favorita
|
│ ├── star.svg # Ícone de Estrela notarial / favorita
|
||||||
|
│ ├── close.svg # Ícone Fechar / Cancelar (cruz geométrica moderna)
|
||||||
|
│ ├── arrow-right.svg # Ícone Avançar / Seta direcional
|
||||||
|
│ ├── zoom-in.svg # Ícone Zoom In (+)
|
||||||
|
│ ├── zoom-out.svg # Ícone Zoom Out (-)
|
||||||
|
│ ├── zoom-fit.svg # Ícone Ajustar à Tela
|
||||||
|
│ └── refresh.svg # Ícone Atualizar / Recarregar listas e status
|
||||||
│
|
│
|
||||||
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência)
|
└── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
|
||||||
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
|
├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
|
||||||
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
|
├── app-icon.png # Fallback em PNG (256x256 px)
|
||||||
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
|
├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
|
||||||
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
|
├── app-logo.png # Fallback em PNG (480x120 px)
|
||||||
|
├── brand-banner.svg # Banner de cabeçalho e marca em SVG (600x160 px)
|
||||||
|
├── brand-banner.png # Fallback em PNG (600x160 px)
|
||||||
|
├── default-seal.svg # Selo de assinatura padrão ICP-Brasil em SVG (640x164 px)
|
||||||
|
└── default-seal.png # Fallback em PNG (640x164 px)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -45,20 +55,21 @@ assets/
|
|||||||
## 🎨 Como Customizar Qualquer Ícone ou Imagem
|
## 🎨 Como Customizar Qualquer Ícone ou Imagem
|
||||||
|
|
||||||
1. **Ícones:**
|
1. **Ícones:**
|
||||||
- Estão no formato `.svg` padrão.
|
- Todos estão no formato `.svg` padrão da W3C.
|
||||||
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**.
|
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
||||||
- Se preferir 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.
|
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
||||||
|
|
||||||
2. **Imagens do Sistema:**
|
2. **Imagens do Sistema:**
|
||||||
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
|
- Tanto o logotipo horizontal (`app-logo.svg`), o ícone do sistema (`app-icon.svg`), o banner institucional (`brand-banner.svg`) e o selo notarial padrão (`default-seal.svg`) agora são arquivos **SVG vetoriais editáveis**.
|
||||||
- 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.
|
- Você pode alterar cores, tipografias, brasões e dimensões diretamente nos arquivos SVG.
|
||||||
|
- O `AssetManager` renderiza os arquivos SVG preservando fidelidade geométrica e suportando gradientes e alinhamentos.
|
||||||
|
|
||||||
3. **Ordem de Prioridade e Hot-Reloading:**
|
3. **Ordem de Resolução e Carregamento:**
|
||||||
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
|
Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
|
||||||
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!
|
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Arquivos `.svg` editados aqui têm prioridade máxima e entram em vigor imediatamente sem necessidade de recompilar.
|
||||||
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
|
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Para customizações no diretório pessoal do usuário.
|
||||||
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
|
3. **Recursos Empacotados no JAR (`/assets/`)**: SVGs e imagens internas empacotadas no pacote de distribuição.
|
||||||
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
|
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique desprovida de grafismos caso algum arquivo seja removido.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
<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>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<!-- Círculo azul e letra 'i' informativa centrada em (24, 24) -->
|
||||||
|
<circle cx="24" cy="24" r="18" fill="#0288d1"/>
|
||||||
|
<circle cx="24" cy="15" r="2.5" fill="#ffffff"/>
|
||||||
|
<line x1="24" y1="21" x2="24" y2="33" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 362 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 8 5 L 15 12 L 8 19" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 6 6 L 18 18 M 18 6 L 6 18" fill="none" stroke="#64748b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
@@ -1,6 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
<!-- Lua crescente ciano/índigo -->
|
<!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
|
||||||
<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"/>
|
<path d="M 24 8 A 16 16 0 1 0 40 24 A 14 14 0 0 1 24 8 Z" fill="#38bdf8"/>
|
||||||
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
|
<circle cx="34" cy="14" r="1.8" fill="#818cf8"/>
|
||||||
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
|
<circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 344 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 20 4 L 20 9 L 15 9" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M 4 20 L 4 15 L 9 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
@@ -1,8 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
<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) -->
|
<!-- Engrenagem corporativa simétrica 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"/>
|
<path fill="#0288d1" fill-rule="evenodd" d="M 21.5 5 L 26.5 5 L 27.2 10.2 C 29 10.9 30.7 11.9 32.2 13.1 L 36.6 10.6 L 40.1 14.1 L 37.6 18.5 C 38.8 20 39.8 21.7 40.5 23.5 L 45.7 24.2 L 45.7 29.2 L 40.5 29.9 C 39.8 31.7 38.8 33.4 37.6 34.9 L 40.1 39.3 L 36.6 42.8 L 32.2 40.3 C 30.7 41.5 29 42.5 27.2 43.2 L 26.5 48.4 L 21.5 48.4 L 20.8 43.2 C 19 42.5 17.3 41.5 15.8 40.3 L 11.4 42.8 L 7.9 39.3 L 10.4 34.9 C 9.2 33.4 8.2 31.7 7.5 29.9 L 2.3 29.2 L 2.3 24.2 L 7.5 23.5 C 8.2 21.7 9.2 20 10.4 18.5 L 7.9 14.1 L 11.4 10.6 L 15.8 13.1 C 17.3 11.9 19 10.9 20.8 10.2 Z M 24 17.5 A 6.5 6.5 0 1 0 24 30.5 A 6.5 6.5 0 1 0 24 17.5 Z"/>
|
||||||
<!-- 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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 784 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 4 9 L 4 4 L 9 4 M 15 4 L 20 4 L 20 9 M 20 15 L 20 20 L 15 20 M 9 20 L 4 20 L 4 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
|
||||||
|
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<path d="M 10 7 L 10 13 M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
|
||||||
|
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<path d="M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1,37 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1976d2"/>
|
||||||
|
<stop offset="45%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="topFacetGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.55"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="leftFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.75"/>
|
||||||
|
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.35"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="rightFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.85"/>
|
||||||
|
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.45"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
|
||||||
|
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo com cantos arredondados -->
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="68" ry="68" fill="url(#bgGrad)"/>
|
||||||
|
<rect x="10" y="10" width="236" height="236" rx="66" ry="66" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-opacity="0.25"/>
|
||||||
|
|
||||||
|
<!-- Facetas 3D do Cristal / Diamante Azul -->
|
||||||
|
<polygon points="128,46 210,113 128,143 46,113" fill="url(#topFacetGrad)"/>
|
||||||
|
<polygon points="46,113 128,143 128,215 72,179" fill="url(#leftFacetGrad)"/>
|
||||||
|
<polygon points="210,113 128,143 128,215 184,179" fill="url(#rightFacetGrad)"/>
|
||||||
|
|
||||||
|
<!-- Brilho estelar central -->
|
||||||
|
<circle cx="128" cy="128" r="38" fill="url(#centerGlow)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoIconBg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1976d2"/>
|
||||||
|
<stop offset="50%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoTopFacet" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
|
||||||
|
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoLeftFacet" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.8"/>
|
||||||
|
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoRightFacet" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.5"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Container do Ícone Cristal 3D -->
|
||||||
|
<rect x="14" y="16" width="88" height="88" rx="24" ry="24" fill="url(#logoIconBg)"/>
|
||||||
|
<rect x="16" y="18" width="84" height="84" rx="22" ry="22" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-opacity="0.25"/>
|
||||||
|
|
||||||
|
<!-- Facetas Cristalinas (cx=58, cy=60, sz=63) -->
|
||||||
|
<polygon points="58,32 86,55 58,66 30,55" fill="url(#logoTopFacet)"/>
|
||||||
|
<polygon points="30,55 58,66 58,92 39,79" fill="url(#logoLeftFacet)"/>
|
||||||
|
<polygon points="86,55 58,66 58,92 77,79" fill="url(#logoRightFacet)"/>
|
||||||
|
<circle cx="58" cy="60" r="14" fill="#ffffff" fill-opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Tipografia Oficial "CyberCore" -->
|
||||||
|
<text x="124" y="62" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="#0a2540">Cyber<tspan fill="#0288d1">Core</tspan></text>
|
||||||
|
|
||||||
|
<!-- Badge "SIGNER" -->
|
||||||
|
<rect x="124" y="76" width="94" height="22" rx="6" ry="6" fill="#0288d1"/>
|
||||||
|
<text x="139" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#ffffff" letter-spacing="1">SIGNER</text>
|
||||||
|
|
||||||
|
<!-- Subtítulo ICP-Brasil -->
|
||||||
|
<text x="230" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" fill="#64748b">ICP-Brasil • Assinador Digital Seguro</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" width="600" height="160">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bannerBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="60%" stop-color="#0284c7"/>
|
||||||
|
<stop offset="100%" stop-color="#0369a1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="bannerIconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1e88e5"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo do Banner -->
|
||||||
|
<rect x="0" y="0" width="600" height="160" rx="16" ry="16" fill="url(#bannerBgGrad)"/>
|
||||||
|
|
||||||
|
<!-- Formas geométricas de segurança no fundo -->
|
||||||
|
<circle cx="480" cy="50" r="110" fill="#ffffff" fill-opacity="0.08"/>
|
||||||
|
<circle cx="540" cy="120" r="80" fill="#ffffff" fill-opacity="0.06"/>
|
||||||
|
<circle cx="380" cy="-20" r="90" fill="#ffffff" fill-opacity="0.04"/>
|
||||||
|
|
||||||
|
<!-- Ícone Cristal à esquerda -->
|
||||||
|
<rect x="30" y="35" width="90" height="90" rx="22" ry="22" fill="url(#bannerIconGrad)"/>
|
||||||
|
<rect x="33" y="38" width="84" height="84" rx="19" ry="19" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.3"/>
|
||||||
|
<polygon points="75,52 101,80 75,108 49,80" fill="#ffffff" fill-opacity="0.9"/>
|
||||||
|
<polygon points="75,52 75,108 49,80" fill="#80d8ff" fill-opacity="0.5"/>
|
||||||
|
|
||||||
|
<!-- Textos do Banner -->
|
||||||
|
<text x="148" y="74" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" font-weight="bold" fill="#ffffff">CyberCore Signer</text>
|
||||||
|
<text x="148" y="104" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="normal" fill="#e0f2fe">Agente de Assinatura Digital ICP-Brasil A1 & A3</text>
|
||||||
|
<text x="148" y="128" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#38bdf8" letter-spacing="1">PADES • XADES • CADES • TIMESTAMPS • CCSP/1</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 164" width="640" height="164">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0288d1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo do Selo -->
|
||||||
|
<rect x="2" y="2" width="636" height="160" rx="12" ry="12" fill="#f8fafc" stroke="#0288d1" stroke-width="2.5"/>
|
||||||
|
|
||||||
|
<!-- Brasão Notarial no lado esquerdo -->
|
||||||
|
<rect x="18" y="16" width="130" height="132" rx="10" ry="10" fill="url(#badgeGrad)"/>
|
||||||
|
|
||||||
|
<!-- Textos do Brasão -->
|
||||||
|
<text x="83" y="44" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">ASSINADO</text>
|
||||||
|
<text x="83" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">DIGITALMENTE</text>
|
||||||
|
|
||||||
|
<!-- Estrela dourada -->
|
||||||
|
<circle cx="83" cy="85" r="14" fill="#fbbf24"/>
|
||||||
|
<text x="83" y="91" font-family="'Segoe UI', sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">★</text>
|
||||||
|
|
||||||
|
<text x="83" y="126" font-family="'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="bold" fill="#e0f2fe" text-anchor="middle" letter-spacing="1">ICP-BRASIL</text>
|
||||||
|
|
||||||
|
<!-- Linha divisória vertical -->
|
||||||
|
<line x1="166" y1="16" x2="166" y2="148" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||||
|
|
||||||
|
<!-- Informações da Assinatura -->
|
||||||
|
<text x="184" y="36" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#64748b" letter-spacing="0.5">DOCUMENTO ASSINADO DIGITALMENTE POR:</text>
|
||||||
|
<text x="184" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="17" font-weight="bold" fill="#0a2540">NOME DO TITULAR DO CERTIFICADO</text>
|
||||||
|
<text x="184" y="82" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">CPF: ***.000.000-**</text>
|
||||||
|
<text x="184" y="102" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">Data e Hora: 11/10/2026 12:00:00 -03:00</text>
|
||||||
|
<text x="184" y="128" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-style="italic" fill="#0288d1">A conformidade com a ICP-Brasil pode ser verificada no ITI.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,28 +1,55 @@
|
|||||||
# CyberCore Signing Protocol (CCSP/1)
|
# 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**.
|
> **Especificação Técnica do Protocolo de Comunicação Local e API REST (Versão 1)**
|
||||||
|
|
||||||
A versão inicial descrita neste documento é a **CCSP/1**.
|
O **CyberCore Signing Protocol (CCSP)** é o protocolo padrão para comunicação local entre aplicações web rodando no navegador do usuário e o agente desktop **CyberCore Signer**.
|
||||||
|
|
||||||
|
A versão atual especificada neste documento é a **CCSP/1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Princípios de Segurança
|
## 1. Princípios de Segurança e Isolamento
|
||||||
|
|
||||||
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).
|
O protocolo CCSP/1 adota o princípio de menor privilégio e segurança em profundidade:
|
||||||
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.
|
1. **Loopback Estrito (`127.0.0.1`):** O servidor HTTP local vincula seu socket exclusivamente ao endereço IP `127.0.0.1` na porta padrão `21325`. O agente rejeita escuta em `0.0.0.0`, prevenindo qualquer acesso proveniente da rede local (LAN) ou externa (WAN).
|
||||||
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.
|
2. **Validação de Cabeçalhos e DNS Rebinding:** O agente valida o cabeçalho `Host` para garantir que as requisições se originem estritamente de `127.0.0.1:21325` ou `localhost:21325`.
|
||||||
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`).
|
3. **Controle de Origem (CORS):** Todas as respostas incluem cabeçalhos CORS restritivos. Métodos HTTP `OPTIONS` prévios (preflight) são tratados para liberar apenas métodos e headers homologados (`Content-Type`, `Origin`, `X-CyberCore-AppId`).
|
||||||
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.
|
4. **Handshake de Autorização de Aplicações:** Nenhuma página web desconhecida pode listar certificados ou solicitar assinaturas. Uma aplicação deve se identificar (`POST /ccsp/v1/authorize`) e obter aprovação explícita do usuário via diálogo nativo do sistema operacional.
|
||||||
|
5. **Proteção Anti-Replay com Nonce e TTL:** Todas as chamadas de assinatura exigem um identificador pseudoaleatório `nonce` único e um timestamp de expiração (`expires_at`). Requisições com nonces repetidos são sumariamente rejeitadas com erro estruturado `REPLAY_DETECTED`.
|
||||||
|
6. **Privacidade Criptográfica por Digest:** Por padrão, a aplicação web calcula o hash SHA-256 no próprio navegador utilizando a Web Crypto API nativa e envia apenas o digest de 32 bytes (em Base64). O documento confidencial nunca trafega para o agente e a chave privada nunca sai do computador ou do chip criptográfico do usuário.
|
||||||
|
7. **Trava FIFO Serializada (`signLock`):** O processamento de assinaturas físicas em smartcards/tokens USB A3 é serializado em fila FIFO para evitar colisões no hardware mono-thread do middleware do fabricante.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Endpoints do Protocolo (CCSP/1)
|
## 2. Visão Geral dos Endpoints (CCSP/1)
|
||||||
|
|
||||||
### 2.1 `GET /ccsp/v1/status`
|
| Método | Endpoint | Finalidade | Autenticação Requerida |
|
||||||
Verifica se o agente está instalado, em execução e pronto para operações.
|
|---|---|---|---|
|
||||||
|
| `GET` | `/ccsp/v1/status` | Verifica integridade e status de prontidão do agente local | Nenhuma |
|
||||||
|
| `POST` | `/ccsp/v1/authorize` | Handshake de autorização inicial de uma aplicação web | Consentimento visual do usuário |
|
||||||
|
| `GET` | `/ccsp/v1/certificates` | Lista certificados digitais ICP-Brasil válidos e operacionais | Aplicação Autorizada |
|
||||||
|
| `GET` | `/ccsp/v1/seals` | Lista temas e estilos visuais de selos suportados pelo agente | Aplicação Autorizada |
|
||||||
|
| `POST` | `/ccsp/v1/sign` | Executa assinatura digital do digest informado (PAdES/CAdES/XMLDSig) | Aplicação Autorizada + Diálogo Nativo |
|
||||||
|
| `POST` | `/ccsp/v1/sign-pdf` | Assina documento PDF em Base64 com selo posicionado e devolve PDF assinado | Aplicação Autorizada + Diálogo Nativo |
|
||||||
|
| `POST` | `/ccsp/v1/sign-xml` | Assina documento XML (XAdES-BES / XMLDSig) e devolve XML assinado | Aplicação Autorizada + Diálogo Nativo |
|
||||||
|
| `POST` | `/ccsp/v1/authenticate` | Realiza login seguro via desafio criptográfico (*Challenge-Response*) | Aplicação Autorizada + Diálogo Nativo |
|
||||||
|
|
||||||
#### Resposta de Sucesso (200 OK)
|
---
|
||||||
|
|
||||||
|
## 3. Especificação Detalhada dos Endpoints
|
||||||
|
|
||||||
|
### 3.1 `GET /ccsp/v1/status`
|
||||||
|
Verifica se o agente desktop está instalado, em execução e pronto para operações criptográficas.
|
||||||
|
|
||||||
|
#### Requisição
|
||||||
|
```http
|
||||||
|
GET /ccsp/v1/status HTTP/1.1
|
||||||
|
Host: 127.0.0.1:21325
|
||||||
|
Origin: https://meusistema.empresa.com.br
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
@@ -30,20 +57,58 @@ Verifica se o agente está instalado, em execução e pronto para operações.
|
|||||||
"agent": "CyberCore Signer",
|
"agent": "CyberCore Signer",
|
||||||
"agent_version": "0.1.0",
|
"agent_version": "0.1.0",
|
||||||
"os": "Windows 11",
|
"os": "Windows 11",
|
||||||
"timestamp": "2026-10-08T19:40:00Z"
|
"timestamp": "2026-10-11T20:45:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.2 `GET /ccsp/v1/certificates`
|
### 3.2 `POST /ccsp/v1/authorize`
|
||||||
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura.
|
Inicia o handshake para autorizar uma nova aplicação web. Quando a aplicação ainda não foi homologada pelo usuário, o CyberCore Signer suspende a requisição e exibe um modal nativo na tela do desktop para confirmação.
|
||||||
|
|
||||||
#### Headers / Parâmetros
|
#### Cabeçalhos
|
||||||
- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`).
|
- `Content-Type: application/json`
|
||||||
|
- `Origin: https://meusistema.empresa.com.br`
|
||||||
|
|
||||||
|
#### Payload da Requisição
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"name": "Portal Jurídico e Tributário"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"status": "authorized",
|
||||||
|
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"message": "Aplicação autorizada pelo usuário com sucesso."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resposta de Recusa do Usuário (`403 Forbidden`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"status": "error",
|
||||||
|
"error_code": "APPLICATION_NOT_AUTHORIZED",
|
||||||
|
"message": "O usuário recusou a autorização para esta aplicação."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 `GET /ccsp/v1/certificates`
|
||||||
|
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura no sistema (A1 importados e A3 conectados via Windows Store ou PKCS#11).
|
||||||
|
|
||||||
|
#### Cabeçalhos / Parâmetros
|
||||||
- `Origin`: Origem web autorizada.
|
- `Origin`: Origem web autorizada.
|
||||||
|
- `X-CyberCore-AppId` (Header) ou `?application_id=uuid` (Query Param).
|
||||||
|
|
||||||
#### Resposta de Sucesso (200 OK)
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
@@ -51,16 +116,18 @@ Retorna a lista de certificados digitais atualmente disponíveis e aptos para as
|
|||||||
"certificates": [
|
"certificates": [
|
||||||
{
|
{
|
||||||
"id": "win-4340656a",
|
"id": "win-4340656a",
|
||||||
"alias": "CLAUDIO SILVA:12345678900",
|
"alias": "MARIA DA SILVA:12345678900",
|
||||||
"common_name": "CLAUDIO SILVA:12345678900",
|
"common_name": "MARIA DA SILVA:12345678900",
|
||||||
"subject_dn": "CN=CLAUDIO SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
|
"subject_dn": "CN=MARIA DA SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
|
||||||
"issuer_dn": "CN=AC SERPRO v5, O=ICP-Brasil, C=BR",
|
"issuer_dn": "CN=AC SERPRO v5, OU=Servico Federal de Processamento de Dados, O=ICP-Brasil, C=BR",
|
||||||
"type": "A3",
|
"type": "A3",
|
||||||
"provider_id": "provider-win-store",
|
"provider_id": "provider-win-store",
|
||||||
"device_name": "Token / Smart Card (Windows CAPI/CNG)",
|
"device_name": "Token USB / Smart Card (Windows CAPI/CNG)",
|
||||||
"valid_from": "2024-05-10T12:00:00Z",
|
"valid_from": "2025-01-10T10:00:00Z",
|
||||||
"valid_to": "2027-05-10T12:00:00Z",
|
"valid_to": "2028-01-10T10:00:00Z",
|
||||||
"serial_number": "72910482910482019482",
|
"serial_number": "72910482910482019482",
|
||||||
|
"cpf": "12345678900",
|
||||||
|
"cnpj": null,
|
||||||
"can_sign": true,
|
"can_sign": true,
|
||||||
"status": "AVAILABLE"
|
"status": "AVAILABLE"
|
||||||
}
|
}
|
||||||
@@ -70,33 +137,10 @@ Retorna a lista de certificados digitais atualmente disponíveis e aptos para as
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.3 `POST /ccsp/v1/authorize`
|
### 3.4 `POST /ccsp/v1/sign`
|
||||||
Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário.
|
Solicita a assinatura digital local do digest informado com o certificado escolhido.
|
||||||
|
|
||||||
#### Requisição
|
#### Payload da 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
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
@@ -106,103 +150,245 @@ Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquiv
|
|||||||
"algorithm": "SHA256withRSA",
|
"algorithm": "SHA256withRSA",
|
||||||
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
|
||||||
"certificate_hint": "win-4340656a",
|
"certificate_hint": "win-4340656a",
|
||||||
"document_name": "contrato_locacao.pdf",
|
"document_name": "contrato_prestacao_servicos.pdf",
|
||||||
"document_type": "PDF",
|
"document_type": "PDF",
|
||||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
||||||
"expires_at": "2026-10-08T19:45:00Z"
|
"expires_at": "2026-10-11T20:50:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Resposta de Sucesso (200 OK)
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||||
"status": "signed",
|
"status": "signed",
|
||||||
"signature": "MEUCIQDq...base64...",
|
"signature": "MEUCIQDq95kZc...Base64...",
|
||||||
"certificate": "MIIFazC...base64...",
|
"certificate": "MIIFazCCBF...Base64...",
|
||||||
"certificate_chain": [
|
"certificate_chain": [
|
||||||
"MIIFazC...base64...",
|
"MIIFazCCBF...Base64...",
|
||||||
"MIIEazC...base64..."
|
"MIIEazCCA1...Base64..."
|
||||||
],
|
],
|
||||||
"algorithm": "SHA256withRSA",
|
"algorithm": "SHA256withRSA",
|
||||||
"subject_name": "CLAUDIO SILVA",
|
"subject_name": "MARIA DA SILVA",
|
||||||
"subject_cpf_cnpj": "12345678900",
|
"subject_cpf_cnpj": "12345678900",
|
||||||
"document_name": "contrato_locacao.pdf",
|
"document_name": "contrato_prestacao_servicos.pdf",
|
||||||
"document_type": "PDF"
|
"document_type": "PDF"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2.5 `POST /ccsp/v1/authenticate`
|
### 3.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).
|
Executa o login criptográfico de um usuário em sistemas web através do mecanismo de desafio e resposta (*Challenge-Response*).
|
||||||
|
|
||||||
#### Requisição
|
#### Payload da Requisição
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
"request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
|
||||||
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
"operation": "authenticate",
|
"operation": "authenticate",
|
||||||
"challenge": "server-session-challenge-nonce-2026",
|
"challenge": "session-nonce-random-server-token-998877",
|
||||||
"certificate_hint": "win-4340656a",
|
"certificate_hint": "win-4340656a",
|
||||||
"purpose": "AUTHENTICATION",
|
"purpose": "AUTHENTICATION",
|
||||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
"nonce": "d3b07384d113edec49eaa6238ad5ff00",
|
||||||
"expires_at": "2026-10-08T19:45:00Z"
|
"expires_at": "2026-10-11T20:50:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Resposta de Sucesso (200 OK)
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
"request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
|
||||||
"status": "authenticated",
|
"status": "authenticated",
|
||||||
"signature": "MEUCIQDq...base64...",
|
"signature": "MEQCIF61pP...Base64...",
|
||||||
"certificate": "MIIFazC...base64...",
|
"certificate": "MIIFazCCBF...Base64...",
|
||||||
"certificate_chain": [
|
"certificate_chain": [
|
||||||
"MIIFazC...base64..."
|
"MIIFazCCBF...Base64...",
|
||||||
|
"MIIEazCCA1...Base64..."
|
||||||
],
|
],
|
||||||
"algorithm": "SHA256withRSA",
|
"algorithm": "SHA256withRSA",
|
||||||
"subject_name": "CLAUDIO SILVA",
|
"subject_name": "MARIA DA SILVA",
|
||||||
"subject_cpf_cnpj": "12345678900",
|
"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
|
### 3.6 `GET /ccsp/v1/seals`
|
||||||
|
Retorna a lista de temas e estilos de selos visuais embutidos e instalados no CyberCore Signer.
|
||||||
|
|
||||||
| Código | Significado |
|
#### Cabeçalhos
|
||||||
|---|---|
|
- `Origin: https://meusistema.empresa.com.br`
|
||||||
| `CERTIFICATE_NOT_FOUND` | Certificado indicado no hint não foi encontrado. |
|
- `X-CyberCore-AppId: 550e8400-e29b-41d4-a716-446655440000`
|
||||||
| `CERTIFICATE_EXPIRED` | O certificado selecionado está fora do período de vigência. |
|
|
||||||
| `CERTIFICATE_INVALID` | O certificado é inválido ou corrompido. |
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível ou ausente. |
|
```json
|
||||||
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. |
|
{
|
||||||
| `TOKEN_REMOVED` | Dispositivo de hardware foi removido durante a assinatura. |
|
"version": "1",
|
||||||
| `PIN_REQUIRED` | PIN obrigatório para liberar chave privada em PKCS#11. |
|
"status": "success",
|
||||||
| `PIN_INCORRECT` | PIN digitado está incorreto. |
|
"default_seal_id": "icp-brasil",
|
||||||
| `PKCS11_ERROR` | Falha no middleware PKCS#11 do fabricante. |
|
"seals": [
|
||||||
| `SIGNATURE_FAILED` | Erro durante a geração da assinatura digital. |
|
{
|
||||||
| `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou recusada pelo usuário. |
|
"id": "icp-brasil",
|
||||||
| `REQUEST_EXPIRED` | A data/hora de validade da requisição expirou. |
|
"name": "ICP-Brasil Institucional",
|
||||||
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. |
|
"description": "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
|
||||||
| `INVALID_ORIGIN` | O cabeçalho Origin não corresponde ao cadastro da aplicação. |
|
"primary_color": "#0d47a1",
|
||||||
| `UNSUPPORTED_ALGORITHM` | Algoritmo não suportado (ex: diferente de SHA256withRSA). |
|
"secondary_color": "#0284c7",
|
||||||
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela de confirmação. |
|
"border_color": "#93c5fd",
|
||||||
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário está inativo no SO. |
|
"background_color": "#ffffff",
|
||||||
| `COMMUNICATION_ERROR` | Erro interno no agente de comunicação local. |
|
"text_color": "#0f172a",
|
||||||
|
"icon_style": "shield",
|
||||||
|
"width": 320,
|
||||||
|
"height": 82,
|
||||||
|
"default_cpf_mode": "FULL",
|
||||||
|
"default_include_date_time": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gov-br",
|
||||||
|
"name": "Governamental (Padrão Gov.br / ITI)",
|
||||||
|
"description": "Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001",
|
||||||
|
"primary_color": "#047857",
|
||||||
|
"secondary_color": "#eab308"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 `POST /ccsp/v1/sign-pdf`
|
||||||
|
Recebe um documento PDF em Base64, a página e as coordenadas desejadas (`x`, `y`, `scale`), aplica o selo visual e a assinatura digital PAdES (ISO 32000-1 / RFC 5652) e retorna o PDF assinado completo em Base64.
|
||||||
|
|
||||||
|
#### Payload da Requisição
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
|
||||||
|
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"operation": "sign-pdf",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"pdf": "JVBERi0xLjQKJ...Base64...",
|
||||||
|
"page": 1,
|
||||||
|
"x": 60.0,
|
||||||
|
"y": 60.0,
|
||||||
|
"scale": 1.0,
|
||||||
|
"visible_seal": true,
|
||||||
|
"seal_theme_id": "icp-brasil",
|
||||||
|
"reason": "Aprovado e assinado digitalmente",
|
||||||
|
"location": "Curitiba, PR",
|
||||||
|
"nonce": "c301d011-8012-4261-bcfa-e7912ab5a420"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Selo Customizado Inline (`seal_custom`):**
|
||||||
|
> Em vez de `seal_theme_id`, a requisição pode fornecer um objeto `seal_custom` com cores hexadecimais, título customizado, ícone vetorial (`shield`, `certificate`, `lock`) e política de privacidade LGPD (`FULL`, `MASKED`, `HIDDEN`).
|
||||||
|
|
||||||
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
|
||||||
|
"status": "signed",
|
||||||
|
"signed_pdf": "JVBERi0xLjQKJ...PDF_ASSINADO_BASE64...",
|
||||||
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"certificate": "MIIFazCCBF...Base64...",
|
||||||
|
"algorithm": "SHA256withRSA"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 `POST /ccsp/v1/sign-xml`
|
||||||
|
Executa a assinatura digital de documentos XML em conformidade com o padrão XMLDSig W3C ou XAdES-BES ICP-Brasil (Padrão NF-e / NFC-e / CT-e / NFS-e). O documento assinado completo é devolvido diretamente ao sistema web.
|
||||||
|
|
||||||
|
#### Payload da Requisição
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "e44d32e1-455b-4357-9351-41c10d3b6f22",
|
||||||
|
"application_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"operation": "sign-xml",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"xml": "<nfeProc versao=\"4.00\" xmlns=\"http://www.portalfiscal.inf.br/nfe\">...</nfeProc>",
|
||||||
|
"profile": "XADES_BES",
|
||||||
|
"target_element_tag": "infNFe",
|
||||||
|
"target_element_id": "NFe35261012345678000199550010000001001234567890",
|
||||||
|
"nonce": "d80b0322-8012-4261-bcfa-e7912ab5a420"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "e44d32e1-455b-4357-9351-41c10d3b6f22",
|
||||||
|
"status": "signed",
|
||||||
|
"signed_xml": "<nfeProc versao=\"4.00\" ...><Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">...</Signature></nfeProc>",
|
||||||
|
"certificate": "MIIFazCCBF...Base64...",
|
||||||
|
"algorithm": "SHA256withRSA"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Estrutura Padrão de Erro e Códigos HTTP
|
||||||
|
|
||||||
|
Quando qualquer operação falha, o servidor local CCSP/1 responde com um objeto JSON padronizado e o código de status HTTP correspondente:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||||
|
"status": "error",
|
||||||
|
"error_code": "PIN_INCORRECT",
|
||||||
|
"message": "O PIN informado para o token A3 é inválido."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mapeamento de Status HTTP
|
||||||
|
|
||||||
|
| Código HTTP | Erro Típico | Descrição |
|
||||||
|
|---|---|---|
|
||||||
|
| `400 Bad Request` | `USER_CANCELLED`, `PIN_INCORRECT`, `SIGNATURE_FAILED` | Requisição malformada, cancelamento pelo usuário ou falha de PIN |
|
||||||
|
| `401 Unauthorized` | `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou sem cabeçalho `X-CyberCore-AppId` |
|
||||||
|
| `403 Forbidden` | `INVALID_ORIGIN` | Cabeçalho `Origin` não confere com o domínio cadastrado |
|
||||||
|
| `404 Not Found` | `CERTIFICATE_NOT_FOUND`, `TOKEN_NOT_FOUND` | Certificado ou dispositivo físico ausente |
|
||||||
|
| `409 Conflict` | `REPLAY_DETECTED` | Nonce já utilizado anteriormente na janela de validação |
|
||||||
|
| `422 Unprocessable`| `REQUEST_EXPIRED`, `CERTIFICATE_EXPIRED` | Parâmetros sintaticamente válidos mas expirados temporalmente |
|
||||||
|
| `500 Server Error` | `PKCS11_ERROR`, `PROVIDER_NOT_AVAILABLE` | Falha interna no middleware do fabricante ou driver criptográfico |
|
||||||
|
| `503 Service Unavail`| `TOKEN_REMOVED`, `COMMUNICATION_ERROR` | Hardware desconectado ou serviço em recuperação |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Diagrama de Sequência Completo
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Usuário
|
||||||
|
participant Browser as Aplicação Web (Browser)
|
||||||
|
participant SDK as @cybercore/signer-sdk
|
||||||
|
participant CCSP as CyberCore CcspServer (127.0.0.1)
|
||||||
|
participant UI as CyberCore Desktop UI
|
||||||
|
participant HW as Hardware Token / Windows MY
|
||||||
|
|
||||||
|
Browser->>SDK: signer.sign({ digest, certId })
|
||||||
|
SDK->>CCSP: POST /ccsp/v1/sign (Headers, AppId, Nonce, Digest)
|
||||||
|
CCSP->>CCSP: Valida Origin, AppId e Nonce (anti-replay)
|
||||||
|
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
|
||||||
|
UI->>User: Exibe Diálogo Nativo (Aplicação, Documento, Certificado)
|
||||||
|
User->>UI: Clica em "Assinar"
|
||||||
|
opt Certificado A3 exige PIN
|
||||||
|
UI->>User: Solicita PIN do Hardware
|
||||||
|
User->>UI: Digita PIN
|
||||||
|
end
|
||||||
|
UI-->>CCSP: Confirmação concedida
|
||||||
|
CCSP->>HW: signLock.lock() -> JCA/JCE Signature.sign()
|
||||||
|
HW-->>CCSP: Bytes da assinatura + Cadeia X.509
|
||||||
|
CCSP->>CCSP: signLock.unlock()
|
||||||
|
CCSP-->>SDK: 200 OK (signature Base64, cert, subject)
|
||||||
|
SDK-->>Browser: Retorna resultado para a página web
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Documentação Técnica — CyberCore Signer
|
||||||
|
|
||||||
|
> **Índice Unificado e Guias Técnicos do Agente Desktop e Protocolo CCSP**
|
||||||
|
|
||||||
|
Bem-vindo à documentação oficial do **CyberCore Signer**. O agente foi desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica nativa JCA/JCE, permitindo que aplicações web autorizadas realizem operações de assinatura digital e autenticação com certificados ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) mantendo as chaves privadas estritamente no dispositivo do usuário.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Trilhas de Navegação por Perfil
|
||||||
|
|
||||||
|
Para facilitar a consulta, os documentos estão organizados de acordo com o seu perfil de atuação:
|
||||||
|
|
||||||
|
### 🌐 1. Para Desenvolvedores Web e Integradores
|
||||||
|
Se você desenvolve aplicações web (frontend/backend) que precisam interagir com o agente do usuário:
|
||||||
|
|
||||||
|
* **[Protocolo CCSP/1 (CyberCore Signing Protocol)](CCSP.md):** Especificação completa da API REST local (`127.0.0.1:21325`), endpoints, ciclo de handshake, proteção anti-replay e exemplos de requisição/resposta.
|
||||||
|
* **[SDK Web JavaScript (@cybercore/signer-sdk)](sdk.md):** Manual de instalação e uso da biblioteca oficial cliente para integração em aplicações web (Vite, Next.js, Vue, Angular, Vanilla JS).
|
||||||
|
* **[Autenticação e Login Digital ICP-Brasil](authentication-login.md):** Fluxo de autenticação segura via desafio criptográfico (*Challenge-Response*) para substituição ou complemento de senhas tradicionais.
|
||||||
|
* **[Catálogo Estruturado de Erros](errors.md):** Lista completa de códigos de erro (`CcspErrorCode`), status HTTP correspondentes e orientações de tratamento na interface web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ☕ 2. Para Desenvolvedores Desktop e Contribuidores
|
||||||
|
Se você atua no desenvolvimento, manutenção ou auditoria do código-fonte do agente desktop:
|
||||||
|
|
||||||
|
* **[Arquitetura do Sistema](architecture.md):** Organização modular Maven (`cybercore-core`, `cybercore-ui`, `cybercore-app`), fluxo de dados por digest, modelo de concorrência com fila FIFO (`signLock`) e isolamento de dependências.
|
||||||
|
* **[Certificados A1, A3 e PKCS#11](a1-a3-certificates.md):** Funcionamento dos provedores `Pkcs12CertificateProvider`, `WindowsCertificateStoreProvider` (SunMSCAPI/CNG) e `Pkcs11CertificateProvider` (SunPKCS11), monitoramento dinâmico PC/SC (`SmartCardWatcher`) e extração de OIDs ICP-Brasil.
|
||||||
|
* **[Assinaturas PDF (PAdES) e Selos Visuais SVG](pades.md):** Motor de assinatura PDFBox + Bouncy Castle, salvamento incremental, renderização de alta resolução (300 DPI Java2D) e gerenciamento de selos digitais.
|
||||||
|
* **[Assinaturas XML (XAdES-BES e XMLDSig)](xml-xmldsig.md):** Conformidade com o padrão ETSI TS 101 903 / DOC-ICP-15.03 e suporte a notas fiscais eletrônicas (NF-e, CT-e, MDF-e, eSocial).
|
||||||
|
* **[Layout Dual-Theme e Assinatura em Lote](serpro-layout-and-batch.md):** Especificação de interface gráfica moderna, alternância dinâmica de tema (Claro/Escuro) e fila de processamento em lote.
|
||||||
|
* **[Modo de Desenvolvimento](development-mode.md):** Guia de inicialização em modo `--dev`, políticas de não utilização de mocks em produção e geração de certificados A1 de teste locais.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛡️ 3. Para DevOps, Infraestrutura e Segurança
|
||||||
|
Se você precisa implantar, homologar ou validar os aspectos de conformidade e segurança da solução:
|
||||||
|
|
||||||
|
* **[Modelo de Segurança e Privacidade](security.md):** Diretrizes de menor privilégio, proteção contra CSRF e DNS Rebinding, descarte seguro de senhas e PIN em memória e conformidade com a LGPD.
|
||||||
|
* **[Instalação e Empacotamento Nativo](installation.md):** Criação de instaladores autocontidos para Windows (MSI/EXE) via `jpackage`, requisitos do sistema e parâmetros de instalação corporativa silenciosa.
|
||||||
|
* **[Diagnóstico e Troubleshooting](diagnostics.md):** Subsistema de auto-teste, checklist de integridade de runtime e guia de resolução de problemas comuns de hardware/middleware.
|
||||||
|
* **[Limitações Técnicas Declaradas](limitations.md):** Declaração formal de requisitos de drivers, limitações de hardware mono-thread e dependências de sistema operacional (em conformidade com a Regra 23 do projeto).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Sumário Geral de Arquivos
|
||||||
|
|
||||||
|
| Documento | Assunto Principal | Público-Alvo |
|
||||||
|
|---|---|---|
|
||||||
|
| [`CCSP.md`](CCSP.md) | Especificação técnica do protocolo local CCSP versão 1 | Integradores Web & Core Devs |
|
||||||
|
| [`architecture.md`](architecture.md) | Estrutura modular, threading e fluxo de operações | Desenvolvedores Desktop |
|
||||||
|
| [`a1-a3-certificates.md`](a1-a3-certificates.md) | Provedores A1/A3, PKCS#11, PC/SC e OIDs ICP-Brasil | Especialistas em Criptografia |
|
||||||
|
| [`pades.md`](pades.md) | Assinatura PAdES, envelope CMS e selos vetoriais | Desenvolvedores Desktop & Web |
|
||||||
|
| [`xml-xmldsig.md`](xml-xmldsig.md) | XAdES-BES, XMLDSig e documentos fiscais | Integradores de NF-e/XAdES |
|
||||||
|
| [`authentication-login.md`](authentication-login.md) | Login Challenge-Response via ICP-Brasil | Integradores Web |
|
||||||
|
| [`errors.md`](errors.md) | Tabela estruturada de erros e mapeamentos HTTP | Todos |
|
||||||
|
| [`security.md`](security.md) | Políticas de isolamento, memória e privacidade | Segurança & Compliance |
|
||||||
|
| [`diagnostics.md`](diagnostics.md) | Auto-testes e guia de resolução de problemas | Suporte & Operações |
|
||||||
|
| [`installation.md`](installation.md) | Empacotamento com `jpackage` e instaladores MSI | DevOps & Usuários Finais |
|
||||||
|
| [`limitations.md`](limitations.md) | Dependências de drivers e limitações declaradas | Todos |
|
||||||
|
| [`development-mode.md`](development-mode.md) | Parâmetro `--dev` e certificados de teste | Desenvolvedores Desktop |
|
||||||
|
| [`sdk.md`](sdk.md) | Guia completo da biblioteca JavaScript | Desenvolvedores Web |
|
||||||
|
| [`serpro-layout-and-batch.md`](serpro-layout-and-batch.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> Dúvidas ou inconsistências? Verifique os testes unitários e de arquitetura em `modules/cybercore-core/src/test` ou consulte o arquivo [README.md principal](../README.md).
|
||||||
@@ -1,46 +1,100 @@
|
|||||||
# Certificados Digitais A1, A3 e PKCS#11
|
# 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.
|
> **Guia Técnico de Suporte a Provedores Criptográficos, Hardware PC/SC e ICP-Brasil**
|
||||||
|
|
||||||
|
O **CyberCore Signer** fornece uma camada unificada de abstração para gerenciamento e consumo de certificados digitais brasileiros padrão **ICP-Brasil**, cobrindo certificados em arquivo de software (**A1**) e dispositivos físicos criptográficos em hardware (**A3** via tokens USB e cartões inteligentes smartcards).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Certificados Digitais A1 (Software / PKCS#12)
|
## 1. Certificados Digitais A1 (Software / PKCS#12)
|
||||||
|
|
||||||
Os certificados A1 são arquivos com extensão `.pfx` ou `.p12` protegidos por senha.
|
Os certificados padrão A1 são contêineres criptográficos protegidos por senha em formato `.pfx` ou `.p12`.
|
||||||
|
|
||||||
### Fluxo de Uso
|
### 1.1 Arquitetura e Ciclo de Vida
|
||||||
1. O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou importa via tela de Dashboard.
|
1. **Importação:** O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou pelo método de importação no 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.
|
2. **Coleta Segura de Senha:** A senha é obtida por meio do diálogo `A1PasswordDialog` e armazenada estritamente em um array de caracteres `char[]` volátil. Logo após o carregamento do `KeyStore`, o array é preenchido com zeros via `Arrays.fill(passwordChars, (char) 0)`.
|
||||||
3. O provedor `Pkcs12CertificateProvider` instancia um `KeyStore.getInstance("PKCS12")` e extrai os metadados do certificado X.509 (`subject`, `issuer`, `validade`, `algoritmo`, `tamanho da chave`).
|
3. **Instanciação JCA/JCE:** O provedor `Pkcs12CertificateProvider` inicializa um `KeyStore.getInstance("PKCS12")` e extrai o certificado X.509 e o manipulador da chave privada (`PrivateKey`).
|
||||||
4. Durante a assinatura, a chave privada em memória gera a assinatura digital via `Signature.getInstance("SHA256withRSA")`.
|
4. **Isolamento de Memória:** A chave privada nunca é serializada, exportada para disco ou enviada em logs. Durante a assinatura, a chave privada em memória gera os bytes da assinatura via `Signature.getInstance("SHA256withRSA")`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Certificados Digitais A3 no Windows (Windows Certificate Store)
|
## 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`).
|
No sistema operacional Microsoft Windows (Windows 10 e Windows 11), a grande maioria dos middlewares de fabricantes de tokens e smartcards registra os certificados automaticamente no repositório de certificados pessoais do usuário (`Windows-MY`).
|
||||||
|
|
||||||
### Vantagens do Provider `WindowsCertificateStoreProvider`
|
### 2.1 Funcionamento do `WindowsCertificateStoreProvider`
|
||||||
- Acessa certificados físicos via CryptoAPI (CAPI) e Cryptography Next Generation (CNG) através do provedor JDK `SunMSCAPI`.
|
* **Integração SunMSCAPI / CNG:** O agente consome o provedor JCA nativo `SunMSCAPI` (`KeyStore.getInstance("Windows-MY")`), integrando-se diretamente à CryptoAPI e à moderna Cryptography Next Generation (CNG) do Windows.
|
||||||
- A chave privada permanece dentro do chip criptográfico do token.
|
* **Preservação no Chip Físico:** A chave privada nunca sai do chip criptográfico do token. As operações de assinatura delegam a cifragem ao módulo de segurança em hardware (HSM/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.
|
* **Diálogo Nativo de PIN do Fabricante:** Quando o token exige autenticação por PIN para liberar a chave privada, a janela nativa do próprio driver do fabricante (ex: diálogo nativo SafeSign ou SafeNet) é exibida com total segurança pelo subsistema de segurança do Windows.
|
||||||
|
* **Tratamento de Chaves Não Exportáveis:** O CyberCore Signer respeita integralmente a flag de chave privada não exportável, utilizando apenas referências de handle sem tentar copiar os bytes brutos da chave.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Hardware PKCS#11 Direto (Tokens USB e Smartcards)
|
## 3. Acesso Direto a Hardware PKCS#11 (Tokens USB e Smartcards)
|
||||||
|
|
||||||
Para ambientes onde se deseja acesso direto ao middleware ou no Linux:
|
Para ambientes corporativos que requerem comunicação direta com bibliotecas de middleware ou para execução em ambientes Linux, o componente `Pkcs11CertificateProvider` interage com o driver PKCS#11 do fabricante através do módulo do JDK `jdk.crypto.cryptoki` (`SunPKCS11`).
|
||||||
- `Pkcs11CertificateProvider` pesquisa bibliotecas DLL / SO padrão:
|
|
||||||
- `eps2003csp11.dll` (SafeNet / eToken)
|
### 3.1 Mapeamento de Middlewares Homologados no Brasil
|
||||||
- `aetpkss1.dll` (SafeSign AET)
|
|
||||||
- `acospkcs11.dll` (ACOS / G&D)
|
O CyberCore Signer possui rotinas automáticas de varredura nos diretórios padrão do sistema (`System32`, `SysWOW64` no Windows e `/usr/lib/`, `/usr/local/lib/` no Linux):
|
||||||
- `opensc-pkcs11.dll` / `opensc-pkcs11.so` (OpenSC)
|
|
||||||
- Utiliza o provider SunPKCS11 do JDK (`jdk.crypto.cryptoki`).
|
| Fabricante / Modelo | Biblioteca Windows (DLL) | Biblioteca Linux (.so) | Tipo de Dispositivo |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **SafeSign Identity Client** (AET / GD) | `aetpkss1.dll` | `libaetpkss.so` | Tokens e Smartcards (Certisign, Valid, Serasa) |
|
||||||
|
| **SafeNet Authentication Client** (Gemalto / Thales) | `eps2003csp11.dll`, `eTPKCS11.dll` | `libeTPkcs11.so` | Tokens eToken Pro / 5110 (Serpro, Safeweb) |
|
||||||
|
| **ACOS / Oberthur** (Idemia) | `acospkcs11.dll`, `OcsCryptoki.dll` | `libacospkcs11.so` | Smartcards e leitores de cartão |
|
||||||
|
| **Watchdata / PROXKey** | `Watchdata ProxKey PKCS11.dll` | `libwatchdata.so` | Tokens USB |
|
||||||
|
| **OpenSC (Padrão Aberto)** | `opensc-pkcs11.dll` | `opensc-pkcs11.so` | Leitores genéricos PC/SC e smartcards |
|
||||||
|
|
||||||
|
### 3.2 Configuração Dinâmica do Provedor SunPKCS11
|
||||||
|
A configuração do provedor é criada dinamicamente em memória (sem gravar arquivos temporários em disco com dados sensíveis):
|
||||||
|
|
||||||
|
```properties
|
||||||
|
name = CyberCore-Token
|
||||||
|
library = C:\Windows\System32\aetpkss1.dll
|
||||||
|
slotListIndex = 0
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`)
|
## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`)
|
||||||
|
|
||||||
O componente `SmartCardWatcher` monitora os leitores PC/SC em segundo plano:
|
O componente `SmartCardWatcher` executa um loop em segundo plano baseado na API padrão `javax.smartcardio` (subsistema PC/SC):
|
||||||
- **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`.
|
1. **Detecção de Conexão (Card Inserted):**
|
||||||
|
- Detecta quando um novo smartcard ou token USB é conectado a qualquer leitora do sistema.
|
||||||
|
- Dispara recarregamento assíncrono do repositório de certificados.
|
||||||
|
- Notifica a interface gráfica (`UIEventListener`) e emite evento de auditoria (`AUDITORIA [TOKEN_INSERTED]`).
|
||||||
|
2. **Detecção de Desconexão (Card Removed):**
|
||||||
|
- Ao detectar a remoção do dispositivo, registra imediatamente o evento de auditoria `AUDITORIA [TOKEN_REMOVED]`.
|
||||||
|
- Limpa e zera os buffers de memória de PIN mantidos pelo `PinSessionManager`.
|
||||||
|
- Força o encerramento das conexões com o leitor via `SmartCardSessionManager` para liberar handles travados no sistema operacional.
|
||||||
|
3. **Serialização de Concorrência (`signLock`):**
|
||||||
|
- Como grande parte dos drivers de smartcard e chips criptográficos físicos não suporta acesso concorrente multi-thread, todas as requisições de assinatura são processadas de forma estritamente sequencial (fila FIFO garantida por `ReentrantLock`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Extração e Parsing de Identidade ICP-Brasil (OIDs X.509)
|
||||||
|
|
||||||
|
O CyberCore Signer implementa o analisador de extensões X.509 conforme os padrões da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)** estabelecidos pelo ITI (normas DOC-ICP-04).
|
||||||
|
|
||||||
|
As informações são extraídas da extensão `SubjectAlternativeName` (OID `2.5.29.17`) e analisadas a partir de seus identificadores de objeto (OIDs):
|
||||||
|
|
||||||
|
### 5.1 OID ICP-Brasil de Pessoa Física (e-CPF) — `2.16.76.1.3.1`
|
||||||
|
Contém uma estrutura ASN.1 com as informações cadastrais do titular:
|
||||||
|
* **Data de Nascimento:** 8 caracteres no formato `DDMMAAAA`;
|
||||||
|
* **CPF do Titular:** 11 dígitos numéricos;
|
||||||
|
* **Número do NIS/PIS/PASEP:** 11 dígitos numéricos;
|
||||||
|
* **Registro Geral (RG):** Número do documento de identidade;
|
||||||
|
* **Órgão Expedidor e UF:** Identificação do órgão emissor.
|
||||||
|
|
||||||
|
### 5.2 OID ICP-Brasil de Pessoa Jurídica (e-CNPJ) — `2.16.76.1.3.3`
|
||||||
|
Contém os dados cadastrais da empresa e do seu representante legal:
|
||||||
|
* **CNPJ da Entidade:** 14 dígitos numéricos;
|
||||||
|
* **Nome do Responsável:** Nome completo do signatário cadastrado na Receita Federal.
|
||||||
|
|
||||||
|
### 5.3 Demais OIDs Mapeados
|
||||||
|
* **OID `2.16.76.1.3.2`:** Título de Eleitor, zona e seção eleitoral.
|
||||||
|
* **OID `2.16.76.1.3.4`:** Cadastro Específico do INSS (CEI / CAEPF).
|
||||||
|
|
||||||
|
Essa extração permite ao agente preencher automaticamente o nome e documento do signatário para o SDK Web e selos visuais em PDF, garantindo conformidade com a legislação e auditoria fiscal.
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Arquitetura do CyberCore Signer
|
# 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.
|
> **Especificação da Arquitetura Modular, Modelo de Threads e Fluxo Criptográfico**
|
||||||
|
|
||||||
|
O **CyberCore Signer** foi concebido a partir de princípios de alta coesão, baixo acoplamento e estrito isolamento de camadas. O software é desenvolvido em **Java 21+ LTS (Java 25 LTS)**, utilizando o runtime modular do JDK, JavaFX para a camada de apresentação e os provedores JCA/JCE da plataforma para operações criptográficas seguras.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Visão Geral da Arquitetura em Módulos
|
## 1. Visão Geral da Arquitetura em Módulos
|
||||||
|
|
||||||
O projeto adota uma arquitetura desacoplada em múltiplos módulos Maven independentes:
|
O projeto adota uma divisão modular rigorosa no Maven, garantindo que regras de negócio e algoritmos de criptografia nunca se acoplem a componentes visuais:
|
||||||
|
|
||||||
```
|
```
|
||||||
cybercore-signer/
|
cybercore-signer/
|
||||||
@@ -19,43 +21,75 @@ cybercore-signer/
|
|||||||
└── docs/ # Especificações técnicas e manuais de integração
|
└── docs/ # Especificações técnicas e manuais de integração
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.1 `cybercore-core`
|
### 1.1 Módulo `cybercore-core` (Agnóstico de UI)
|
||||||
- **Domínio:** Modelos imutáveis como `ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType` e `CertificateStatus`.
|
O módulo de núcleo é totalmente independente de JavaFX ou bibliotecas visuais:
|
||||||
- **Criptografia:** `SigningService` e `DigestAlgorithm` utilizando exclusivamente os provedores padrão JCA/JCE (sem reimplementação manual de algoritmos).
|
* **Domínio Imutável:** Modelos imutáveis (`ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType`, `CertificateStatus`).
|
||||||
- **Provedores:** Abstração `CertificateProvider` com implementações:
|
* **Serviços de Assinatura:** `SigningService` orquestra a aplicação de algoritmos certificados (`SHA256withRSA`) delegando a operação ao provedor de certificado resolvido.
|
||||||
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória protegida.
|
* **Provedores de Certificados (`CertificateProvider`):** Abstração que unifica:
|
||||||
- `WindowsCertificateStoreProvider`: integração nativa com o repositório de chaves do Windows (`Windows-MY` via SunMSCAPI/CNG).
|
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória com limpeza volátil de senha.
|
||||||
- `Pkcs11CertificateProvider`: integração com drivers de middleware e tokens USB/Smartcards via `SunPKCS11`.
|
- `WindowsCertificateStoreProvider`: integração com o repositório nativo do Windows (`Windows-MY` via SunMSCAPI/CNG).
|
||||||
- **Protocolo & Rede Local:** `CcspServer` escuta estritamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1.
|
- `Pkcs11CertificateProvider`: integração direta com bibliotecas dinâmicas (DLL/SO) de drivers e tokens USB através do SunPKCS11.
|
||||||
- **Segurança & Configurações:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` (persistência em `~/.cybercore/config.json`).
|
* **Protocolo e Rede Local:** `CcspServer` escuta exclusivamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1 com validação de origens e controle de concorrência.
|
||||||
- **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).
|
* **Segurança e Persistência:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` com armazenamento persistente em diretório de usuário (`~/.cybercore/`).
|
||||||
- **Selos Visuais Personalizados:** `CustomSealStorage` (persistência em `~/.cybercore/seals/`), `PdfSealThemeRegistry` dinâmico e `CpfDisplayMode` (privacidade LGPD).
|
* **Assinatura de Documentos e Validação:** `PdfSigningService` (PAdES com salvamento incremental e carimbo TSA via PDFBox e Bouncy Castle), `XmlSigningService` (XAdES-BES e XMLDSig via `javax.xml.crypto`) e `SignatureValidationService` para conferência de integridade e signatários.
|
||||||
|
|
||||||
### 1.2 `cybercore-ui`
|
### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
|
||||||
- Interface desenvolvida com JavaFX 25, empregando layout moderno e simétrico de grade 3x3 de operações e coluna lateral de utilitários:
|
Responsável por toda a interação com o usuário:
|
||||||
- `MainAppView`: grade 3x3 otimizada:
|
* **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade 3x3 de operações rápidas (PDF, XML, Arquivo Genérico, Assinatura em Lote, Assinatura Atachada, Co-Assinatura, Carimbo de Tempo, Designer de Selos, Validador) e coluna lateral de utilitários (Logs em tempo real, Configurações e Diagnóstico).
|
||||||
- Linha 0: Assinar PDF, Assinar XML, Assinatura Atachada
|
* **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
|
||||||
- Linha 1: Co-Assinar Documento, Carimbo do Tempo, Gerenciar Selos
|
* **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
|
||||||
- Linha 2: Validar Assinatura, Validar Carimbo, Sair do Agente
|
* **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
|
||||||
- `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`
|
### 1.3 Módulo `cybercore-app` (Orquestração e Bootstrap)
|
||||||
- Main bootstrap (`CyberCoreSignerApplication`).
|
Ponto de partida da aplicação (`CyberCoreApp`):
|
||||||
- Tratamento de parâmetros de inicialização (`--dev`, `--headless`).
|
* Processa argumentos de linha de comando (`--dev`, `--headless`).
|
||||||
- Execução de diagnósticos preliminares de ambiente e engine criptográfica antes do início das threads de escuta.
|
* Executa rotinas de diagnóstico pré-inicialização do ambiente Java e disponibilidade criptográfica.
|
||||||
- Geração de Fat JAR autocontido via `maven-shade-plugin` e preparação para distribuição `jpackage`.
|
* Inicia o `CcspServer` em segundo plano e abre a interface gráfica na `JavaFX Application Thread`.
|
||||||
|
* Empacota o executável consolidado via `maven-shade-plugin` e orquestra o build nativo via `jpackage`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Fluxo de Dados e Princípio de Menor Privilégio
|
## 2. Modelo de Threads e Concorrência
|
||||||
|
|
||||||
O CyberCore Signer opera sob o **modelo de assinatura descentralizada por digest**:
|
Para manter a interface gráfica sempre fluida e ágil, o CyberCore Signer utiliza um particionamento rígido de threads:
|
||||||
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.
|
```mermaid
|
||||||
3. O documento completo nunca sai do navegador e a chave privada nunca sai do computador ou do hardware do usuário.
|
graph TD
|
||||||
|
subgraph UI Layer
|
||||||
|
JFX[JavaFX Application Thread]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Local Protocol Layer
|
||||||
|
HTTPPool[CcspServer Worker Pool (FixedThreadPool 8)]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Background Services
|
||||||
|
Watcher[SmartCardWatcher Thread (PC/SC Loop)]
|
||||||
|
AsyncTasks[JavaFX Task / CompletableFuture Workers]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Hardware Critical Section
|
||||||
|
Lock[Trava Serializada FIFO: signLock]
|
||||||
|
Hardware[Token Físico A3 / SmartCard Driver]
|
||||||
|
end
|
||||||
|
|
||||||
|
HTTPPool -->|Solicita Aprovação Visual| JFX
|
||||||
|
JFX -->|Consentimento Concedido| HTTPPool
|
||||||
|
HTTPPool --> Lock
|
||||||
|
AsyncTasks --> Lock
|
||||||
|
Lock --> Hardware
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **JavaFX Application Thread:** Dedicada exclusivamente à renderização da interface e tratamento de eventos visuais. Nenhuma operação criptográfica, leitura de disco ou chamada de rede é executada nesta thread.
|
||||||
|
2. **CcspServer Worker Pool:** Pool de 8 threads daemon dedicadas ao atendimento de requisições HTTP da API REST CCSP/1.
|
||||||
|
3. **SmartCardWatcher Thread:** Thread daemon isolada que executa `cardTerminal.waitForCardPresent()` e `waitForCardAbsent()` para monitorar hardware PC/SC sem onerar a CPU.
|
||||||
|
4. **Trava Serializada FIFO (`signLock`):** Implementada via `ReentrantLock(true)`. Garante que operações de assinatura em cartões inteligentes físicos sejam enfileiradas ordenadamente, prevenindo falhas de hardware e estados corrompidos em drivers de fabricantes mono-thread.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Fluxo de Dados e Assinatura Descentralizada por Digest
|
||||||
|
|
||||||
|
A arquitetura do CyberCore Signer prioriza a privacidade e o modelo de **assinatura por digest**:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
@@ -66,14 +100,25 @@ sequenceDiagram
|
|||||||
participant UI as CyberCore JavaFX
|
participant UI as CyberCore JavaFX
|
||||||
participant Crypto as SigningService & Providers
|
participant Crypto as SigningService & Providers
|
||||||
|
|
||||||
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce)
|
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce, expires_at)
|
||||||
CCSP->>CCSP: Valida autorização, nonce anti-replay e digest
|
CCSP->>CCSP: Valida autorização, nonce anti-replay e integridade do digest
|
||||||
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
|
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
|
||||||
UI->>Usuario: Exibe Diálogo (Hash, Certificado, Aplicação)
|
UI->>Usuario: Exibe Diálogo Nativo (Nome do Documento, Hash SHA-256, Certificado)
|
||||||
Usuario->>UI: Aprova assinatura (e insere PIN se A3)
|
Usuario->>UI: Clica em "Autorizar Assinatura" (e digita PIN se requerido)
|
||||||
UI-->>CCSP: Confirmação concedida
|
UI-->>CCSP: Confirmação concedida
|
||||||
CCSP->>Crypto: sign(SigningContext) [Trava FIFO]
|
CCSP->>Crypto: sign(SigningContext) sob signLock.lock()
|
||||||
Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign()
|
Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign()
|
||||||
Crypto-->>CCSP: SigningResult (signatureBytes, certChain)
|
Crypto-->>CCSP: SigningResult (signatureBytes, certChain)
|
||||||
CCSP-->>Web: JSON { status: "signed", signature: "...", certificate: "..." }
|
CCSP-->>Web: 200 OK JSON { status: "signed", signature: "Base64", certificate: "Base64" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Persistência de Dados e Configurações
|
||||||
|
|
||||||
|
Todos os dados de configuração e personalizações do usuário são armazenados no diretório do usuário no sistema operacional:
|
||||||
|
|
||||||
|
* **Diretório Base:** `~/.cybercore/` (ou `%USERPROFILE%\.cybercore\` no Windows).
|
||||||
|
* **`config.json`:** Configurações globais do agente (política de cache de PIN, tema padrão, selo padrão ativo).
|
||||||
|
* **`authorized-apps.json`:** Registro de aplicações web autorizadas pelo usuário, contendo `application_id`, nome amigável e data da aprovação.
|
||||||
|
* **`seals/*.json`:** Selos visuais SVG personalizados criados ou importados pelo usuário no designer de selos.
|
||||||
|
|||||||
@@ -1,48 +1,89 @@
|
|||||||
# Modo de Desenvolvimento e Diagnóstico Detalhado
|
# Modo de Desenvolvimento e Homologação
|
||||||
|
|
||||||
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**.
|
> **Diretrizes para Execução Local com Argumento `--dev`, Testes e Criação de Certificados A1 de Homologação**
|
||||||
|
|
||||||
|
O **CyberCore Signer** opera exclusivamente com certificados digitais e provedores criptográficos reais (arquivos `.pfx`/`.p12`, Windows-MY e tokens físicos PKCS#11). Em cumprimento às normas de integridade do projeto, **não existem mocks ou certificados artificiais no runtime de produção**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Ativação do Modo de Desenvolvimento
|
## 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`:
|
Para iniciar o agente em ambiente de homologação e desenvolvimento com logs estendidos de auditoria e detalhamento de operações de rede, utilize o argumento de linha de comando `--dev`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar cybercore-signer.jar --dev
|
# Executando via script Linux / macOS:
|
||||||
|
./run.sh --dev
|
||||||
|
|
||||||
|
# Executando no Windows:
|
||||||
|
run.bat --dev
|
||||||
|
|
||||||
|
# Ou diretamente pelo JAR compilado:
|
||||||
|
java -jar modules/cybercore-app/target/cybercore-signer.jar --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Ao iniciar, uma mensagem informativa é registrada no log:
|
Ao ser iniciado com esta flag, o agente imprime no terminal o banner de diagnóstico:
|
||||||
```
|
```
|
||||||
=========================================================
|
=========================================================
|
||||||
MODO DE DESENVOLVIMENTO ATIVADO (--dev)
|
CYBERCORE SIGNER — MODO DE DESENVOLVIMENTO ATIVADO (--dev)
|
||||||
Logs detalhados ativos. Operação com certificados reais.
|
Logs detalhados ativos. Operação com certificados reais JCA/JCE.
|
||||||
|
Servidor local CCSP/1 pronto em: http://127.0.0.1:21325
|
||||||
=========================================================
|
=========================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Testando em Ambiente de Desenvolvimento
|
## 2. Geração de Certificados A1 de Teste com OpenSSL
|
||||||
|
|
||||||
Para desenvolvedores testarem fluxos completos de assinatura de PDF, XML, arquivos em geral e login digital:
|
Para desenvolvedores e testadores que precisam simular fluxos completos de assinatura digital (PAdES, XAdES, login digital) sem utilizar um certificado ICP-Brasil real de produção, é possível gerar um certificado digital de teste no formato `.pfx` protegido por senha utilizando o utilitário **OpenSSL**:
|
||||||
|
|
||||||
1. **Certificados A1 de Teste/Homologação:**
|
### Passo 1: Gerar a Chave Privada RSA (2048 bits)
|
||||||
- Adicione um certificado A1 `.pfx` ou `.p12` emitido por uma autoridade de teste ou autoassinado.
|
```bash
|
||||||
- Pela interface desktop, clique no botão **"Importar A1 (.pfx/.p12)"** no Dashboard e informe o arquivo e a senha.
|
openssl genrsa -out teste_signer.key 2048
|
||||||
- O certificado ficará disponível imediatamente para todas as operações CCSP/1.
|
```
|
||||||
|
|
||||||
2. **Repositório Nativo do Windows (Windows-MY):**
|
### Passo 2: Gerar o Certificado X.509 Autoassinado
|
||||||
- Instale o certificado no repositório de certificados pessoais do Windows (`certmgr.msc`).
|
```bash
|
||||||
- O provedor `WindowsCertificateStoreProvider` detectará o certificado automaticamente.
|
openssl req -new -x509 -key teste_signer.key -out teste_signer.crt -days 365 \
|
||||||
|
-subj "/C=BR/O=ICP-Brasil Teste/OU=Desenvolvimento CyberCore/CN=DESENVOLVEDOR TESTE:12345678900"
|
||||||
|
```
|
||||||
|
|
||||||
3. **Tokens USB / Smart Cards (PKCS#11):**
|
### Passo 3: Exportar o Certificado e Chave no Contêiner PKCS#12 (`.pfx`)
|
||||||
- Conecte o dispositivo físico com middleware homologado (SafeSign, GD Burti, SafeNet, OpenSC, etc.).
|
```bash
|
||||||
- O provedor `Pkcs11CertificateProvider` detectará o token e seus slots.
|
openssl pkcs12 -export -out certificado_teste.pfx \
|
||||||
|
-inkey teste_signer.key -in teste_signer.crt \
|
||||||
|
-name "DESENVOLVEDOR TESTE:12345678900" \
|
||||||
|
-passout pass:123456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passo 4: Importar no CyberCore Signer
|
||||||
|
1. Abra o CyberCore Signer.
|
||||||
|
2. Na tela principal, clique em **"Importar A1 (.pfx/.p12)"**.
|
||||||
|
3. Selecione o arquivo `certificado_teste.pfx` gerado e informe a senha `123456`.
|
||||||
|
4. O certificado aparecerá instantaneamente como disponível no Dashboard e poderá ser utilizado para todos os testes do SDK e CCSP/1.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Garantia de Operação Real
|
## 3. Execução da Suíte de Testes Automatizados
|
||||||
|
|
||||||
1. Toda assinatura é gerada criptograficamente através dos provedores JCA/JCE reais.
|
O repositório do CyberCore Signer conta com suíte abrangente de testes unitários, testes de concorrência e testes de arquitetura (ArchUnit):
|
||||||
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.
|
```bash
|
||||||
|
# Executar todos os testes unitários do projeto:
|
||||||
|
./mvnw test
|
||||||
|
|
||||||
|
# Executar testes específicos do protocolo CCSP/1:
|
||||||
|
./mvnw test -Dtest=CcspServerTest
|
||||||
|
|
||||||
|
# Executar testes de concorrência e trava FIFO (signLock):
|
||||||
|
./mvnw test -Dtest=CcspConcurrencyTest
|
||||||
|
|
||||||
|
# Executar testes de conformidade arquitetural (garante desacoplamento de UI no core):
|
||||||
|
./mvnw test -Dtest=ArchitectureTest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Garantia de Operação Real
|
||||||
|
|
||||||
|
1. **Assinaturas Autênticas:** Mesmo ao utilizar um certificado autoassinado de teste, os bytes da assinatura são calculados matematicamente pelos algoritmos reais do JCA/JCE (`SHA256withRSA`).
|
||||||
|
2. **Sem Falsos Positivos:** O comportamento do agente é 100% idêntico entre o ambiente de teste e o ambiente de produção com tokens físicos A3.
|
||||||
|
|||||||
@@ -1,34 +1,103 @@
|
|||||||
# Diagnóstico de Ambiente e Auto-Teste
|
# Diagnóstico de Ambiente e Resolução de Problemas
|
||||||
|
|
||||||
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.
|
> **Guia de Auto-Teste, Checklist de Integridade de Runtime e Troubleshooting de Hardware**
|
||||||
|
|
||||||
|
O **CyberCore Signer** conta com um subsistema nativo de diagnóstico e telemetria local (`DiagnosticService`), projetado para validar a integridade da estação de trabalho e identificar rapidamente falhas de drivers, certificados ou subsistemas criptográficos do sistema operacional.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Itens Inspecionados no Diagnóstico
|
## 1. Itens Inspecionados pelo Diagnóstico Automático
|
||||||
|
|
||||||
Ao abrir a aba **Diagnóstico** na interface ou invocar `diagnosticService.runDiagnostic()`, o agente avalia:
|
Ao acessar a aba **Diagnóstico** na interface do agente desktop ou ao inicializar em modo `--dev`, o agente executa uma auditoria completa nos seguintes componentes:
|
||||||
|
|
||||||
1. **Ambiente Java / Runtime:**
|
1. **Ambiente Java / Runtime Nativo:**
|
||||||
- Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS);
|
- Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS);
|
||||||
- Vendor do JDK (Temurin, Oracle, etc.);
|
- Vendor do JDK (Eclipse Temurin, Oracle OpenJDK, Amazon Corretto);
|
||||||
- Arquitetura do processador e sistema operacional.
|
- Arquitetura da CPU (x86_64 / amd64) e sistema operacional (Windows 10/11 ou Linux).
|
||||||
2. **Engine Criptográfica JCA/JCE:**
|
2. **Engine Criptográfica JCA/JCE:**
|
||||||
- Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.);
|
- Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.);
|
||||||
- Suporte ao algoritmo de hash `SHA-256`;
|
- Suporte aos algoritmos de hash criptográfico (`SHA-256`, `SHA-512`);
|
||||||
- Suporte ao algoritmo de assinatura `SHA256withRSA`.
|
- Suporte aos algoritmos de assinatura digital (`SHA256withRSA`).
|
||||||
3. **Repositório de Chaves do Sistema Operacional:**
|
3. **Repositório de Certificados do Sistema Operacional:**
|
||||||
- Detecção do repositório `Windows-MY` (Windows CryptoAPI / CNG).
|
- Detecção do repositório de chaves `Windows-MY` via SunMSCAPI / CryptoAPI / CNG.
|
||||||
4. **Middlewares PKCS#11 e SmartCard:**
|
4. **Middlewares PKCS#11 e Subsistema PC/SC:**
|
||||||
- Verificação de bibliotecas de smartcard registradas no sistema (`eps2003csp11.dll`, `aetpkss1.dll`, `opensc-pkcs11.dll`, etc.);
|
- Presença das bibliotecas dinâmicas do fabricante (`aetpkss1.dll`, `eps2003csp11.dll`, `acospkcs11.dll`, `opensc-pkcs11.dll`);
|
||||||
- Disponibilidade do subsistema PC/SC `javax.smartcardio`.
|
- Disponibilidade do subsistema de leitoras de smartcard `javax.smartcardio` (PC/SC Daemon / Smart Card Service do Windows).
|
||||||
|
5. **Servidor Local CCSP/1:**
|
||||||
|
- Teste de bind do socket em `127.0.0.1:21325` e integridade do pool de threads.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Auto-Teste de Assinatura Local (Self-Test)
|
## 2. Auto-Teste de Assinatura Local (Self-Test)
|
||||||
|
|
||||||
A aba de diagnóstico disponibiliza o botão **"Executar Auto-teste de Assinatura"**:
|
A tela de diagnóstico oferece a função **"Executar Auto-teste de Assinatura"**:
|
||||||
1. Seleciona o primeiro certificado operacional disponível na máquina.
|
1. O agente identifica o primeiro certificado digital operacional conectado.
|
||||||
2. Gera um hash SHA-256 interno de teste: `"Documento_Teste_CyberCore_<timestamp>"`.
|
2. Gera um hash SHA-256 interno sintético para teste: `"Documento_Teste_CyberCore_<timestamp>"`.
|
||||||
3. Abre o diálogo de confirmação visual para validação do usuário.
|
3. Abre o diálogo nativo de confirmação visual para validação do usuário.
|
||||||
4. Executa a assinatura real JCA/JCE e afere os bytes gerados.
|
4. Executa a assinatura real através do provedor JCA/JCE correspondente (solicitando PIN se A3).
|
||||||
5. Emite relatório de sucesso com tempo decorrido e tamanho da assinatura.
|
5. Valida os bytes da assinatura gerada e emite relatório com tempo decorrido em milissegundos e tamanho da assinatura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Guia de Resolução de Problemas (Troubleshooting)
|
||||||
|
|
||||||
|
### 3.1 Token USB ou Smartcard A3 não é detectado
|
||||||
|
* **Sintoma:** O token está plugado na porta USB mas não aparece no CyberCore nem na lista de certificados.
|
||||||
|
* **Causa Comum:** O serviço de cartão inteligente do Windows (`ScardSvr`) está desativado ou o middleware do fabricante (SafeSign/SafeNet) não está instalado.
|
||||||
|
* **Solução:**
|
||||||
|
1. No Windows, pressione `Win + R`, digite `services.msc` e pressione Enter.
|
||||||
|
2. Localize o serviço **Cartão Inteligente (Smart Card)** e garanta que seu status esteja **Em Execução** (Tipo de Inicialização: Automático).
|
||||||
|
3. Verifique se o aplicativo de gerenciamento do token (ex: *SafeSign Token Utility* ou *SafeNet Authentication Client Tools*) reconhece o token.
|
||||||
|
4. Desconecte o token USB, aguarde 3 segundos e reconecte em outra porta USB.
|
||||||
|
|
||||||
|
### 3.2 Erro `NTE_BAD_KEYSET` ou `Chave Privada Não Acessível`
|
||||||
|
* **Sintoma:** O certificado aparece na lista, mas a assinatura falha com erro `PRIVATE_KEY_NOT_AVAILABLE` ou código Windows `0x80090016`.
|
||||||
|
* **Causa Comum:** O certificado foi importado no Windows sem a chave privada correspondente, ou o usuário não tem permissão de leitura sobre o contêiner de chaves.
|
||||||
|
* **Solução:**
|
||||||
|
1. Abra o gerenciador de certificados do Windows: `certmgr.msc`.
|
||||||
|
2. Navegue até **Pessoal -> Certificados** e dê um duplo clique sobre o certificado.
|
||||||
|
3. Verifique se na parte inferior da aba Geral consta a mensagem: *"Você tem uma chave privada correspondente a este certificado"*. Se não constar, reinstale o arquivo A1 com a opção "Marcar esta chave como exportável" ativada.
|
||||||
|
|
||||||
|
### 3.3 Porta local `21325` já em uso (`BindException`)
|
||||||
|
* **Sintoma:** O CyberCore Signer não inicializa e os logs acusam `Address already in use: bind`.
|
||||||
|
* **Causa Comum:** Outra instância do CyberCore Signer ou aplicativo conflitante já está em execução na porta `21325`.
|
||||||
|
* **Solução:**
|
||||||
|
1. No Windows, abra o Gerenciador de Tarefas (`Ctrl + Shift + Esc`) e encerre eventuais processos `cybercore-signer` ou `javaw.exe` órfãos.
|
||||||
|
2. No terminal (PowerShell), você pode identificar o processo ocupando a porta:
|
||||||
|
```powershell
|
||||||
|
Get-NetTCPConnection -LocalPort 21325 | Select-Object OwningProcess
|
||||||
|
```
|
||||||
|
3. No Linux:
|
||||||
|
```bash
|
||||||
|
sudo lsof -i :21325
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Aplicação Web acusa "Agente não instalado" ou Falha de Conexão
|
||||||
|
* **Sintoma:** A chamada `signer.isInstalled()` no navegador retorna `false` ou dá timeout.
|
||||||
|
* **Causa Comum:** O CyberCore Signer está fechado, o antivírus bloqueou a conexão loopback local, ou a aplicação web tenta conectar com protocolo incorreto.
|
||||||
|
* **Solução:**
|
||||||
|
1. Verifique se o ícone do CyberCore Signer está ativo na barra de tarefas (System Tray).
|
||||||
|
2. Abra uma nova aba no navegador e acesse diretamente: `http://127.0.0.1:21325/ccsp/v1/status`.
|
||||||
|
3. O navegador deve exibir o JSON com `"status": "ready"`. Se não exibir, abra o executável do CyberCore Signer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Comandos Úteis de Diagnóstico no Terminal
|
||||||
|
|
||||||
|
### No Windows:
|
||||||
|
```cmd
|
||||||
|
:: Testar leitura de smartcards e drivers via utilitário nativo do Windows:
|
||||||
|
certutil -scinfo
|
||||||
|
|
||||||
|
:: Listar certificados instalados no repositório Pessoal do usuário:
|
||||||
|
certutil -user -store My
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Linux:
|
||||||
|
```bash
|
||||||
|
:: Verificar leitoras e cartões conectados via subsistema PC/SC:
|
||||||
|
pcsc_scan
|
||||||
|
|
||||||
|
:: Verificar status do serviço pcscd:
|
||||||
|
systemctl status pcscd
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,28 +1,80 @@
|
|||||||
# Catálogo Estruturado de Erros
|
# 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.
|
> **Especificação Unificada de Códigos de Erro (`CcspErrorCode`), Status HTTP e Tratamento no SDK**
|
||||||
|
|
||||||
|
O **CyberCore Signer** adota uma taxonomia estrita e estável de erros estruturados, garantindo que o agente desktop Java, a API REST do protocolo CCSP/1 e o SDK JavaScript manipulem falhas com previsibilidade e sem expor detalhes internos sensíveis ao usuário final.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Tabela de Erros Estruturados
|
## 1. Tabela Completa de Códigos de Erro
|
||||||
|
|
||||||
| Código de Erro | Descrição Detalhada | Ação Recomendada para a Aplicação Web |
|
| Código de Erro (`CcspErrorCode`) | Status HTTP | Descrição do Erro | 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_NOT_FOUND` | `404 Not Found` | O certificado indicado pelo `certificate_hint` não foi localizado na máquina. | Solicitar 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_EXPIRED` | `422 Unprocessable` | O certificado digital selecionado ultrapassou sua data limite de validade. | Orientar o usuário a renovar o certificado digital junto à autoridade certificadora (AC). |
|
||||||
| `CERTIFICATE_INVALID` | O certificado possui estrutura corrompida ou ainda não é válido. | Verificar a data e integridade do arquivo/dispositivo. |
|
| `CERTIFICATE_INVALID` | `400 Bad Request` | Estrutura ASN.1/X.509 corrompida ou ainda não iniciada (`NotBefore`). | Verificar integridade do arquivo `.pfx` ou relógio do sistema operacional. |
|
||||||
| `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. |
|
| `PRIVATE_KEY_NOT_AVAILABLE` | `404 Not Found` | O certificado está presente mas a chave privada não está acessível no repositório. | Em tokens A3, verificar se o middleware/driver do token 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_NOT_FOUND` | `404 Not Found` | Nenhum dispositivo criptográfico físico ou leitora de cartão foi detectado. | Solicitar 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. |
|
| `TOKEN_REMOVED` | `503 Service Unavail`| O token USB ou cartão foi desconectado fisicamente durante o processamento. | Orientar o usuário a 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_REQUIRED` | `400 Bad Request` | O token exige PIN para liberar a chave privada e nenhum PIN foi informado. | Exibir campo para inserção do PIN do dispositivo. |
|
||||||
| `PIN_INCORRECT` | O PIN digitado está incorreto. | Informar o usuário e alertar sobre o risco de bloqueio de PUK após 3 tentativas. |
|
| `PIN_INCORRECT` | `400 Bad Request` | O PIN digitado para o token A3 é incorreto. | Alertar o usuário sobre o risco de bloqueio do cartão (PUK) após tentativas consecutivas. |
|
||||||
| `PKCS11_ERROR` | Falha no driver ou biblioteca PKCS#11 do fabricante. | Verificar a instalação do SafeSign, SafeNet ou OpenSC. |
|
| `PKCS11_ERROR` | `500 Server Error` | Falha na biblioteca DLL/SO do middleware ou driver do fabricante. | Verificar a instalação do SafeSign, SafeNet, GD Burti ou OpenSC. |
|
||||||
| `SIGNATURE_FAILED` | Erro durante a geração dos bytes da assinatura digital. | Tentar novamente ou reiniciar o agente desktop. |
|
| `SIGNATURE_FAILED` | `400 Bad Request` | Falha criptográfica na geração dos bytes da assinatura digital. | Reiniciar a operação ou verificar integridade do algoritmo especificado. |
|
||||||
| `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. |
|
| `APPLICATION_NOT_AUTHORIZED` | `401 Unauthorized` | A aplicação web teve autorização recusada pelo usuário ou não foi cadastrada. | Notificar o usuário que é necessário aceitar a solicitação na janela do CyberCore. |
|
||||||
| `REQUEST_EXPIRED` | O timestamp ou `expires_at` da requisição expirou. | Gerar nova requisição de assinatura com timestamp atualizado. |
|
| `REQUEST_EXPIRED` | `422 Unprocessable` | A validade temporal (`expires_at`) da requisição foi ultrapassada. | Gerar nova requisição de assinatura com timestamp atualizado no backend/frontend. |
|
||||||
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | Sempre gerar um UUID único para cada chamada de assinatura. |
|
| `REPLAY_DETECTED` | `409 Conflict` | O `nonce` informado já foi processado anteriormente dentro da janela ativa. | Sempre gerar um UUID criptograficamente novo 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. |
|
| `INVALID_ORIGIN` | `403 Forbidden` | O cabeçalho `Origin` não confere com o domínio autorizado da aplicação. | Conectar a partir da URL e domínio homologados pelo usuário. |
|
||||||
| `UNSUPPORTED_ALGORITHM` | Algoritmo solicitado não é suportado pelo agente ou certificado. | Utilizar algoritmo padrão homologado (`SHA256withRSA`). |
|
| `UNSUPPORTED_ALGORITHM` | `400 Bad Request` | O algoritmo de hash ou assinatura solicitado não é suportado pelo provedor. | Utilizar os algoritmos padrão homologados (`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. |
|
| `USER_CANCELLED` | `400 Bad Request` | O usuário clicou em Cancelar ou fechou o diálogo nativo de confirmação. | Tratar como cancelamento voluntário do usuário sem exibir alerta de erro crítico. |
|
||||||
| `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. |
|
| `PROVIDER_NOT_AVAILABLE` | `500 Server Error` | O provedor criptográfico necessário não está ativo no sistema operacional. | Verificar o Windows Certificate Store ou bibliotecas de runtime do Java. |
|
||||||
| `COMMUNICATION_ERROR` | Erro interno de socket ou timeout de rede local. | Verificar se a porta `21325` não está bloqueada por antivírus local. |
|
| `COMMUNICATION_ERROR` | `503 Service Unavail`| Erro de socket, timeout na confirmação visual ou porta ocupada. | Verificar se o CyberCore Signer está em execução e se a porta `21325` está livre. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Estrutura do Payload JSON de Erro
|
||||||
|
|
||||||
|
Em qualquer resposta com status diferente de `200 OK`, o payload retornado pela API CCSP/1 adota o seguinte formato:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
|
||||||
|
"status": "error",
|
||||||
|
"error_code": "PIN_INCORRECT",
|
||||||
|
"message": "O PIN informado para o token A3 é incorreto."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Tratamento Tipado no SDK JavaScript
|
||||||
|
|
||||||
|
No SDK `@cybercore/signer-sdk`, cada código estruturado possui uma classe de erro correspondente herdando de `CyberCoreError`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
UserCancelledError,
|
||||||
|
PinIncorrectError,
|
||||||
|
TokenNotFoundError,
|
||||||
|
CertificateNotFoundError
|
||||||
|
} from '@cybercore/signer-sdk/errors';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await signer.sign({
|
||||||
|
certificateId: cert.id,
|
||||||
|
digest: documentoDigest
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UserCancelledError) {
|
||||||
|
console.info('Assinatura cancelada voluntariamente pelo usuário.');
|
||||||
|
} else if (error instanceof PinIncorrectError) {
|
||||||
|
alert('PIN incorreto! Verifique a senha do seu token para evitar o bloqueio.');
|
||||||
|
} else if (error instanceof TokenNotFoundError) {
|
||||||
|
alert('Dispositivo não detectado. Por favor, conecte seu token USB na porta do computador.');
|
||||||
|
} else if (error instanceof CertificateNotFoundError) {
|
||||||
|
alert('O certificado selecionado não foi encontrado ou expirou.');
|
||||||
|
} else {
|
||||||
|
console.error('Erro na operação de assinatura:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,43 +1,99 @@
|
|||||||
# Instalação e Empacotamento Nativo
|
# 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.
|
> **Guia de Distribuição com Runtime Embutido (`jpackage`), Instaladores MSI e Implantação Corporativa**
|
||||||
|
|
||||||
|
O **CyberCore Signer** foi projetado para distribuição nativa através da ferramenta `jpackage` (introduzida no JDK 14 e consolidada nas versões LTS 21/25). Isso significa que o usuário final **não precisa instalar o Java ou configurar variáveis de ambiente (JRE/JDK)** em sua máquina. O aplicativo carrega um runtime Java reduzido e isolado exclusivamente para sua execução.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Requisitos de Sistema
|
## 1. Requisitos de Sistema
|
||||||
|
|
||||||
- **Sistema Operacional Homologado:** Windows 10 (64-bit) ou Windows 11 (64-bit).
|
* **Sistema Operacional Alvo:** Windows 10 (64-bit) ou Windows 11 (64-bit).
|
||||||
- **Compatibilidade Adicional:** Linux x86_64 e macOS (arquitetura preparada).
|
* **Compatibilidade Adicional:** Distribuições Linux modernas x86_64 (Ubuntu 20.04+, Debian 11+, Fedora 38+, RHEL 9+).
|
||||||
- **Hardware:** Mínimo de 4 GB RAM, 150 MB de espaço em disco.
|
* **Memória RAM:** Mínimo de 4 GB (Recomendado: 8 GB).
|
||||||
- **Porta de Rede Local:** A porta TCP `21325` deve estar livre em `127.0.0.1`.
|
* **Espaço em Disco:** 180 MB livres para os arquivos da aplicação e runtime embutido.
|
||||||
|
* **Rede Local:** A porta TCP `21325` deve estar livre no endereço de loopback `127.0.0.1`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage`
|
## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage`
|
||||||
|
|
||||||
O empacotamento utiliza a ferramenta nativa `jpackage` incluída no JDK 21/25.
|
### 2.1 Pré-requisitos para a Máquina de Build
|
||||||
|
Para compilar e gerar o pacote de instalação no Windows:
|
||||||
|
1. **JDK 21+ LTS:** Com a variável `JAVA_HOME` devidamente configurada.
|
||||||
|
2. **WiX Toolset (v3.11+):** Necessário pelo `jpackage` para gerar instaladores `.msi`. Baixe em [wixtoolset.org](https://wixtoolset.org/) e adicione a pasta `bin` ao `PATH`.
|
||||||
|
|
||||||
|
### 2.2 Execução do Script de Empacotamento
|
||||||
|
Na raiz do projeto, execute o script em lote:
|
||||||
|
|
||||||
### No Windows:
|
|
||||||
Execute o script em lote localizado na raiz:
|
|
||||||
```cmd
|
```cmd
|
||||||
scripts\package-windows.bat
|
scripts\package-windows.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
### O que o script realiza:
|
Ou execute manualmente os comandos do pipeline:
|
||||||
1. Compila o projeto em modo release:
|
|
||||||
```cmd
|
```cmd
|
||||||
|
:: 1. Compilação do projeto e geração do Fat JAR consolidado
|
||||||
mvnw clean package -DskipTests
|
mvnw clean package -DskipTests
|
||||||
|
|
||||||
|
:: 2. Geração do instalador nativo via jpackage
|
||||||
|
jpackage ^
|
||||||
|
--type msi ^
|
||||||
|
--name "CyberCore Signer" ^
|
||||||
|
--app-version 0.1.0 ^
|
||||||
|
--vendor "CyberCore Systems" ^
|
||||||
|
--input modules\cybercore-app\target ^
|
||||||
|
--main-jar cybercore-signer.jar ^
|
||||||
|
--main-class br.com.cybercore.signer.app.CyberCoreApp ^
|
||||||
|
--icon assets\cybercore.ico ^
|
||||||
|
--win-menu ^
|
||||||
|
--win-shortcut ^
|
||||||
|
--win-dir-chooser ^
|
||||||
|
--dest dist
|
||||||
```
|
```
|
||||||
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.).
|
O instalador `CyberCore Signer-0.1.0.msi` será gerado dentro da pasta `dist/`.
|
||||||
- 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
|
## 3. Empacotamento para Linux (DEB / RPM / AppImage)
|
||||||
|
|
||||||
1. Execute o instalador `CyberCore-Signer-0.1.0.msi`.
|
No ambiente Linux x86_64:
|
||||||
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.
|
```bash
|
||||||
4. O serviço local `http://127.0.0.1:21325` estará imediatamente disponível para atender as aplicações web autorizadas.
|
# Executar o script nativo:
|
||||||
|
./scripts/package-jpackage.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script utiliza o `jpackage` com o parâmetro `--type deb` ou `--type app-image`, empacotando os binários nativos e dependências do JavaFX com suporte ao subsistema gráfico X11/Wayland.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Implantação Corporativa e Instalação Silenciosa
|
||||||
|
|
||||||
|
Para administradores de rede (AD / GPO / SCCM / Intune) que precisam instalar o agente em centenas de estações de trabalho de forma automatizada:
|
||||||
|
|
||||||
|
### Instalação Silenciosa via Linha de Comando (MSI):
|
||||||
|
```cmd
|
||||||
|
msiexec /i "CyberCore Signer-0.1.0.msi" /qn /norestart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instalação com Registro de Log de Auditoria:
|
||||||
|
```cmd
|
||||||
|
msiexec /i "CyberCore Signer-0.1.0.msi" /qn /l*v "C:\Temp\cybercore_install.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Desinstalação Silenciosa:
|
||||||
|
```cmd
|
||||||
|
msiexec /x "CyberCore Signer-0.1.0.msi" /qn /norestart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Inicialização com o Sistema e Área de Notificação (System Tray)
|
||||||
|
|
||||||
|
1. **Atalhos Criados:** O instalador cria automaticamente atalhos no Menu Iniciar e na Área de Trabalho.
|
||||||
|
2. **Inicialização Automática:** Durante a instalação, pode ser adicionada uma entrada no registro do Windows:
|
||||||
|
`HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
|
||||||
|
Chave: `CyberCoreSigner` -> Caminho: `"C:\Program Files\CyberCore Systems\CyberCore Signer\CyberCore Signer.exe" --minimized`
|
||||||
|
3. **Área de Notificação:** Quando iniciado, o agente minimiza para a bandeja do sistema (System Tray) ao lado do relógio do Windows, garantindo que o servidor local `127.0.0.1:21325` esteja sempre pronto para atender as solicitações das aplicações web homologadas.
|
||||||
|
|||||||
@@ -1,25 +1,42 @@
|
|||||||
# Limitações Técnicas e Dependências Declaradas
|
# 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:
|
> **Declaração Formal de Dependências de Plataforma, Limitações de Hardware e Concorrência (Regra 23)**
|
||||||
|
|
||||||
|
Em estrita conformidade com a **Regra 23** do projeto CyberCore Signer (*"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 técnicas e dependências são formalmente declaradas:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Dependências de Sistema Operacional e Hardware
|
## 1. Dependências de Sistema Operacional e Repositório Nativo
|
||||||
|
|
||||||
1. **Windows Certificate Store (Windows-MY):**
|
1. **Repositório Nativo do Windows (`Windows-MY`):**
|
||||||
- Disponível apenas em sistemas operacionais Microsoft Windows (Windows 10/11).
|
- O provedor `WindowsCertificateStoreProvider` depende exclusivamente das APIs Win32/CryptoAPI e CNG expostas pelo provedor `SunMSCAPI` do JDK.
|
||||||
- Em Linux e macOS, o provedor `WindowsCertificateStoreProvider` declara-se automaticamente indisponível (`isAvailable() == false`), operando via arquivos A1 ou PKCS#11 direto.
|
- **Disponibilidade:** Suportado exclusivamente em Microsoft Windows (Windows 10 e Windows 11).
|
||||||
2. **Tokens A3 e Middlewares Proprietários:**
|
- **Comportamento em Linux e macOS:** O provedor declara-se indisponível (`isAvailable() == false`) em sistemas não-Windows, operando através de arquivos A1 (PKCS#12) ou acesso PKCS#11 direto.
|
||||||
- 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.
|
2. **Arquitetura de CPU dos Middlewares Proprietários:**
|
||||||
3. **Bloqueio de Cartão por Tentativas de PIN:**
|
- A maioria dos drivers comerciais de tokens USB no Brasil (ex: SafeSign Identity Client da AET Europe e SafeNet da Thales) é compilada nativamente para a arquitetura **x86_64 (64-bit)**.
|
||||||
- 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.
|
- Ambientes executando em processadores ARM64 (como novos computadores Windows on ARM ou Apple Silicon) requerem a camada de emulação x64 do sistema operacional para carregar essas bibliotecas dinâmicas legadas.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Limitações de Rede e Concorrência
|
## 2. Dependências de Hardware e Middlewares de Terceiros
|
||||||
|
|
||||||
1. **Escuta Exclusiva em Loopback:**
|
1. **Tokens A3 e Leitores de Smartcard Físicos:**
|
||||||
- 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.
|
- O CyberCore Signer **não substitui os drivers do fabricante do token**. É indispensável que os drivers de hardware e o middleware do fabricante (SafeSign, SafeNet, GD Burti, Certisign, OpenSC, etc.) estejam instalados na estação de trabalho para que o sistema operacional e a leitora USB reconheçam o chip físico.
|
||||||
2. **Operações Concorrentes no Mesmo Token Físico:**
|
- O agente detecta a presença da biblioteca e interage com o dispositivo através de interfaces padronizadas (PKCS#11 ou CAPI/CNG).
|
||||||
- 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.
|
|
||||||
|
2. **Política de Bloqueio por PIN Incorreto (Controle do Chip Físico):**
|
||||||
|
- O chip criptográfico de qualquer smartcard ou token ICP-Brasil possui um contador de segurança interno em hardware (geralmente limitado a 3 tentativas incorretas consecutivas).
|
||||||
|
- O bloqueio do cartão e a exigência do código de desbloqueio (PUK) são governados com exclusividade pelo hardware do fabricante. O software do CyberCore Signer não tem como burlar, redefinir ou impedir esse bloqueio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Concorrência e Acesso a Dispositivos Mono-Thread
|
||||||
|
|
||||||
|
1. **Serialização Mandatória de Operações (`signLock`):**
|
||||||
|
- A especificação PKCS#11 e a maioria das implementações de drivers de smartcard operam sob canal serial exclusivo de I/O por leitora.
|
||||||
|
- Requisições simultâneas de assinatura contra o mesmo token físico causam erro de sessão ou congelamento no driver caso executadas em paralelo.
|
||||||
|
- **Solução Arquitetural:** O CyberCore Signer enfileira todas as operações criptográficas através de uma trava FIFO (`signLock`), processando cada assinatura individualmente e garantindo a estabilidade do hardware.
|
||||||
|
|
||||||
|
2. **Isolamento de Origem e Rede Loopback:**
|
||||||
|
- O servidor local CCSP/1 vincula-se estritamente ao endereço `127.0.0.1:21325`. O agente não suporta e deliberadamente rejeita conexões de computadores remotos da rede local (LAN), priorizando a segurança e a inviolabilidade da máquina do usuário.
|
||||||
|
|||||||
@@ -90,3 +90,55 @@ A posição e a página da estampa são parametrizáveis via `PdfSignatureOption
|
|||||||
- **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`.
|
- **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`.
|
||||||
- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256.
|
- **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.
|
- **Salva Incremental:** Preserva a integridade de assinaturas anteriores e do documento original sem quebrar certificações prévias.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Ferramentas Web do SDK (@cybercore/signer-sdk)
|
||||||
|
|
||||||
|
O SDK web fornece componentes visuais nativos em JavaScript vanilla para aplicações web:
|
||||||
|
|
||||||
|
### 5.1 `PdfStampPicker` (Posicionador Visual de Selo)
|
||||||
|
Permite que o usuário final suba um PDF no navegador, folheie páginas, selecione cantos predefinidos ou arraste a estampa visual interativamente. O componente mapeia as coordenadas do canvas para o sistema de coordenadas do PDFBox (ISO 32000, com origem no canto inferior esquerdo) e gera em tempo real o payload JSON para a requisição.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { PdfStampPicker } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
const picker = new PdfStampPicker({
|
||||||
|
container: '#picker-container',
|
||||||
|
pdfSource: pdfArrayBuffer, // ou URL / Base64
|
||||||
|
initialScale: 1.0,
|
||||||
|
onPositionChange: (pos) => {
|
||||||
|
console.log('Página:', pos.page, 'X:', pos.x, 'Y:', pos.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtém parâmetros prontos para a requisição CCSP
|
||||||
|
const stampConfig = picker.getStampConfig();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 `PdfSealDesigner` (Criador & Personalizador de Selos)
|
||||||
|
Componente interativo com live preview em SVG vetorial para personalização de selos:
|
||||||
|
- Paleta de cores primária, secundária, fundo e texto.
|
||||||
|
- Ícones vetoriais (`shield`, `certificate`, `lock`) e upload de logotipo em Base64.
|
||||||
|
- Regras LGPD para mascaramento ou ocultação de CPF/CNPJ.
|
||||||
|
- Exportação da configuração JSON para envio inline via `seal_custom` na requisição `signPdfDocument()`.
|
||||||
|
|
||||||
|
### 5.3 Assinatura Completa via Base64 (`signPdfDocument`)
|
||||||
|
Permite enviar o PDF completo em Base64 e receber de volta o PDF assinado completo em Base64 para envio direto ao backend:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const result = await signer.signPdfDocument({
|
||||||
|
certificateId: cert.id,
|
||||||
|
pdf: pdfArrayBuffer, // ou base64
|
||||||
|
page: stampConfig.page,
|
||||||
|
x: stampConfig.x,
|
||||||
|
y: stampConfig.y,
|
||||||
|
scale: stampConfig.scale,
|
||||||
|
seal_theme_id: 'icp-brasil', // ou seal_custom: customSealConfig
|
||||||
|
reason: 'Documento homologado',
|
||||||
|
location: 'São Paulo, SP'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('PDF Assinado Base64:', result.signed_pdf);
|
||||||
|
console.log('SHA-256 do Documento Final:', result.sha256);
|
||||||
|
```
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
@@ -1,17 +1,30 @@
|
|||||||
# SDK Web JavaScript (@cybercore/signer-sdk)
|
# 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**.
|
> **Manual de Integração de Aplicações Web Modernas com o Agente Desktop CyberCore Signer**
|
||||||
|
|
||||||
|
O **`@cybercore/signer-sdk`** é a biblioteca cliente oficial em JavaScript (TypeScript ready) desenvolvida para permitir que aplicações web modernas (React, Next.js, Vue, Angular ou JavaScript nativo) se comuniquem com o agente desktop **CyberCore Signer** através do protocolo local **CCSP/1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Instalação e Importação
|
## 1. Instalação e Configuração
|
||||||
|
|
||||||
### Via Módulo ES6 (Browser ou Bundler Vite/Webpack/Next.js)
|
### 1.1 Instalação via Gerenciador de Pacotes
|
||||||
|
```bash
|
||||||
|
npm install @cybercore/signer-sdk
|
||||||
|
# ou
|
||||||
|
yarn add @cybercore/signer-sdk
|
||||||
|
# ou
|
||||||
|
pnpm add @cybercore/signer-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Inicialização do Cliente
|
||||||
```javascript
|
```javascript
|
||||||
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
const signer = new CyberCoreSigner({
|
const signer = new CyberCoreSigner({
|
||||||
applicationName: 'Portal Meu Negócio'
|
applicationName: 'Portal Meu Negócio', // Nome exibido no diálogo nativo do usuário
|
||||||
|
port: 21325, // Porta padrão do agente local (opcional)
|
||||||
|
timeoutMs: 30000 // Timeout padrão para operações (opcional)
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -19,99 +32,173 @@ const signer = new CyberCoreSigner({
|
|||||||
|
|
||||||
## 2. Métodos da API
|
## 2. Métodos da API
|
||||||
|
|
||||||
### `isInstalled(timeoutMs = 2000): Promise<boolean>`
|
### 2.1 Verificação de Disponibilidade e Conexão
|
||||||
Testa rapidamente se o agente desktop está ativo na porta local `21325`.
|
|
||||||
|
|
||||||
### `connect(): Promise<boolean>`
|
#### `isInstalled(timeoutMs = 2000): 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.
|
Testa de forma rápida e silenciosa se o agente desktop está ativo na máquina do usuário.
|
||||||
|
|
||||||
### `getCertificates(): Promise<Array<Object>>`
|
|
||||||
Retorna todos os certificados A1 e A3 disponíveis e operacionais na máquina do usuário.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const certs = await signer.getCertificates();
|
const disponivel = await signer.isInstalled();
|
||||||
certs.forEach(cert => {
|
if (!disponivel) {
|
||||||
console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`);
|
alert('O CyberCore Signer não foi detectado em sua máquina. Por favor, instale o aplicativo para continuar.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `connect(): Promise<boolean>`
|
||||||
|
Inicia o handshake de autorização com o agente. Se a aplicação ainda não foi homologada pelo usuário, abre um diálogo nativo na tela do computador para autorização expressa.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const autorizado = await signer.connect();
|
||||||
|
if (!autorizado) {
|
||||||
|
console.warn('O usuário recusou a autorização da aplicação.');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 Descoberta de Certificados Digitais
|
||||||
|
|
||||||
|
#### `getCertificates(): Promise<Array<CertificateInfo>>`
|
||||||
|
Retorna a lista de certificados digitais válidos e disponíveis na máquina (A1 importados e A3 conectados via USB).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const certificados = await signer.getCertificates();
|
||||||
|
|
||||||
|
certificados.forEach(cert => {
|
||||||
|
console.log(`[${cert.type}] ${cert.common_name}`);
|
||||||
|
console.log(`Titular: ${cert.subject_dn}`);
|
||||||
|
console.log(`Validade até: ${cert.valid_to}`);
|
||||||
|
console.log(`CPF/CNPJ: ${cert.cpf || cert.cnpj}`);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### `sign(options): Promise<Object>`
|
---
|
||||||
Solicita a assinatura digital local de um digest ou conteúdo bruto.
|
|
||||||
|
|
||||||
### `signPdf(pdfFile, options): Promise<Object>`
|
### 2.3 Operações de Assinatura Digital
|
||||||
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.
|
#### `sign(options): Promise<SignatureResult>`
|
||||||
|
Assina o hash SHA-256 (digest) de qualquer informação. A chave privada nunca sai do computador e apenas o digest é assinado.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const pdfResult = await signer.signPdf(fileInput.files[0], {
|
const resultado = await signer.sign({
|
||||||
certificateId: certs[0].id
|
certificateId: 'win-4340656a',
|
||||||
|
digest: '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', // Hash SHA-256 em Base64
|
||||||
|
documentName: 'contrato_prestacao_servicos.pdf',
|
||||||
|
documentType: 'PDF'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Assinatura gerada (Base64):', resultado.signature);
|
||||||
|
console.log('Certificado do signatário:', resultado.certificate);
|
||||||
|
console.log('Cadeia de certificação:', resultado.certificate_chain);
|
||||||
```
|
```
|
||||||
|
|
||||||
### `signXml(xmlContent, options): Promise<Object>`
|
#### `signPdf(file, options): Promise<SignatureResult>`
|
||||||
Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`.
|
Calcula o hash do documento PDF no próprio navegador e solicita a assinatura ao CyberCore Signer.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const xmlResult = await signer.signXml(xmlString, {
|
const inputElement = document.getElementById('arquivo-pdf');
|
||||||
certificateId: certs[0].id,
|
const arquivoPdf = inputElement.files[0];
|
||||||
|
|
||||||
|
const resultadoPdf = await signer.signPdf(arquivoPdf, {
|
||||||
|
certificateId: certificados[0].id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('PDF assinado digitalmente com sucesso!');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `signXml(xmlContent, options): Promise<SignatureResult>`
|
||||||
|
Assina o hash de documentos XML ou notas fiscais eletrônicas (NF-e, CT-e, MDF-e, XAdES). Aceita `string` com o XML ou arquivo `File`/`Blob`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const xmlString = '<infNFe Id="NFe3524...">...</infNFe>';
|
||||||
|
|
||||||
|
const resultadoXml = await signer.signXml(xmlString, {
|
||||||
|
certificateId: certificados[0].id,
|
||||||
documentName: 'nfe-3524.xml'
|
documentName: 'nfe-3524.xml'
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### `signFile(file, options): Promise<Object>`
|
#### `signFile(file, options): Promise<SignatureResult>`
|
||||||
Assina qualquer arquivo (ZIP, DOCX, imagens, binários, executáveis) gerando pacote de assinatura detached / CAdES.
|
Assina arquivos genéricos de qualquer formato (ZIP, DOCX, XLSX, imagens, binários).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const fileResult = await signer.signFile(fileInput.files[0], {
|
const fileInput = document.getElementById('input-qualquer-arquivo');
|
||||||
certificateId: certs[0].id
|
|
||||||
|
const resultadoArquivo = await signer.signFile(fileInput.files[0], {
|
||||||
|
certificateId: certificados[0].id
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### `authenticate(options): Promise<Object>` (ou `login(options)`)
|
---
|
||||||
Executa login seguro com certificado digital via desafio criptográfico (Challenge-Response).
|
|
||||||
|
### 2.4 Autenticação e Login Digital (Challenge-Response)
|
||||||
|
|
||||||
|
#### `authenticate(options): Promise<AuthResult>`
|
||||||
|
Realiza o login seguro de um usuário através de seu certificado digital ICP-Brasil. O backend da sua aplicação fornece um desafio único (*challenge / nonce*) e o CyberCore assina esse desafio.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const auth = await signer.authenticate({
|
// 1. Obter desafio único do backend da sua aplicação
|
||||||
certificateId: certs[0].id,
|
const { challenge } = await fetch('/api/auth/challenge').then(res => res.json());
|
||||||
challenge: 'server-nonce-12345'
|
|
||||||
|
// 2. Solicitar assinatura do desafio ao CyberCore Signer
|
||||||
|
const authResult = await signer.authenticate({
|
||||||
|
certificateId: certificados[0].id,
|
||||||
|
challenge: challenge
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Bem-vindo,', auth.subjectName);
|
// 3. Enviar a assinatura e o certificado para o backend validar e autenticar a sessão
|
||||||
console.log('CPF/CNPJ:', auth.subjectCpfCnpj);
|
const response = await fetch('/api/auth/validate-login', {
|
||||||
console.log('Assinatura do Desafio:', auth.signature);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
challenge: authResult.challenge,
|
||||||
|
signature: authResult.signature,
|
||||||
|
certificate: authResult.certificate
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(`Login aprovado para: ${authResult.subjectName} (${authResult.subjectCpfCnpj})`);
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `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
|
## 3. Tratamento Estruturado de Erros
|
||||||
|
|
||||||
O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`:
|
O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`, permitindo tratar cenários de forma elegante na interface:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import {
|
import {
|
||||||
UserCancelledError,
|
UserCancelledError,
|
||||||
CertificateNotFoundError,
|
|
||||||
PinIncorrectError,
|
PinIncorrectError,
|
||||||
TokenNotFoundError
|
TokenNotFoundError,
|
||||||
|
CertificateNotFoundError,
|
||||||
|
CertificateExpiredError
|
||||||
} from '@cybercore/signer-sdk/errors';
|
} from '@cybercore/signer-sdk/errors';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await signer.sign({ certificateId: 'cert-1', data: 'Contrato' });
|
const res = await signer.sign({
|
||||||
} catch (err) {
|
certificateId: certId,
|
||||||
if (err instanceof UserCancelledError) {
|
digest: documentoDigest
|
||||||
alert('Você cancelou a assinatura no CyberCore Signer.');
|
});
|
||||||
} else if (err instanceof PinIncorrectError) {
|
} catch (error) {
|
||||||
alert('O PIN do token informado está incorreto.');
|
if (error instanceof UserCancelledError) {
|
||||||
} else if (err instanceof TokenNotFoundError) {
|
// Usuário cancelou a assinatura voluntariamente na janela nativa
|
||||||
alert('Conecte o seu token USB na máquina para assinar.');
|
console.info('Operação cancelada pelo usuário.');
|
||||||
|
} else if (error instanceof PinIncorrectError) {
|
||||||
|
// PIN do token A3 incorreto
|
||||||
|
alert('PIN incorreto! Por favor, digite o PIN correto do seu token.');
|
||||||
|
} else if (error instanceof TokenNotFoundError) {
|
||||||
|
// Token USB desconectado
|
||||||
|
alert('Token não encontrado! Por favor, conecte seu dispositivo USB para assinar.');
|
||||||
|
} else if (error instanceof CertificateExpiredError) {
|
||||||
|
// Certificado ultrapassou a data de validade
|
||||||
|
alert('O certificado selecionado expirou. Entre em contato com sua autoridade certificadora.');
|
||||||
} else {
|
} else {
|
||||||
console.error('Erro na assinatura:', err);
|
// Demais falhas
|
||||||
|
console.error('Falha na operação com o CyberCore Signer:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,46 +1,71 @@
|
|||||||
# Modelo de Segurança e Privacidade
|
# 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*.
|
> **Diretrizes de Segurança em Profundidade, Proteção Criptográfica e Conformidade LGPD**
|
||||||
|
|
||||||
|
O **CyberCore Signer** foi concebido sob os princípios de *Security by Design* (Segurança desde a Concepção) e *Least Privilege* (Menor Privilégio), seguindo as normas da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)** e as exigências da **Lei Geral de Proteção de Dados (LGPD — Lei nº 13.709/2018)**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Isolamento de Rede
|
## 1. Isolamento de Rede e Proteção Local
|
||||||
|
|
||||||
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`.
|
### 1.1 Ligação Estrita ao Loopback (`127.0.0.1`)
|
||||||
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.
|
O servidor HTTP do protocolo CCSP/1 vincula-se exclusivamente ao endereço IPv4 local `127.0.0.1`.
|
||||||
|
* **Sem Escuta Externa:** O agente nunca realiza bind em `0.0.0.0` ou em interfaces de rede de área local (LAN) ou externa (WAN).
|
||||||
|
* **Bloqueio no Socket:** Tentativas de acesso originadas fora da própria estação de trabalho são sumariamente recusadas pelo subsistema de rede do sistema operacional antes de qualquer processamento da aplicação.
|
||||||
|
|
||||||
|
### 1.2 Proteção Contra Ataques de DNS Rebinding
|
||||||
|
Para neutralizar ataques em que domínios externos tentam resolver para `127.0.0.1` e burlar a política de mesma origem do navegador:
|
||||||
|
* O `CcspServer` valida expressamente o cabeçalho HTTP `Host`.
|
||||||
|
* Somente requisições com `Host: 127.0.0.1:21325` ou `Host: localhost:21325` são aceitas; qualquer outro valor é rejeitado com status `403 Forbidden`.
|
||||||
|
|
||||||
|
### 1.3 Controle de Origem (CORS) e Preflight
|
||||||
|
* Requisições prévias HTTP `OPTIONS` são validadas contra as origens web homologadas.
|
||||||
|
* O cabeçalho `Access-Control-Allow-Origin` é devolvido estritamente com a origem solicitante autorizada (nunca com wildcard `*`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Autenticação e Autorização de Aplicações
|
## 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:
|
Nenhuma aplicação web externa tem acesso a certificados digitais ou serviços de assinatura sem autorização prévia e explícita do usuário:
|
||||||
|
|
||||||
1. **Handshake de Autorização (`POST /ccsp/v1/authorize`):**
|
1. **Handshake de Aprovação (`POST /ccsp/v1/authorize`):**
|
||||||
- A aplicação web identifica-se enviando seu `application_id` (UUID estável) e seu nome amigável.
|
- Ao tentar comunicar-se pela primeira vez, a aplicação web deve fornecer 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.
|
- O CyberCore Signer suspende a requisição e exibe um modal nativo no desktop informando o nome da aplicação e solicitando autorização do usuário.
|
||||||
2. **Armazenamento Seguro das Autorizações:**
|
2. **Armazenamento Seguro de Permissões:**
|
||||||
- As aplicações autorizadas são gravadas em `~/.cybercore/authorized-apps.json`.
|
- As aplicações autorizadas são gravadas no arquivo local `~/.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.
|
- O usuário pode visualizar e **revogar a qualquer momento** a autorização de qualquer aplicação através da aba **Configurações** na interface gráfica.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Proteção Contra Ataques de Replay
|
## 3. Proteção Contra Ataques de Replay
|
||||||
|
|
||||||
Cada solicitação de assinatura enviada pelo SDK deve incluir:
|
Para impedir que requisições de assinatura capturadas por softwares maliciosos na máquina sejam reenviadas:
|
||||||
- Um `nonce` (UUID criptograficamente aleatório);
|
1. **Identificador Único (`nonce`):** Toda requisição ao `/ccsp/v1/sign` deve conter um UUID aleatório criptograficamente forte gerado pelo chamador.
|
||||||
- Um campo `expires_at` (ou `timestamp`).
|
2. **Janela de Validade Temporal (`expires_at`):** A requisição inclui uma estampa de tempo limite (TTL).
|
||||||
|
3. **Gerenciador de Nonces (`NonceManager`):**
|
||||||
O componente `NonceManager`:
|
- Mantém uma estrutura em memória com os identificadores recentes;
|
||||||
- Armazena os nonces recentes em memória protegida com expiração temporal (TTL);
|
- Se o mesmo `nonce` for submetido mais de uma vez dentro da janela de validade, a operação é rejeitada com erro estruturado `REPLAY_DETECTED` (HTTP `409 Conflict`);
|
||||||
- Rejeita com código estruturado `REPLAY_DETECTED` caso o mesmo nonce seja submetido mais de uma vez;
|
- Se a data/hora atual ultrapassar `expires_at`, a operação é rejeitada com erro `REQUEST_EXPIRED` (HTTP `422 Unprocessable Entity`).
|
||||||
- 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
|
## 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.
|
1. **Assinatura Exclusivamente Assistida:** Toda operação de assinatura exige interação humana voluntária. O agente nunca assina documentos em segundo plano de forma silenciosa ou desassistida via API.
|
||||||
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.
|
2. **Inviolabilidade da Chave Privada:** As chaves privadas ICP-Brasil nunca são exportadas em formato de bytes brutos. Operações em certificados A3 utilizam handles criptográficos do hardware, garantindo que a chave nunca deixe o chip criptográfico.
|
||||||
3. **Proteção de PIN e Senhas:**
|
3. **Higienização Segura de Memória Volátil:**
|
||||||
- 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.
|
- O PIN de smartcards e senhas de certificados A1 nunca são convertidos para strings imutáveis do Java (`java.lang.String`).
|
||||||
- Em caso de remoção do hardware, o `SmartCardWatcher` limpa e zera os buffers de memória de PIN imediatamente.
|
- São manipulados estritamente como arrays de caracteres mutáveis `char[]`.
|
||||||
|
- Imediatamente após o carregamento da chave, o array é sobrescrito com zeros através de `Arrays.fill(pinBuffer, (char) 0)`.
|
||||||
|
4. **Remoção de Hardware (`TOKEN_REMOVED`):** Se o usuário desconectar o token USB ou retirar o cartão do leitor durante ou após a assinatura, o `SmartCardWatcher` força a limpeza imediata do cache de sessão e dos buffers do sistema operacional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Privacidade de Dados e Conformidade com a LGPD
|
||||||
|
|
||||||
|
1. **Assinatura Descentralizada por Digest:** O CyberCore Signer opera prioritariamente recebendo apenas o hash SHA-256 do arquivo. O conteúdo completo do documento (contrato, laudo, prontuário) não transita pela API local nem é gravado em logs.
|
||||||
|
2. **Mascaramento e Sigilo de CPF:** Nos selos visuais em PDF, o usuário pode configurar o modo de exibição de seu CPF (`CpfDisplayMode`):
|
||||||
|
- `FULL`: Exibição completa (`123.456.789-00`);
|
||||||
|
- `MASKED`: Dígitos sensíveis ocultados (`***.456.789-**`);
|
||||||
|
- `NONE`: Remoção completa do CPF do gráfico visual do selo.
|
||||||
|
3. **Auditoria e Logs Transparentes:** Os logs do agente registram apenas identificadores de correlação (`request_id`), algoritmo utilizado e status da operação, sem armazenar hashes de documentos sigilosos ou dados pessoais desnecessários.
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
# Assinaturas XML (XMLDSig e ICP-Brasil)
|
# Assinaturas XML no Padrão XAdES 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.
|
Este documento descreve a especificação, conformidade e o funcionamento da assinatura digital de arquivos XML no padrão avançado **XAdES (XML Advanced Electronic Signatures - ETSI TS 101 903 e ICP-Brasil DOC-ICP-15.03)** e **W3C XMLDSig** no CyberCore Signer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Visão Geral
|
## 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`).
|
O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`), garantindo conformidade com o padrão **XAdES-BES** adotado pelo ITI (Instituto Nacional de Tecnologia da Informação) na infraestrutura ICP-Brasil (perfil AD-RB).
|
||||||
|
|
||||||
### 1.1 Casos de Uso Suportados
|
### 1.1 Perfis Suportados
|
||||||
|
|
||||||
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML.
|
* **`XAdES-BES` (Padrão Recomendado / ICP-Brasil):**
|
||||||
|
- Conforme especificação técnica **ETSI TS 101 903** e norma **DOC-ICP-15.03**.
|
||||||
|
- Encapsula o elemento `<ds:Object>` contendo `<xades:QualifyingProperties>` e `<xades:SignedProperties>`.
|
||||||
|
- Inclui propriedade qualificada de data/hora da assinatura (`<xades:SigningTime>`) em formato UTC ISO-8601.
|
||||||
|
- Inclui vínculo criptográfico irrefutável do certificado do signatário (`<xades:SigningCertificate>`) contendo o digest SHA-256 do certificado DER e referências do emissor/serial (`<xades:IssuerSerial>`).
|
||||||
|
- Protege as propriedades qualificadas através de uma `<ds:Reference>` em `<ds:SignedInfo>` com `Type="http://uri.etsi.org/01903#SignedProperties"`.
|
||||||
|
* **`XMLDSig` Clássico (W3C Enveloped):**
|
||||||
|
- Mantido para compatibilidade com layouts estritos de notas fiscais eletrônicas legadas (ex: NF-e, CT-e, MDF-e SEFAZ).
|
||||||
|
|
||||||
|
### 1.2 Casos de Uso Suportados
|
||||||
|
|
||||||
|
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações, laudos e relatórios em XML.
|
||||||
- **Assinatura de Elementos Específicos por ID:**
|
- **Assinatura de Elementos Específicos por ID:**
|
||||||
- **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">`
|
- **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...">`
|
- **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">`
|
||||||
@@ -19,19 +30,69 @@ O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Padrões Criptográficos
|
## 2. Estrutura Criptográfica do XAdES-BES
|
||||||
|
|
||||||
- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários).
|
```xml
|
||||||
- **Transformações:**
|
<ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||||
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest).
|
<ds:SignedInfo>
|
||||||
- C14N padrão.
|
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
|
||||||
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`).
|
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-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>`.
|
<!-- Referência 0: Dados de Negócio (Documento completo ou elemento específico) -->
|
||||||
|
<ds:Reference URI="#[ElementoId]">
|
||||||
|
<ds:Transforms>
|
||||||
|
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
||||||
|
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
|
||||||
|
</ds:Transforms>
|
||||||
|
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||||
|
<ds:DigestValue>[Digest do nó em Base64]</ds:DigestValue>
|
||||||
|
</ds:Reference>
|
||||||
|
|
||||||
|
<!-- Referência 1: Propriedades Qualificadas Assinadas XAdES -->
|
||||||
|
<ds:Reference URI="#signed-props-[UUID]" Type="http://uri.etsi.org/01903#SignedProperties">
|
||||||
|
<ds:Transforms>
|
||||||
|
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
|
||||||
|
</ds:Transforms>
|
||||||
|
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||||
|
<ds:DigestValue>[Digest das SignedProperties em Base64]</ds:DigestValue>
|
||||||
|
</ds:Reference>
|
||||||
|
</ds:SignedInfo>
|
||||||
|
|
||||||
|
<ds:SignatureValue>[Assinatura RSA-SHA256 em Base64]</ds:SignatureValue>
|
||||||
|
|
||||||
|
<ds:KeyInfo>
|
||||||
|
<ds:X509Data>
|
||||||
|
<ds:X509Certificate>[Certificado DER do signatário em Base64]</ds:X509Certificate>
|
||||||
|
</ds:X509Data>
|
||||||
|
</ds:KeyInfo>
|
||||||
|
|
||||||
|
<ds:Object>
|
||||||
|
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#sig-[UUID]">
|
||||||
|
<xades:SignedProperties Id="signed-props-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
|
||||||
|
<xades:SignedSignatureProperties>
|
||||||
|
<xades:SigningTime>2026-10-11T14:05:00Z</xades:SigningTime>
|
||||||
|
<xades:SigningCertificate>
|
||||||
|
<xades:Cert>
|
||||||
|
<xades:CertDigest>
|
||||||
|
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
||||||
|
<ds:DigestValue>[SHA-256 do Certificado]</ds:DigestValue>
|
||||||
|
</xades:CertDigest>
|
||||||
|
<xades:IssuerSerial>
|
||||||
|
<ds:X509IssuerName>[Emissor X.500]</ds:X509IssuerName>
|
||||||
|
<ds:X509SerialNumber>[Número Serial]</ds:X509SerialNumber>
|
||||||
|
</xades:IssuerSerial>
|
||||||
|
</xades:Cert>
|
||||||
|
</xades:SigningCertificate>
|
||||||
|
</xades:SignedSignatureProperties>
|
||||||
|
</xades:SignedProperties>
|
||||||
|
</xades:QualifyingProperties>
|
||||||
|
</ds:Object>
|
||||||
|
</ds:Signature>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Segurança e Mitigação contra XXE
|
## 3. Segurança e Mitigação contra Ataques XXE
|
||||||
|
|
||||||
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
|
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
|
||||||
|
|
||||||
@@ -47,10 +108,35 @@ dbf.setExpandEntityReferences(false);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Validação Pós-Assinatura
|
## 4. Validação Pós-Assinatura e Auditoria
|
||||||
|
|
||||||
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando:
|
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` e o `SignatureValidationService` realizam:
|
||||||
|
|
||||||
1. A integridade criptográfica da assinatura (`SignedInfo`).
|
1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
|
||||||
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`).
|
2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
|
||||||
3. O certificado X.509 público extraído da chave.
|
3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
|
||||||
|
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Integração Web e SDK (@cybercore/signer-sdk)
|
||||||
|
|
||||||
|
As aplicações web autorizadas podem assinar arquivos XML diretamente via protocolo CCSP/1 com o método `signXmlDocument()`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CyberCoreSigner } from '@cybercore/signer-sdk';
|
||||||
|
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
|
||||||
|
// Assinatura de documento XML ou evento fiscal
|
||||||
|
const result = await signer.signXmlDocument({
|
||||||
|
certificateId: cert.id,
|
||||||
|
xml: xmlString, // texto puro ou xml_base64
|
||||||
|
profile: 'XADES_BES', // ou 'XMLDSIG'
|
||||||
|
targetElementTag: 'infNFe', // opcional
|
||||||
|
targetElementId: 'NFe35261012345678000199550010000001001234567890' // opcional
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('XML Assinado Retornado:', result.signed_xml);
|
||||||
|
console.log('Algoritmo Utilizado:', result.algorithm);
|
||||||
|
```
|
||||||
|
|||||||
@@ -131,12 +131,12 @@ public class CyberCoreSignerApplication {
|
|||||||
private static void printBanner() {
|
private static void printBanner() {
|
||||||
System.out.println("""
|
System.out.println("""
|
||||||
============================================================
|
============================================================
|
||||||
____ _ ____
|
____ _ ____ \s
|
||||||
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___
|
/ ___| _ _ | |__ ___ _ __ / ___|___ _ __ ___ \s
|
||||||
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\
|
| | | | | | | '_ \\ / _ \\ '__| | | / _ \\| '__/ _ \\ \s
|
||||||
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/
|
| |___ | |_| | | |_) | __/ | | |__| (_) | | | __/ \s
|
||||||
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___|
|
\\____| \\__, | |_.__/ \\___|_| \\____\\___/|_| \\___| \s
|
||||||
|___/
|
|___/ \s
|
||||||
SIGNER AGENT v0.1.0
|
SIGNER AGENT v0.1.0
|
||||||
============================================================
|
============================================================
|
||||||
""");
|
""");
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package br.com.cybercore.signer.app.log;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.log.AppLogBuffer;
|
||||||
|
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||||
|
import ch.qos.logback.core.AppenderBase;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appender do Logback que intercepta eventos reais de log e os encaminha para o buffer em memória.
|
||||||
|
*/
|
||||||
|
public class InMemoryLogAppender extends AppenderBase<ILoggingEvent> {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void append(ILoggingEvent eventObject) {
|
||||||
|
if (eventObject == null) return;
|
||||||
|
|
||||||
|
LocalDateTime time = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(eventObject.getTimeStamp()),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
|
String timestamp = time.format(FORMATTER);
|
||||||
|
String level = eventObject.getLevel().toString();
|
||||||
|
String loggerName = eventObject.getLoggerName();
|
||||||
|
if (loggerName != null && loggerName.length() > 30) {
|
||||||
|
int lastDot = loggerName.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
loggerName = loggerName.substring(lastDot + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String message = eventObject.getFormattedMessage();
|
||||||
|
|
||||||
|
String line = String.format("[%s] [%s] %s - %s", timestamp, level, loggerName, message);
|
||||||
|
AppLogBuffer.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,11 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<appender name="MEMORY" class="br.com.cybercore.signer.app.log.InMemoryLogAppender" />
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="CONSOLE" />
|
<appender-ref ref="CONSOLE" />
|
||||||
|
<appender-ref ref="MEMORY" />
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="br.com.cybercore.signer" level="DEBUG" />
|
<logger name="br.com.cybercore.signer" level="DEBUG" />
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package br.com.cybercore.signer.core.log;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffer circular thread-safe em memória para armazenar eventos reais de log da aplicação,
|
||||||
|
* permitindo auditoria visual em tempo real pela interface gráfica desktop.
|
||||||
|
*/
|
||||||
|
public final class AppLogBuffer {
|
||||||
|
|
||||||
|
private static final int MAX_ENTRIES = 1000;
|
||||||
|
private static final Deque<String> BUFFER = new ArrayDeque<>(MAX_ENTRIES);
|
||||||
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
|
||||||
|
|
||||||
|
static {
|
||||||
|
appendSystemMessage("CyberCore Signer iniciado. Aguardando eventos criptográficos e conexões locais...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppLogBuffer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void append(String logLine) {
|
||||||
|
if (logLine == null || logLine.isBlank()) return;
|
||||||
|
if (BUFFER.size() >= MAX_ENTRIES) {
|
||||||
|
BUFFER.pollFirst();
|
||||||
|
}
|
||||||
|
BUFFER.addLast(logLine.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void appendSystemMessage(String msg) {
|
||||||
|
String timestamp = LocalDateTime.now().format(FORMATTER);
|
||||||
|
append("[" + timestamp + "] [INFO] " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized List<String> getLogs() {
|
||||||
|
return new ArrayList<>(BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void clear() {
|
||||||
|
BUFFER.clear();
|
||||||
|
appendSystemMessage("Histórico de logs limpo pelo usuário.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ public record PdfSignatureOptions(
|
|||||||
Integer targetPageNumber,
|
Integer targetPageNumber,
|
||||||
CpfDisplayMode cpfDisplayMode,
|
CpfDisplayMode cpfDisplayMode,
|
||||||
Boolean includeDateTime,
|
Boolean includeDateTime,
|
||||||
String customExtraInfo
|
String customExtraInfo,
|
||||||
|
Double customSealScale
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public PdfSignatureOptions {
|
public PdfSignatureOptions {
|
||||||
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
|
|||||||
if (includeDateTime == null) {
|
if (includeDateTime == null) {
|
||||||
includeDateTime = sealTheme.defaultIncludeDateTime();
|
includeDateTime = sealTheme.defaultIncludeDateTime();
|
||||||
}
|
}
|
||||||
|
if (customSealScale == null || customSealScale <= 0) {
|
||||||
|
customSealScale = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construtor para compatibilidade com assinaturas anteriores a customSealScale.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
String customExtraInfo) {
|
||||||
|
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
|
||||||
|
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, customExtraInfo, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +77,7 @@ public record PdfSignatureOptions(
|
|||||||
String reason, String location, Float customX, Float customY,
|
String reason, String location, Float customX, Float customY,
|
||||||
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
|
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
|
||||||
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
|
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
|
||||||
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null);
|
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +86,7 @@ public record PdfSignatureOptions(
|
|||||||
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
|
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
|
||||||
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
|
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
|
||||||
String reason, String location) {
|
String reason, String location) {
|
||||||
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null);
|
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
|
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
|
||||||
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String customExtraInfo;
|
private String customExtraInfo;
|
||||||
|
private Double customSealScale = 1.0;
|
||||||
|
|
||||||
public Builder includeDateTime(boolean include) {
|
public Builder includeDateTime(boolean include) {
|
||||||
this.includeDateTime = include;
|
this.includeDateTime = include;
|
||||||
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder customSealScale(Double scale) {
|
||||||
|
this.customSealScale = scale;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder customPosition(Float x, Float y, Integer pageNumber) {
|
public Builder customPosition(Float x, Float y, Integer pageNumber) {
|
||||||
this.customX = x;
|
this.customX = x;
|
||||||
this.customY = y;
|
this.customY = y;
|
||||||
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
|
|||||||
targetPageNumber,
|
targetPageNumber,
|
||||||
cpfDisplayMode,
|
cpfDisplayMode,
|
||||||
includeDateTime,
|
includeDateTime,
|
||||||
customExtraInfo
|
customExtraInfo,
|
||||||
|
customSealScale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package br.com.cybercore.signer.core.pdf;
|
package br.com.cybercore.signer.core.pdf;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
||||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
|
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.PdfSealData;
|
||||||
@@ -12,10 +13,19 @@ import org.apache.pdfbox.Loader;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
@@ -172,6 +182,10 @@ public class PdfSigningService {
|
|||||||
String issuerCn = extractIssuerCn(cert.issuerDn());
|
String issuerCn = extractIssuerCn(cert.issuerDn());
|
||||||
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
|
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
|
||||||
|
|
||||||
|
// Extrai o nome amigável do titular (removendo qualquer sufixo como :CPF ou :CNPJ para o nome do assinante)
|
||||||
|
String rawName = cert.commonName() != null ? cert.commonName() : cert.alias();
|
||||||
|
String signerName = X509CertificateUtils.extractHolderName(rawName);
|
||||||
|
|
||||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] pdfDigest = sha256.digest(pdfBytes);
|
byte[] pdfDigest = sha256.digest(pdfBytes);
|
||||||
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
||||||
@@ -183,7 +197,7 @@ public class PdfSigningService {
|
|||||||
|
|
||||||
// 3. Monta dados do selo visual
|
// 3. Monta dados do selo visual
|
||||||
PdfSealData sealData = new PdfSealData(
|
PdfSealData sealData = new PdfSealData(
|
||||||
cert.commonName() != null ? cert.commonName() : cert.alias(),
|
signerName,
|
||||||
signerDoc,
|
signerDoc,
|
||||||
signingTimeStr,
|
signingTimeStr,
|
||||||
issuerCn,
|
issuerCn,
|
||||||
@@ -195,92 +209,78 @@ public class PdfSigningService {
|
|||||||
effectiveExtraInfo
|
effectiveExtraInfo
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Carrega o PDF e estampa o selo se solicitado
|
// 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
|
||||||
byte[] intermediatePdf;
|
// preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
|
||||||
if (options.visibleSeal()) {
|
// o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
|
||||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
// das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
|
||||||
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();
|
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
||||||
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
|
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||||
PDSignature signature = new PDSignature();
|
PDSignature signature = new PDSignature();
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||||
signature.setName(cert.commonName());
|
signature.setName(signerName);
|
||||||
signature.setLocation(options.location());
|
signature.setLocation(options.location());
|
||||||
signature.setReason(options.reason());
|
signature.setReason(options.reason());
|
||||||
signature.setSignDate(Calendar.getInstance());
|
signature.setSignDate(Calendar.getInstance());
|
||||||
|
|
||||||
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
|
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
|
||||||
|
|
||||||
|
if (options.visibleSeal()) {
|
||||||
|
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||||
|
int targetPage = resolveTargetPage(doc, options);
|
||||||
|
PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options);
|
||||||
|
|
||||||
|
try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng);
|
||||||
|
SignatureOptions signatureOptions = new SignatureOptions()) {
|
||||||
|
signatureOptions.setVisualSignature(visualTemplate);
|
||||||
|
signatureOptions.setPage(targetPage);
|
||||||
|
doc.addSignature(signature, signer, signatureOptions);
|
||||||
|
doc.saveIncremental(finalOut);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
doc.addSignature(signature, signer);
|
doc.addSignature(signature, signer);
|
||||||
doc.saveIncremental(finalOut);
|
doc.saveIncremental(finalOut);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
||||||
return finalOut.toByteArray();
|
return finalOut.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
|
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
|
||||||
PdfSignatureOptions options) throws IOException {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
|
||||||
int totalPages = doc.getNumberOfPages();
|
int totalPages = doc.getNumberOfPages();
|
||||||
if (totalPages == 0) {
|
if (totalPages <= 0) {
|
||||||
return pdfBytes;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||||
targetPageIdx = options.targetPageNumber() - 1;
|
return 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) {
|
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||||
endPage = 0;
|
return 0;
|
||||||
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
|
}
|
||||||
startPage = totalPages - 1;
|
return totalPages - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = startPage; i <= endPage; i++) {
|
private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
|
||||||
PDPage page = doc.getPage(i);
|
PDPage page = doc.getPage(pageIndex);
|
||||||
PDRectangle mediaBox = page.getMediaBox();
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
float pageW = mediaBox.getWidth();
|
float pageW = mediaBox.getWidth();
|
||||||
float pageH = mediaBox.getHeight();
|
float pageH = mediaBox.getHeight();
|
||||||
|
|
||||||
|
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
|
||||||
|
? options.customSealScale().floatValue()
|
||||||
|
: 1.0f;
|
||||||
|
float sealWidth = (theme.width() * 0.72f) * scale;
|
||||||
|
float sealHeight = (theme.height() * 0.72f) * scale;
|
||||||
|
float margin = 20f;
|
||||||
|
|
||||||
float x;
|
float x;
|
||||||
float y;
|
float y;
|
||||||
|
|
||||||
|
if (options.customX() != null && options.customY() != null) {
|
||||||
|
x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||||
|
y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||||
|
} else {
|
||||||
switch (options.position()) {
|
switch (options.position()) {
|
||||||
case BOTTOM_LEFT -> {
|
case BOTTOM_LEFT -> {
|
||||||
x = margin;
|
x = margin;
|
||||||
@@ -299,15 +299,58 @@ public class PdfSigningService {
|
|||||||
y = margin;
|
y = margin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
|
||||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new PDRectangle(x, y, sealWidth, sealHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual
|
||||||
|
* como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget).
|
||||||
|
* O stream /Contents da página do documento original permanece completamente inalterado.
|
||||||
|
*/
|
||||||
|
private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum,
|
||||||
|
PDRectangle rect, byte[] sealPngBytes) throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
|
||||||
|
doc.addPage(page);
|
||||||
|
|
||||||
|
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||||
|
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||||
|
|
||||||
|
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||||
|
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||||
|
acroForm.getFields().add(signatureField);
|
||||||
|
acroForm.setSignaturesExist(true);
|
||||||
|
acroForm.setAppendOnly(true);
|
||||||
|
acroForm.getCOSObject().setDirect(true);
|
||||||
|
|
||||||
|
widget.setRectangle(rect);
|
||||||
|
|
||||||
|
PDStream stream = new PDStream(doc);
|
||||||
|
PDFormXObject form = new PDFormXObject(stream);
|
||||||
|
PDResources res = new PDResources();
|
||||||
|
form.setResources(res);
|
||||||
|
form.setFormType(1);
|
||||||
|
|
||||||
|
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
|
||||||
|
form.setBBox(bbox);
|
||||||
|
|
||||||
|
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
|
||||||
|
appearance.getCOSObject().setDirect(true);
|
||||||
|
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
|
||||||
|
appearance.setNormalAppearance(appearanceStream);
|
||||||
|
widget.setAppearance(appearance);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
|
||||||
|
PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealPngBytes, "cybercore_seal");
|
||||||
|
cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
doc.save(baos);
|
doc.save(baos);
|
||||||
|
return new ByteArrayInputStream(baos.toByteArray());
|
||||||
}
|
}
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ public final class PdfSealRenderer {
|
|||||||
String headerText = resolveHeaderText(theme);
|
String headerText = resolveHeaderText(theme);
|
||||||
String footerText = resolveFooterText(theme, null);
|
String footerText = resolveFooterText(theme, null);
|
||||||
|
|
||||||
|
Color footerColor = new Color(100, 116, 139); // Cinza corporativo neutro e sóbrio (#64748b)
|
||||||
|
|
||||||
if (hasIdentity && hasDate) {
|
if (hasIdentity && hasDate) {
|
||||||
// Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé
|
// Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/CNPJ/Matrícula), Data, Rodapé
|
||||||
g2d.setColor(primaryColor);
|
g2d.setColor(primaryColor);
|
||||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
|
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
|
||||||
g2d.drawString(headerText, textX, 16);
|
g2d.drawString(headerText, textX, 16);
|
||||||
@@ -127,9 +129,8 @@ public final class PdfSealRenderer {
|
|||||||
|
|
||||||
g2d.drawString("Data: " + data.signingDate(), textX, 60);
|
g2d.drawString("Data: " + data.signingDate(), textX, 60);
|
||||||
|
|
||||||
g2d.setColor(secondaryColor);
|
g2d.setColor(footerColor);
|
||||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
|
drawFittedText(g2d, footerText, textX, 73, textWidthAvailable, 7.8f);
|
||||||
g2d.drawString(footerText, textX, 73);
|
|
||||||
|
|
||||||
} else if (hasIdentity || hasDate) {
|
} else if (hasIdentity || hasDate) {
|
||||||
// Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé
|
// Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé
|
||||||
@@ -145,9 +146,8 @@ public final class PdfSealRenderer {
|
|||||||
String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate());
|
String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate());
|
||||||
drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f);
|
drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f);
|
||||||
|
|
||||||
g2d.setColor(secondaryColor);
|
g2d.setColor(footerColor);
|
||||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8));
|
drawFittedText(g2d, footerText, textX, 71, textWidthAvailable, 7.8f);
|
||||||
g2d.drawString(footerText, textX, 71);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
|
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
|
||||||
@@ -158,9 +158,8 @@ public final class PdfSealRenderer {
|
|||||||
g2d.setColor(textColor);
|
g2d.setColor(textColor);
|
||||||
drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f);
|
drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f);
|
||||||
|
|
||||||
g2d.setColor(secondaryColor);
|
g2d.setColor(footerColor);
|
||||||
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9));
|
drawFittedText(g2d, footerText, textX, 68, textWidthAvailable, 8.0f);
|
||||||
g2d.drawString(footerText, textX, 68);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@@ -250,19 +249,28 @@ public final class PdfSealRenderer {
|
|||||||
* Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado.
|
* Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado.
|
||||||
*/
|
*/
|
||||||
private static String formatSignerDoc(PdfSealData data) {
|
private static String formatSignerDoc(PdfSealData data) {
|
||||||
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL;
|
String rawDoc = data.identifier() != null ? data.identifier().trim() : "";
|
||||||
String rawDoc = data.identifier() != null ? data.identifier() : "";
|
if (rawDoc.isBlank()) return "";
|
||||||
|
|
||||||
|
String digits = rawDoc.replaceAll("\\D", "");
|
||||||
|
|
||||||
|
// Pessoa Jurídica (14 dígitos): Rótulo CNPJ, SEMPRE completo, sem mascaramento
|
||||||
|
if (digits.length() == 14 || rawDoc.toUpperCase().contains("CNPJ")) {
|
||||||
|
if (digits.length() == 14) {
|
||||||
|
return "CNPJ: " + String.format("%s.%s.%s/%s-%s",
|
||||||
|
digits.substring(0, 2), digits.substring(2, 5), digits.substring(5, 8),
|
||||||
|
digits.substring(8, 12), digits.substring(12, 14));
|
||||||
|
}
|
||||||
|
return rawDoc.toUpperCase().startsWith("CNPJ") ? rawDoc : "CNPJ: " + rawDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pessoa Física (11 dígitos): Rótulo CPF, respeitando CpfDisplayMode
|
||||||
|
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL;
|
||||||
if (mode == CpfDisplayMode.NONE) {
|
if (mode == CpfDisplayMode.NONE) {
|
||||||
// Se for CPF, oculta. Se for CNPJ, pode manter ou formatar.
|
|
||||||
if (rawDoc.toUpperCase().contains("CPF")) {
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return rawDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawDoc.toUpperCase().contains("CPF")) {
|
if (digits.length() == 11) {
|
||||||
String digits = rawDoc.replaceAll("\\D", "");
|
|
||||||
String formatted = mode.formatCpf(digits);
|
String formatted = mode.formatCpf(digits);
|
||||||
return formatted.isBlank() ? "" : "CPF: " + formatted;
|
return formatted.isBlank() ? "" : "CPF: " + formatted;
|
||||||
}
|
}
|
||||||
@@ -378,10 +386,10 @@ public final class PdfSealRenderer {
|
|||||||
return theme.customHeader().toUpperCase();
|
return theme.customHeader().toUpperCase();
|
||||||
}
|
}
|
||||||
return switch (theme.id().toLowerCase()) {
|
return switch (theme.id().toLowerCase()) {
|
||||||
case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL";
|
case "cybercore-modern" -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
||||||
case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
||||||
case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001";
|
case "gov-br" -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
||||||
default -> "ASSINADO DIGITALMENTE • ICP-BRASIL";
|
default -> "DOCUMENTO ASSINADO DIGITALMENTE";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,10 +401,10 @@ public final class PdfSealRenderer {
|
|||||||
return "Hash Criptográfico: " + hash;
|
return "Hash Criptográfico: " + hash;
|
||||||
}
|
}
|
||||||
return switch (theme.id().toLowerCase()) {
|
return switch (theme.id().toLowerCase()) {
|
||||||
case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local";
|
case "cybercore-modern" -> "Verifique a autenticidade em https://validar.iti.gov.br";
|
||||||
case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil";
|
case "minimal-clean" -> "Verifique a autenticidade em https://validar.iti.gov.br";
|
||||||
case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001";
|
case "gov-br" -> "Verifique a autenticidade em https://validar.iti.gov.br";
|
||||||
default -> "Conformidade ICP-Brasil e validade jurídica plena";
|
default -> "Verifique a autenticidade em https://validar.iti.gov.br";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ public final class PdfSealThemeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void registerBuiltInThemes() {
|
private static void registerBuiltInThemes() {
|
||||||
// 1. Tema ICP-Brasil Institucional
|
// Único Selo Padrão Embutido do Sistema: ICP-Brasil Institucional Oficial
|
||||||
registerTheme(new PdfSealTheme(
|
registerTheme(new PdfSealTheme(
|
||||||
"icp-brasil",
|
DEFAULT_THEME_ID,
|
||||||
"ICP-Brasil Institucional",
|
"ICP-Brasil Institucional (Padrão)",
|
||||||
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
|
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
|
||||||
320,
|
320,
|
||||||
82,
|
82,
|
||||||
@@ -51,7 +51,7 @@ public final class PdfSealThemeRegistry {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
|
<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="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}"/>
|
<rect x="1" y="1" width="6" height="80" rx="3" fill="${THEME_PRIMARY}"/>
|
||||||
<!-- Escudo Criptográfico Vetorial -->
|
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
|
||||||
<g transform="translate(14, 18)">
|
<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="${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"/>
|
<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"/>
|
||||||
@@ -65,104 +65,7 @@ public final class PdfSealThemeRegistry {
|
|||||||
<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="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="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="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>
|
<text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">Verifique a autenticidade em https://validar.iti.gov.br</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>
|
</svg>
|
||||||
"""
|
"""
|
||||||
));
|
));
|
||||||
@@ -205,10 +108,9 @@ public final class PdfSealThemeRegistry {
|
|||||||
if (themeId == null) return false;
|
if (themeId == null) return false;
|
||||||
String idLower = themeId.toLowerCase();
|
String idLower = themeId.toLowerCase();
|
||||||
|
|
||||||
// Protege temas padrão
|
// Protege o tema padrão
|
||||||
if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") ||
|
if (idLower.equals(DEFAULT_THEME_ID)) {
|
||||||
idLower.equals("minimal-clean") || idLower.equals("gov-br")) {
|
throw new IllegalArgumentException("Não é permitido excluir o selo padrão pré-instalado do sistema.");
|
||||||
throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean deleted = STORAGE.deleteSeal(idLower);
|
boolean deleted = STORAGE.deleteSeal(idLower);
|
||||||
@@ -218,7 +120,11 @@ public final class PdfSealThemeRegistry {
|
|||||||
|
|
||||||
public static Optional<PdfSealTheme> findById(String id) {
|
public static Optional<PdfSealTheme> findById(String id) {
|
||||||
if (id == null) return Optional.empty();
|
if (id == null) return Optional.empty();
|
||||||
return Optional.ofNullable(THEMES.get(id.toLowerCase()));
|
PdfSealTheme theme = THEMES.get(id.toLowerCase());
|
||||||
|
if (theme == null && ("cybercore-modern".equalsIgnoreCase(id) || "minimal-clean".equalsIgnoreCase(id) || "gov-br".equalsIgnoreCase(id))) {
|
||||||
|
return Optional.of(getDefaultTheme());
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<PdfSealTheme> getTheme(String id) {
|
public static Optional<PdfSealTheme> getTheme(String id) {
|
||||||
|
|||||||
@@ -56,7 +56,67 @@ public record CcspRequest(
|
|||||||
String purpose,
|
String purpose,
|
||||||
|
|
||||||
@JsonProperty("challenge")
|
@JsonProperty("challenge")
|
||||||
String challenge
|
String challenge,
|
||||||
|
|
||||||
|
@JsonProperty("pdf")
|
||||||
|
String pdf,
|
||||||
|
|
||||||
|
@JsonProperty("page")
|
||||||
|
Integer page,
|
||||||
|
|
||||||
|
@JsonProperty("page_choice")
|
||||||
|
String pageChoice,
|
||||||
|
|
||||||
|
@JsonProperty("position")
|
||||||
|
String position,
|
||||||
|
|
||||||
|
@JsonProperty("x")
|
||||||
|
Float x,
|
||||||
|
|
||||||
|
@JsonProperty("y")
|
||||||
|
Float y,
|
||||||
|
|
||||||
|
@JsonProperty("scale")
|
||||||
|
Double scale,
|
||||||
|
|
||||||
|
@JsonProperty("visible_seal")
|
||||||
|
Boolean visibleSeal,
|
||||||
|
|
||||||
|
@JsonProperty("seal_theme_id")
|
||||||
|
String sealThemeId,
|
||||||
|
|
||||||
|
@JsonProperty("seal_custom")
|
||||||
|
java.util.Map<String, Object> sealCustom,
|
||||||
|
|
||||||
|
@JsonProperty("reason")
|
||||||
|
String reason,
|
||||||
|
|
||||||
|
@JsonProperty("location")
|
||||||
|
String location,
|
||||||
|
|
||||||
|
@JsonProperty("cpf_display_mode")
|
||||||
|
String cpfDisplayMode,
|
||||||
|
|
||||||
|
@JsonProperty("include_date_time")
|
||||||
|
Boolean includeDateTime,
|
||||||
|
|
||||||
|
@JsonProperty("custom_extra_info")
|
||||||
|
String customExtraInfo,
|
||||||
|
|
||||||
|
@JsonProperty("xml")
|
||||||
|
String xml,
|
||||||
|
|
||||||
|
@JsonProperty("xml_base64")
|
||||||
|
String xmlBase64,
|
||||||
|
|
||||||
|
@JsonProperty("profile")
|
||||||
|
String profile,
|
||||||
|
|
||||||
|
@JsonProperty("target_element_tag")
|
||||||
|
String targetElementTag,
|
||||||
|
|
||||||
|
@JsonProperty("target_element_id")
|
||||||
|
String targetElementId
|
||||||
) implements Serializable {
|
) implements Serializable {
|
||||||
|
|
||||||
public static final String PROTOCOL_VERSION = "1";
|
public static final String PROTOCOL_VERSION = "1";
|
||||||
@@ -68,7 +128,9 @@ public record CcspRequest(
|
|||||||
) {
|
) {
|
||||||
this(version, requestId, applicationId, operation, algorithm, digest,
|
this(version, requestId, applicationId, operation, algorithm, digest,
|
||||||
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
||||||
null, null, null, null);
|
null, null, null, null,
|
||||||
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVersionSupported() {
|
public boolean isVersionSupported() {
|
||||||
|
|||||||
@@ -49,7 +49,16 @@ public record CcspResponse(
|
|||||||
String documentName,
|
String documentName,
|
||||||
|
|
||||||
@JsonProperty("document_type")
|
@JsonProperty("document_type")
|
||||||
String documentType
|
String documentType,
|
||||||
|
|
||||||
|
@JsonProperty("signed_pdf")
|
||||||
|
String signedPdf,
|
||||||
|
|
||||||
|
@JsonProperty("signed_xml")
|
||||||
|
String signedXml,
|
||||||
|
|
||||||
|
@JsonProperty("sha256")
|
||||||
|
String sha256
|
||||||
) implements Serializable {
|
) implements Serializable {
|
||||||
|
|
||||||
public CcspResponse {
|
public CcspResponse {
|
||||||
@@ -88,7 +97,58 @@ public record CcspResponse(
|
|||||||
subjectName,
|
subjectName,
|
||||||
subjectCpfCnpj,
|
subjectCpfCnpj,
|
||||||
documentName,
|
documentName,
|
||||||
documentType
|
documentType,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CcspResponse pdfSuccess(String requestId, String signedPdfBase64, String sha256,
|
||||||
|
String signatureBase64, String certBase64, List<String> chain,
|
||||||
|
String algorithm, String subjectName, String subjectCpfCnpj,
|
||||||
|
String documentName) {
|
||||||
|
return new CcspResponse(
|
||||||
|
CcspRequest.PROTOCOL_VERSION,
|
||||||
|
requestId,
|
||||||
|
STATUS_SIGNED,
|
||||||
|
signatureBase64,
|
||||||
|
certBase64,
|
||||||
|
chain,
|
||||||
|
algorithm,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
subjectName,
|
||||||
|
subjectCpfCnpj,
|
||||||
|
documentName,
|
||||||
|
"PDF",
|
||||||
|
signedPdfBase64,
|
||||||
|
null,
|
||||||
|
sha256
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CcspResponse xmlSuccess(String requestId, String signedXml, String sha256,
|
||||||
|
String signatureBase64, String certBase64, List<String> chain,
|
||||||
|
String algorithm, String subjectName, String subjectCpfCnpj,
|
||||||
|
String documentName) {
|
||||||
|
return new CcspResponse(
|
||||||
|
CcspRequest.PROTOCOL_VERSION,
|
||||||
|
requestId,
|
||||||
|
STATUS_SIGNED,
|
||||||
|
signatureBase64,
|
||||||
|
certBase64,
|
||||||
|
chain,
|
||||||
|
algorithm,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
subjectName,
|
||||||
|
subjectCpfCnpj,
|
||||||
|
documentName,
|
||||||
|
"XML",
|
||||||
|
null,
|
||||||
|
signedXml,
|
||||||
|
sha256
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +168,10 @@ public record CcspResponse(
|
|||||||
subjectName,
|
subjectName,
|
||||||
subjectCpfCnpj,
|
subjectCpfCnpj,
|
||||||
null,
|
null,
|
||||||
"AUTH_CHALLENGE"
|
"AUTH_CHALLENGE",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +189,9 @@ public record CcspResponse(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.crypto.SigningService;
|
|||||||
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
||||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||||
|
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
|
||||||
import br.com.cybercore.signer.core.domain.SigningResult;
|
import br.com.cybercore.signer.core.domain.SigningResult;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
@@ -22,6 +23,20 @@ import com.sun.net.httpserver.HttpServer;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSealPageChoice;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSealPosition;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.pdf.PdfSigningService;
|
||||||
|
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
|
||||||
|
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
|
||||||
|
import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSigningService;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
@@ -56,6 +71,8 @@ public class CcspServer {
|
|||||||
private final int port;
|
private final int port;
|
||||||
private final CertificateManager certificateManager;
|
private final CertificateManager certificateManager;
|
||||||
private final SigningService signingService;
|
private final SigningService signingService;
|
||||||
|
private final PdfSigningService pdfSigningService;
|
||||||
|
private final XmlSigningService xmlSigningService;
|
||||||
private final ApplicationRegistry applicationRegistry;
|
private final ApplicationRegistry applicationRegistry;
|
||||||
private final NonceManager nonceManager;
|
private final NonceManager nonceManager;
|
||||||
private final OriginValidator originValidator;
|
private final OriginValidator originValidator;
|
||||||
@@ -75,6 +92,8 @@ public class CcspServer {
|
|||||||
this.port = port;
|
this.port = port;
|
||||||
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
|
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
|
||||||
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
|
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
|
||||||
|
this.pdfSigningService = new PdfSigningService(this.certificateManager);
|
||||||
|
this.xmlSigningService = new XmlSigningService(this.certificateManager);
|
||||||
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
|
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
|
||||||
this.promptListener = promptListener;
|
this.promptListener = promptListener;
|
||||||
this.nonceManager = new NonceManager();
|
this.nonceManager = new NonceManager();
|
||||||
@@ -114,6 +133,9 @@ public class CcspServer {
|
|||||||
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
|
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
|
||||||
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
|
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
|
||||||
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
|
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
|
||||||
|
httpServer.createContext("/ccsp/v1/sign-pdf", new SignPdfHandler());
|
||||||
|
httpServer.createContext("/ccsp/v1/sign-xml", new SignXmlHandler());
|
||||||
|
httpServer.createContext("/ccsp/v1/seals", new SealsHandler());
|
||||||
|
|
||||||
httpServer.start();
|
httpServer.start();
|
||||||
running = true;
|
running = true;
|
||||||
@@ -306,10 +328,14 @@ public class CcspServer {
|
|||||||
try (InputStream is = exchange.getRequestBody()) {
|
try (InputStream is = exchange.getRequestBody()) {
|
||||||
request = objectMapper.readValue(is, CcspRequest.class);
|
request = objectMapper.readValue(is, CcspRequest.class);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
logger.warn("[CCSP REQ INVÁLIDO] POST /ccsp/v1/sign de Origin: {} | Erro parsing: {}", origin, e.getMessage());
|
||||||
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
|
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("[CCSP REQ] POST /ccsp/v1/sign | Origin: {} | ReqId: {} | App: {} | Op: {} | Algoritmo: {} | CertHint: {}",
|
||||||
|
origin, request.requestId(), request.applicationId(), request.operation(), request.algorithm(), request.certificateHint());
|
||||||
|
|
||||||
// 1. Validação de versão
|
// 1. Validação de versão
|
||||||
if (!request.isVersionSupported()) {
|
if (!request.isVersionSupported()) {
|
||||||
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
|
||||||
@@ -458,6 +484,525 @@ public class CcspServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SignPdfHandler 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) {
|
||||||
|
logger.warn("[CCSP REQ INVÁLIDO] POST /ccsp/v1/sign-pdf de Origin: {} | Erro parsing: {}", origin, e.getMessage());
|
||||||
|
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pdfLength = request.pdf() != null ? request.pdf().length() : 0;
|
||||||
|
logger.info("[CCSP REQ] POST /ccsp/v1/sign-pdf | Origin: {} | ReqId: {} | App: {} | Doc: '{}' | Base64: {} chars | CertHint: {}",
|
||||||
|
origin, request.requestId(), request.applicationId(), request.documentName(), pdfLength, request.certificateHint());
|
||||||
|
|
||||||
|
if (!request.isVersionSupported()) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
|
||||||
|
"Versão do protocolo não suportada: " + request.version());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isExpired()) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
|
||||||
|
"A requisição de assinatura expirou.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.pdf() == null || request.pdf().isBlank()) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"O campo 'pdf' contendo o documento em Base64 é obrigatório.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pdfBytes;
|
||||||
|
try {
|
||||||
|
pdfBytes = Base64.getDecoder().decode(request.pdf());
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"O campo 'pdf' não contém dados Base64 válidos.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfSealTheme sealTheme;
|
||||||
|
if (request.sealCustom() != null && !request.sealCustom().isEmpty()) {
|
||||||
|
sealTheme = parseCustomSealTheme(request.sealCustom());
|
||||||
|
} else if (request.sealThemeId() != null && !request.sealThemeId().isBlank()) {
|
||||||
|
sealTheme = PdfSealThemeRegistry.findById(request.sealThemeId())
|
||||||
|
.orElse(PdfSealThemeRegistry.getDefaultTheme());
|
||||||
|
} else {
|
||||||
|
sealTheme = PdfSealThemeRegistry.getDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("[CCSP REQ] Selo visual configurado: '{}' (custom={}, hasLogo={}, header='{}')",
|
||||||
|
sealTheme.name(), sealTheme.isCustom(), sealTheme.customLogoBase64() != null, sealTheme.customHeader());
|
||||||
|
|
||||||
|
PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT;
|
||||||
|
if (request.x() != null && request.y() != null) {
|
||||||
|
position = PdfSealPosition.CUSTOM;
|
||||||
|
} else if (request.position() != null) {
|
||||||
|
try {
|
||||||
|
position = PdfSealPosition.valueOf(request.position().toUpperCase());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfSealPageChoice pageChoice = PdfSealPageChoice.LAST_PAGE;
|
||||||
|
if (request.pageChoice() != null) {
|
||||||
|
try {
|
||||||
|
pageChoice = PdfSealPageChoice.valueOf(request.pageChoice().toUpperCase());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
CpfDisplayMode cpfMode = sealTheme.defaultCpfMode();
|
||||||
|
if (request.cpfDisplayMode() != null) {
|
||||||
|
try {
|
||||||
|
cpfMode = CpfDisplayMode.valueOf(request.cpfDisplayMode().toUpperCase());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean includeDateTime = request.includeDateTime() != null
|
||||||
|
? request.includeDateTime()
|
||||||
|
: sealTheme.defaultIncludeDateTime();
|
||||||
|
|
||||||
|
boolean visibleSeal = request.visibleSeal() == null || Boolean.TRUE.equals(request.visibleSeal());
|
||||||
|
double customScale = request.scale() != null && request.scale() > 0 ? request.scale() : 1.0;
|
||||||
|
|
||||||
|
PdfSignatureOptions options = new PdfSignatureOptions(
|
||||||
|
cert.id(),
|
||||||
|
null,
|
||||||
|
visibleSeal,
|
||||||
|
sealTheme,
|
||||||
|
position,
|
||||||
|
pageChoice,
|
||||||
|
request.reason() != null ? request.reason() : "Assinatura Digital de Documento",
|
||||||
|
request.location() != null ? request.location() : "Brasil",
|
||||||
|
request.x(),
|
||||||
|
request.y(),
|
||||||
|
request.page(),
|
||||||
|
cpfMode,
|
||||||
|
includeDateTime,
|
||||||
|
request.customExtraInfo(),
|
||||||
|
customScale
|
||||||
|
);
|
||||||
|
|
||||||
|
signLock.lock();
|
||||||
|
try {
|
||||||
|
byte[] signedPdfBytes = pdfSigningService.signPdf(pdfBytes, options);
|
||||||
|
String signedPdfBase64 = Base64.getEncoder().encodeToString(signedPdfBytes);
|
||||||
|
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] shaBytes = md.digest(signedPdfBytes);
|
||||||
|
String shaHex = bytesToHex(shaBytes);
|
||||||
|
|
||||||
|
Optional<SigningKeyMaterial> keyMat = certificateManager.getKeyMaterial(cert.id(), null);
|
||||||
|
String certBase64 = null;
|
||||||
|
List<String> chainBase64 = Collections.emptyList();
|
||||||
|
if (keyMat.isPresent()) {
|
||||||
|
try {
|
||||||
|
certBase64 = Base64.getEncoder().encodeToString(keyMat.get().signingCertificate().getEncoded());
|
||||||
|
chainBase64 = Arrays.stream(keyMat.get().certificateChain())
|
||||||
|
.map(c -> {
|
||||||
|
try {
|
||||||
|
return Base64.getEncoder().encodeToString(c.getEncoded());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.toList();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
|
||||||
|
String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
|
||||||
|
|
||||||
|
CcspResponse response = CcspResponse.pdfSuccess(
|
||||||
|
request.requestId(),
|
||||||
|
signedPdfBase64,
|
||||||
|
shaHex,
|
||||||
|
null,
|
||||||
|
certBase64,
|
||||||
|
chainBase64,
|
||||||
|
"SHA256withRSA",
|
||||||
|
subjectName,
|
||||||
|
cpfCnpj,
|
||||||
|
request.documentName() != null ? request.documentName() : "documento.pdf"
|
||||||
|
);
|
||||||
|
|
||||||
|
sendJsonResponse(exchange, 200, response);
|
||||||
|
|
||||||
|
} catch (CertificateProviderException cpe) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Falha ao assinar PDF: {}", e.getMessage(), e);
|
||||||
|
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"Erro ao assinar PDF: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
signLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SignXmlHandler 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.isVersionSupported()) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
|
||||||
|
"Versão do protocolo não suportada: " + request.version());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isExpired()) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
|
||||||
|
"A requisição de assinatura expirou.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] xmlBytes;
|
||||||
|
if (request.xmlBase64() != null && !request.xmlBase64().isBlank()) {
|
||||||
|
try {
|
||||||
|
xmlBytes = Base64.getDecoder().decode(request.xmlBase64());
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"O campo 'xml_base64' não contém dados válidos.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (request.xml() != null && !request.xml().isBlank()) {
|
||||||
|
xmlBytes = request.xml().getBytes(StandardCharsets.UTF_8);
|
||||||
|
} else {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"É obrigatório fornecer o XML em texto ('xml') ou Base64 ('xml_base64').");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 XML: {}", e.getMessage());
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
|
||||||
|
"Confirmação da assinatura expirou ou foi cancelada.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XmlSignatureProfile profile = XmlSignatureProfile.XADES_BES;
|
||||||
|
if (request.profile() != null) {
|
||||||
|
try {
|
||||||
|
profile = XmlSignatureProfile.valueOf(request.profile().toUpperCase());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
String algo = request.algorithm() != null && !request.algorithm().isBlank()
|
||||||
|
? request.algorithm()
|
||||||
|
: XmlSignatureOptions.DEFAULT_ALGORITHM;
|
||||||
|
|
||||||
|
XmlSignatureOptions options = new XmlSignatureOptions(
|
||||||
|
cert.id(),
|
||||||
|
null,
|
||||||
|
request.targetElementTag(),
|
||||||
|
request.targetElementId(),
|
||||||
|
algo,
|
||||||
|
profile
|
||||||
|
);
|
||||||
|
|
||||||
|
signLock.lock();
|
||||||
|
try {
|
||||||
|
byte[] signedXmlBytes = xmlSigningService.signXml(xmlBytes, options);
|
||||||
|
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] shaBytes = md.digest(signedXmlBytes);
|
||||||
|
String shaHex = bytesToHex(shaBytes);
|
||||||
|
|
||||||
|
Optional<SigningKeyMaterial> keyMat = certificateManager.getKeyMaterial(cert.id(), null);
|
||||||
|
String certBase64 = null;
|
||||||
|
List<String> chainBase64 = Collections.emptyList();
|
||||||
|
if (keyMat.isPresent()) {
|
||||||
|
try {
|
||||||
|
certBase64 = Base64.getEncoder().encodeToString(keyMat.get().signingCertificate().getEncoded());
|
||||||
|
chainBase64 = Arrays.stream(keyMat.get().certificateChain())
|
||||||
|
.map(c -> {
|
||||||
|
try {
|
||||||
|
return Base64.getEncoder().encodeToString(c.getEncoded());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.toList();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
|
||||||
|
String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
|
||||||
|
|
||||||
|
CcspResponse response = CcspResponse.xmlSuccess(
|
||||||
|
request.requestId(),
|
||||||
|
signedXmlStr,
|
||||||
|
shaHex,
|
||||||
|
null,
|
||||||
|
certBase64,
|
||||||
|
chainBase64,
|
||||||
|
algo,
|
||||||
|
subjectName,
|
||||||
|
cpfCnpj,
|
||||||
|
request.documentName() != null ? request.documentName() : "documento.xml"
|
||||||
|
);
|
||||||
|
|
||||||
|
sendJsonResponse(exchange, 200, response);
|
||||||
|
|
||||||
|
} catch (CertificateProviderException cpe) {
|
||||||
|
sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Falha ao assinar XML: {}", e.getMessage(), e);
|
||||||
|
sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
|
||||||
|
"Erro ao assinar XML: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
signLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SealsHandler 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> sealList = PdfSealThemeRegistry.listThemes().stream().map(t -> {
|
||||||
|
Map<String, Object> dto = new HashMap<>();
|
||||||
|
dto.put("id", t.id());
|
||||||
|
dto.put("name", t.name());
|
||||||
|
dto.put("description", t.description());
|
||||||
|
dto.put("width", t.width());
|
||||||
|
dto.put("height", t.height());
|
||||||
|
dto.put("primary_color", t.primaryColor());
|
||||||
|
dto.put("secondary_color", t.secondaryColor());
|
||||||
|
dto.put("background_color", t.backgroundColor());
|
||||||
|
dto.put("border_color", t.borderColor());
|
||||||
|
dto.put("text_color", t.textColor());
|
||||||
|
dto.put("icon_style", t.iconStyle());
|
||||||
|
dto.put("is_custom", t.isCustom());
|
||||||
|
dto.put("default_cpf_mode", t.defaultCpfMode() != null ? t.defaultCpfMode().name() : "FULL");
|
||||||
|
dto.put("default_include_date_time", t.defaultIncludeDateTime());
|
||||||
|
return dto;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
Map<String, Object> resp = new HashMap<>();
|
||||||
|
resp.put("version", CcspRequest.PROTOCOL_VERSION);
|
||||||
|
resp.put("status", "success");
|
||||||
|
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
|
||||||
|
resp.put("seals", sealList);
|
||||||
|
|
||||||
|
sendJsonResponse(exchange, 200, resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getMapString(Map<String, Object> map, String defaultVal, String... keys) {
|
||||||
|
if (map == null) return defaultVal;
|
||||||
|
for (String k : keys) {
|
||||||
|
Object v = map.get(k);
|
||||||
|
if (v != null && !v.toString().isBlank()) {
|
||||||
|
return v.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getMapInt(Map<String, Object> map, int defaultVal, String... keys) {
|
||||||
|
if (map == null) return defaultVal;
|
||||||
|
for (String k : keys) {
|
||||||
|
Object v = map.get(k);
|
||||||
|
if (v instanceof Number n) {
|
||||||
|
return n.intValue();
|
||||||
|
} else if (v != null) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(v.toString().trim());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdfSealTheme parseCustomSealTheme(Map<String, Object> map) {
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
return PdfSealThemeRegistry.getDefaultTheme();
|
||||||
|
}
|
||||||
|
String id = "custom-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
String name = getMapString(map, "Selo Personalizado", "name", "theme_name", "title");
|
||||||
|
String description = getMapString(map, "Selo customizado via aplicação web", "description", "desc");
|
||||||
|
int width = getMapInt(map, 320, "width", "w");
|
||||||
|
int height = getMapInt(map, 82, "height", "h");
|
||||||
|
String primaryColor = getMapString(map, "#0d47a1", "primaryColor", "primary_color");
|
||||||
|
String secondaryColor = getMapString(map, "#0284c7", "secondaryColor", "secondary_color");
|
||||||
|
String backgroundColor = getMapString(map, "#ffffff", "backgroundColor", "background_color");
|
||||||
|
String borderColor = getMapString(map, "#cbd5e1", "borderColor", "border_color");
|
||||||
|
String textColor = getMapString(map, "#0f172a", "textColor", "text_color");
|
||||||
|
String customHeader = getMapString(map, null, "customHeader", "custom_header", "title");
|
||||||
|
String customFooter = getMapString(map, null, "customFooter", "custom_footer");
|
||||||
|
String iconStyle = getMapString(map, "shield", "iconStyle", "icon_style");
|
||||||
|
String customLogoBase64 = getMapString(map, null, "customLogoBase64", "custom_logo_base64", "logo", "logoBase64");
|
||||||
|
String customFieldLabel = getMapString(map, null, "customFieldLabel", "custom_field_label");
|
||||||
|
String customFieldValue = getMapString(map, null, "customFieldValue", "custom_field_value");
|
||||||
|
|
||||||
|
CpfDisplayMode cpfMode = CpfDisplayMode.FULL;
|
||||||
|
String rawCpfMode = getMapString(map, null, "defaultCpfMode", "default_cpf_mode", "cpfMode", "cpf_mode");
|
||||||
|
if (rawCpfMode != null) {
|
||||||
|
try {
|
||||||
|
cpfMode = CpfDisplayMode.valueOf(rawCpfMode.toUpperCase());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean includeDateTime = true;
|
||||||
|
Object dtObj = map.containsKey("defaultIncludeDateTime") ? map.get("defaultIncludeDateTime")
|
||||||
|
: (map.containsKey("default_include_date_time") ? map.get("default_include_date_time")
|
||||||
|
: (map.containsKey("includeDateTime") ? map.get("includeDateTime")
|
||||||
|
: map.get("include_date_time")));
|
||||||
|
if (dtObj instanceof Boolean b) {
|
||||||
|
includeDateTime = b;
|
||||||
|
} else if (dtObj != null) {
|
||||||
|
includeDateTime = Boolean.parseBoolean(dtObj.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PdfSealTheme(
|
||||||
|
id, name, description, width, height,
|
||||||
|
primaryColor, secondaryColor, backgroundColor, borderColor, textColor,
|
||||||
|
null, customHeader, customFooter, iconStyle,
|
||||||
|
cpfMode, includeDateTime, true, customLogoBase64,
|
||||||
|
customFieldLabel, customFieldValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
if (bytes == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Utilitários de Segurança e HTTP
|
// Utilitários de Segurança e HTTP
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -494,7 +1039,8 @@ public class CcspServer {
|
|||||||
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", 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-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-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
||||||
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
||||||
|
|
||||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
@@ -531,6 +1077,22 @@ public class CcspServer {
|
|||||||
try (OutputStream os = exchange.getResponseBody()) {
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
os.write(bytes);
|
os.write(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
|
if (data instanceof CcspResponse resp) {
|
||||||
|
if ("signed".equalsIgnoreCase(resp.status())) {
|
||||||
|
logger.info("[CCSP RESP] {} -> HTTP {} | ReqId: {} | Status: signed | Resposta: {} bytes | SHA-256: {}",
|
||||||
|
path, statusCode, resp.requestId(), bytes.length, resp.sha256() != null ? resp.sha256() : "n/a");
|
||||||
|
} else if ("error".equalsIgnoreCase(resp.status())) {
|
||||||
|
logger.warn("[CCSP RESP ERROR] {} -> HTTP {} | ReqId: {} | Code: {} | Msg: {}",
|
||||||
|
path, statusCode, resp.requestId(), resp.errorCode(), resp.message());
|
||||||
|
} else {
|
||||||
|
logger.info("[CCSP RESP] {} -> HTTP {} | ReqId: {} | Status: {} | Resposta: {} bytes",
|
||||||
|
path, statusCode, resp.requestId(), resp.status(), bytes.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("[CCSP RESP] {} -> HTTP {} | Resposta: {} bytes", path, statusCode, bytes.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException {
|
private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import javax.xml.crypto.dsig.XMLSignature;
|
import javax.xml.crypto.dsig.XMLSignature;
|
||||||
@@ -221,32 +222,59 @@ public final class SignatureValidationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validação de assinaturas embutidas em documentos XML (XMLDSig).
|
* Validação de assinaturas embutidas em documentos XML (XAdES ou XMLDSig).
|
||||||
*/
|
*/
|
||||||
private ValidationResult validateXml(byte[] xmlBytes, String fileName) {
|
private ValidationResult validateXml(byte[] xmlBytes, String fileName) {
|
||||||
try {
|
try {
|
||||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
dbf.setNamespaceAware(true);
|
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);
|
||||||
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
|
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
|
||||||
|
|
||||||
|
// Registro de atributos Id no DOM
|
||||||
|
registerAllIdAttributes(doc.getDocumentElement());
|
||||||
|
|
||||||
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||||
|
if (nl.getLength() == 0) {
|
||||||
|
nl = doc.getElementsByTagName("Signature");
|
||||||
|
}
|
||||||
if (nl.getLength() == 0) {
|
if (nl.getLength() == 0) {
|
||||||
return new ValidationResult(
|
return new ValidationResult(
|
||||||
ValidationStatus.INVALID,
|
ValidationStatus.INVALID,
|
||||||
ValidationType.XML_DSIG,
|
ValidationType.XML_DSIG,
|
||||||
"Nenhum elemento <Signature> XMLDSig encontrado no documento.",
|
"Nenhum elemento <Signature> XML encontrado no documento.",
|
||||||
List.of(),
|
List.of(),
|
||||||
null,
|
null,
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identificação de padrão XAdES
|
||||||
|
NodeList qpList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "QualifyingProperties");
|
||||||
|
if (qpList.getLength() == 0) {
|
||||||
|
qpList = doc.getElementsByTagName("QualifyingProperties");
|
||||||
|
}
|
||||||
|
boolean isXades = qpList.getLength() > 0;
|
||||||
|
ValidationType valType = isXades ? ValidationType.XADES_XML : ValidationType.XML_DSIG;
|
||||||
|
|
||||||
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
||||||
List<ValidationSignerInfo> signers = new ArrayList<>();
|
List<ValidationSignerInfo> signers = new ArrayList<>();
|
||||||
boolean allValid = true;
|
boolean allValid = true;
|
||||||
|
|
||||||
for (int i = 0; i < nl.getLength(); i++) {
|
for (int i = 0; i < nl.getLength(); i++) {
|
||||||
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(i));
|
Element sigElement = (Element) nl.item(i);
|
||||||
|
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), sigElement);
|
||||||
|
|
||||||
|
// Registra nós com atributo Id no valContext
|
||||||
|
NodeList allNodes = doc.getElementsByTagName("*");
|
||||||
|
for (int n = 0; n < allNodes.getLength(); n++) {
|
||||||
|
if (allNodes.item(n) instanceof Element e && e.hasAttribute("Id")) {
|
||||||
|
valContext.setIdAttributeNS(e, null, "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
||||||
boolean coreValidity = signature.validate(valContext);
|
boolean coreValidity = signature.validate(valContext);
|
||||||
|
|
||||||
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
|
|||||||
allValid = false;
|
allValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extração de metadados do signatário a partir do KeyInfo
|
||||||
|
String signerName = "Signatário XML #" + (i + 1);
|
||||||
|
String identifier = "";
|
||||||
|
String issuer = isXades ? "ICP-Brasil / XAdES" : "Assinatura Enveloped XML";
|
||||||
|
String certSubject = "XML Signature Element";
|
||||||
|
|
||||||
|
if (signature.getKeyInfo() != null) {
|
||||||
|
for (Object kiContent : signature.getKeyInfo().getContent()) {
|
||||||
|
if (kiContent instanceof javax.xml.crypto.dsig.keyinfo.X509Data x509Data) {
|
||||||
|
for (Object o : x509Data.getContent()) {
|
||||||
|
if (o instanceof X509Certificate cert) {
|
||||||
|
certSubject = cert.getSubjectX500Principal().getName();
|
||||||
|
signerName = X509CertificateUtils.extractCommonName(certSubject);
|
||||||
|
String cpf = X509CertificateUtils.extractCpf(cert, signerName);
|
||||||
|
if (cpf != null) {
|
||||||
|
identifier = "CPF: " + cpf;
|
||||||
|
} else {
|
||||||
|
String cnpj = X509CertificateUtils.extractCnpj(cert, signerName);
|
||||||
|
if (cnpj != null) {
|
||||||
|
identifier = "CNPJ: " + cnpj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issuer = X509CertificateUtils.extractCommonName(cert.getIssuerX500Principal().getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data de assinatura (SigningTime se XAdES, senão data atual)
|
||||||
|
String dateStr = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date());
|
||||||
|
if (isXades) {
|
||||||
|
NodeList stList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SigningTime");
|
||||||
|
if (stList.getLength() > 0) {
|
||||||
|
dateStr = stList.item(0).getTextContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
signers.add(new ValidationSignerInfo(
|
signers.add(new ValidationSignerInfo(
|
||||||
"Signatário XML #" + (i + 1),
|
signerName,
|
||||||
"",
|
identifier,
|
||||||
"Assinatura Enveloped XML",
|
issuer,
|
||||||
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()),
|
dateStr,
|
||||||
coreValidity,
|
coreValidity,
|
||||||
coreValidity,
|
coreValidity,
|
||||||
signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
|
signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
|
||||||
"XMLDSig Element",
|
certSubject,
|
||||||
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA")
|
(isXades ? "Padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil)" : "Padrão XMLDSig W3C")
|
||||||
|
+ " • " + (coreValidity ? "Assinatura íntegra" : "Integridade violada")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID;
|
ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID;
|
||||||
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada.";
|
String msg = allValid
|
||||||
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName);
|
? (isXades
|
||||||
|
? "Documento XML assinado e íntegro no padrão avançado XAdES-BES (ICP-Brasil)."
|
||||||
|
: "Documento XML assinado e válido conforme padrão W3C XMLDSig.")
|
||||||
|
: "Assinatura XML inválida ou com integridade violada.";
|
||||||
|
return new ValidationResult(status, valType, msg, signers, null, fileName);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Erro ao validar XML: {}", e.getMessage(), e);
|
logger.error("Erro ao validar XML: {}", e.getMessage(), e);
|
||||||
@@ -284,6 +356,23 @@ public final class SignatureValidationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void registerAllIdAttributes(Element root) {
|
||||||
|
if (root == null) return;
|
||||||
|
if (root.hasAttribute("Id")) {
|
||||||
|
root.setIdAttribute("Id", true);
|
||||||
|
} else if (root.hasAttribute("id")) {
|
||||||
|
root.setIdAttribute("id", true);
|
||||||
|
} else if (root.hasAttribute("ID")) {
|
||||||
|
root.setIdAttribute("ID", true);
|
||||||
|
}
|
||||||
|
NodeList children = root.getChildNodes();
|
||||||
|
for (int i = 0; i < children.getLength(); i++) {
|
||||||
|
if (children.item(i) instanceof Element child) {
|
||||||
|
registerAllIdAttributes(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processa e valida criptograficamente os signatários de um pacote CMSSignedData.
|
* Processa e valida criptograficamente os signatários de um pacote CMSSignedData.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public enum ValidationType {
|
|||||||
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
|
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
|
||||||
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true),
|
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true),
|
||||||
PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true),
|
PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true),
|
||||||
|
XADES_XML("Assinatura Incorporada em XML (XAdES / ICP-Brasil)", true),
|
||||||
XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true),
|
XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true),
|
||||||
UNKNOWN("Formato Desconhecido", false);
|
UNKNOWN("Formato Desconhecido", false);
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parâmetros de configuração para a operação de assinatura digital XMLDSig.
|
* Parâmetros de configuração para a operação de assinatura digital XML (XAdES ou XMLDSig).
|
||||||
*/
|
*/
|
||||||
public record XmlSignatureOptions(
|
public record XmlSignatureOptions(
|
||||||
String certificateId,
|
String certificateId,
|
||||||
char[] pinOrPassword,
|
char[] pinOrPassword,
|
||||||
String targetElementTag,
|
String targetElementTag,
|
||||||
String targetElementId,
|
String targetElementId,
|
||||||
String signatureAlgorithm
|
String signatureAlgorithm,
|
||||||
|
XmlSignatureProfile profile
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
|
public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
|
||||||
|
public static final XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
|
||||||
|
|
||||||
public XmlSignatureOptions {
|
public XmlSignatureOptions {
|
||||||
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
|
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
|
||||||
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
|
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
|
||||||
signatureAlgorithm = DEFAULT_ALGORITHM;
|
signatureAlgorithm = DEFAULT_ALGORITHM;
|
||||||
}
|
}
|
||||||
|
if (profile == null) {
|
||||||
|
profile = DEFAULT_PROFILE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) {
|
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) {
|
||||||
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM);
|
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword, XmlSignatureProfile profile) {
|
||||||
|
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) {
|
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) {
|
||||||
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM);
|
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id, XmlSignatureProfile profile) {
|
||||||
|
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package br.com.cybercore.signer.core.xml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
|
||||||
|
*/
|
||||||
|
public enum XmlSignatureProfile {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
|
||||||
|
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
|
||||||
|
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
|
||||||
|
* e referência criptográfica do certificado do signatário (SigningCertificate).
|
||||||
|
*/
|
||||||
|
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
|
||||||
|
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
|
||||||
|
*/
|
||||||
|
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
XmlSignatureProfile(String displayName, String description) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
|
|||||||
import javax.xml.crypto.KeySelectorException;
|
import javax.xml.crypto.KeySelectorException;
|
||||||
import javax.xml.crypto.KeySelectorResult;
|
import javax.xml.crypto.KeySelectorResult;
|
||||||
import javax.xml.crypto.XMLCryptoContext;
|
import javax.xml.crypto.XMLCryptoContext;
|
||||||
|
import javax.xml.crypto.XMLStructure;
|
||||||
|
import javax.xml.crypto.dom.DOMStructure;
|
||||||
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
||||||
import javax.xml.crypto.dsig.DigestMethod;
|
import javax.xml.crypto.dsig.DigestMethod;
|
||||||
import javax.xml.crypto.dsig.Reference;
|
import javax.xml.crypto.dsig.Reference;
|
||||||
import javax.xml.crypto.dsig.SignatureMethod;
|
|
||||||
import javax.xml.crypto.dsig.SignedInfo;
|
import javax.xml.crypto.dsig.SignedInfo;
|
||||||
import javax.xml.crypto.dsig.Transform;
|
import javax.xml.crypto.dsig.Transform;
|
||||||
|
import javax.xml.crypto.dsig.XMLObject;
|
||||||
import javax.xml.crypto.dsig.XMLSignature;
|
import javax.xml.crypto.dsig.XMLSignature;
|
||||||
import javax.xml.crypto.dsig.XMLSignatureFactory;
|
import javax.xml.crypto.dsig.XMLSignatureFactory;
|
||||||
import javax.xml.crypto.dsig.dom.DOMSignContext;
|
import javax.xml.crypto.dsig.dom.DOMSignContext;
|
||||||
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.security.Key;
|
import java.security.MessageDigest;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo o padrão W3C XMLDSig e ICP-Brasil.
|
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
|
||||||
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.).
|
* - XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)
|
||||||
|
* - W3C XMLDSig convencional (Enveloped / NF-e / SPED legado)
|
||||||
|
*
|
||||||
* Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig).
|
* Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig).
|
||||||
*/
|
*/
|
||||||
public class XmlSigningService {
|
public class XmlSigningService {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class);
|
private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class);
|
||||||
|
|
||||||
|
public static final String XADES_132_NS = "http://uri.etsi.org/01903/v1.3.2#";
|
||||||
|
public static final String XADES_SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";
|
||||||
|
public static final String XMLDSIG_NS = XMLSignature.XMLNS;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter ISO_INSTANT_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
|
||||||
|
|
||||||
private final CertificateManager certificateManager;
|
private final CertificateManager certificateManager;
|
||||||
|
|
||||||
public XmlSigningService(CertificateManager certificateManager) {
|
public XmlSigningService(CertificateManager certificateManager) {
|
||||||
@@ -85,7 +103,7 @@ public class XmlSigningService {
|
|||||||
try (FileOutputStream fos = new FileOutputStream(targetXml)) {
|
try (FileOutputStream fos = new FileOutputStream(targetXml)) {
|
||||||
fos.write(signedBytes);
|
fos.write(signedBytes);
|
||||||
}
|
}
|
||||||
logger.info("XML assinado com sucesso e salvo em: {}", targetXml.getAbsolutePath());
|
logger.info("XML assinado com sucesso ({}) e salvo em: {}", options.profile().getDisplayName(), targetXml.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,7 +135,8 @@ public class XmlSigningService {
|
|||||||
}
|
}
|
||||||
SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
|
SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
|
||||||
|
|
||||||
logger.info("Iniciando assinatura XMLDSig com certificado '{}' [tipo={}]", cert.commonName(), cert.type());
|
logger.info("Iniciando assinatura XML [perfil={}] com certificado '{}' [tipo={}]",
|
||||||
|
options.profile(), cert.commonName(), cert.type());
|
||||||
|
|
||||||
// 2. Parse Seguro do XML (Prevenção rigorosa de XXE)
|
// 2. Parse Seguro do XML (Prevenção rigorosa de XXE)
|
||||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
@@ -134,13 +153,13 @@ public class XmlSigningService {
|
|||||||
// 3. Configuração do XMLSignatureFactory
|
// 3. Configuração do XMLSignatureFactory
|
||||||
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
||||||
|
|
||||||
// Transformações padrão: Enveloped + C14N
|
// Transformações padrão para os dados de negócio: Enveloped + C14N
|
||||||
List<Transform> transforms = new ArrayList<>();
|
List<Transform> dataTransforms = new ArrayList<>();
|
||||||
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
|
dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
|
||||||
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
|
dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
|
||||||
|
|
||||||
// Determinação da Referência e Ponto de Inserção
|
// Determinação da Referência de Dados e Ponto de Inserção da Assinatura
|
||||||
Reference ref;
|
Reference dataRef;
|
||||||
Node insertionParent;
|
Node insertionParent;
|
||||||
|
|
||||||
if (options.targetElementId() != null && !options.targetElementId().isBlank()) {
|
if (options.targetElementId() != null && !options.targetElementId().isBlank()) {
|
||||||
@@ -150,7 +169,7 @@ public class XmlSigningService {
|
|||||||
throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento.");
|
throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento.");
|
||||||
}
|
}
|
||||||
targetElement.setIdAttribute("Id", true);
|
targetElement.setIdAttribute("Id", true);
|
||||||
ref = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null);
|
dataRef = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
|
||||||
insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement();
|
insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement();
|
||||||
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
|
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
|
||||||
NodeList list = doc.getElementsByTagName(options.targetElementTag());
|
NodeList list = doc.getElementsByTagName(options.targetElementTag());
|
||||||
@@ -160,42 +179,87 @@ public class XmlSigningService {
|
|||||||
if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) {
|
if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) {
|
||||||
String idVal = elem.getAttribute("Id");
|
String idVal = elem.getAttribute("Id");
|
||||||
elem.setIdAttribute("Id", true);
|
elem.setIdAttribute("Id", true);
|
||||||
ref = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null);
|
dataRef = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
|
||||||
insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement();
|
insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement();
|
||||||
} else {
|
} else {
|
||||||
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null);
|
dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
|
||||||
insertionParent = doc.getDocumentElement();
|
insertionParent = doc.getDocumentElement();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assinatura Enveloped do documento completo
|
// Assinatura Enveloped do documento completo
|
||||||
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null);
|
dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
|
||||||
insertionParent = doc.getDocumentElement();
|
insertionParent = doc.getDocumentElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Montagem do SignedInfo
|
List<Reference> references = new ArrayList<>();
|
||||||
|
references.add(dataRef);
|
||||||
|
|
||||||
|
List<XMLObject> xmlObjects = new ArrayList<>();
|
||||||
|
String signatureUniqueId = "sig-" + UUID.randomUUID();
|
||||||
|
Element signedPropertiesElem = null;
|
||||||
|
|
||||||
|
// 4. Se for perfil XAdES-BES, constrói as QualifyingProperties e a Reference correspondente
|
||||||
|
if (options.profile() == XmlSignatureProfile.XADES_BES) {
|
||||||
|
String signedPropsId = "signed-props-" + UUID.randomUUID();
|
||||||
|
signedPropertiesElem = buildXadesSignedProperties(doc, signedPropsId, keyMaterial.signingCertificate(), Instant.now());
|
||||||
|
|
||||||
|
Element qualifyingProperties = doc.createElementNS(XADES_132_NS, "xades:QualifyingProperties");
|
||||||
|
qualifyingProperties.setAttribute("Target", "#" + signatureUniqueId);
|
||||||
|
qualifyingProperties.appendChild(signedPropertiesElem);
|
||||||
|
|
||||||
|
XMLStructure xadesStructure = new DOMStructure(qualifyingProperties);
|
||||||
|
XMLObject xadesObject = fac.newXMLObject(Collections.singletonList(xadesStructure), null, null, null);
|
||||||
|
xmlObjects.add(xadesObject);
|
||||||
|
|
||||||
|
// Adiciona Referência Criptográfica cobrindo as SignedProperties
|
||||||
|
List<Transform> xadesTransforms = Collections.singletonList(
|
||||||
|
fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)
|
||||||
|
);
|
||||||
|
Reference xadesRef = fac.newReference(
|
||||||
|
"#" + signedPropsId,
|
||||||
|
fac.newDigestMethod(DigestMethod.SHA256, null),
|
||||||
|
xadesTransforms,
|
||||||
|
XADES_SIGNED_PROPERTIES_TYPE,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
references.add(xadesRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Montagem do SignedInfo
|
||||||
SignedInfo si = fac.newSignedInfo(
|
SignedInfo si = fac.newSignedInfo(
|
||||||
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
|
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
|
||||||
fac.newSignatureMethod(options.signatureAlgorithm(), null),
|
fac.newSignatureMethod(options.signatureAlgorithm(), null),
|
||||||
Collections.singletonList(ref)
|
references
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Inclusão da Cadeia de Certificados no KeyInfo
|
// 6. Inclusão da Cadeia de Certificados no KeyInfo
|
||||||
KeyInfoFactory kif = fac.getKeyInfoFactory();
|
KeyInfoFactory kif = fac.getKeyInfoFactory();
|
||||||
List<Object> x509Content = new ArrayList<>();
|
List<Object> x509Content = new ArrayList<>();
|
||||||
x509Content.add(keyMaterial.signingCertificate());
|
x509Content.add(keyMaterial.signingCertificate());
|
||||||
X509Data x509Data = kif.newX509Data(x509Content);
|
X509Data x509Data = kif.newX509Data(x509Content);
|
||||||
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
|
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
|
||||||
|
|
||||||
// 6. Execução da Assinatura DOM
|
// 7. Execução da Assinatura DOM
|
||||||
DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent);
|
DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent);
|
||||||
if (keyMaterial.jcaProvider() != null) {
|
if (keyMaterial.jcaProvider() != null) {
|
||||||
dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider());
|
dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLSignature signature = fac.newXMLSignature(si, ki);
|
if (signedPropertiesElem != null) {
|
||||||
|
dsc.setIdAttributeNS(signedPropertiesElem, null, "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
XMLSignature signature = fac.newXMLSignature(
|
||||||
|
si,
|
||||||
|
ki,
|
||||||
|
xmlObjects,
|
||||||
|
signatureUniqueId,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
signature.sign(dsc);
|
signature.sign(dsc);
|
||||||
|
|
||||||
// 7. Serialização do XML Assinado
|
// 8. Serialização do XML Assinado
|
||||||
TransformerFactory tf = TransformerFactory.newInstance();
|
TransformerFactory tf = TransformerFactory.newInstance();
|
||||||
Transformer trans = tf.newTransformer();
|
Transformer trans = tf.newTransformer();
|
||||||
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
||||||
@@ -205,12 +269,13 @@ public class XmlSigningService {
|
|||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
trans.transform(new DOMSource(doc), new StreamResult(baos));
|
trans.transform(new DOMSource(doc), new StreamResult(baos));
|
||||||
|
|
||||||
logger.info("Assinatura XMLDSig gerada com sucesso. Tamanho: {} bytes", baos.size());
|
logger.info("Assinatura XML ({}) gerada com sucesso. Tamanho final: {} bytes",
|
||||||
|
options.profile(), baos.size());
|
||||||
return baos.toByteArray();
|
return baos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valida a integridade criptográfica de um documento XML assinado com XMLDSig.
|
* Valida a integridade criptográfica de um documento XML assinado com XAdES ou XMLDSig.
|
||||||
*/
|
*/
|
||||||
public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception {
|
public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception {
|
||||||
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
|
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
|
||||||
@@ -218,24 +283,17 @@ public class XmlSigningService {
|
|||||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
dbf.setNamespaceAware(true);
|
dbf.setNamespaceAware(true);
|
||||||
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", 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);
|
||||||
|
|
||||||
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
|
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
|
||||||
|
|
||||||
// Registra nós com atributo Id/id/ID para que o ResourceResolver do XMLDSig localize a referência
|
// Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
|
||||||
NodeList allElements = doc.getElementsByTagName("*");
|
registerIdAttributes(doc.getDocumentElement());
|
||||||
for (int i = 0; i < allElements.getLength(); i++) {
|
|
||||||
if (allElements.item(i) instanceof Element e) {
|
|
||||||
if (e.hasAttribute("Id")) {
|
|
||||||
e.setIdAttribute("Id", true);
|
|
||||||
} else if (e.hasAttribute("id")) {
|
|
||||||
e.setIdAttribute("id", true);
|
|
||||||
} else if (e.hasAttribute("ID")) {
|
|
||||||
e.setIdAttribute("ID", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
|
||||||
if (nl.getLength() == 0) {
|
if (nl.getLength() == 0) {
|
||||||
nl = doc.getElementsByTagName("Signature");
|
nl = doc.getElementsByTagName("Signature");
|
||||||
}
|
}
|
||||||
@@ -248,8 +306,168 @@ public class XmlSigningService {
|
|||||||
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
||||||
DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement);
|
DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement);
|
||||||
|
|
||||||
|
// Registra explicitamente elementos com atributo Id no valContext
|
||||||
|
NodeList allNodes = doc.getElementsByTagName("*");
|
||||||
|
for (int i = 0; i < allNodes.getLength(); i++) {
|
||||||
|
if (allNodes.item(i) instanceof Element e) {
|
||||||
|
if (e.hasAttribute("Id")) {
|
||||||
|
valContext.setIdAttributeNS(e, null, "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
|
||||||
return signature.validate(valContext);
|
boolean coreValidity = signature.validate(valContext);
|
||||||
|
if (!coreValidity) {
|
||||||
|
boolean sv = signature.getSignatureValue().validate(valContext);
|
||||||
|
logger.warn("Validação XML falhou. SignatureValue válido: {}", sv);
|
||||||
|
int refIdx = 0;
|
||||||
|
for (Object o : signature.getSignedInfo().getReferences()) {
|
||||||
|
if (o instanceof Reference r) {
|
||||||
|
boolean rValid = r.validate(valContext);
|
||||||
|
byte[] calcDigest = r.getCalculatedDigestValue();
|
||||||
|
byte[] storedDigest = r.getDigestValue();
|
||||||
|
String calcB64 = calcDigest != null ? Base64.getEncoder().encodeToString(calcDigest) : "null";
|
||||||
|
String storedB64 = storedDigest != null ? Base64.getEncoder().encodeToString(storedDigest) : "null";
|
||||||
|
logger.warn("Reference[{}] URI='{}' válida: {} | calcDigest={} | storedDigest={}",
|
||||||
|
refIdx++, r.getURI(), rValid, calcB64, storedB64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coreValidity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspeciona e extrai informações detalhadas da assinatura XML (XAdES ou XMLDSig).
|
||||||
|
*/
|
||||||
|
public XmlSignatureDetails inspectXmlSignature(byte[] signedXmlBytes) throws Exception {
|
||||||
|
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
|
||||||
|
|
||||||
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
|
dbf.setNamespaceAware(true);
|
||||||
|
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||||
|
|
||||||
|
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
|
||||||
|
registerIdAttributes(doc.getDocumentElement());
|
||||||
|
|
||||||
|
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
|
||||||
|
if (nl.getLength() == 0) {
|
||||||
|
nl = doc.getElementsByTagName("Signature");
|
||||||
|
}
|
||||||
|
if (nl.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Element sigElem = (Element) nl.item(0);
|
||||||
|
String sigId = sigElem.getAttribute("Id");
|
||||||
|
|
||||||
|
// Verifica presença de XAdES
|
||||||
|
NodeList qpNodes = doc.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties");
|
||||||
|
boolean isXades = qpNodes.getLength() > 0;
|
||||||
|
|
||||||
|
String signingTime = null;
|
||||||
|
if (isXades) {
|
||||||
|
NodeList timeNodes = doc.getElementsByTagNameNS(XADES_132_NS, "SigningTime");
|
||||||
|
if (timeNodes.getLength() > 0) {
|
||||||
|
signingTime = timeNodes.item(0).getTextContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação criptográfica
|
||||||
|
boolean isValid = false;
|
||||||
|
try {
|
||||||
|
isValid = validateXmlSignature(signedXmlBytes);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.warn("Validação da assinatura XML falhou durante inspeção: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sigMethod = "";
|
||||||
|
NodeList smNodes = sigElem.getElementsByTagNameNS(XMLDSIG_NS, "SignatureMethod");
|
||||||
|
if (smNodes.getLength() > 0 && smNodes.item(0) instanceof Element smElem) {
|
||||||
|
sigMethod = smElem.getAttribute("Algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XmlSignatureDetails(
|
||||||
|
isValid,
|
||||||
|
isXades ? XmlSignatureProfile.XADES_BES : XmlSignatureProfile.XMLDSIG,
|
||||||
|
sigMethod,
|
||||||
|
signingTime,
|
||||||
|
sigId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói a estrutura DOM das propriedades assinadas do XAdES (SignedProperties).
|
||||||
|
*/
|
||||||
|
private Element buildXadesSignedProperties(Document doc, String signedPropsId, X509Certificate cert, Instant signInstant) throws Exception {
|
||||||
|
Element signedProperties = doc.createElementNS(XADES_132_NS, "xades:SignedProperties");
|
||||||
|
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xades", XADES_132_NS);
|
||||||
|
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ds", XMLDSIG_NS);
|
||||||
|
signedProperties.setAttribute("Id", signedPropsId);
|
||||||
|
signedProperties.setIdAttribute("Id", true);
|
||||||
|
|
||||||
|
Element signedSignatureProperties = doc.createElementNS(XADES_132_NS, "xades:SignedSignatureProperties");
|
||||||
|
|
||||||
|
// 1. Data e Hora UTC da Assinatura
|
||||||
|
Element signingTime = doc.createElementNS(XADES_132_NS, "xades:SigningTime");
|
||||||
|
signingTime.setTextContent(ISO_INSTANT_FORMATTER.format(signInstant));
|
||||||
|
signedSignatureProperties.appendChild(signingTime);
|
||||||
|
|
||||||
|
// 2. Vínculo Criptográfico do Certificado do Signatário (SigningCertificate)
|
||||||
|
Element signingCertificate = doc.createElementNS(XADES_132_NS, "xades:SigningCertificate");
|
||||||
|
Element certElem = doc.createElementNS(XADES_132_NS, "xades:Cert");
|
||||||
|
|
||||||
|
// Digest SHA-256 do Certificado DER
|
||||||
|
Element certDigest = doc.createElementNS(XADES_132_NS, "xades:CertDigest");
|
||||||
|
Element digestMethod = doc.createElementNS(XMLDSIG_NS, "ds:DigestMethod");
|
||||||
|
digestMethod.setAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256");
|
||||||
|
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] certHash = md.digest(cert.getEncoded());
|
||||||
|
String certHashB64 = Base64.getEncoder().encodeToString(certHash);
|
||||||
|
|
||||||
|
Element digestValue = doc.createElementNS(XMLDSIG_NS, "ds:DigestValue");
|
||||||
|
digestValue.setTextContent(certHashB64);
|
||||||
|
|
||||||
|
certDigest.appendChild(digestMethod);
|
||||||
|
certDigest.appendChild(digestValue);
|
||||||
|
certElem.appendChild(certDigest);
|
||||||
|
|
||||||
|
// Dados do Emissor e Serial do Certificado
|
||||||
|
Element issuerSerial = doc.createElementNS(XADES_132_NS, "xades:IssuerSerial");
|
||||||
|
Element x509IssuerName = doc.createElementNS(XMLDSIG_NS, "ds:X509IssuerName");
|
||||||
|
x509IssuerName.setTextContent(cert.getIssuerX500Principal().getName());
|
||||||
|
|
||||||
|
Element x509SerialNumber = doc.createElementNS(XMLDSIG_NS, "ds:X509SerialNumber");
|
||||||
|
x509SerialNumber.setTextContent(cert.getSerialNumber().toString());
|
||||||
|
|
||||||
|
issuerSerial.appendChild(x509IssuerName);
|
||||||
|
issuerSerial.appendChild(x509SerialNumber);
|
||||||
|
certElem.appendChild(issuerSerial);
|
||||||
|
|
||||||
|
signingCertificate.appendChild(certElem);
|
||||||
|
signedSignatureProperties.appendChild(signingCertificate);
|
||||||
|
|
||||||
|
signedProperties.appendChild(signedSignatureProperties);
|
||||||
|
return signedProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void registerIdAttributes(Element root) {
|
||||||
|
if (root == null) return;
|
||||||
|
if (root.hasAttribute("Id")) {
|
||||||
|
root.setIdAttribute("Id", true);
|
||||||
|
} else if (root.hasAttribute("id")) {
|
||||||
|
root.setIdAttribute("id", true);
|
||||||
|
} else if (root.hasAttribute("ID")) {
|
||||||
|
root.setIdAttribute("ID", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeList children = root.getChildNodes();
|
||||||
|
for (int i = 0; i < children.getLength(); i++) {
|
||||||
|
if (children.item(i) instanceof Element child) {
|
||||||
|
registerIdAttributes(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Element findElementById(Document doc, String id, String hintTag) {
|
private static Element findElementById(Document doc, String id, String hintTag) {
|
||||||
@@ -264,6 +482,17 @@ public class XmlSigningService {
|
|||||||
return doc.getElementById(id);
|
return doc.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO de detalhes de uma assinatura XML.
|
||||||
|
*/
|
||||||
|
public record XmlSignatureDetails(
|
||||||
|
boolean valid,
|
||||||
|
XmlSignatureProfile profile,
|
||||||
|
String algorithm,
|
||||||
|
String signingTime,
|
||||||
|
String signatureId
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo.
|
* Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 54" width="36" height="54">
|
||||||
|
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
|
||||||
|
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="#0d47a1" opacity="0.14"/>
|
||||||
|
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="none" stroke="#0d47a1" stroke-width="2.2" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- Corpo do Cadeado de Segurança -->
|
||||||
|
<rect x="11" y="24" width="14" height="12" rx="2.5" fill="#0284c7"/>
|
||||||
|
|
||||||
|
<!-- Arco Superior do Cadeado -->
|
||||||
|
<path d="M13.5 24 V19 C13.5 16.5 15.5 14.5 18 14.5 C20.5 14.5 22.5 16.5 22.5 19 V24" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Orifício da Chave -->
|
||||||
|
<circle cx="18" cy="29" r="1.6" fill="#ffffff"/>
|
||||||
|
<path d="M17.3 29.5 L18.7 29.5 L19.2 33 L16.8 33 Z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 858 B |
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
|||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import br.com.cybercore.signer.core.validation.SignatureValidationService;
|
||||||
|
import br.com.cybercore.signer.core.validation.ValidationResult;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
|
|||||||
private CertificateManager certificateManager;
|
private CertificateManager certificateManager;
|
||||||
private PdfSigningService pdfSigningService;
|
private PdfSigningService pdfSigningService;
|
||||||
private String testCertId;
|
private String testCertId;
|
||||||
|
private String testCert2Id;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
|
|||||||
char[] pwd = "password123".toCharArray();
|
char[] pwd = "password123".toCharArray();
|
||||||
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
|
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
|
||||||
|
|
||||||
|
// Gera segundo par de chaves e certificado para co-assinatura
|
||||||
|
KeyPair kp2 = kpg.generateKeyPair();
|
||||||
|
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
|
||||||
|
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
|
||||||
|
subject2,
|
||||||
|
BigInteger.valueOf(System.currentTimeMillis() + 100),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject2,
|
||||||
|
kp2.getPublic()
|
||||||
|
);
|
||||||
|
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
|
||||||
|
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
|
||||||
|
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
|
||||||
|
|
||||||
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
|
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
|
||||||
provider.setKeyStore(ks, pwd);
|
provider.setKeyStore(ks, pwd);
|
||||||
certificateManager.registerProvider(provider);
|
certificateManager.registerProvider(provider);
|
||||||
|
|
||||||
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
||||||
assertThat(list).isNotEmpty();
|
assertThat(list).hasSize(2);
|
||||||
this.testCertId = list.get(0).id();
|
this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
|
||||||
|
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
|
||||||
|
|
||||||
this.pdfSigningService = new PdfSigningService(certificateManager);
|
this.pdfSigningService = new PdfSigningService(certificateManager);
|
||||||
}
|
}
|
||||||
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
|
|||||||
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
||||||
assertThat(pngBytes).isNotEmpty();
|
assertThat(pngBytes).isNotEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
|
||||||
|
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
|
||||||
|
byte[] originalPdf = createSamplePdf();
|
||||||
|
|
||||||
|
// 1ª Assinatura com o primeiro certificado
|
||||||
|
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.reason("Primeira Aprovação")
|
||||||
|
.location("Brasília, DF")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
|
||||||
|
assertThat(firstSignedPdf).isNotNull();
|
||||||
|
|
||||||
|
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
|
||||||
|
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.TOP_LEFT)
|
||||||
|
.reason("Segunda Aprovação Conjunta")
|
||||||
|
.location("São Paulo, SP")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
|
||||||
|
assertThat(doubleSignedPdf).isNotNull();
|
||||||
|
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
|
||||||
|
|
||||||
|
// Validação estrutural das assinaturas no PDDocument
|
||||||
|
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
|
||||||
|
List<PDSignature> signatures = doc.getSignatureDictionaries();
|
||||||
|
assertThat(signatures).hasSize(2);
|
||||||
|
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
|
||||||
|
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
|
||||||
|
assertThat(signatures.get(1).getName()).contains("Maria Souza");
|
||||||
|
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
|
||||||
|
SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
|
||||||
|
|
||||||
|
assertThat(validationResult.isValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers()).hasSize(2);
|
||||||
|
|
||||||
|
// Ambas as assinaturas e digests devem ser 100% válidos
|
||||||
|
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
|
||||||
|
|
||||||
|
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,43 @@ import br.com.cybercore.signer.core.provider.CertificateProvider;
|
|||||||
import br.com.cybercore.signer.core.provider.ProviderType;
|
import br.com.cybercore.signer.core.provider.ProviderType;
|
||||||
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
import br.com.cybercore.signer.core.security.ApplicationRegistry;
|
||||||
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
import br.com.cybercore.signer.core.security.AuthorizedApplication;
|
||||||
|
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
|
||||||
|
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.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -49,6 +70,28 @@ class CcspServerTest {
|
|||||||
SigningService signingService = mock(SigningService.class);
|
SigningService signingService = mock(SigningService.class);
|
||||||
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
|
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
|
||||||
|
|
||||||
|
// Gera par de chaves e certificado real em memória para suporte a PDF e XML
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
KeyPair kp = kpg.generateKeyPair();
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Date notBefore = new Date(now - 100000);
|
||||||
|
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
|
||||||
|
|
||||||
|
X500Name subject = new X500Name("CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR");
|
||||||
|
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||||
|
subject,
|
||||||
|
BigInteger.valueOf(System.currentTimeMillis()),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject,
|
||||||
|
kp.getPublic()
|
||||||
|
);
|
||||||
|
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
|
||||||
|
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
|
||||||
|
|
||||||
// Registra uma aplicação de teste
|
// Registra uma aplicação de teste
|
||||||
appRegistry.register(new AuthorizedApplication(
|
appRegistry.register(new AuthorizedApplication(
|
||||||
"app-test",
|
"app-test",
|
||||||
@@ -64,8 +107,8 @@ class CcspServerTest {
|
|||||||
"AliasTeste",
|
"AliasTeste",
|
||||||
CertificateType.A1,
|
CertificateType.A1,
|
||||||
CertificateStatus.AVAILABLE,
|
CertificateStatus.AVAILABLE,
|
||||||
"CN=Teste ICP-Brasil, C=BR",
|
"CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
|
||||||
"CN=AC Raiz, C=BR",
|
"CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
|
||||||
"12345678",
|
"12345678",
|
||||||
"a1b2c3",
|
"a1b2c3",
|
||||||
"d4e5f6",
|
"d4e5f6",
|
||||||
@@ -89,6 +132,9 @@ class CcspServerTest {
|
|||||||
@Override public SigningResult sign(SigningContext context) {
|
@Override public SigningResult sign(SigningContext context) {
|
||||||
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
|
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
|
||||||
}
|
}
|
||||||
|
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
|
||||||
|
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
certManager.registerProvider(dummyProvider);
|
certManager.registerProvider(dummyProvider);
|
||||||
@@ -149,7 +195,7 @@ class CcspServerTest {
|
|||||||
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
assertThat(resp.statusCode()).isEqualTo(200);
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
assertThat(resp.body()).contains("\"status\":\"success\"");
|
assertThat(resp.body()).contains("\"status\":\"success\"");
|
||||||
assertThat(resp.body()).contains("CN=Teste ICP-Brasil, C=BR");
|
assertThat(resp.body()).contains("Teste ICP-Brasil");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -320,4 +366,196 @@ class CcspServerTest {
|
|||||||
assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.pdf\"");
|
assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.pdf\"");
|
||||||
assertThat(response.body()).contains("\"document_type\":\"PDF\"");
|
assertThat(response.body()).contains("\"document_type\":\"PDF\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] createSamplePdf() throws Exception {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage page = new PDPage();
|
||||||
|
doc.addPage(page);
|
||||||
|
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 12);
|
||||||
|
cs.newLineAtOffset(50, 700);
|
||||||
|
cs.showText("PDF de Teste CCSP - CyberCore Signer");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
doc.save(baos);
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /seals deve listar temas de selos visuais disponíveis")
|
||||||
|
void testGetSealsThemes() throws Exception {
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/seals?application_id=app-test"))
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(resp.body()).contains("\"status\":\"success\"");
|
||||||
|
assertThat(resp.body()).contains("\"seals\":");
|
||||||
|
assertThat(resp.body()).contains("icp-brasil");
|
||||||
|
assertThat(resp.body()).contains("gov-br");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign-pdf deve assinar documento PDF em Base64 e retornar signed_pdf válido")
|
||||||
|
void testSignPdfSuccess() throws Exception {
|
||||||
|
byte[] pdfBytes = createSamplePdf();
|
||||||
|
String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign-pdf",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"pdf": "%s",
|
||||||
|
"page": 1,
|
||||||
|
"x": 60.0,
|
||||||
|
"y": 60.0,
|
||||||
|
"scale": 1.0,
|
||||||
|
"seal_theme_id": "icp-brasil",
|
||||||
|
"reason": "Aprovado e assinado via CCSP",
|
||||||
|
"location": "Curitiba, PR",
|
||||||
|
"nonce": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), pdfBase64, UUID.randomUUID());
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign-pdf"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(resp.body()).contains("\"status\":\"signed\"");
|
||||||
|
assertThat(resp.body()).contains("\"signed_pdf\":");
|
||||||
|
assertThat(resp.body()).contains("\"sha256\":");
|
||||||
|
|
||||||
|
// Verifica que o PDF assinado retornado pode ser carregado pelo PDFBox
|
||||||
|
int signedPdfIdx = resp.body().indexOf("\"signed_pdf\":\"") + 14;
|
||||||
|
int endIdx = resp.body().indexOf("\"", signedPdfIdx);
|
||||||
|
String signedPdfBase64 = resp.body().substring(signedPdfIdx, endIdx);
|
||||||
|
|
||||||
|
byte[] signedBytes = Base64.getDecoder().decode(signedPdfBase64);
|
||||||
|
try (PDDocument signedDoc = Loader.loadPDF(signedBytes)) {
|
||||||
|
assertThat(signedDoc.getNumberOfPages()).isEqualTo(1);
|
||||||
|
assertThat(signedDoc.getSignatureDictionaries()).isNotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign-pdf com seal_custom inline deve assinar com sucesso")
|
||||||
|
void testSignPdfCustomSealSuccess() throws Exception {
|
||||||
|
byte[] pdfBytes = createSamplePdf();
|
||||||
|
String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign-pdf",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"pdf": "%s",
|
||||||
|
"page": 1,
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"scale": 0.9,
|
||||||
|
"seal_custom": {
|
||||||
|
"name": "Custom Seal Test",
|
||||||
|
"title": "SELADO DIGITALMENTE",
|
||||||
|
"primary_color": "#2c3e50",
|
||||||
|
"accent_color": "#18bc9c",
|
||||||
|
"show_timestamp": true,
|
||||||
|
"show_document_hash": true
|
||||||
|
},
|
||||||
|
"nonce": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), pdfBase64, UUID.randomUUID());
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign-pdf"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(resp.body()).contains("\"status\":\"signed\"");
|
||||||
|
assertThat(resp.body()).contains("\"signed_pdf\":");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign-xml deve assinar XML plano com perfil XMLDSig")
|
||||||
|
void testSignXmlSuccess() throws Exception {
|
||||||
|
String xmlContent = "<documento id=\"doc-123\"><dados>Contrato de Teste</dados></documento>";
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign-xml",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"xml": "%s",
|
||||||
|
"profile": "XMLDSIG",
|
||||||
|
"nonce": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), xmlContent.replace("\"", "\\\""), UUID.randomUUID());
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign-xml"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(resp.body()).contains("\"status\":\"signed\"");
|
||||||
|
assertThat(resp.body()).contains("\"signed_xml\":");
|
||||||
|
assertThat(resp.body()).contains("SignatureValue");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Endpoint /sign-xml deve assinar XML em Base64 com perfil XAdES-BES")
|
||||||
|
void testSignXmlBase64XadesBes() throws Exception {
|
||||||
|
String xmlContent = "<pedido id=\"ped-999\"><valor>150.00</valor></pedido>";
|
||||||
|
String xmlBase64 = Base64.getEncoder().encodeToString(xmlContent.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
String jsonPayload = """
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"request_id": "%s",
|
||||||
|
"application_id": "app-test",
|
||||||
|
"operation": "sign-xml",
|
||||||
|
"certificate_hint": "cert-1",
|
||||||
|
"xml_base64": "%s",
|
||||||
|
"profile": "XADES_BES",
|
||||||
|
"nonce": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(UUID.randomUUID(), xmlBase64, UUID.randomUUID());
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + "/ccsp/v1/sign-xml"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Origin", "https://meusistema.com.br")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertThat(resp.statusCode()).isEqualTo(200);
|
||||||
|
assertThat(resp.body()).contains("\"status\":\"signed\"");
|
||||||
|
assertThat(resp.body()).contains("\"signed_xml\":");
|
||||||
|
assertThat(resp.body()).contains("QualifyingProperties");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ class XmlSigningServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso")
|
@DisplayName("Deve assinar documento XML no padrão XAdES-BES por padrão e validar com sucesso")
|
||||||
void shouldSignWholeXmlDocumentAndValidate() throws Exception {
|
void shouldSignWholeXmlDocumentWithXadesBesByDefaultAndValidate() throws Exception {
|
||||||
String xml = """
|
String xml = """
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<pedido id="ped-1001">
|
<pedido id="ped-1001">
|
||||||
@@ -86,19 +86,38 @@ class XmlSigningServiceTest {
|
|||||||
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
|
|
||||||
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Verifica tags essenciais do XMLDSig
|
||||||
assertThat(signedXmlStr).contains("<Signature");
|
assertThat(signedXmlStr).contains("<Signature");
|
||||||
assertThat(signedXmlStr).contains("<SignedInfo");
|
assertThat(signedXmlStr).contains("<SignedInfo");
|
||||||
assertThat(signedXmlStr).contains("<SignatureValue");
|
assertThat(signedXmlStr).contains("<SignatureValue");
|
||||||
assertThat(signedXmlStr).contains("<KeyInfo");
|
assertThat(signedXmlStr).contains("<KeyInfo");
|
||||||
|
|
||||||
// Validação criptográfica
|
// Verifica propriedades qualificadas do XAdES-BES (ETSI TS 101 903 / ICP-Brasil)
|
||||||
|
assertThat(signedXmlStr).contains("QualifyingProperties");
|
||||||
|
assertThat(signedXmlStr).contains("SignedProperties");
|
||||||
|
assertThat(signedXmlStr).contains("SignedSignatureProperties");
|
||||||
|
assertThat(signedXmlStr).contains("SigningTime");
|
||||||
|
assertThat(signedXmlStr).contains("SigningCertificate");
|
||||||
|
assertThat(signedXmlStr).contains("CertDigest");
|
||||||
|
assertThat(signedXmlStr).contains("IssuerSerial");
|
||||||
|
assertThat(signedXmlStr).contains("Type=\"http://uri.etsi.org/01903#SignedProperties\"");
|
||||||
|
|
||||||
|
// Validação criptográfica do documento e das propriedades XAdES
|
||||||
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
|
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
|
||||||
assertThat(isValid).isTrue();
|
assertThat(isValid).isTrue();
|
||||||
|
|
||||||
|
// Inspeção detalhada
|
||||||
|
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
|
||||||
|
assertThat(details).isNotNull();
|
||||||
|
assertThat(details.valid()).isTrue();
|
||||||
|
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XADES_BES);
|
||||||
|
assertThat(details.signingTime()).isNotBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Deve assinar nó XML específico com atributo Id (estilo NF-e) e validar")
|
@DisplayName("Deve assinar nó XML específico com atributo Id no padrão XAdES-BES e validar")
|
||||||
void shouldSignSpecificXmlElementById() throws Exception {
|
void shouldSignSpecificXmlElementWithXadesBes() throws Exception {
|
||||||
String nfeXml = """
|
String nfeXml = """
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
|
<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
|
||||||
@@ -117,21 +136,48 @@ class XmlSigningServiceTest {
|
|||||||
testCertId,
|
testCertId,
|
||||||
null,
|
null,
|
||||||
"infNFe",
|
"infNFe",
|
||||||
"NFe35261012345678000195550010000000011000000010"
|
"NFe35261012345678000195550010000000011000000010",
|
||||||
|
XmlSignatureProfile.XADES_BES
|
||||||
);
|
);
|
||||||
|
|
||||||
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
|
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
|
String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
|
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
|
||||||
|
assertThat(signedStr).contains("QualifyingProperties");
|
||||||
|
assertThat(signedStr).contains("SignedProperties");
|
||||||
|
|
||||||
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
|
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
|
||||||
assertThat(isValid).isTrue();
|
assertThat(isValid).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação")
|
@DisplayName("Deve assinar documento XML no perfil XMLDSig clássico quando explicitamente solicitado")
|
||||||
void shouldDetectTamperingInSignedXml() throws Exception {
|
void shouldSignWithClassicXmlDsigWhenConfigured() throws Exception {
|
||||||
|
String xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<documento>
|
||||||
|
<campo>Valor de teste</campo>
|
||||||
|
</documento>
|
||||||
|
""";
|
||||||
|
|
||||||
|
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null, XmlSignatureProfile.XMLDSIG);
|
||||||
|
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
|
|
||||||
|
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
||||||
|
assertThat(signedXmlStr).contains("<Signature");
|
||||||
|
assertThat(signedXmlStr).doesNotContain("QualifyingProperties");
|
||||||
|
|
||||||
|
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
|
||||||
|
assertThat(isValid).isTrue();
|
||||||
|
|
||||||
|
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
|
||||||
|
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XMLDSIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve detectar adulteração no conteúdo do XML assinado com XAdES")
|
||||||
|
void shouldDetectTamperingInSignedXmlContent() throws Exception {
|
||||||
String xml = """
|
String xml = """
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<contrato>
|
<contrato>
|
||||||
@@ -142,11 +188,36 @@ class XmlSigningServiceTest {
|
|||||||
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
|
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
|
||||||
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
|
|
||||||
// Adulteração intencional do conteúdo original
|
// Adulteração no payload de negócio
|
||||||
String tampered = new String(signedXml, StandardCharsets.UTF_8)
|
String tampered = new String(signedXml, StandardCharsets.UTF_8)
|
||||||
.replace("R$ 100,00", "R$ 999.999,00");
|
.replace("R$ 100,00", "R$ 999.999,00");
|
||||||
|
|
||||||
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
|
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
|
||||||
assertThat(isValid).isFalse();
|
assertThat(isValid).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve detectar adulteração nas propriedades qualificadas do XAdES (SigningTime)")
|
||||||
|
void shouldDetectTamperingInXadesSignedProperties() throws Exception {
|
||||||
|
String xml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<contrato>
|
||||||
|
<clausula>Assinado conforme regras XAdES</clausula>
|
||||||
|
</contrato>
|
||||||
|
""";
|
||||||
|
|
||||||
|
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
|
||||||
|
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
||||||
|
|
||||||
|
String signedStr = new String(signedXml, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Adulteração fraudulenta da data da assinatura no nó XAdES
|
||||||
|
String tampered = signedStr.replaceFirst(
|
||||||
|
"<xades:SigningTime>[^<]+</xades:SigningTime>",
|
||||||
|
"<xades:SigningTime>1999-01-01T00:00:00Z</xades:SigningTime>"
|
||||||
|
);
|
||||||
|
|
||||||
|
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
|
||||||
|
assertThat(isValid).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
|
|
||||||
// Inicializa serviços centrais reais da plataforma
|
// Inicializa serviços centrais reais da plataforma
|
||||||
this.certificateManager = CertificateManager.createDefault();
|
this.certificateManager = CertificateManager.createDefault();
|
||||||
|
this.signingService = new SigningService(this.certificateManager);
|
||||||
|
|
||||||
if (developmentMode) {
|
if (developmentMode) {
|
||||||
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
||||||
@@ -424,6 +425,9 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
byte[] digestBytes;
|
byte[] digestBytes;
|
||||||
if (request.digest() != null && !request.digest().isBlank()) {
|
if (request.digest() != null && !request.digest().isBlank()) {
|
||||||
digestBytes = Base64.getDecoder().decode(request.digest());
|
digestBytes = Base64.getDecoder().decode(request.digest());
|
||||||
|
} else if (request.pdf() != null && !request.pdf().isBlank()) {
|
||||||
|
byte[] rawPdf = Base64.getDecoder().decode(request.pdf());
|
||||||
|
digestBytes = MessageDigest.getInstance("SHA-256").digest(rawPdf);
|
||||||
} else if (request.challenge() != null && !request.challenge().isBlank()) {
|
} else if (request.challenge() != null && !request.challenge().isBlank()) {
|
||||||
digestBytes = MessageDigest.getInstance("SHA-256")
|
digestBytes = MessageDigest.getInstance("SHA-256")
|
||||||
.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
|
.digest(request.challenge().getBytes(StandardCharsets.UTF_8));
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import javafx.scene.Group;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.paint.CycleMethod;
|
||||||
|
import javafx.scene.paint.LinearGradient;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.paint.RadialGradient;
|
||||||
|
import javafx.scene.paint.Stop;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.scene.shape.Line;
|
import javafx.scene.shape.Line;
|
||||||
import javafx.scene.shape.Polygon;
|
import javafx.scene.shape.Polygon;
|
||||||
@@ -17,7 +23,6 @@ import javafx.scene.shape.Shape;
|
|||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.FontWeight;
|
import javafx.scene.text.FontWeight;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.transform.Scale;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
@@ -26,19 +31,20 @@ import org.w3c.dom.NodeList;
|
|||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gerenciador centralizado de recursos visuais (ícones e imagens).
|
* Gerenciador centralizado de recursos visuais (ícones e imagens).
|
||||||
* Permite que o usuário customize ou substitua facilmente qualquer ícone (SVG/PNG) ou imagem (PNG/JPG)
|
* Permite que o usuário customize ou substitua facilmente qualquer ícone ou imagem (SVG/PNG)
|
||||||
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
|
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
|
||||||
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
|
* na raiz do projeto ou no diretório ~/.cybercore/assets/.
|
||||||
*/
|
*/
|
||||||
@@ -121,7 +127,7 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retorna um Node de ícone customizado (SVG ou PNG) ou invoca o fallback caso não exista imagem customizada.
|
* Retorna um Node de ícone customizado (prioritariamente SVG, com fallback para PNG ou supplier vetorial).
|
||||||
*/
|
*/
|
||||||
public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) {
|
public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) {
|
||||||
if (iconName != null && !iconName.isBlank()) {
|
if (iconName != null && !iconName.isBlank()) {
|
||||||
@@ -140,7 +146,20 @@ public final class AssetManager {
|
|||||||
return userSvg.get();
|
return userSvg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Se não houver SVG, tenta imagem bitmap (.png, .jpg) nas pastas externas
|
// 3. Procura SVG no Classpath interno
|
||||||
|
String resSvgPath = "/assets/icons/" + cleanName + ".svg";
|
||||||
|
InputStream resStream = AssetManager.class.getResourceAsStream(resSvgPath);
|
||||||
|
if (resStream != null) {
|
||||||
|
try (resStream) {
|
||||||
|
Node node = parseSvgToNode(resStream, size, size);
|
||||||
|
if (node != null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Se não houver SVG, tenta imagem bitmap (.png, .jpg)
|
||||||
Optional<Image> customImg = loadImage("icons", cleanName, size, size);
|
Optional<Image> customImg = loadImage("icons", cleanName, size, size);
|
||||||
if (customImg.isPresent() && !customImg.get().isError()) {
|
if (customImg.isPresent() && !customImg.get().isError()) {
|
||||||
ImageView iv = new ImageView(customImg.get());
|
ImageView iv = new ImageView(customImg.get());
|
||||||
@@ -155,28 +174,76 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retorna a imagem do logotipo do sistema ou o fallback vetorial.
|
* Retorna uma imagem institucional ou elemento gráfico (prioritariamente SVG, com fallback para PNG ou supplier).
|
||||||
*/
|
*/
|
||||||
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
|
public static Node getImageOrDefault(String imageName, double width, double height, Supplier<Node> fallbackSupplier) {
|
||||||
// Tenta carregar app-logo bitmap (.png) primeiro
|
if (imageName != null && !imageName.isBlank()) {
|
||||||
Optional<Image> customLogo = loadImage("images", "app-logo", width, height);
|
String cleanName = cleanBaseName(imageName);
|
||||||
if (customLogo.isPresent() && !customLogo.get().isError()) {
|
|
||||||
ImageView iv = new ImageView(customLogo.get());
|
// 1. Prioridade para SVG em ./assets/images/
|
||||||
|
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
|
||||||
|
if (Files.isRegularFile(localSvg)) {
|
||||||
|
try (InputStream in = new FileInputStream(localSvg.toFile())) {
|
||||||
|
Node node = parseSvgToNode(in, width, height);
|
||||||
|
if (node != null) {
|
||||||
|
logger.debug("Carregada imagem SVG local: {}", localSvg);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Procura SVG em ~/.cybercore/assets/images/
|
||||||
|
String userHome = System.getProperty("user.home", ".");
|
||||||
|
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
|
||||||
|
if (Files.isRegularFile(userSvg)) {
|
||||||
|
try (InputStream in = new FileInputStream(userSvg.toFile())) {
|
||||||
|
Node node = parseSvgToNode(in, width, height);
|
||||||
|
if (node != null) {
|
||||||
|
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Procura SVG no Classpath interno
|
||||||
|
String resPath = "/assets/images/" + cleanName + ".svg";
|
||||||
|
InputStream resStream = AssetManager.class.getResourceAsStream(resPath);
|
||||||
|
if (resStream != null) {
|
||||||
|
try (resStream) {
|
||||||
|
Node node = parseSvgToNode(resStream, width, height);
|
||||||
|
if (node != null) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Falha ao ler recurso interno SVG {}: {}", resPath, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback para imagem bitmap (.png, .jpg)
|
||||||
|
Optional<Image> customImg = loadImage("images", cleanName, width, height);
|
||||||
|
if (customImg.isPresent() && !customImg.get().isError()) {
|
||||||
|
ImageView iv = new ImageView(customImg.get());
|
||||||
iv.setFitWidth(width);
|
iv.setFitWidth(width);
|
||||||
iv.setFitHeight(height);
|
iv.setFitHeight(height);
|
||||||
iv.setPreserveRatio(true);
|
iv.setPreserveRatio(true);
|
||||||
return iv;
|
return iv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenta carregar SVG de app-logo se existir
|
|
||||||
Optional<Node> svgLogo = loadSvgIconFromPath(Path.of("assets", "images", "app-logo.svg"), height);
|
|
||||||
if (svgLogo.isPresent()) {
|
|
||||||
return svgLogo.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
|
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna a imagem do logotipo do sistema (SVG ou bitmap).
|
||||||
|
*/
|
||||||
|
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
|
||||||
|
return getImageOrDefault("app-logo", width, height, fallbackSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
private static String cleanBaseName(String name) {
|
private static String cleanBaseName(String name) {
|
||||||
String clean = name;
|
String clean = name;
|
||||||
int dotIdx = clean.lastIndexOf('.');
|
int dotIdx = clean.lastIndexOf('.');
|
||||||
@@ -189,7 +256,7 @@ public final class AssetManager {
|
|||||||
private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
|
private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
|
||||||
if (Files.isRegularFile(path)) {
|
if (Files.isRegularFile(path)) {
|
||||||
try (InputStream in = new FileInputStream(path.toFile())) {
|
try (InputStream in = new FileInputStream(path.toFile())) {
|
||||||
Node node = parseSvgToNode(in, size);
|
Node node = parseSvgToNode(in, size, size);
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
logger.debug("Carregado ícone SVG customizado de: {}", path);
|
logger.debug("Carregado ícone SVG customizado de: {}", path);
|
||||||
return Optional.of(node);
|
return Optional.of(node);
|
||||||
@@ -202,9 +269,16 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser analítico leve de SVG padrão para nós gráficos JavaFX com preservação geométrica exata.
|
* Parser analítico de SVG para nós gráficos JavaFX com suporte a dimensões quadradas.
|
||||||
*/
|
*/
|
||||||
public static Node parseSvgToNode(InputStream in, double targetSize) {
|
public static Node parseSvgToNode(InputStream in, double targetSize) {
|
||||||
|
return parseSvgToNode(in, targetSize, targetSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser analítico completo de SVG para nós gráficos JavaFX com dimensões proporcionais e suporte a defs/gradientes.
|
||||||
|
*/
|
||||||
|
public static Node parseSvgToNode(InputStream in, double targetWidth, double targetHeight) {
|
||||||
try {
|
try {
|
||||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
dbf.setNamespaceAware(false);
|
dbf.setNamespaceAware(false);
|
||||||
@@ -249,27 +323,34 @@ public final class AssetManager {
|
|||||||
if (vbWidth <= 0) vbWidth = 48;
|
if (vbWidth <= 0) vbWidth = 48;
|
||||||
if (vbHeight <= 0) vbHeight = 48;
|
if (vbHeight <= 0) vbHeight = 48;
|
||||||
|
|
||||||
Group contentGroup = new Group();
|
Map<String, Paint> defsMap = new HashMap<>();
|
||||||
renderChildren(svg, contentGroup);
|
parseDefs(svg, defsMap);
|
||||||
|
|
||||||
double maxDim = Math.max(vbWidth, vbHeight);
|
Group contentGroup = new Group();
|
||||||
double scale = (targetSize * 0.90) / maxDim;
|
renderChildren(svg, contentGroup, defsMap);
|
||||||
|
|
||||||
|
double scaleX = targetWidth / vbWidth;
|
||||||
|
double scaleY = targetHeight / vbHeight;
|
||||||
|
double scale = Math.min(scaleX, scaleY);
|
||||||
|
if (scale <= 0) scale = 1.0;
|
||||||
|
|
||||||
double drawnW = vbWidth * scale;
|
double drawnW = vbWidth * scale;
|
||||||
double drawnH = vbHeight * scale;
|
double drawnH = vbHeight * scale;
|
||||||
double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale);
|
double tx = (targetWidth - drawnW) / 2.0 - (vbMinX * scale);
|
||||||
double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale);
|
double ty = (targetHeight - drawnH) / 2.0 - (vbMinY * scale);
|
||||||
|
|
||||||
|
contentGroup.setManaged(false);
|
||||||
|
contentGroup.setLayoutX(0);
|
||||||
|
contentGroup.setLayoutY(0);
|
||||||
contentGroup.getTransforms().setAll(
|
contentGroup.getTransforms().setAll(
|
||||||
new javafx.scene.transform.Translate(tx, ty),
|
new javafx.scene.transform.Translate(tx, ty),
|
||||||
new javafx.scene.transform.Scale(scale, scale, 0, 0)
|
new javafx.scene.transform.Scale(scale, scale, 0, 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
StackPane container = new StackPane(contentGroup);
|
Pane container = new Pane(contentGroup);
|
||||||
container.setPrefSize(targetSize, targetSize);
|
container.setPrefSize(targetWidth, targetHeight);
|
||||||
container.setMaxSize(targetSize, targetSize);
|
container.setMaxSize(targetWidth, targetHeight);
|
||||||
container.setMinSize(targetSize, targetSize);
|
container.setMinSize(targetWidth, targetHeight);
|
||||||
container.setAlignment(Pos.CENTER);
|
|
||||||
return container;
|
return container;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -278,12 +359,100 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void renderChildren(Element parent, Group targetGroup) {
|
private static void parseDefs(Element root, Map<String, Paint> defsMap) {
|
||||||
|
NodeList defsList = root.getElementsByTagName("defs");
|
||||||
|
for (int i = 0; i < defsList.getLength(); i++) {
|
||||||
|
org.w3c.dom.Node defNode = defsList.item(i);
|
||||||
|
if (defNode instanceof Element defsEl) {
|
||||||
|
NodeList children = defsEl.getChildNodes();
|
||||||
|
for (int j = 0; j < children.getLength(); j++) {
|
||||||
|
org.w3c.dom.Node c = children.item(j);
|
||||||
|
if (c instanceof Element gradEl) {
|
||||||
|
String id = gradEl.getAttribute("id");
|
||||||
|
if (!id.isBlank()) {
|
||||||
|
Paint paint = createPaintFromGradient(gradEl);
|
||||||
|
if (paint != null) {
|
||||||
|
defsMap.put(id, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Paint createPaintFromGradient(Element gradEl) {
|
||||||
|
String tag = gradEl.getTagName().toLowerCase();
|
||||||
|
List<Stop> stops = new ArrayList<>();
|
||||||
|
NodeList stopNodes = gradEl.getElementsByTagName("stop");
|
||||||
|
for (int i = 0; i < stopNodes.getLength(); i++) {
|
||||||
|
org.w3c.dom.Node s = stopNodes.item(i);
|
||||||
|
if (s instanceof Element stopEl) {
|
||||||
|
double offset = parseOffset(stopEl.getAttribute("offset"));
|
||||||
|
String colorStr = stopEl.getAttribute("stop-color");
|
||||||
|
double opacity = parseDouble(stopEl.getAttribute("stop-opacity"), 1.0);
|
||||||
|
Color baseColor = parseColor(colorStr, Color.BLACK);
|
||||||
|
Color finalColor = new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), baseColor.getOpacity() * opacity);
|
||||||
|
stops.add(new Stop(offset, finalColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stops.isEmpty()) {
|
||||||
|
stops.add(new Stop(0, Color.web("#0288d1")));
|
||||||
|
stops.add(new Stop(1, Color.web("#01579b")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("lineargradient".equals(tag)) {
|
||||||
|
double x1 = parseCoordinate(gradEl.getAttribute("x1"), 0);
|
||||||
|
double y1 = parseCoordinate(gradEl.getAttribute("y1"), 0);
|
||||||
|
double x2 = parseCoordinate(gradEl.getAttribute("x2"), 1);
|
||||||
|
double y2 = parseCoordinate(gradEl.getAttribute("y2"), 1);
|
||||||
|
return new LinearGradient(x1, y1, x2, y2, true, CycleMethod.NO_CYCLE, stops);
|
||||||
|
} else if ("radialgradient".equals(tag)) {
|
||||||
|
double cx = parseCoordinate(gradEl.getAttribute("cx"), 0.5);
|
||||||
|
double cy = parseCoordinate(gradEl.getAttribute("cy"), 0.5);
|
||||||
|
double r = parseCoordinate(gradEl.getAttribute("r"), 0.5);
|
||||||
|
return new RadialGradient(0, 0, cx, cy, r, true, CycleMethod.NO_CYCLE, stops);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double parseOffset(String str) {
|
||||||
|
if (str == null || str.isBlank()) return 0;
|
||||||
|
String s = str.trim();
|
||||||
|
try {
|
||||||
|
if (s.endsWith("%")) {
|
||||||
|
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(s);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double parseCoordinate(String str, double def) {
|
||||||
|
if (str == null || str.isBlank()) return def;
|
||||||
|
String s = str.trim();
|
||||||
|
try {
|
||||||
|
if (s.endsWith("%")) {
|
||||||
|
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(s);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderChildren(Element parent, Group targetGroup, Map<String, Paint> defsMap) {
|
||||||
NodeList children = parent.getChildNodes();
|
NodeList children = parent.getChildNodes();
|
||||||
for (int i = 0; i < children.getLength(); i++) {
|
for (int i = 0; i < children.getLength(); i++) {
|
||||||
org.w3c.dom.Node n = children.item(i);
|
org.w3c.dom.Node n = children.item(i);
|
||||||
if (n instanceof Element el) {
|
if (n instanceof Element el) {
|
||||||
Node jfxNode = createElementNode(el);
|
if ("defs".equalsIgnoreCase(el.getTagName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Node jfxNode = createElementNode(el, defsMap);
|
||||||
if (jfxNode != null) {
|
if (jfxNode != null) {
|
||||||
targetGroup.getChildren().add(jfxNode);
|
targetGroup.getChildren().add(jfxNode);
|
||||||
}
|
}
|
||||||
@@ -291,7 +460,7 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Node createElementNode(Element el) {
|
private static Node createElementNode(Element el, Map<String, Paint> defsMap) {
|
||||||
String tagName = el.getTagName().toLowerCase();
|
String tagName = el.getTagName().toLowerCase();
|
||||||
Shape shape = null;
|
Shape shape = null;
|
||||||
|
|
||||||
@@ -350,12 +519,12 @@ public final class AssetManager {
|
|||||||
double x = parseDouble(el.getAttribute("x"), 0);
|
double x = parseDouble(el.getAttribute("x"), 0);
|
||||||
double y = parseDouble(el.getAttribute("y"), 0);
|
double y = parseDouble(el.getAttribute("y"), 0);
|
||||||
String content = el.getTextContent();
|
String content = el.getTextContent();
|
||||||
Text t = new Text(x, y, content);
|
Text t = new Text(x, y, content != null ? content.trim() : "");
|
||||||
double fs = parseDouble(el.getAttribute("font-size"), 10);
|
double fs = parseDouble(el.getAttribute("font-size"), 12);
|
||||||
String fw = el.getAttribute("font-weight");
|
String fw = el.getAttribute("font-weight");
|
||||||
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
|
||||||
if (el.hasAttribute("fill")) {
|
if (el.hasAttribute("fill")) {
|
||||||
t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK));
|
t.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.BLACK));
|
||||||
}
|
}
|
||||||
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
|
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
|
||||||
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
t.setTextOrigin(javafx.geometry.VPos.CENTER);
|
||||||
@@ -364,38 +533,40 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
case "g" -> {
|
case "g" -> {
|
||||||
Group g = new Group();
|
Group g = new Group();
|
||||||
renderChildren(el, g);
|
renderChildren(el, g, defsMap);
|
||||||
if (el.hasAttribute("stroke")) {
|
if (el.hasAttribute("stroke")) {
|
||||||
Color strokeCol = parseColor(el.getAttribute("stroke"), null);
|
Paint strokePaint = parsePaint(el.getAttribute("stroke"), defsMap, null);
|
||||||
double sw = parseDouble(el.getAttribute("stroke-width"), 1.0);
|
double sw = parseDouble(el.getAttribute("stroke-width"), 1.0);
|
||||||
for (Node child : g.getChildren()) {
|
for (Node child : g.getChildren()) {
|
||||||
if (child instanceof Shape s) {
|
if (child instanceof Shape s) {
|
||||||
if (strokeCol != null) s.setStroke(strokeCol);
|
if (strokePaint != null) s.setStroke(strokePaint);
|
||||||
s.setStrokeWidth(sw);
|
s.setStrokeWidth(sw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyElementTransform(el, g);
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shape != null) {
|
if (shape != null) {
|
||||||
applyShapeStyles(el, shape);
|
applyShapeStyles(el, shape, defsMap);
|
||||||
|
applyElementTransform(el, shape);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyShapeStyles(Element el, Shape shape) {
|
private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
|
||||||
if (el.hasAttribute("fill")) {
|
if (el.hasAttribute("fill")) {
|
||||||
shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT));
|
shape.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.TRANSPARENT));
|
||||||
} else if (shape instanceof Line) {
|
} else if (shape instanceof Line) {
|
||||||
shape.setFill(Color.TRANSPARENT);
|
shape.setFill(Color.TRANSPARENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke")) {
|
if (el.hasAttribute("stroke")) {
|
||||||
shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT));
|
shape.setStroke(parsePaint(el.getAttribute("stroke"), defsMap, Color.TRANSPARENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.hasAttribute("stroke-width")) {
|
if (el.hasAttribute("stroke-width")) {
|
||||||
@@ -409,6 +580,22 @@ public final class AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Paint parsePaint(String str, Map<String, Paint> defsMap, Paint fallback) {
|
||||||
|
if (str == null || str.isBlank() || "none".equalsIgnoreCase(str)) {
|
||||||
|
return Color.TRANSPARENT;
|
||||||
|
}
|
||||||
|
String trimmed = str.trim();
|
||||||
|
if (trimmed.startsWith("url(#") && trimmed.endsWith(")")) {
|
||||||
|
String id = trimmed.substring(5, trimmed.length() - 1).trim();
|
||||||
|
Paint defPaint = defsMap.get(id);
|
||||||
|
if (defPaint != null) {
|
||||||
|
return defPaint;
|
||||||
|
}
|
||||||
|
return Color.web("#0288d1");
|
||||||
|
}
|
||||||
|
return parseColor(trimmed, fallback instanceof Color ? (Color) fallback : Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) {
|
private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) {
|
||||||
String[] tokens = ptsStr.trim().split("[,\\s]+");
|
String[] tokens = ptsStr.trim().split("[,\\s]+");
|
||||||
for (String t : tokens) {
|
for (String t : tokens) {
|
||||||
@@ -446,4 +633,52 @@ public final class AssetManager {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyElementTransform(Element el, Node node) {
|
||||||
|
if (!el.hasAttribute("transform")) return;
|
||||||
|
String tr = el.getAttribute("transform").trim();
|
||||||
|
if (tr.isBlank()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (tr.contains("rotate(")) {
|
||||||
|
int start = tr.indexOf("rotate(") + 7;
|
||||||
|
int close = tr.indexOf(')', start);
|
||||||
|
if (close > 0) {
|
||||||
|
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
|
||||||
|
if (args.length >= 3) {
|
||||||
|
double deg = parseDouble(args[0], 0);
|
||||||
|
double px = parseDouble(args[1], 0);
|
||||||
|
double py = parseDouble(args[2], 0);
|
||||||
|
node.getTransforms().add(new javafx.scene.transform.Rotate(deg, px, py));
|
||||||
|
} else if (args.length >= 1) {
|
||||||
|
double deg = parseDouble(args[0], 0);
|
||||||
|
node.getTransforms().add(new javafx.scene.transform.Rotate(deg));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tr.contains("translate(")) {
|
||||||
|
int start = tr.indexOf("translate(") + 10;
|
||||||
|
int close = tr.indexOf(')', start);
|
||||||
|
if (close > 0) {
|
||||||
|
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
|
||||||
|
double tx = parseDouble(args[0], 0);
|
||||||
|
double ty = args.length > 1 ? parseDouble(args[1], 0) : 0;
|
||||||
|
node.getTransforms().add(new javafx.scene.transform.Translate(tx, ty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tr.contains("scale(")) {
|
||||||
|
int start = tr.indexOf("scale(") + 6;
|
||||||
|
int close = tr.indexOf(')', start);
|
||||||
|
if (close > 0) {
|
||||||
|
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
|
||||||
|
double sx = parseDouble(args[0], 1.0);
|
||||||
|
double sy = args.length > 1 ? parseDouble(args[1], sx) : sx;
|
||||||
|
node.getTransforms().add(new javafx.scene.transform.Scale(sx, sy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,17 +38,16 @@ public final class CertificateCardNode extends VBox {
|
|||||||
HBox topRow = new HBox(10);
|
HBox topRow = new HBox(10);
|
||||||
topRow.setAlignment(Pos.CENTER_LEFT);
|
topRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Label titleLabel = new Label(certificate.commonName() != null ? certificate.commonName() : certificate.alias());
|
String rawName = certificate.commonName() != null ? certificate.commonName() : certificate.alias();
|
||||||
|
String cleanTitle = rawName != null && rawName.contains(":") ? rawName.split(":")[0].trim() : (rawName != null ? rawName.trim() : "Certificado Digital");
|
||||||
|
Label titleLabel = new Label(cleanTitle);
|
||||||
titleLabel.getStyleClass().add("cert-title");
|
titleLabel.getStyleClass().add("cert-title");
|
||||||
HBox.setHgrow(titleLabel, Priority.ALWAYS);
|
HBox.setHgrow(titleLabel, Priority.ALWAYS);
|
||||||
|
|
||||||
Label badgeLabel = new Label();
|
Label badgeLabel = new Label("[" + certificate.type().getCode() + "]");
|
||||||
if (certificate.type() == CertificateType.A1) {
|
if (certificate.type() == CertificateType.A1) {
|
||||||
badgeLabel.setText("A1 • Certificado local");
|
|
||||||
badgeLabel.getStyleClass().add("cert-badge-a1");
|
badgeLabel.getStyleClass().add("cert-badge-a1");
|
||||||
} else {
|
} else {
|
||||||
String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB";
|
|
||||||
badgeLabel.setText("A3 • " + tokenDesc);
|
|
||||||
badgeLabel.getStyleClass().add("cert-badge-a3");
|
badgeLabel.getStyleClass().add("cert-badge-a3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -709,6 +709,10 @@ public final class SvgIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Node closeIcon(double size) {
|
public static Node closeIcon(double size) {
|
||||||
|
return AssetManager.getIconOrDefault("close", size, () -> buildCloseIcon(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildCloseIcon(double size) {
|
||||||
StackPane box = new StackPane();
|
StackPane box = new StackPane();
|
||||||
box.setPrefSize(size, size);
|
box.setPrefSize(size, size);
|
||||||
box.setMaxSize(size, size);
|
box.setMaxSize(size, size);
|
||||||
@@ -738,6 +742,10 @@ public final class SvgIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Node arrowRightIcon(double size, String colorHex) {
|
public static Node arrowRightIcon(double size, String colorHex) {
|
||||||
|
return AssetManager.getIconOrDefault("arrow-right", size, () -> buildArrowRightIcon(size, colorHex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildArrowRightIcon(double size, String colorHex) {
|
||||||
StackPane box = new StackPane();
|
StackPane box = new StackPane();
|
||||||
box.setPrefSize(size, size);
|
box.setPrefSize(size, size);
|
||||||
box.setMaxSize(size, size);
|
box.setMaxSize(size, size);
|
||||||
@@ -764,6 +772,10 @@ public final class SvgIcons {
|
|||||||
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
|
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
|
||||||
*/
|
*/
|
||||||
public static Node zoomInIcon(double size) {
|
public static Node zoomInIcon(double size) {
|
||||||
|
return AssetManager.getIconOrDefault("zoom-in", size, () -> buildZoomInIcon(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildZoomInIcon(double size) {
|
||||||
StackPane box = new StackPane();
|
StackPane box = new StackPane();
|
||||||
box.setPrefSize(size, size);
|
box.setPrefSize(size, size);
|
||||||
box.setMaxSize(size, size);
|
box.setMaxSize(size, size);
|
||||||
@@ -786,6 +798,10 @@ public final class SvgIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Node zoomOutIcon(double size) {
|
public static Node zoomOutIcon(double size) {
|
||||||
|
return AssetManager.getIconOrDefault("zoom-out", size, () -> buildZoomOutIcon(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildZoomOutIcon(double size) {
|
||||||
StackPane box = new StackPane();
|
StackPane box = new StackPane();
|
||||||
box.setPrefSize(size, size);
|
box.setPrefSize(size, size);
|
||||||
box.setMaxSize(size, size);
|
box.setMaxSize(size, size);
|
||||||
@@ -808,6 +824,10 @@ public final class SvgIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Node zoomFitIcon(double size) {
|
public static Node zoomFitIcon(double size) {
|
||||||
|
return AssetManager.getIconOrDefault("zoom-fit", size, () -> buildZoomFitIcon(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildZoomFitIcon(double size) {
|
||||||
StackPane box = new StackPane();
|
StackPane box = new StackPane();
|
||||||
box.setPrefSize(size, size);
|
box.setPrefSize(size, size);
|
||||||
box.setMaxSize(size, size);
|
box.setMaxSize(size, size);
|
||||||
@@ -828,4 +848,33 @@ public final class SvgIcons {
|
|||||||
box.getChildren().add(path);
|
box.getChildren().add(path);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ícone de Atualização / Sincronização.
|
||||||
|
*/
|
||||||
|
public static Node refreshIcon(double size) {
|
||||||
|
return AssetManager.getIconOrDefault("refresh", size, () -> buildRefreshIcon(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node buildRefreshIcon(double size) {
|
||||||
|
StackPane box = new StackPane();
|
||||||
|
box.setPrefSize(size, size);
|
||||||
|
box.setMaxSize(size, size);
|
||||||
|
box.setMinSize(size, size);
|
||||||
|
box.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
|
SVGPath path = new SVGPath();
|
||||||
|
path.setContent("M 20 4 L 20 9 L 15 9 M 4 20 L 4 15 L 9 15 M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15");
|
||||||
|
path.setStroke(Color.web("#0284c7"));
|
||||||
|
path.setStrokeWidth(1.8);
|
||||||
|
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
|
||||||
|
path.setFill(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
double scale = size / 24.0;
|
||||||
|
path.setScaleX(scale);
|
||||||
|
path.setScaleY(scale);
|
||||||
|
|
||||||
|
box.getChildren().add(path);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package br.com.cybercore.signer.ui.view;
|
package br.com.cybercore.signer.ui.view;
|
||||||
|
|
||||||
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
|
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
|
||||||
|
import br.com.cybercore.signer.ui.assets.AssetManager;
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Painel informativo Sobre o CyberCore Signer.
|
* Painel informativo Sobre o CyberCore Signer com exibição de identidade visual SVG e dados do agente.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("this-escape")
|
@SuppressWarnings("this-escape")
|
||||||
public final class AboutView extends VBox {
|
public final class AboutView extends VBox {
|
||||||
@@ -16,13 +19,12 @@ public final class AboutView extends VBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initView() {
|
private void initView() {
|
||||||
setSpacing(16);
|
setSpacing(14);
|
||||||
setAlignment(Pos.CENTER);
|
setAlignment(Pos.CENTER);
|
||||||
getStyleClass().add("content-pane");
|
getStyleClass().add("content-pane");
|
||||||
|
|
||||||
Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME);
|
// Logotipo oficial vetorial SVG (app-logo.svg) com fallback para cristal 3D
|
||||||
brandLabel.getStyleClass().add("brand-title");
|
Node logoNode = AssetManager.getLogoOrDefault(320, 80, () -> SvgIcons.createAppCrystalLogo(54));
|
||||||
brandLabel.setStyle("-fx-font-size: 24px;");
|
|
||||||
|
|
||||||
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
|
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
|
||||||
versionLabel.getStyleClass().add("status-badge-active");
|
versionLabel.getStyleClass().add("status-badge-active");
|
||||||
@@ -44,9 +46,9 @@ public final class AboutView extends VBox {
|
|||||||
detailsBox.getChildren().add(lbl);
|
detailsBox.getChildren().add(lbl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Label copyLabel = new Label("© 2026 CyberCore. Todos os direitos reservados.");
|
Label copyLabel = new Label("© 2026 CyberCore Systems. Todos os direitos reservados.");
|
||||||
copyLabel.getStyleClass().add("dialog-subtitle");
|
copyLabel.getStyleClass().add("dialog-subtitle");
|
||||||
|
|
||||||
getChildren().addAll(brandLabel, versionLabel, descLabel, detailsBox, copyLabel);
|
getChildren().addAll(logoNode, versionLabel, descLabel, detailsBox, copyLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ public class ApplicationApprovalDialog {
|
|||||||
root.setPadding(new Insets(24));
|
root.setPadding(new Insets(24));
|
||||||
root.getStyleClass().add("dialog-container");
|
root.getStyleClass().add("dialog-container");
|
||||||
|
|
||||||
// Cabeçalho
|
// Cabeçalho com Ícone SVG de Escudo
|
||||||
|
HBox header = new HBox(14);
|
||||||
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
VBox headerBox = new VBox(4);
|
VBox headerBox = new VBox(4);
|
||||||
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
|
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
|
||||||
titleLabel.getStyleClass().add("dialog-header");
|
titleLabel.getStyleClass().add("dialog-header");
|
||||||
@@ -48,6 +51,8 @@ public class ApplicationApprovalDialog {
|
|||||||
subtitleLabel.getStyleClass().add("dialog-subtitle");
|
subtitleLabel.getStyleClass().add("dialog-subtitle");
|
||||||
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
|
headerBox.getChildren().addAll(titleLabel, subtitleLabel);
|
||||||
|
|
||||||
|
header.getChildren().addAll(br.com.cybercore.signer.ui.components.SvgIcons.cyberShieldLogo(34), headerBox);
|
||||||
|
|
||||||
// Painel de Detalhes da Aplicação
|
// Painel de Detalhes da Aplicação
|
||||||
VBox detailsBox = new VBox(10);
|
VBox detailsBox = new VBox(10);
|
||||||
detailsBox.getStyleClass().add("dialog-box");
|
detailsBox.getStyleClass().add("dialog-box");
|
||||||
@@ -106,7 +111,7 @@ public class ApplicationApprovalDialog {
|
|||||||
|
|
||||||
actionsRow.getChildren().addAll(btnReject, btnAuthorize);
|
actionsRow.getChildren().addAll(btnReject, btnAuthorize);
|
||||||
|
|
||||||
root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow);
|
root.getChildren().addAll(header, detailsBox, secWarning, actionsRow);
|
||||||
|
|
||||||
Scene scene = new Scene(root);
|
Scene scene = new Scene(root);
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ public final class BatchSigningDialog {
|
|||||||
Path dest = targetDir.resolve(outPdfName);
|
Path dest = targetDir.resolve(outPdfName);
|
||||||
pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
|
pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
|
||||||
} else if (lowerName.endsWith(".xml")) {
|
} else if (lowerName.endsWith(".xml")) {
|
||||||
// Assinatura XMLDSig de documento XML
|
// Assinatura XAdES-BES de documento XML (ETSI TS 101 903 / ICP-Brasil)
|
||||||
br.com.cybercore.signer.core.xml.XmlSigningService xmlService =
|
br.com.cybercore.signer.core.xml.XmlSigningService xmlService =
|
||||||
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
|
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
|
||||||
br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
|
br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public final class CertificatesManagerDialog {
|
|||||||
btnAddA1.getStyleClass().add("btn-primary");
|
btnAddA1.getStyleClass().add("btn-primary");
|
||||||
btnAddA1.setOnAction(e -> handleImportA1());
|
btnAddA1.setOnAction(e -> handleImportA1());
|
||||||
|
|
||||||
Button btnRefresh = new Button("Atualizar Lista");
|
Button btnRefresh = new Button(" Atualizar Lista", SvgIcons.refreshIcon(14));
|
||||||
btnRefresh.getStyleClass().add("btn-secondary");
|
btnRefresh.getStyleClass().add("btn-secondary");
|
||||||
btnRefresh.setOnAction(e -> refreshList());
|
btnRefresh.setOnAction(e -> refreshList());
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ public final class CertificatesManagerDialog {
|
|||||||
|
|
||||||
HBox footer = new HBox();
|
HBox footer = new HBox();
|
||||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
Button btnClose = new Button("Fechar");
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().add("btn-secondary");
|
btnClose.getStyleClass().add("btn-secondary");
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
footer.getChildren().add(btnClose);
|
footer.getChildren().add(btnClose);
|
||||||
|
|||||||
@@ -57,24 +57,43 @@ public final class LogsViewerDialog {
|
|||||||
txtLogs.getStyleClass().add("log-console-area");
|
txtLogs.getStyleClass().add("log-console-area");
|
||||||
VBox.setVgrow(txtLogs, Priority.ALWAYS);
|
VBox.setVgrow(txtLogs, Priority.ALWAYS);
|
||||||
|
|
||||||
loadInitialLogs();
|
loadLogs();
|
||||||
|
|
||||||
HBox footer = new HBox(10);
|
HBox footer = new HBox(10);
|
||||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
Button btnCopy = new Button("Copiar");
|
||||||
|
btnCopy.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
|
btnCopy.setPrefWidth(90);
|
||||||
|
btnCopy.setOnAction(e -> {
|
||||||
|
javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent();
|
||||||
|
content.putString(txtLogs.getText());
|
||||||
|
javafx.scene.input.Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
btnCopy.setText("✓ Copiado");
|
||||||
|
javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.seconds(2));
|
||||||
|
pause.setOnFinished(ev -> btnCopy.setText("Copiar"));
|
||||||
|
pause.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button btnClear = new Button("Limpar");
|
||||||
|
btnClear.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
|
btnClear.setPrefWidth(90);
|
||||||
|
btnClear.setOnAction(e -> {
|
||||||
|
br.com.cybercore.signer.core.log.AppLogBuffer.clear();
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
|
||||||
Button btnRefresh = new Button("Atualizar");
|
Button btnRefresh = new Button("Atualizar");
|
||||||
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnRefresh.setPrefWidth(110);
|
btnRefresh.setPrefWidth(100);
|
||||||
btnRefresh.setMinWidth(110);
|
btnRefresh.setOnAction(e -> loadLogs());
|
||||||
btnRefresh.setOnAction(e -> loadInitialLogs());
|
|
||||||
|
|
||||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||||
btnClose.setPrefWidth(110);
|
btnClose.setPrefWidth(100);
|
||||||
btnClose.setMinWidth(110);
|
|
||||||
btnClose.setOnAction(e -> stage.close());
|
btnClose.setOnAction(e -> stage.close());
|
||||||
|
|
||||||
footer.getChildren().addAll(btnRefresh, btnClose);
|
footer.getChildren().addAll(btnCopy, btnClear, btnRefresh, btnClose);
|
||||||
|
|
||||||
root.getChildren().addAll(header, txtLogs, footer);
|
root.getChildren().addAll(header, txtLogs, footer);
|
||||||
|
|
||||||
@@ -84,15 +103,15 @@ public final class LogsViewerDialog {
|
|||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadInitialLogs() {
|
private void loadLogs() {
|
||||||
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
java.util.List<String> logs = br.com.cybercore.signer.core.log.AppLogBuffer.getLogs();
|
||||||
String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" +
|
if (logs.isEmpty()) {
|
||||||
"[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" +
|
txtLogs.setText("[Aguardando eventos de log...]");
|
||||||
"[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" +
|
} else {
|
||||||
"[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\n" +
|
txtLogs.setText(String.join("\n", logs) + "\n");
|
||||||
"[" + now + "] [INFO] Política anti-replay: nonces protegidos em memória.\n" +
|
}
|
||||||
"[" + now + "] [INFO] Registro de aplicações web ativo e pronto para autorização segura.\n";
|
txtLogs.positionCaret(txtLogs.getText().length());
|
||||||
txtLogs.setText(sb);
|
txtLogs.setScrollTop(Double.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox createHeaderLabel(String title, String subtitle) {
|
private VBox createHeaderLabel(String title, String subtitle) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import javafx.scene.layout.GridPane;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
@@ -204,14 +205,20 @@ public final class MainAppView {
|
|||||||
|
|
||||||
private void updateThemeToggleButton() {
|
private void updateThemeToggleButton() {
|
||||||
boolean dark = ThemeManager.isDark();
|
boolean dark = ThemeManager.isDark();
|
||||||
HBox content = new HBox(6);
|
HBox content = new HBox(8);
|
||||||
content.setAlignment(Pos.CENTER);
|
content.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15);
|
Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15);
|
||||||
|
StackPane iconBox = new StackPane(icon);
|
||||||
|
iconBox.setPrefSize(18, 18);
|
||||||
|
iconBox.setMinSize(18, 18);
|
||||||
|
iconBox.setMaxSize(18, 18);
|
||||||
|
iconBox.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
|
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
|
||||||
label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
|
label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
|
||||||
|
|
||||||
content.getChildren().addAll(icon, label);
|
content.getChildren().addAll(iconBox, label);
|
||||||
themeToggleBtn.setGraphic(content);
|
themeToggleBtn.setGraphic(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,16 +230,22 @@ public final class MainAppView {
|
|||||||
btn.setPrefHeight(68);
|
btn.setPrefHeight(68);
|
||||||
btn.setMinHeight(68);
|
btn.setMinHeight(68);
|
||||||
|
|
||||||
HBox content = new HBox(10);
|
HBox content = new HBox(12);
|
||||||
content.setAlignment(Pos.CENTER_LEFT);
|
content.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Label lbl = new Label(text);
|
Label lbl = new Label(text);
|
||||||
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
|
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
|
||||||
lbl.setWrapText(true);
|
lbl.setWrapText(true);
|
||||||
|
lbl.setAlignment(Pos.CENTER_LEFT);
|
||||||
HBox.setHgrow(lbl, Priority.ALWAYS);
|
HBox.setHgrow(lbl, Priority.ALWAYS);
|
||||||
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
content.getChildren().addAll(icon, lbl);
|
StackPane iconSlot = new StackPane(icon);
|
||||||
|
iconSlot.setPrefSize(40, 40);
|
||||||
|
iconSlot.setMinSize(40, 40);
|
||||||
|
iconSlot.setMaxSize(40, 40);
|
||||||
|
iconSlot.setAlignment(Pos.CENTER);
|
||||||
|
content.getChildren().addAll(iconSlot, lbl);
|
||||||
} else {
|
} else {
|
||||||
content.getChildren().add(lbl);
|
content.getChildren().add(lbl);
|
||||||
}
|
}
|
||||||
@@ -251,16 +264,22 @@ public final class MainAppView {
|
|||||||
btn.setMinHeight(42);
|
btn.setMinHeight(42);
|
||||||
btn.setMaxWidth(Double.MAX_VALUE);
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
HBox content = new HBox(10);
|
HBox content = new HBox(12);
|
||||||
content.setAlignment(Pos.CENTER_LEFT);
|
content.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
Label lbl = new Label(text);
|
Label lbl = new Label(text);
|
||||||
lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text");
|
lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text");
|
||||||
lbl.setWrapText(true);
|
lbl.setWrapText(false);
|
||||||
|
lbl.setAlignment(Pos.CENTER_LEFT);
|
||||||
HBox.setHgrow(lbl, Priority.ALWAYS);
|
HBox.setHgrow(lbl, Priority.ALWAYS);
|
||||||
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
content.getChildren().addAll(icon, lbl);
|
StackPane iconSlot = new StackPane(icon);
|
||||||
|
iconSlot.setPrefSize(26, 26);
|
||||||
|
iconSlot.setMinSize(26, 26);
|
||||||
|
iconSlot.setMaxSize(26, 26);
|
||||||
|
iconSlot.setAlignment(Pos.CENTER);
|
||||||
|
content.getChildren().addAll(iconSlot, lbl);
|
||||||
} else {
|
} else {
|
||||||
content.getChildren().add(lbl);
|
content.getChildren().add(lbl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package br.com.cybercore.signer.ui.view;
|
package br.com.cybercore.signer.ui.view;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
||||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||||
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
|
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
|
||||||
@@ -54,6 +55,22 @@ public final class PdfCertificateSignDialog {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
|
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
|
||||||
|
|
||||||
public record SealConfig(
|
public record SealConfig(
|
||||||
|
boolean visibleSeal,
|
||||||
|
PdfSealTheme sealTheme,
|
||||||
|
CpfDisplayMode cpfDisplayMode,
|
||||||
|
boolean includeDateTime,
|
||||||
|
String customExtraInfo,
|
||||||
|
PdfSignDialog.PositionOption positionOption,
|
||||||
|
PdfSignDialog.PageOption pageOption,
|
||||||
|
boolean isCustomPosition,
|
||||||
|
Float customX,
|
||||||
|
Float customY,
|
||||||
|
Integer customPageNumber,
|
||||||
|
String reason,
|
||||||
|
String location,
|
||||||
|
Double customSealScale
|
||||||
|
) {
|
||||||
|
public SealConfig(
|
||||||
boolean visibleSeal,
|
boolean visibleSeal,
|
||||||
PdfSealTheme sealTheme,
|
PdfSealTheme sealTheme,
|
||||||
CpfDisplayMode cpfDisplayMode,
|
CpfDisplayMode cpfDisplayMode,
|
||||||
@@ -67,7 +84,11 @@ public final class PdfCertificateSignDialog {
|
|||||||
Integer customPageNumber,
|
Integer customPageNumber,
|
||||||
String reason,
|
String reason,
|
||||||
String location
|
String location
|
||||||
) {}
|
) {
|
||||||
|
this(visibleSeal, sealTheme, cpfDisplayMode, includeDateTime, customExtraInfo, positionOption,
|
||||||
|
pageOption, isCustomPosition, customX, customY, customPageNumber, reason, location, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final CertificateManager certManager;
|
private final CertificateManager certManager;
|
||||||
@@ -196,8 +217,9 @@ public final class PdfCertificateSignDialog {
|
|||||||
if (empty || item == null) {
|
if (empty || item == null) {
|
||||||
setText(null);
|
setText(null);
|
||||||
} else {
|
} else {
|
||||||
|
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
|
||||||
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
|
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
|
||||||
setText(item.commonName() + doc + " (" + item.type() + ")");
|
setText(holderName + doc + " (" + item.type() + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -208,8 +230,9 @@ public final class PdfCertificateSignDialog {
|
|||||||
if (empty || item == null) {
|
if (empty || item == null) {
|
||||||
setText(null);
|
setText(null);
|
||||||
} else {
|
} else {
|
||||||
|
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
|
||||||
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
|
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
|
||||||
setText(item.commonName() + doc + " (" + item.type() + ")");
|
setText(holderName + doc + " (" + item.type() + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -429,7 +452,8 @@ public final class PdfCertificateSignDialog {
|
|||||||
.includeDateTime(sealConfig.includeDateTime())
|
.includeDateTime(sealConfig.includeDateTime())
|
||||||
.customExtraInfo(sealConfig.customExtraInfo())
|
.customExtraInfo(sealConfig.customExtraInfo())
|
||||||
.reason(sealConfig.reason())
|
.reason(sealConfig.reason())
|
||||||
.location(sealConfig.location());
|
.location(sealConfig.location())
|
||||||
|
.customSealScale(sealConfig.customSealScale());
|
||||||
|
|
||||||
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
|
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
|
||||||
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
|
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import javafx.scene.layout.Region;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -127,6 +129,24 @@ public final class PdfSignDialog {
|
|||||||
private ComboBox<PageOption> pageCombo;
|
private ComboBox<PageOption> pageCombo;
|
||||||
private ImageView sealPreviewImageView;
|
private ImageView sealPreviewImageView;
|
||||||
|
|
||||||
|
// Controle de Escala / Redimensionamento do Selo (1.0 = 100%)
|
||||||
|
private double sealScale = 1.0;
|
||||||
|
private static final double MIN_SEAL_SCALE = 0.40;
|
||||||
|
private static final double MAX_SEAL_SCALE = 1.80;
|
||||||
|
private boolean isResizeModeActive = false;
|
||||||
|
|
||||||
|
// Componentes do grupo do selo e alça interativa
|
||||||
|
private Pane sealGroupPane;
|
||||||
|
private Rectangle sealSelectionBorder;
|
||||||
|
private Circle resizeHandle;
|
||||||
|
private Button btnToggleResize;
|
||||||
|
private Button btnResetSealScale;
|
||||||
|
private Label lblSealScaleIndicator;
|
||||||
|
private double resizeStartSceneX;
|
||||||
|
private double resizeStartBaseWidth;
|
||||||
|
private boolean isResizingSealInProgress = false;
|
||||||
|
private long lastResizeActionTimestamp = 0;
|
||||||
|
|
||||||
// Visualizador Interativo da Folha
|
// Visualizador Interativo da Folha
|
||||||
private ScrollPane sheetScroll;
|
private ScrollPane sheetScroll;
|
||||||
private StackPane pageSheetContainer;
|
private StackPane pageSheetContainer;
|
||||||
@@ -463,13 +483,34 @@ public final class PdfSignDialog {
|
|||||||
|
|
||||||
zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit);
|
zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit);
|
||||||
|
|
||||||
|
// Grupo de Tamanho do Selo
|
||||||
|
HBox resizeToolBox = new HBox(6);
|
||||||
|
resizeToolBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
resizeToolBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
|
||||||
|
|
||||||
|
btnToggleResize = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13));
|
||||||
|
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
|
||||||
|
btnToggleResize.setTooltip(new Tooltip("Ativar/desativar alça interativa para alterar o tamanho do selo"));
|
||||||
|
btnToggleResize.setOnAction(e -> toggleResizeMode());
|
||||||
|
|
||||||
|
lblSealScaleIndicator = new Label("100%");
|
||||||
|
lblSealScaleIndicator.setStyle("-fx-font-size: 11px; -fx-font-weight: bold; -fx-min-width: 36px; -fx-alignment: center;");
|
||||||
|
|
||||||
|
btnResetSealScale = new Button(" 100%", SvgIcons.refreshIcon(12));
|
||||||
|
btnResetSealScale.getStyleClass().addAll("btn-secondary", "btn-compact");
|
||||||
|
btnResetSealScale.setTooltip(new Tooltip("Restaurar tamanho padrão original (100%)"));
|
||||||
|
btnResetSealScale.setVisible(false);
|
||||||
|
btnResetSealScale.setOnAction(e -> applySealScale(1.0));
|
||||||
|
|
||||||
|
resizeToolBox.getChildren().addAll(btnToggleResize, lblSealScaleIndicator, btnResetSealScale);
|
||||||
|
|
||||||
Region tbSpacer = new Region();
|
Region tbSpacer = new Region();
|
||||||
HBox.setHgrow(tbSpacer, Priority.ALWAYS);
|
HBox.setHgrow(tbSpacer, Priority.ALWAYS);
|
||||||
|
|
||||||
lblPositionFeedback = new Label("👉 Clique na folha para posicionar");
|
lblPositionFeedback = new Label("Clique na folha para posicionar");
|
||||||
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
|
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
|
||||||
|
|
||||||
toolbar.getChildren().addAll(pageNavBox, zoomBox, tbSpacer, lblPositionFeedback);
|
toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
|
||||||
|
|
||||||
// ScrollPane da Folha de Papel
|
// ScrollPane da Folha de Papel
|
||||||
sheetScroll = new ScrollPane();
|
sheetScroll = new ScrollPane();
|
||||||
@@ -508,23 +549,61 @@ public final class PdfSignDialog {
|
|||||||
interactiveOverlayPane = new Pane();
|
interactiveOverlayPane = new Pane();
|
||||||
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
|
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
|
||||||
interactiveOverlayPane.setOnMouseClicked(event -> {
|
interactiveOverlayPane.setOnMouseClicked(event -> {
|
||||||
if (sealDragOccurred) {
|
if (sealDragOccurred || isResizingSealInProgress || (System.currentTimeMillis() - lastResizeActionTimestamp < 400)) {
|
||||||
// Previne que a soltura do mouse no término do arraste cause clique residual
|
// Previne que a soltura do mouse no término do redimensionamento ou arraste cause clique residual
|
||||||
sealDragOccurred = false;
|
sealDragOccurred = false;
|
||||||
event.consume();
|
event.consume();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isResizeModeActive) {
|
||||||
|
// No modo redimensionamento ativo, clicar fora não deve saltar o selo acidentalmente
|
||||||
|
event.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleSheetClick(event);
|
handleSheetClick(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Grupo visual do selo composto pela imagem, borda de seleção e alça de quina
|
||||||
|
sealGroupPane = new Pane();
|
||||||
|
sealGroupPane.setVisible(false);
|
||||||
|
|
||||||
sealStampOverlayView = new ImageView();
|
sealStampOverlayView = new ImageView();
|
||||||
sealStampOverlayView.setPreserveRatio(true);
|
sealStampOverlayView.setPreserveRatio(true);
|
||||||
sealStampOverlayView.setCursor(Cursor.MOVE);
|
sealStampOverlayView.setCursor(Cursor.MOVE);
|
||||||
DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65));
|
DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65));
|
||||||
sealStampOverlayView.setEffect(sealShadow);
|
sealStampOverlayView.setEffect(sealShadow);
|
||||||
sealStampOverlayView.setVisible(false);
|
|
||||||
|
|
||||||
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo (sem salto e sem drift)
|
sealSelectionBorder = new Rectangle();
|
||||||
|
sealSelectionBorder.setFill(Color.TRANSPARENT);
|
||||||
|
sealSelectionBorder.setStroke(Color.rgb(2, 132, 199));
|
||||||
|
sealSelectionBorder.setStrokeWidth(1.8);
|
||||||
|
sealSelectionBorder.getStrokeDashArray().addAll(5.0, 4.0);
|
||||||
|
sealSelectionBorder.setVisible(false);
|
||||||
|
sealSelectionBorder.setOnMousePressed(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseDragged(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseReleased(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseClicked(e -> e.consume());
|
||||||
|
|
||||||
|
resizeHandle = new Circle(8);
|
||||||
|
resizeHandle.setFill(Color.rgb(2, 132, 199));
|
||||||
|
resizeHandle.setStroke(Color.WHITE);
|
||||||
|
resizeHandle.setStrokeWidth(2.0);
|
||||||
|
resizeHandle.setCursor(Cursor.SE_RESIZE);
|
||||||
|
DropShadow handleShadow = new DropShadow(4, 0, 1, Color.rgb(0, 0, 0, 0.35));
|
||||||
|
resizeHandle.setEffect(handleShadow);
|
||||||
|
resizeHandle.setVisible(false);
|
||||||
|
Tooltip.install(resizeHandle, new Tooltip("Puxe a quina para redimensionar o selo proporcionalmente"));
|
||||||
|
|
||||||
|
// Duplo clique no selo para ativar/desativar o modo de redimensionamento
|
||||||
|
sealStampOverlayView.setOnMouseClicked(event -> {
|
||||||
|
sealDragOccurred = false;
|
||||||
|
if (event.getClickCount() == 2) {
|
||||||
|
toggleResizeMode();
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo
|
||||||
sealStampOverlayView.setOnMousePressed(event -> {
|
sealStampOverlayView.setOnMousePressed(event -> {
|
||||||
dragStartOffsetInSealX = event.getX();
|
dragStartOffsetInSealX = event.getX();
|
||||||
dragStartOffsetInSealY = event.getY();
|
dragStartOffsetInSealY = event.getY();
|
||||||
@@ -536,7 +615,6 @@ public final class PdfSignDialog {
|
|||||||
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
sealDragOccurred = true;
|
sealDragOccurred = true;
|
||||||
|
|
||||||
// Converte coordenada da cena para coordenadas locais do painel da folha
|
|
||||||
Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY());
|
Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY());
|
||||||
double targetX = localInSheet.getX() - dragStartOffsetInSealX;
|
double targetX = localInSheet.getX() - dragStartOffsetInSealX;
|
||||||
double targetY = localInSheet.getY() - dragStartOffsetInSealY;
|
double targetY = localInSheet.getY() - dragStartOffsetInSealY;
|
||||||
@@ -545,16 +623,47 @@ public final class PdfSignDialog {
|
|||||||
});
|
});
|
||||||
|
|
||||||
sealStampOverlayView.setOnMouseReleased(event -> {
|
sealStampOverlayView.setOnMouseReleased(event -> {
|
||||||
// Garante que o evento de soltura do mouse não borbulhe para a folha
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
|
||||||
sealStampOverlayView.setOnMouseClicked(event -> {
|
// Eventos da Alça de Redimensionamento Interativa
|
||||||
sealDragOccurred = false;
|
resizeHandle.setOnMousePressed(event -> {
|
||||||
|
isResizingSealInProgress = true;
|
||||||
|
sealDragOccurred = true;
|
||||||
|
resizeStartSceneX = event.getSceneX();
|
||||||
|
resizeStartBaseWidth = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
|
||||||
interactiveOverlayPane.getChildren().add(sealStampOverlayView);
|
resizeHandle.setOnMouseDragged(event -> {
|
||||||
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
|
isResizingSealInProgress = true;
|
||||||
|
sealDragOccurred = true;
|
||||||
|
double deltaX = event.getSceneX() - resizeStartSceneX;
|
||||||
|
double newWidth = resizeStartBaseWidth + deltaX;
|
||||||
|
double baseW = currentBaseSealWidth * zoomFactor;
|
||||||
|
if (baseW > 0) {
|
||||||
|
applySealScalePreservingOrigin(newWidth / baseW);
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeHandle.setOnMouseReleased(event -> {
|
||||||
|
isResizingSealInProgress = false;
|
||||||
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
sealDragOccurred = true;
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
resizeHandle.setOnMouseClicked(event -> {
|
||||||
|
isResizingSealInProgress = false;
|
||||||
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
|
sealGroupPane.getChildren().addAll(sealStampOverlayView, sealSelectionBorder, resizeHandle);
|
||||||
|
interactiveOverlayPane.getChildren().add(sealGroupPane);
|
||||||
|
|
||||||
pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane);
|
pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane);
|
||||||
sheetWrapper.getChildren().add(pageSheetContainer);
|
sheetWrapper.getChildren().add(pageSheetContainer);
|
||||||
sheetScroll.setContent(sheetWrapper);
|
sheetScroll.setContent(sheetWrapper);
|
||||||
@@ -563,6 +672,98 @@ public final class PdfSignDialog {
|
|||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void toggleResizeMode() {
|
||||||
|
this.isResizeModeActive = !this.isResizeModeActive;
|
||||||
|
this.lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
if (sealSelectionBorder != null) {
|
||||||
|
sealSelectionBorder.setVisible(isResizeModeActive);
|
||||||
|
}
|
||||||
|
if (resizeHandle != null) {
|
||||||
|
resizeHandle.setVisible(isResizeModeActive);
|
||||||
|
}
|
||||||
|
if (btnToggleResize != null) {
|
||||||
|
if (isResizeModeActive) {
|
||||||
|
btnToggleResize.setText("✓ Concluir");
|
||||||
|
btnToggleResize.setStyle("-fx-background-color: #0284c7; -fx-text-fill: white; -fx-font-weight: bold;");
|
||||||
|
lblPositionFeedback.setText("👉 Puxe a alça azul no canto do selo para ajustar o tamanho");
|
||||||
|
} else {
|
||||||
|
btnToggleResize.setText("📏 Redimensionar");
|
||||||
|
btnToggleResize.setStyle("");
|
||||||
|
updateFeedbackLabel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySealScalePreservingOrigin(double newScale) {
|
||||||
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
|
|
||||||
|
double currentX = sealGroupPane != null ? sealGroupPane.getLayoutX() : 0;
|
||||||
|
double currentY = sealGroupPane != null ? sealGroupPane.getLayoutY() : 0;
|
||||||
|
|
||||||
|
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
|
||||||
|
if (lblSealScaleIndicator != null) {
|
||||||
|
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
|
||||||
|
}
|
||||||
|
if (btnResetSealScale != null) {
|
||||||
|
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerZoomDimensions();
|
||||||
|
|
||||||
|
double sheetW = currentBaseSheetWidth * zoomFactor;
|
||||||
|
double sheetH = currentBaseSheetHeight * zoomFactor;
|
||||||
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
|
|
||||||
|
// Mantém a quina superior esquerda ancorada no mesmo ponto exato
|
||||||
|
double clampedX = Math.max(0, Math.min(currentX, sheetW - sealW));
|
||||||
|
double clampedY = Math.max(0, Math.min(currentY, sheetH - sealH));
|
||||||
|
|
||||||
|
if (sealGroupPane != null) {
|
||||||
|
sealGroupPane.setLayoutX(clampedX);
|
||||||
|
sealGroupPane.setLayoutY(clampedY);
|
||||||
|
sealGroupPane.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
|
||||||
|
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
|
||||||
|
|
||||||
|
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
||||||
|
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
||||||
|
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
||||||
|
float scaledPdfW = basePdfW * (float) sealScale;
|
||||||
|
float scaledPdfH = basePdfH * (float) sealScale;
|
||||||
|
|
||||||
|
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
|
||||||
|
float calculatedPdfX = (float) (clampedX / scale);
|
||||||
|
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
|
||||||
|
|
||||||
|
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
|
||||||
|
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
|
||||||
|
this.customPageNumber = currentPageIndex + 1;
|
||||||
|
this.isCustomPositionActive = true;
|
||||||
|
|
||||||
|
if (positionCombo.getValue() != PositionOption.CUSTOM) {
|
||||||
|
positionCombo.setValue(PositionOption.CUSTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeedbackLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySealScale(double newScale) {
|
||||||
|
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
|
||||||
|
if (lblSealScaleIndicator != null) {
|
||||||
|
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
|
||||||
|
}
|
||||||
|
if (btnResetSealScale != null) {
|
||||||
|
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerZoomDimensions();
|
||||||
|
repositionSealByRelativeCoords();
|
||||||
|
updateFeedbackLabel();
|
||||||
|
}
|
||||||
|
|
||||||
private void adjustZoom(double delta) {
|
private void adjustZoom(double delta) {
|
||||||
setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta)));
|
setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta)));
|
||||||
}
|
}
|
||||||
@@ -592,8 +793,19 @@ public final class PdfSignDialog {
|
|||||||
pageImageView.setFitHeight(currentH);
|
pageImageView.setFitHeight(currentH);
|
||||||
}
|
}
|
||||||
if (sealStampOverlayView != null) {
|
if (sealStampOverlayView != null) {
|
||||||
sealStampOverlayView.setFitWidth(currentBaseSealWidth * zoomFactor);
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
sealStampOverlayView.setFitHeight(currentBaseSealHeight * zoomFactor);
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
|
sealStampOverlayView.setFitWidth(sealW);
|
||||||
|
sealStampOverlayView.setFitHeight(sealH);
|
||||||
|
|
||||||
|
if (sealSelectionBorder != null) {
|
||||||
|
sealSelectionBorder.setWidth(sealW);
|
||||||
|
sealSelectionBorder.setHeight(sealH);
|
||||||
|
}
|
||||||
|
if (resizeHandle != null) {
|
||||||
|
resizeHandle.setCenterX(sealW);
|
||||||
|
resizeHandle.setCenterY(sealH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,15 +814,17 @@ public final class PdfSignDialog {
|
|||||||
|
|
||||||
double sheetW = currentBaseSheetWidth * zoomFactor;
|
double sheetW = currentBaseSheetWidth * zoomFactor;
|
||||||
double sheetH = currentBaseSheetHeight * zoomFactor;
|
double sheetH = currentBaseSheetHeight * zoomFactor;
|
||||||
double sealW = currentBaseSealWidth * zoomFactor;
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
double sealH = currentBaseSealHeight * zoomFactor;
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
|
|
||||||
double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW));
|
double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW));
|
||||||
double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH));
|
double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH));
|
||||||
|
|
||||||
sealStampOverlayView.setLayoutX(posX);
|
if (sealGroupPane != null) {
|
||||||
sealStampOverlayView.setLayoutY(posY);
|
sealGroupPane.setLayoutX(posX);
|
||||||
sealStampOverlayView.setVisible(true);
|
sealGroupPane.setLayoutY(posY);
|
||||||
|
sealGroupPane.setVisible(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -653,7 +867,9 @@ public final class PdfSignDialog {
|
|||||||
renderCurrentPage();
|
renderCurrentPage();
|
||||||
|
|
||||||
if (chkVisibleSeal.isSelected()) {
|
if (chkVisibleSeal.isSelected()) {
|
||||||
sealStampOverlayView.setVisible(true);
|
if (sealGroupPane != null) {
|
||||||
|
sealGroupPane.setVisible(true);
|
||||||
|
}
|
||||||
resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT);
|
resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -751,38 +967,42 @@ public final class PdfSignDialog {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
private void handleSheetClick(MouseEvent event) {
|
private void handleSheetClick(MouseEvent event) {
|
||||||
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
double sealW = currentBaseSealWidth * zoomFactor;
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
double sealH = currentBaseSealHeight * zoomFactor;
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0);
|
updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) {
|
private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) {
|
||||||
double sheetW = currentBaseSheetWidth * zoomFactor;
|
double sheetW = currentBaseSheetWidth * zoomFactor;
|
||||||
double sheetH = currentBaseSheetHeight * zoomFactor;
|
double sheetH = currentBaseSheetHeight * zoomFactor;
|
||||||
double sealW = currentBaseSealWidth * zoomFactor;
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
double sealH = currentBaseSealHeight * zoomFactor;
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
|
|
||||||
double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW));
|
double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW));
|
||||||
double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH));
|
double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH));
|
||||||
|
|
||||||
sealStampOverlayView.setLayoutX(clampedX);
|
if (sealGroupPane != null) {
|
||||||
sealStampOverlayView.setLayoutY(clampedY);
|
sealGroupPane.setLayoutX(clampedX);
|
||||||
sealStampOverlayView.setVisible(true);
|
sealGroupPane.setLayoutY(clampedY);
|
||||||
|
sealGroupPane.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
|
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
|
||||||
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
|
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
|
||||||
|
|
||||||
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
||||||
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
||||||
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
||||||
|
float scaledPdfW = basePdfW * (float) sealScale;
|
||||||
|
float scaledPdfH = basePdfH * (float) sealScale;
|
||||||
|
|
||||||
// Conversão direta e exata da escala visual na tela para os pontos do PDF
|
// Conversão direta e exata da escala visual na tela para os pontos do PDF
|
||||||
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
|
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
|
||||||
float calculatedPdfX = (float) (clampedX / scale);
|
float calculatedPdfX = (float) (clampedX / scale);
|
||||||
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - sealPdfH);
|
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
|
||||||
|
|
||||||
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW));
|
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
|
||||||
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH));
|
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
|
||||||
this.customPageNumber = currentPageIndex + 1;
|
this.customPageNumber = currentPageIndex + 1;
|
||||||
this.isCustomPositionActive = true;
|
this.isCustomPositionActive = true;
|
||||||
|
|
||||||
@@ -799,8 +1019,10 @@ public final class PdfSignDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
||||||
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
||||||
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
||||||
|
float sealPdfW = basePdfW * (float) sealScale;
|
||||||
|
float sealPdfH = basePdfH * (float) sealScale;
|
||||||
float margin = 20f;
|
float margin = 20f;
|
||||||
|
|
||||||
float targetPdfX;
|
float targetPdfX;
|
||||||
@@ -853,16 +1075,22 @@ public final class PdfSignDialog {
|
|||||||
|
|
||||||
private void updateFeedbackLabel() {
|
private void updateFeedbackLabel() {
|
||||||
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
|
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
|
||||||
lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]",
|
String scaleStr = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" | Escala: %.0f%%", sealScale * 100) : "";
|
||||||
customPageNumber, customPdfX, customPdfY));
|
lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]%s",
|
||||||
|
customPageNumber, customPdfX, customPdfY, scaleStr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleVisibleSeal(boolean active) {
|
private void toggleVisibleSeal(boolean active) {
|
||||||
positionCombo.setDisable(!active);
|
positionCombo.setDisable(!active);
|
||||||
pageCombo.setDisable(!active);
|
pageCombo.setDisable(!active);
|
||||||
|
if (btnToggleResize != null) {
|
||||||
|
btnToggleResize.setDisable(!active);
|
||||||
|
}
|
||||||
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
|
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
|
||||||
sealStampOverlayView.setVisible(active && selectedPdfBytes != null);
|
if (sealGroupPane != null) {
|
||||||
|
sealGroupPane.setVisible(active && selectedPdfBytes != null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSealPreview() {
|
private void updateSealPreview() {
|
||||||
@@ -927,19 +1155,21 @@ public final class PdfSignDialog {
|
|||||||
customPdfY,
|
customPdfY,
|
||||||
customPageNumber,
|
customPageNumber,
|
||||||
DEFAULT_REASON,
|
DEFAULT_REASON,
|
||||||
DEFAULT_LOCATION
|
DEFAULT_LOCATION,
|
||||||
|
sealScale
|
||||||
);
|
);
|
||||||
|
|
||||||
String sealSummary;
|
String sealSummary;
|
||||||
if (chkVisibleSeal.isSelected()) {
|
if (chkVisibleSeal.isSelected()) {
|
||||||
String themeName = theme.name();
|
String themeName = theme.name();
|
||||||
|
String scaleInfo = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" [Escala: %.0f%%]", sealScale * 100) : "";
|
||||||
|
|
||||||
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
|
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
|
||||||
sealSummary = String.format("Selo Estampado (%s) — Pág. %d [X: %.0f pt, Y: %.0f pt]",
|
sealSummary = String.format("Selo Estampado (%s)%s — Pág. %d [X: %.0f pt, Y: %.0f pt]",
|
||||||
themeName, customPageNumber, customPdfX, customPdfY);
|
themeName, scaleInfo, customPageNumber, customPdfX, customPdfY);
|
||||||
} else {
|
} else {
|
||||||
sealSummary = String.format("Selo Estampado (%s) — %s (%s)",
|
sealSummary = String.format("Selo Estampado (%s)%s — %s (%s)",
|
||||||
themeName, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página"));
|
themeName, scaleInfo, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)";
|
sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
|||||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||||
import br.com.cybercore.signer.core.security.PinSessionManager;
|
import br.com.cybercore.signer.core.security.PinSessionManager;
|
||||||
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
|
import br.com.cybercore.signer.ui.CyberCoreUiConstants;
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -56,17 +57,33 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
||||||
String documentName, String documentType, boolean isAuthentication) {
|
String documentName, String documentType, boolean isAuthentication) {
|
||||||
VBox root = new VBox(18);
|
VBox root = new VBox(16);
|
||||||
root.getStyleClass().add("dialog-container");
|
root.getStyleClass().add("dialog-container");
|
||||||
root.setPrefWidth(540);
|
root.setPadding(new Insets(20, 24, 20, 24));
|
||||||
|
root.setPrefWidth(560);
|
||||||
|
|
||||||
Label header = new Label(isAuthentication ? "🔑 SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA");
|
HBox headerBox = new HBox(14);
|
||||||
|
headerBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
VBox titleBox = new VBox(3);
|
||||||
|
HBox.setHgrow(titleBox, Priority.ALWAYS);
|
||||||
|
titleBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN" : "SOLICITAÇÃO DE ASSINATURA");
|
||||||
header.getStyleClass().add("dialog-header");
|
header.getStyleClass().add("dialog-header");
|
||||||
|
|
||||||
Label desc = new Label(isAuthentication
|
Label desc = new Label(isAuthentication
|
||||||
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
|
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
|
||||||
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
|
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
|
||||||
desc.getStyleClass().add("brand-subtitle");
|
desc.getStyleClass().add("brand-subtitle");
|
||||||
|
desc.setWrapText(true);
|
||||||
|
desc.setMaxWidth(480);
|
||||||
|
titleBox.getChildren().addAll(header, desc);
|
||||||
|
|
||||||
|
headerBox.getChildren().addAll(
|
||||||
|
isAuthentication ? SvgIcons.certificatesIcon() : SvgIcons.cyberShieldLogo(26),
|
||||||
|
titleBox
|
||||||
|
);
|
||||||
|
|
||||||
// Caixa de Detalhes
|
// Caixa de Detalhes
|
||||||
VBox detailsBox = new VBox(12);
|
VBox detailsBox = new VBox(12);
|
||||||
@@ -89,9 +106,24 @@ public class SignaturePromptDialog {
|
|||||||
}
|
}
|
||||||
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
|
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
|
||||||
}
|
}
|
||||||
addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias());
|
String rawCertName = cert.commonName() != null ? cert.commonName() : cert.alias();
|
||||||
addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " +
|
String holderName = rawCertName.contains(":") ? rawCertName.split(":")[0].trim() : rawCertName.trim();
|
||||||
(cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel()));
|
String docInfo = "";
|
||||||
|
if (cert.cnpj() != null && !cert.cnpj().isBlank()) {
|
||||||
|
docInfo = ", CNPJ " + cert.cnpj();
|
||||||
|
} else if (cert.cpf() != null && !cert.cpf().isBlank()) {
|
||||||
|
docInfo = ", CPF " + cert.cpf();
|
||||||
|
} else if (rawCertName.contains(":")) {
|
||||||
|
String candidate = rawCertName.split(":")[1].trim();
|
||||||
|
if (candidate.length() == 14) {
|
||||||
|
docInfo = ", CNPJ " + candidate;
|
||||||
|
} else if (candidate.length() == 11) {
|
||||||
|
docInfo = ", CPF " + candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String certDesc = holderName + docInfo + " [" + cert.type().getCode() + "]";
|
||||||
|
addField(grid, row++, "Certificado:", certDesc);
|
||||||
|
addField(grid, row++, "Tipo:", cert.type().getCode() + " (" + cert.type().getLabel() + ")");
|
||||||
addField(grid, row++, "Algoritmo:", context.algorithm());
|
addField(grid, row++, "Algoritmo:", context.algorithm());
|
||||||
|
|
||||||
detailsBox.getChildren().add(grid);
|
detailsBox.getChildren().add(grid);
|
||||||
@@ -134,7 +166,8 @@ public class SignaturePromptDialog {
|
|||||||
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
|
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
|
||||||
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
|
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
|
||||||
);
|
);
|
||||||
winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
winDesc.getStyleClass().add("dialog-subtitle");
|
||||||
|
winDesc.setStyle("-fx-font-size: 12px;");
|
||||||
winDesc.setWrapText(true);
|
winDesc.setWrapText(true);
|
||||||
|
|
||||||
winBox.getChildren().addAll(winTitle, winDesc);
|
winBox.getChildren().addAll(winTitle, winDesc);
|
||||||
@@ -166,6 +199,7 @@ public class SignaturePromptDialog {
|
|||||||
if (nowVisible) {
|
if (nowVisible) {
|
||||||
cbSavePin.setSelected(true);
|
cbSavePin.setSelected(true);
|
||||||
}
|
}
|
||||||
|
stage.sizeToScene();
|
||||||
});
|
});
|
||||||
|
|
||||||
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
|
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
|
||||||
@@ -173,25 +207,26 @@ public class SignaturePromptDialog {
|
|||||||
} else {
|
} else {
|
||||||
VBox inputWrapper = new VBox(8);
|
VBox inputWrapper = new VBox(8);
|
||||||
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
|
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
|
||||||
pinPromptLabel.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
pinPromptLabel.getStyleClass().add("dialog-subtitle");
|
||||||
|
pinPromptLabel.setStyle("-fx-font-size: 12px;");
|
||||||
|
|
||||||
cbSavePin.setSelected(false);
|
cbSavePin.setSelected(false);
|
||||||
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
|
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
|
||||||
pinContainer.getChildren().add(inputWrapper);
|
pinContainer.getChildren().add(inputWrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detailsBox.getChildren().add(pinContainer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label de Alerta de Validação
|
// Label de Alerta de Validação
|
||||||
Label validationLabel = new Label();
|
Label validationLabel = new Label();
|
||||||
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
|
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
|
||||||
validationLabel.setVisible(false);
|
validationLabel.setVisible(false);
|
||||||
|
validationLabel.setManaged(false);
|
||||||
|
|
||||||
// Botões de Ação
|
// Botões de Ação
|
||||||
HBox actionsRow = new HBox(12);
|
HBox actionsRow = new HBox(12);
|
||||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
actionsRow.setPadding(new Insets(4, 0, 0, 0));
|
||||||
|
|
||||||
Button btnCancel = new Button("Cancelar");
|
Button btnCancel = new Button("Cancelar");
|
||||||
btnCancel.getStyleClass().add("btn-cancel");
|
btnCancel.getStyleClass().add("btn-cancel");
|
||||||
@@ -211,6 +246,8 @@ public class SignaturePromptDialog {
|
|||||||
if (!hasStoredPin && !pinProvided) {
|
if (!hasStoredPin && !pinProvided) {
|
||||||
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
|
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
|
||||||
validationLabel.setVisible(true);
|
validationLabel.setVisible(true);
|
||||||
|
validationLabel.setManaged(true);
|
||||||
|
stage.sizeToScene();
|
||||||
pinField.requestFocus();
|
pinField.requestFocus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -231,12 +268,18 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
|
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
|
||||||
|
|
||||||
root.getChildren().addAll(header, desc, detailsBox, validationLabel, actionsRow);
|
if (isA3) {
|
||||||
|
root.getChildren().addAll(headerBox, detailsBox, pinContainer, validationLabel, actionsRow);
|
||||||
|
} else {
|
||||||
|
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
|
||||||
|
}
|
||||||
|
|
||||||
Scene scene = new Scene(root);
|
Scene scene = new Scene(root);
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
|
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.centerOnScreen();
|
||||||
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
|
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +295,8 @@ public class SignaturePromptDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Boolean> showAndAwait() {
|
public CompletableFuture<Boolean> showAndAwait() {
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.centerOnScreen();
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
return approvalFuture;
|
return approvalFuture;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
|||||||
import br.com.cybercore.signer.core.provider.CertificateManager;
|
import br.com.cybercore.signer.core.provider.CertificateManager;
|
||||||
import br.com.cybercore.signer.core.security.PinSessionManager;
|
import br.com.cybercore.signer.core.security.PinSessionManager;
|
||||||
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
|
import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
|
||||||
|
import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
|
||||||
import br.com.cybercore.signer.core.xml.XmlSigningService;
|
import br.com.cybercore.signer.core.xml.XmlSigningService;
|
||||||
import br.com.cybercore.signer.ui.components.SvgIcons;
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||||
@@ -25,6 +26,7 @@ import javafx.scene.control.PasswordField;
|
|||||||
import javafx.scene.control.ProgressIndicator;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.control.RadioButton;
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Separator;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.control.ToggleGroup;
|
import javafx.scene.control.ToggleGroup;
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
@@ -68,7 +70,9 @@ public final class XmlSignDialog {
|
|||||||
private PasswordField pinField;
|
private PasswordField pinField;
|
||||||
private Label lblPinNotice;
|
private Label lblPinNotice;
|
||||||
|
|
||||||
// Controles de Modo de Assinatura
|
// Controles de Padrão e Modo de Assinatura
|
||||||
|
private RadioButton rbXadesBes;
|
||||||
|
private RadioButton rbXmlDsig;
|
||||||
private RadioButton rbWholeDocument;
|
private RadioButton rbWholeDocument;
|
||||||
private RadioButton rbSpecificElement;
|
private RadioButton rbSpecificElement;
|
||||||
private TextField txtElementTag;
|
private TextField txtElementTag;
|
||||||
@@ -94,10 +98,10 @@ public final class XmlSignDialog {
|
|||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
this.stage.initOwner(owner);
|
this.stage.initOwner(owner);
|
||||||
}
|
}
|
||||||
this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XMLDSig)");
|
this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XAdES / ICP-Brasil)");
|
||||||
this.stage.setResizable(true);
|
this.stage.setResizable(true);
|
||||||
this.stage.setMinWidth(720);
|
this.stage.setMinWidth(720);
|
||||||
this.stage.setMinHeight(580);
|
this.stage.setMinHeight(600);
|
||||||
|
|
||||||
initLayout();
|
initLayout();
|
||||||
}
|
}
|
||||||
@@ -111,9 +115,9 @@ public final class XmlSignDialog {
|
|||||||
HBox header = new HBox(14);
|
HBox header = new HBox(14);
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
VBox titleBox = new VBox(4);
|
VBox titleBox = new VBox(4);
|
||||||
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XMLDSIG)");
|
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XADES / ICP-BRASIL)");
|
||||||
lblTitle.getStyleClass().add("dialog-header");
|
lblTitle.getStyleClass().add("dialog-header");
|
||||||
Label lblSub = new Label("Assine digitalmente documentos XML no padrão W3C XMLDSig e ICP-Brasil (NF-e, CT-e, MDF-e, eSocial, etc.).");
|
Label lblSub = new Label("Assine digitalmente documentos XML no padrão avançado XAdES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03) ou XMLDSig.");
|
||||||
lblSub.getStyleClass().add("dialog-subtitle");
|
lblSub.getStyleClass().add("dialog-subtitle");
|
||||||
titleBox.getChildren().addAll(lblTitle, lblSub);
|
titleBox.getChildren().addAll(lblTitle, lblSub);
|
||||||
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
|
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
|
||||||
@@ -198,7 +202,21 @@ public final class XmlSignDialog {
|
|||||||
// 3. Seção: Modo de Assinatura XMLDSig
|
// 3. Seção: Modo de Assinatura XMLDSig
|
||||||
VBox xmlModeSection = new VBox(12);
|
VBox xmlModeSection = new VBox(12);
|
||||||
xmlModeSection.getStyleClass().add("dialog-box");
|
xmlModeSection.getStyleClass().add("dialog-box");
|
||||||
Label lblModeTitle = new Label("MODO DE ASSINATURA XMLDSIG");
|
Label lblProfileTitle = new Label("PADRÃO DE ASSINATURA XML");
|
||||||
|
lblProfileTitle.getStyleClass().add("dialog-field-title");
|
||||||
|
|
||||||
|
ToggleGroup profileGroup = new ToggleGroup();
|
||||||
|
rbXadesBes = new RadioButton("XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)");
|
||||||
|
rbXadesBes.setToggleGroup(profileGroup);
|
||||||
|
rbXadesBes.setSelected(true);
|
||||||
|
|
||||||
|
rbXmlDsig = new RadioButton("XMLDSig Clássico W3C (Enveloped legado / NF-e SEFAZ)");
|
||||||
|
rbXmlDsig.setToggleGroup(profileGroup);
|
||||||
|
|
||||||
|
VBox profileBox = new VBox(8);
|
||||||
|
profileBox.getChildren().addAll(rbXadesBes, rbXmlDsig);
|
||||||
|
|
||||||
|
Label lblModeTitle = new Label("ESCOPO DE ASSINATURA");
|
||||||
lblModeTitle.getStyleClass().add("dialog-field-title");
|
lblModeTitle.getStyleClass().add("dialog-field-title");
|
||||||
|
|
||||||
ToggleGroup modeGroup = new ToggleGroup();
|
ToggleGroup modeGroup = new ToggleGroup();
|
||||||
@@ -247,7 +265,16 @@ public final class XmlSignDialog {
|
|||||||
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
|
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
|
||||||
chkValidateAfter.setSelected(true);
|
chkValidateAfter.setSelected(true);
|
||||||
|
|
||||||
xmlModeSection.getChildren().addAll(lblModeTitle, rbWholeDocument, rbSpecificElement, elementConfigBox, chkValidateAfter);
|
xmlModeSection.getChildren().addAll(
|
||||||
|
lblProfileTitle,
|
||||||
|
profileBox,
|
||||||
|
new Separator(),
|
||||||
|
lblModeTitle,
|
||||||
|
rbWholeDocument,
|
||||||
|
rbSpecificElement,
|
||||||
|
elementConfigBox,
|
||||||
|
chkValidateAfter
|
||||||
|
);
|
||||||
|
|
||||||
formContent.getChildren().addAll(fileSection, certSection, xmlModeSection);
|
formContent.getChildren().addAll(fileSection, certSection, xmlModeSection);
|
||||||
scrollPane.setContent(formContent);
|
scrollPane.setContent(formContent);
|
||||||
@@ -389,6 +416,10 @@ public final class XmlSignDialog {
|
|||||||
pin = pinField.getText().toCharArray();
|
pin = pinField.getText().toCharArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
XmlSignatureProfile profile = rbXadesBes.isSelected()
|
||||||
|
? XmlSignatureProfile.XADES_BES
|
||||||
|
: XmlSignatureProfile.XMLDSIG;
|
||||||
|
|
||||||
XmlSignatureOptions options;
|
XmlSignatureOptions options;
|
||||||
if (rbSpecificElement.isSelected()) {
|
if (rbSpecificElement.isSelected()) {
|
||||||
String tag = txtElementTag.getText().trim();
|
String tag = txtElementTag.getText().trim();
|
||||||
@@ -396,14 +427,14 @@ public final class XmlSignDialog {
|
|||||||
if (tag.isBlank()) {
|
if (tag.isBlank()) {
|
||||||
tag = "infNFe";
|
tag = "infNFe";
|
||||||
}
|
}
|
||||||
options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id);
|
options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id, profile);
|
||||||
} else {
|
} else {
|
||||||
options = XmlSignatureOptions.wholeDocument(cert.id(), pin);
|
options = XmlSignatureOptions.wholeDocument(cert.id(), pin, profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
progressIndicator.setVisible(true);
|
progressIndicator.setVisible(true);
|
||||||
lblStatus.setText("Assinando documento XML (XMLDSig)...");
|
lblStatus.setText("Assinando documento XML (" + profile.getDisplayName() + ")...");
|
||||||
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
|
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
|
||||||
|
|
||||||
boolean validate = chkValidateAfter.isSelected();
|
boolean validate = chkValidateAfter.isSelected();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer
|
# Recursos Visuais (Ícones e Imagens em SVG) — 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**.
|
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente todos os **ícones em SVG** e as **imagens em SVG** do **CyberCore Signer**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,13 +31,23 @@ assets/
|
|||||||
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
|
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
|
||||||
│ ├── check.svg # Ícone Checkmark de sucesso e validação
|
│ ├── check.svg # Ícone Checkmark de sucesso e validação
|
||||||
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
|
│ ├── error.svg # Ícone de Erro / Alerta em vermelho
|
||||||
│ └── star.svg # Ícone de Estrela notarial / favorita
|
│ ├── star.svg # Ícone de Estrela notarial / favorita
|
||||||
|
│ ├── close.svg # Ícone Fechar / Cancelar (cruz geométrica moderna)
|
||||||
|
│ ├── arrow-right.svg # Ícone Avançar / Seta direcional
|
||||||
|
│ ├── zoom-in.svg # Ícone Zoom In (+)
|
||||||
|
│ ├── zoom-out.svg # Ícone Zoom Out (-)
|
||||||
|
│ ├── zoom-fit.svg # Ícone Ajustar à Tela
|
||||||
|
│ └── refresh.svg # Ícone Atualizar / Recarregar listas e status
|
||||||
│
|
│
|
||||||
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência)
|
└── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
|
||||||
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro)
|
├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
|
||||||
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER)
|
├── app-icon.png # Fallback em PNG (256x256 px)
|
||||||
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue)
|
├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
|
||||||
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px)
|
├── app-logo.png # Fallback em PNG (480x120 px)
|
||||||
|
├── brand-banner.svg # Banner de cabeçalho e marca em SVG (600x160 px)
|
||||||
|
├── brand-banner.png # Fallback em PNG (600x160 px)
|
||||||
|
├── default-seal.svg # Selo de assinatura padrão ICP-Brasil em SVG (640x164 px)
|
||||||
|
└── default-seal.png # Fallback em PNG (640x164 px)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -45,20 +55,21 @@ assets/
|
|||||||
## 🎨 Como Customizar Qualquer Ícone ou Imagem
|
## 🎨 Como Customizar Qualquer Ícone ou Imagem
|
||||||
|
|
||||||
1. **Ícones:**
|
1. **Ícones:**
|
||||||
- Estão no formato `.svg` padrão.
|
- Todos estão no formato `.svg` padrão da W3C.
|
||||||
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**.
|
- Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
|
||||||
- Se preferir 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.
|
- Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
|
||||||
|
|
||||||
2. **Imagens do Sistema:**
|
2. **Imagens do Sistema:**
|
||||||
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente).
|
- Tanto o logotipo horizontal (`app-logo.svg`), o ícone do sistema (`app-icon.svg`), o banner institucional (`brand-banner.svg`) e o selo notarial padrão (`default-seal.svg`) agora são arquivos **SVG vetoriais editáveis**.
|
||||||
- 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.
|
- Você pode alterar cores, tipografias, brasões e dimensões diretamente nos arquivos SVG.
|
||||||
|
- O `AssetManager` renderiza os arquivos SVG preservando fidelidade geométrica e suportando gradientes e alinhamentos.
|
||||||
|
|
||||||
3. **Ordem de Prioridade e Hot-Reloading:**
|
3. **Ordem de Resolução e Carregamento:**
|
||||||
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem:
|
Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
|
||||||
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!
|
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Arquivos `.svg` editados aqui têm prioridade máxima e entram em vigor imediatamente sem necessidade de recompilar.
|
||||||
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional.
|
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Para customizações no diretório pessoal do usuário.
|
||||||
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`.
|
3. **Recursos Empacotados no JAR (`/assets/`)**: SVGs e imagens internas empacotadas no pacote de distribuição.
|
||||||
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído.
|
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique desprovida de grafismos caso algum arquivo seja removido.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
<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="#0288d1"/>
|
<!-- Círculo azul e letra 'i' informativa centrada em (24, 24) -->
|
||||||
<circle cx="24" cy="14" r="3" fill="#ffffff"/>
|
<circle cx="24" cy="24" r="18" fill="#0288d1"/>
|
||||||
<line x1="24" y1="21" x2="24" y2="34" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
|
<circle cx="24" cy="15" r="2.5" fill="#ffffff"/>
|
||||||
|
<line x1="24" y1="21" x2="24" y2="33" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 362 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 8 5 L 15 12 L 8 19" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 6 6 L 18 18 M 18 6 L 6 18" fill="none" stroke="#64748b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
@@ -1,6 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||||
<!-- Lua crescente ciano/índigo -->
|
<!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
|
||||||
<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"/>
|
<path d="M 24 8 A 16 16 0 1 0 40 24 A 14 14 0 0 1 24 8 Z" fill="#38bdf8"/>
|
||||||
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/>
|
<circle cx="34" cy="14" r="1.8" fill="#818cf8"/>
|
||||||
<circle cx="42" cy="18" r="1" fill="#818cf8"/>
|
<circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 344 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 20 4 L 20 9 L 15 9" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M 4 20 L 4 15 L 9 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 54" width="36" height="54">
|
||||||
|
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
|
||||||
|
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="#0d47a1" opacity="0.14"/>
|
||||||
|
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="none" stroke="#0d47a1" stroke-width="2.2" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<!-- Corpo do Cadeado de Segurança -->
|
||||||
|
<rect x="11" y="24" width="14" height="12" rx="2.5" fill="#0284c7"/>
|
||||||
|
|
||||||
|
<!-- Arco Superior do Cadeado -->
|
||||||
|
<path d="M13.5 24 V19 C13.5 16.5 15.5 14.5 18 14.5 C20.5 14.5 22.5 16.5 22.5 19 V24" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Orifício da Chave -->
|
||||||
|
<circle cx="18" cy="29" r="1.6" fill="#ffffff"/>
|
||||||
|
<path d="M17.3 29.5 L18.7 29.5 L19.2 33 L16.8 33 Z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 858 B |
@@ -1,8 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
<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) -->
|
<!-- Engrenagem corporativa simétrica 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"/>
|
<path fill="#0288d1" fill-rule="evenodd" d="M 21.5 5 L 26.5 5 L 27.2 10.2 C 29 10.9 30.7 11.9 32.2 13.1 L 36.6 10.6 L 40.1 14.1 L 37.6 18.5 C 38.8 20 39.8 21.7 40.5 23.5 L 45.7 24.2 L 45.7 29.2 L 40.5 29.9 C 39.8 31.7 38.8 33.4 37.6 34.9 L 40.1 39.3 L 36.6 42.8 L 32.2 40.3 C 30.7 41.5 29 42.5 27.2 43.2 L 26.5 48.4 L 21.5 48.4 L 20.8 43.2 C 19 42.5 17.3 41.5 15.8 40.3 L 11.4 42.8 L 7.9 39.3 L 10.4 34.9 C 9.2 33.4 8.2 31.7 7.5 29.9 L 2.3 29.2 L 2.3 24.2 L 7.5 23.5 C 8.2 21.7 9.2 20 10.4 18.5 L 7.9 14.1 L 11.4 10.6 L 15.8 13.1 C 17.3 11.9 19 10.9 20.8 10.2 Z M 24 17.5 A 6.5 6.5 0 1 0 24 30.5 A 6.5 6.5 0 1 0 24 17.5 Z"/>
|
||||||
<!-- 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>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 817 B After Width: | Height: | Size: 784 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path d="M 4 9 L 4 4 L 9 4 M 15 4 L 20 4 L 20 9 M 20 15 L 20 20 L 15 20 M 9 20 L 4 20 L 4 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
|
||||||
|
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<path d="M 10 7 L 10 13 M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
|
||||||
|
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<path d="M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1,37 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1976d2"/>
|
||||||
|
<stop offset="45%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="topFacetGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.55"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="leftFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.75"/>
|
||||||
|
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.35"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="rightFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.85"/>
|
||||||
|
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.45"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
|
||||||
|
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo com cantos arredondados -->
|
||||||
|
<rect x="8" y="8" width="240" height="240" rx="68" ry="68" fill="url(#bgGrad)"/>
|
||||||
|
<rect x="10" y="10" width="236" height="236" rx="66" ry="66" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-opacity="0.25"/>
|
||||||
|
|
||||||
|
<!-- Facetas 3D do Cristal / Diamante Azul -->
|
||||||
|
<polygon points="128,46 210,113 128,143 46,113" fill="url(#topFacetGrad)"/>
|
||||||
|
<polygon points="46,113 128,143 128,215 72,179" fill="url(#leftFacetGrad)"/>
|
||||||
|
<polygon points="210,113 128,143 128,215 184,179" fill="url(#rightFacetGrad)"/>
|
||||||
|
|
||||||
|
<!-- Brilho estelar central -->
|
||||||
|
<circle cx="128" cy="128" r="38" fill="url(#centerGlow)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoIconBg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1976d2"/>
|
||||||
|
<stop offset="50%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoTopFacet" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
|
||||||
|
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoLeftFacet" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.8"/>
|
||||||
|
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoRightFacet" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.9"/>
|
||||||
|
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.5"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Container do Ícone Cristal 3D -->
|
||||||
|
<rect x="14" y="16" width="88" height="88" rx="24" ry="24" fill="url(#logoIconBg)"/>
|
||||||
|
<rect x="16" y="18" width="84" height="84" rx="22" ry="22" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-opacity="0.25"/>
|
||||||
|
|
||||||
|
<!-- Facetas Cristalinas (cx=58, cy=60, sz=63) -->
|
||||||
|
<polygon points="58,32 86,55 58,66 30,55" fill="url(#logoTopFacet)"/>
|
||||||
|
<polygon points="30,55 58,66 58,92 39,79" fill="url(#logoLeftFacet)"/>
|
||||||
|
<polygon points="86,55 58,66 58,92 77,79" fill="url(#logoRightFacet)"/>
|
||||||
|
<circle cx="58" cy="60" r="14" fill="#ffffff" fill-opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- Tipografia Oficial "CyberCore" -->
|
||||||
|
<text x="124" y="62" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="#0a2540">Cyber<tspan fill="#0288d1">Core</tspan></text>
|
||||||
|
|
||||||
|
<!-- Badge "SIGNER" -->
|
||||||
|
<rect x="124" y="76" width="94" height="22" rx="6" ry="6" fill="#0288d1"/>
|
||||||
|
<text x="139" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#ffffff" letter-spacing="1">SIGNER</text>
|
||||||
|
|
||||||
|
<!-- Subtítulo ICP-Brasil -->
|
||||||
|
<text x="230" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" fill="#64748b">ICP-Brasil • Assinador Digital Seguro</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" width="600" height="160">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bannerBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0d47a1"/>
|
||||||
|
<stop offset="60%" stop-color="#0284c7"/>
|
||||||
|
<stop offset="100%" stop-color="#0369a1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="bannerIconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#1e88e5"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo do Banner -->
|
||||||
|
<rect x="0" y="0" width="600" height="160" rx="16" ry="16" fill="url(#bannerBgGrad)"/>
|
||||||
|
|
||||||
|
<!-- Formas geométricas de segurança no fundo -->
|
||||||
|
<circle cx="480" cy="50" r="110" fill="#ffffff" fill-opacity="0.08"/>
|
||||||
|
<circle cx="540" cy="120" r="80" fill="#ffffff" fill-opacity="0.06"/>
|
||||||
|
<circle cx="380" cy="-20" r="90" fill="#ffffff" fill-opacity="0.04"/>
|
||||||
|
|
||||||
|
<!-- Ícone Cristal à esquerda -->
|
||||||
|
<rect x="30" y="35" width="90" height="90" rx="22" ry="22" fill="url(#bannerIconGrad)"/>
|
||||||
|
<rect x="33" y="38" width="84" height="84" rx="19" ry="19" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.3"/>
|
||||||
|
<polygon points="75,52 101,80 75,108 49,80" fill="#ffffff" fill-opacity="0.9"/>
|
||||||
|
<polygon points="75,52 75,108 49,80" fill="#80d8ff" fill-opacity="0.5"/>
|
||||||
|
|
||||||
|
<!-- Textos do Banner -->
|
||||||
|
<text x="148" y="74" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" font-weight="bold" fill="#ffffff">CyberCore Signer</text>
|
||||||
|
<text x="148" y="104" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="normal" fill="#e0f2fe">Agente de Assinatura Digital ICP-Brasil A1 & A3</text>
|
||||||
|
<text x="148" y="128" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#38bdf8" letter-spacing="1">PADES • XADES • CADES • TIMESTAMPS • CCSP/1</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 164" width="640" height="164">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#0288d1"/>
|
||||||
|
<stop offset="100%" stop-color="#01579b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Fundo do Selo -->
|
||||||
|
<rect x="2" y="2" width="636" height="160" rx="12" ry="12" fill="#f8fafc" stroke="#0288d1" stroke-width="2.5"/>
|
||||||
|
|
||||||
|
<!-- Brasão Notarial no lado esquerdo -->
|
||||||
|
<rect x="18" y="16" width="130" height="132" rx="10" ry="10" fill="url(#badgeGrad)"/>
|
||||||
|
|
||||||
|
<!-- Textos do Brasão -->
|
||||||
|
<text x="83" y="44" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">ASSINADO</text>
|
||||||
|
<text x="83" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">DIGITALMENTE</text>
|
||||||
|
|
||||||
|
<!-- Estrela dourada -->
|
||||||
|
<circle cx="83" cy="85" r="14" fill="#fbbf24"/>
|
||||||
|
<text x="83" y="91" font-family="'Segoe UI', sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">★</text>
|
||||||
|
|
||||||
|
<text x="83" y="126" font-family="'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="bold" fill="#e0f2fe" text-anchor="middle" letter-spacing="1">ICP-BRASIL</text>
|
||||||
|
|
||||||
|
<!-- Linha divisória vertical -->
|
||||||
|
<line x1="166" y1="16" x2="166" y2="148" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||||
|
|
||||||
|
<!-- Informações da Assinatura -->
|
||||||
|
<text x="184" y="36" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#64748b" letter-spacing="0.5">DOCUMENTO ASSINADO DIGITALMENTE POR:</text>
|
||||||
|
<text x="184" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="17" font-weight="bold" fill="#0a2540">NOME DO TITULAR DO CERTIFICADO</text>
|
||||||
|
<text x="184" y="82" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">CPF: ***.000.000-**</text>
|
||||||
|
<text x="184" y="102" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">Data e Hora: 11/10/2026 12:00:00 -03:00</text>
|
||||||
|
<text x="184" y="128" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-style="italic" fill="#0288d1">A conformidade com a ICP-Brasil pode ser verificada no ITI.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -6,6 +6,7 @@
|
|||||||
.root {
|
.root {
|
||||||
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-secondary: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
-fx-background: #f1f5f9;
|
-fx-background: #f1f5f9;
|
||||||
-fx-control-inner-background: #ffffff;
|
-fx-control-inner-background: #ffffff;
|
||||||
-fx-text-fill: #0f172a;
|
-fx-text-fill: #0f172a;
|
||||||
|
-fx-text-secondary: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light.cyber-root,
|
.theme-light.cyber-root,
|
||||||
@@ -438,6 +440,7 @@
|
|||||||
-fx-background: #0b0f17;
|
-fx-background: #0b0f17;
|
||||||
-fx-control-inner-background: #131c2e;
|
-fx-control-inner-background: #131c2e;
|
||||||
-fx-text-fill: #f1f5f9;
|
-fx-text-fill: #f1f5f9;
|
||||||
|
-fx-text-secondary: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark.cyber-root,
|
.theme-dark.cyber-root,
|
||||||
@@ -855,7 +858,7 @@
|
|||||||
|
|
||||||
/* Botões Base e Proteção contra Truncamento com Reticências */
|
/* Botões Base e Proteção contra Truncamento com Reticências */
|
||||||
.button {
|
.button {
|
||||||
-fx-min-width: -fx-pref-size;
|
-fx-min-width: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -865,7 +868,6 @@
|
|||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
|
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
|
||||||
}
|
}
|
||||||
@@ -880,7 +882,6 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
@@ -910,7 +911,6 @@
|
|||||||
-fx-text-fill: #475569;
|
-fx-text-fill: #475569;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
@@ -933,14 +933,12 @@
|
|||||||
/* Botões Padronizados para Diálogos e Modais */
|
/* Botões Padronizados para Diálogos e Modais */
|
||||||
.btn-dialog {
|
.btn-dialog {
|
||||||
-fx-min-width: 110px;
|
-fx-min-width: 110px;
|
||||||
-fx-pref-width: -fx-pref-size;
|
|
||||||
-fx-pref-height: 34px;
|
-fx-pref-height: 34px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-dialog-wide {
|
.btn-dialog-wide {
|
||||||
-fx-min-width: 220px;
|
-fx-min-width: 220px;
|
||||||
-fx-pref-width: -fx-pref-size;
|
|
||||||
-fx-pref-height: 36px;
|
-fx-pref-height: 36px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
@@ -949,13 +947,12 @@
|
|||||||
.btn-compact {
|
.btn-compact {
|
||||||
-fx-padding: 5px 12px;
|
-fx-padding: 5px 12px;
|
||||||
-fx-font-size: 11.5px;
|
-fx-font-size: 11.5px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Botão de Perigo / Exclusão / Remoção */
|
/* Botão de Perigo / Exclusão / Remoção */
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
-fx-background-color: #fee2e2;
|
-fx-background-color: #fee2e8;
|
||||||
-fx-text-fill: #b91c1c;
|
-fx-text-fill: #b91c1c;
|
||||||
-fx-border-color: #fca5a5;
|
-fx-border-color: #fca5a5;
|
||||||
-fx-border-radius: 6px;
|
-fx-border-radius: 6px;
|
||||||
@@ -965,7 +962,6 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 6px 14px;
|
-fx-padding: 6px 14px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package br.com.cybercore.signer.ui.assets;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class AssetManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve carregar imagens institucionais em SVG do classpath ou pasta de assets")
|
||||||
|
void shouldLoadSvgImages() {
|
||||||
|
Node logo = AssetManager.getImageOrDefault("app-logo", 480, 120, null);
|
||||||
|
assertThat(logo).isNotNull();
|
||||||
|
|
||||||
|
Node banner = AssetManager.getImageOrDefault("brand-banner", 600, 160, null);
|
||||||
|
assertThat(banner).isNotNull();
|
||||||
|
|
||||||
|
Node icon = AssetManager.getImageOrDefault("app-icon", 256, 256, null);
|
||||||
|
assertThat(icon).isNotNull();
|
||||||
|
|
||||||
|
Node seal = AssetManager.getImageOrDefault("default-seal", 640, 164, null);
|
||||||
|
assertThat(seal).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve carregar ícones SVG dinâmicos através do AssetManager")
|
||||||
|
void shouldLoadSvgIcons() {
|
||||||
|
String[] iconNames = {
|
||||||
|
"pdf", "xml", "file", "batch", "attached", "co-sign",
|
||||||
|
"timestamp", "ltv", "validate", "validate-timestamp", "exit",
|
||||||
|
"logs", "settings", "tutorial", "cert", "about", "sun", "moon",
|
||||||
|
"shield", "seal", "check", "error", "star", "close", "arrow-right",
|
||||||
|
"zoom-in", "zoom-out", "zoom-fit", "refresh"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String name : iconNames) {
|
||||||
|
Node iconNode = AssetManager.getIconOrDefault(name, 24, null);
|
||||||
|
assertThat(iconNode).withFailMessage("Ícone SVG não encontrado ou nulo: %s", name).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve instanciar nós de ícones via fábrica SvgIcons com fallback")
|
||||||
|
void shouldCreateNodesViaSvgIcons() {
|
||||||
|
assertThat(SvgIcons.closeIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.arrowRightIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.zoomInIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.zoomOutIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.zoomFitIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
|
||||||
|
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
|
<script>
|
||||||
|
if (window.pdfjsLib) {
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-base: #090d16;
|
--bg-base: #090d16;
|
||||||
@@ -101,6 +107,7 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
background: linear-gradient(to right, #ffffff, #cbd5e1);
|
background: linear-gradient(to right, #ffffff, #cbd5e1);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,10 +622,13 @@
|
|||||||
<!-- Abas de Navegação -->
|
<!-- Abas de Navegação -->
|
||||||
<div class="tabs-nav">
|
<div class="tabs-nav">
|
||||||
<button class="tab-btn active" data-tab="tab-pdf">
|
<button class="tab-btn active" data-tab="tab-pdf">
|
||||||
<span>📄</span> Assinar PDF
|
<span>📄</span> Assinar PDF (Com Stamp Picker)
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-seals">
|
||||||
|
<span>🎨</span> Designer de Selos
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="tab-xml">
|
<button class="tab-btn" data-tab="tab-xml">
|
||||||
<span>📋</span> Assinar XML (NF-e)
|
<span>📋</span> Assinar XML (XAdES / NF-e)
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="tab-file">
|
<button class="tab-btn" data-tab="tab-file">
|
||||||
<span>📁</span> Arquivos em Geral
|
<span>📁</span> Arquivos em Geral
|
||||||
@@ -628,19 +638,19 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA 1: ASSINAR PDF -->
|
<!-- ABA 1: ASSINAR PDF (INTERATIVO COM RETORNO EM BASE64) -->
|
||||||
<div id="tab-pdf" class="tab-pane active">
|
<div id="tab-pdf" class="tab-pane active">
|
||||||
<div class="card-grid">
|
<div class="card-grid" style="grid-template-columns: 1fr;">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title"><span>📄</span> Seleção do Documento PDF</span>
|
<span class="card-title"><span>📄</span> Seleção do Documento & Posicionador de Selo (Stamp Picker)</span>
|
||||||
<span class="badge-tag">PAdES / Digest</span>
|
<span class="badge-tag">PAdES ISO 32000 / Base64</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pdfDropZone" class="drop-zone">
|
<div id="pdfDropZone" class="drop-zone">
|
||||||
<div class="drop-icon">📑</div>
|
<div class="drop-icon">📑</div>
|
||||||
<p style="font-size: 13px; font-weight: 600; color: #fff;">Arraste um PDF ou clique para selecionar</p>
|
<p style="font-size: 13px; font-weight: 600; color: #fff;">Arraste um PDF ou clique para selecionar</p>
|
||||||
<p style="font-size: 11px; color: var(--text-muted);">Suporta qualquer documento .pdf</p>
|
<p style="font-size: 11px; color: var(--text-muted);">O documento será renderizado abaixo para você escolher onde posicionar o selo</p>
|
||||||
<input type="file" id="pdfFileInput" accept="application/pdf" style="display: none;">
|
<input type="file" id="pdfFileInput" accept="application/pdf" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -649,80 +659,144 @@
|
|||||||
<strong id="pdfFileName" style="color: #fff;">contrato_prestacao.pdf</strong>
|
<strong id="pdfFileName" style="color: #fff;">contrato_prestacao.pdf</strong>
|
||||||
<div id="pdfFileSize" style="color: var(--text-muted); font-size: 11px;">0 KB</div>
|
<div id="pdfFileSize" style="color: var(--text-muted); font-size: 11px;">0 KB</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge-tag">PDF</span>
|
<span class="badge-tag">PDF Carregado</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contêiner do PdfStampPicker -->
|
||||||
|
<div id="stampPickerMount" style="margin-top: 14px; display: none;"></div>
|
||||||
|
|
||||||
|
<!-- Configurações do Selo e da Assinatura -->
|
||||||
|
<div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
||||||
|
<h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4>
|
||||||
|
<div style="display: grid; grid-template-columns: minmax(240px, 360px); gap: 14px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tema do Selo Visual:</label>
|
||||||
|
<select id="pdfSealThemeSelect" class="cert-bar-select">
|
||||||
|
<option value="icp-brasil">Selo Padrão ICP-Brasil (Oficial)</option>
|
||||||
|
<option value="custom">🎨 Selo Personalizado do Designer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<button id="btnSignPdfFull" class="btn btn-primary" style="flex: 2; padding: 12px; font-size: 13px;" disabled>
|
||||||
|
<span>✍️</span> Assinar PDF Completo no Agente (Devolve Base64)
|
||||||
|
</button>
|
||||||
|
<button id="btnSignPdfDigest" class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 12px;" disabled>
|
||||||
|
<span>🛡️</span> Assinar Apenas Digest (PAdES Remoto)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pdfAlert" class="alert alert-info" style="display: none; margin-top: 14px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card de Resultado do PDF Assinado -->
|
||||||
|
<div id="pdfResultCard" class="card" style="display: none;">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title"><span>🎉</span> PDF Assinado Retornado com Sucesso</span>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button id="btnDownloadPdf" class="btn btn-primary" style="padding: 6px 14px; font-size: 12px;">
|
||||||
|
<span>💾</span> Baixar PDF Assinado (.pdf)
|
||||||
|
</button>
|
||||||
|
<button id="btnCopyPdfBase64" class="btn btn-secondary" style="padding: 6px 12px; font-size: 12px;">
|
||||||
|
<span>📋</span> Copiar Base64
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-bottom: 14px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status da Operação:</label>
|
||||||
|
<div id="pdfResultStatus" style="font-weight: 700; color: var(--success); font-size: 13px;">Assinado com Sucesso</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Tamanho do PDF Assinado:</label>
|
||||||
|
<div id="pdfResultSize" style="font-family: var(--font-mono); font-size: 12px; color: #fff;">0 KB</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">SHA-256 do Documento Final:</label>
|
||||||
|
<div id="pdfResultSha256" style="font-family: var(--font-mono); font-size: 11px; color: var(--primary); word-break: break-all;">-</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Digest SHA-256 Calculado no Navegador (Base64):</label>
|
<label class="form-label">Conteúdo do PDF Assinado em Base64 (pronto para envio ao Backend):</label>
|
||||||
<div id="pdfDigestBox" class="code-box">Selecione um arquivo PDF para calcular o digest...</div>
|
<div id="pdfResultBase64" class="code-box" style="max-height: 140px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="btnSignPdf" class="btn btn-primary" style="padding: 12px;" disabled>
|
|
||||||
<span>✍️</span> Assinar PDF com CyberCore
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="pdfAlert" class="alert alert-info" style="display: none;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ABA 2: DESIGNER DE SELOS VISUAIS -->
|
||||||
|
<div id="tab-seals" class="tab-pane">
|
||||||
|
<div class="card-grid" style="grid-template-columns: 1fr;">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title"><span>🛡️</span> Resultado da Assinatura Digital</span>
|
<span class="card-title"><span>🎨</span> Designer Interativo de Selos Visuais</span>
|
||||||
|
<span class="badge-tag">Customização em Tempo Real</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 14px;">
|
||||||
|
Crie, personalize cores, tipografia, logotipos e metadados de selos visuais. As configurações geradas aqui podem ser enviadas diretamente na requisição de assinatura de PDF (<code style="color: var(--primary);">seal_custom</code>) ou salvas no sistema.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="form-group">
|
<div id="sealDesignerMount"></div>
|
||||||
<label class="form-label">Assinatura Digital Retornada (Base64):</label>
|
|
||||||
<div id="pdfSigOutput" class="code-box" style="min-height: 120px;">Aguardando assinatura do PDF...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Cadeia do Certificado Digital X.509:</label>
|
|
||||||
<div id="pdfCertOutput" class="code-box">Nenhum certificado emitido ainda.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABA 2: ASSINAR XML -->
|
<!-- ABA 3: ASSINAR XML -->
|
||||||
<div id="tab-xml" class="tab-pane">
|
<div id="tab-xml" class="tab-pane">
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title"><span>📋</span> Conteúdo do XML (NF-e / XMLDSig)</span>
|
<span class="card-title"><span>📋</span> Conteúdo do XML (NF-e / CT-e / NFS-e)</span>
|
||||||
<button id="btnLoadXmlSample" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px;">
|
<button id="btnLoadXmlSample" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px;">
|
||||||
Carregar Exemplo NF-e
|
Carregar Exemplo NF-e
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Estrutura XML para Assinatura:</label>
|
<label class="form-label">Perfil de Assinatura XML:</label>
|
||||||
<textarea id="xmlContentInput" rows="7" placeholder="Cole o XML ou selecione um arquivo..."></textarea>
|
<select id="xmlProfileSelect" class="cert-bar-select">
|
||||||
|
<option value="XADES_BES">XAdES-BES (Padrão Oficial ICP-Brasil / NF-e)</option>
|
||||||
|
<option value="XMLDSIG">XMLDSig W3C Padrão (Enveloped)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Digest SHA-256 Calculado no Navegador (Base64):</label>
|
<label class="form-label">Estrutura XML para Assinatura:</label>
|
||||||
<div id="xmlDigestBox" class="code-box">Aguardando XML...</div>
|
<textarea id="xmlContentInput" rows="8" placeholder="Cole o XML ou selecione um arquivo..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="btnSignXml" class="btn btn-primary" style="padding: 12px;" disabled>
|
<div style="display: flex; gap: 10px; margin-top: 4px;">
|
||||||
<span>✍️</span> Assinar XML com CyberCore
|
<button id="btnSignXmlFull" class="btn btn-primary" style="flex: 2; padding: 12px;" disabled>
|
||||||
|
<span>✍️</span> Assinar XML Completo no Agente
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btnSignXmlDigest" class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 12px;" disabled>
|
||||||
|
<span>🛡️</span> Apenas Digest
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="xmlAlert" class="alert alert-info" style="display: none;"></div>
|
<div id="xmlAlert" class="alert alert-info" style="display: none; margin-top: 14px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title"><span>🛡️</span> Pacote XML Assinado (XMLDSig)</span>
|
<span class="card-title"><span>🛡️</span> Pacote XML Assinado</span>
|
||||||
|
<div style="display: flex; gap: 6px;">
|
||||||
|
<button id="btnDownloadXml" class="btn btn-primary" style="padding: 4px 10px; font-size: 11px; display: none;">
|
||||||
|
<span>💾</span> Baixar XML
|
||||||
|
</button>
|
||||||
|
<button id="btnCopyXml" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px; display: none;">
|
||||||
|
<span>📋</span> Copiar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Bloco SignatureValue (Base64):</label>
|
<label class="form-label">XML Assinado Completo (com envelope de assinatura):</label>
|
||||||
<div id="xmlSigOutput" class="code-box" style="min-height: 120px;">Aguardando assinatura do XML...</div>
|
<div id="xmlSigOutput" class="code-box" style="min-height: 220px; max-height: 380px;">Aguardando submissão do XML para assinatura...</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Tag XMLDSig Incorporável:</label>
|
|
||||||
<div id="xmlTagOutput" class="code-box"><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -851,7 +925,7 @@
|
|||||||
|
|
||||||
<!-- Lógica Javascript com o SDK -->
|
<!-- Lógica Javascript com o SDK -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { CyberCoreSigner } from '../src/signer-sdk.js';
|
import { CyberCoreSigner, PdfStampPicker, PdfSealDesigner } from '../src/signer-sdk.js';
|
||||||
|
|
||||||
const signer = new CyberCoreSigner({
|
const signer = new CyberCoreSigner({
|
||||||
baseUrl: 'http://127.0.0.1:21325',
|
baseUrl: 'http://127.0.0.1:21325',
|
||||||
@@ -870,6 +944,16 @@
|
|||||||
let currentCertificates = [];
|
let currentCertificates = [];
|
||||||
let selectedCertificateId = null;
|
let selectedCertificateId = null;
|
||||||
|
|
||||||
|
// Componentes e Estado
|
||||||
|
let pdfStampPicker = null;
|
||||||
|
let pdfSealDesigner = null;
|
||||||
|
let currentPdfFile = null;
|
||||||
|
let currentPdfBytes = null;
|
||||||
|
let currentPdfDigest = null;
|
||||||
|
let currentCustomSealConfig = null;
|
||||||
|
let currentSignedPdfBase64 = null;
|
||||||
|
let currentSignedXml = null;
|
||||||
|
|
||||||
// Gerenciador de Abas
|
// Gerenciador de Abas
|
||||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||||
const tabPanes = document.querySelectorAll('.tab-pane');
|
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||||
@@ -879,7 +963,13 @@
|
|||||||
tabButtons.forEach(b => b.classList.remove('active'));
|
tabButtons.forEach(b => b.classList.remove('active'));
|
||||||
tabPanes.forEach(p => p.classList.remove('active'));
|
tabPanes.forEach(p => p.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
document.getElementById(btn.dataset.tab).classList.add('active');
|
const targetTab = btn.dataset.tab;
|
||||||
|
document.getElementById(targetTab).classList.add('active');
|
||||||
|
|
||||||
|
// Lazy-mount do Designer de Selos
|
||||||
|
if (targetTab === 'tab-seals' && !pdfSealDesigner) {
|
||||||
|
initSealDesigner();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -900,6 +990,29 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carregamento de Temas de Selos
|
||||||
|
async function loadSealThemes() {
|
||||||
|
try {
|
||||||
|
const themeSelect = document.getElementById('pdfSealThemeSelect');
|
||||||
|
const themes = await signer.getSealThemes();
|
||||||
|
if (themes && themes.length > 0) {
|
||||||
|
themeSelect.innerHTML = '';
|
||||||
|
themes.forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t.id;
|
||||||
|
opt.textContent = `${t.name} (${t.id})`;
|
||||||
|
themeSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
const customOpt = document.createElement('option');
|
||||||
|
customOpt.value = 'custom';
|
||||||
|
customOpt.textContent = '🎨 Selo Customizado do Designer';
|
||||||
|
themeSelect.appendChild(customOpt);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Não foi possível carregar temas dinâmicos de selos:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Carregamento de Certificados
|
// Carregamento de Certificados
|
||||||
async function loadCertificates() {
|
async function loadCertificates() {
|
||||||
try {
|
try {
|
||||||
@@ -913,10 +1026,35 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
certs.forEach((c, index) => {
|
certs.forEach((c) => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = c.id;
|
opt.value = c.id;
|
||||||
opt.textContent = `${c.common_name || c.alias} [${c.type}] (${c.device_name || c.provider_id})`;
|
|
||||||
|
let rawName = c.common_name || c.alias || 'Certificado';
|
||||||
|
let displayName = rawName;
|
||||||
|
let docNum = c.cpf || c.cnpj || c.cpf_cnpj || '';
|
||||||
|
|
||||||
|
if (rawName.includes(':')) {
|
||||||
|
const parts = rawName.split(':');
|
||||||
|
displayName = parts[0].trim();
|
||||||
|
if (!docNum && parts[1]) {
|
||||||
|
docNum = parts[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const digits = docNum ? String(docNum).replace(/\D/g, '') : '';
|
||||||
|
let docFormatted = '';
|
||||||
|
if (digits.length === 14) {
|
||||||
|
docFormatted = 'CNPJ ' + digits.substring(0, 2) + '.' + digits.substring(2, 5) + '.' + digits.substring(5, 8) + '/' + digits.substring(8, 12) + '-' + digits.substring(12, 14);
|
||||||
|
} else if (digits.length === 11) {
|
||||||
|
docFormatted = 'CPF ' + digits.substring(0, 3) + '.' + digits.substring(3, 6) + '.' + digits.substring(6, 9) + '-' + digits.substring(9, 11);
|
||||||
|
} else if (docNum) {
|
||||||
|
docFormatted = docNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeStr = '[' + (c.type || 'A1') + ']';
|
||||||
|
const text = docFormatted ? `${displayName}, ${docFormatted} ${typeStr}` : `${displayName} ${typeStr}`;
|
||||||
|
opt.textContent = text;
|
||||||
certSelect.appendChild(opt);
|
certSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -937,6 +1075,29 @@
|
|||||||
certTypeBadge.textContent = cert.type || 'A1';
|
certTypeBadge.textContent = cert.type || 'A1';
|
||||||
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
|
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
|
||||||
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
|
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
|
||||||
|
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
const rawDoc = cert.cpf || cert.cnpj || cert.cpf_cnpj || '';
|
||||||
|
const digits = String(rawDoc).replace(/\D/g, '');
|
||||||
|
let docStr = '';
|
||||||
|
if (digits.length === 14) {
|
||||||
|
docStr = 'CNPJ: ' + digits.substring(0, 2) + '.' + digits.substring(2, 5) + '.' + digits.substring(5, 8) + '/' + digits.substring(8, 12) + '-' + digits.substring(12, 14);
|
||||||
|
} else if (digits.length === 11) {
|
||||||
|
docStr = 'CPF: ' + digits.substring(0, 3) + '.' + digits.substring(3, 6) + '.' + digits.substring(6, 9) + '-' + digits.substring(9, 11);
|
||||||
|
} else {
|
||||||
|
docStr = cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil';
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanSignerName = cert.common_name || cert.alias || 'Titular';
|
||||||
|
if (cleanSignerName && cleanSignerName.includes(':')) {
|
||||||
|
cleanSignerName = cleanSignerName.split(':')[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfStampPicker.updateSealTheme(null, {
|
||||||
|
signerName: cleanSignerName,
|
||||||
|
signerDoc: docStr
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
certTypeBadge.style.display = 'none';
|
certTypeBadge.style.display = 'none';
|
||||||
certValidityText.textContent = '';
|
certValidityText.textContent = '';
|
||||||
@@ -949,10 +1110,24 @@
|
|||||||
|
|
||||||
function updateButtonStates() {
|
function updateButtonStates() {
|
||||||
const hasCert = Boolean(selectedCertificateId);
|
const hasCert = Boolean(selectedCertificateId);
|
||||||
document.getElementById('btnSignPdf').disabled = !hasCert || !currentPdfDigest;
|
const hasPdf = Boolean(currentPdfBytes);
|
||||||
document.getElementById('btnSignXml').disabled = !hasCert || !currentXmlDigest;
|
const hasXml = Boolean(document.getElementById('xmlContentInput').value.trim());
|
||||||
document.getElementById('btnSignGen').disabled = !hasCert || !currentGenDigest;
|
|
||||||
document.getElementById('btnAuthLogin').disabled = !hasCert;
|
const btnFull = document.getElementById('btnSignPdfFull');
|
||||||
|
const btnDigest = document.getElementById('btnSignPdfDigest');
|
||||||
|
if (btnFull) btnFull.disabled = !hasCert || !hasPdf;
|
||||||
|
if (btnDigest) btnDigest.disabled = !hasCert || !hasPdf;
|
||||||
|
|
||||||
|
const btnXmlFull = document.getElementById('btnSignXmlFull');
|
||||||
|
const btnXmlDigest = document.getElementById('btnSignXmlDigest');
|
||||||
|
if (btnXmlFull) btnXmlFull.disabled = !hasCert || !hasXml;
|
||||||
|
if (btnXmlDigest) btnXmlDigest.disabled = !hasCert || !hasXml;
|
||||||
|
|
||||||
|
const btnGen = document.getElementById('btnSignGen');
|
||||||
|
if (btnGen) btnGen.disabled = !hasCert || !currentGenDigest;
|
||||||
|
|
||||||
|
const btnAuth = document.getElementById('btnAuthLogin');
|
||||||
|
if (btnAuth) btnAuth.disabled = !hasCert;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conexão e Handshake
|
// Conexão e Handshake
|
||||||
@@ -964,6 +1139,7 @@
|
|||||||
statusDot.classList.add('online');
|
statusDot.classList.add('online');
|
||||||
statusText.textContent = 'Conectado & Autorizado';
|
statusText.textContent = 'Conectado & Autorizado';
|
||||||
await loadCertificates();
|
await loadCertificates();
|
||||||
|
await loadSealThemes();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Erro ao conectar: ' + err.message);
|
alert('Erro ao conectar: ' + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -976,25 +1152,31 @@
|
|||||||
const online = await checkAgentStatus();
|
const online = await checkAgentStatus();
|
||||||
if (online) {
|
if (online) {
|
||||||
await loadCertificates();
|
await loadCertificates();
|
||||||
|
await loadSealThemes();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// ABA 1: LÓGICA DE PDF
|
// ABA 1: LÓGICA DE PDF (STAMP PICKER & ASSINATURA BASE64)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
const pdfDropZone = document.getElementById('pdfDropZone');
|
const pdfDropZone = document.getElementById('pdfDropZone');
|
||||||
const pdfFileInput = document.getElementById('pdfFileInput');
|
const pdfFileInput = document.getElementById('pdfFileInput');
|
||||||
const pdfFileMeta = document.getElementById('pdfFileMeta');
|
const pdfFileMeta = document.getElementById('pdfFileMeta');
|
||||||
const pdfFileName = document.getElementById('pdfFileName');
|
const pdfFileName = document.getElementById('pdfFileName');
|
||||||
const pdfFileSize = document.getElementById('pdfFileSize');
|
const pdfFileSize = document.getElementById('pdfFileSize');
|
||||||
const pdfDigestBox = document.getElementById('pdfDigestBox');
|
const stampPickerMount = document.getElementById('stampPickerMount');
|
||||||
const btnSignPdf = document.getElementById('btnSignPdf');
|
const pdfOptionsSection = document.getElementById('pdfOptionsSection');
|
||||||
|
const pdfSealThemeSelect = document.getElementById('pdfSealThemeSelect');
|
||||||
|
const btnSignPdfFull = document.getElementById('btnSignPdfFull');
|
||||||
|
const btnSignPdfDigest = document.getElementById('btnSignPdfDigest');
|
||||||
const pdfAlert = document.getElementById('pdfAlert');
|
const pdfAlert = document.getElementById('pdfAlert');
|
||||||
const pdfSigOutput = document.getElementById('pdfSigOutput');
|
|
||||||
const pdfCertOutput = document.getElementById('pdfCertOutput');
|
|
||||||
|
|
||||||
let currentPdfFile = null;
|
const pdfResultCard = document.getElementById('pdfResultCard');
|
||||||
let currentPdfDigest = null;
|
const pdfResultSize = document.getElementById('pdfResultSize');
|
||||||
|
const pdfResultSha256 = document.getElementById('pdfResultSha256');
|
||||||
|
const pdfResultBase64 = document.getElementById('pdfResultBase64');
|
||||||
|
const btnDownloadPdf = document.getElementById('btnDownloadPdf');
|
||||||
|
const btnCopyPdfBase64 = document.getElementById('btnCopyPdfBase64');
|
||||||
|
|
||||||
pdfDropZone.addEventListener('click', () => pdfFileInput.click());
|
pdfDropZone.addEventListener('click', () => pdfFileInput.click());
|
||||||
pdfFileInput.addEventListener('change', e => {
|
pdfFileInput.addEventListener('change', e => {
|
||||||
@@ -1019,17 +1201,130 @@
|
|||||||
pdfFileMeta.style.display = 'flex';
|
pdfFileMeta.style.display = 'flex';
|
||||||
|
|
||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
|
currentPdfBytes = buffer;
|
||||||
currentPdfDigest = await signer._calculateSha256(buffer);
|
currentPdfDigest = await signer._calculateSha256(buffer);
|
||||||
pdfDigestBox.textContent = `SHA-256 (Base64): ${currentPdfDigest}\nArquivo: ${file.name} (${file.size} bytes)`;
|
|
||||||
|
stampPickerMount.style.display = 'block';
|
||||||
|
pdfOptionsSection.style.display = 'block';
|
||||||
|
|
||||||
|
const activeCert = currentCertificates.find(c => c.id === selectedCertificateId);
|
||||||
|
const currentThemeVal = pdfSealThemeSelect ? pdfSealThemeSelect.value : 'icp-brasil';
|
||||||
|
const initialTheme = (currentThemeVal === 'custom' && currentCustomSealConfig)
|
||||||
|
? currentCustomSealConfig
|
||||||
|
: currentThemeVal;
|
||||||
|
|
||||||
|
const rawCertName = activeCert ? (activeCert.common_name || activeCert.alias) : '';
|
||||||
|
const cleanCertName = rawCertName.includes(':') ? rawCertName.split(':')[0].trim() : rawCertName;
|
||||||
|
const initialSignerInfo = activeCert ? {
|
||||||
|
signerName: cleanCertName,
|
||||||
|
signerDoc: activeCert.cpf_cnpj ? `CPF: ${activeCert.cpf_cnpj}` : (activeCert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
|
||||||
|
} : {
|
||||||
|
signerName: 'Nome do Titular do Certificado',
|
||||||
|
signerDoc: 'CPF: ***.456.789-**'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pdfStampPicker) {
|
||||||
|
pdfStampPicker = new PdfStampPicker({
|
||||||
|
container: stampPickerMount,
|
||||||
|
pdf: buffer,
|
||||||
|
sealTheme: initialTheme,
|
||||||
|
signerInfo: initialSignerInfo,
|
||||||
|
initialScale: 1.0,
|
||||||
|
onPositionChange: (pos) => {
|
||||||
|
console.log('Posição do selo atualizada:', pos);
|
||||||
|
},
|
||||||
|
onSignRequest: () => {
|
||||||
|
btnSignPdfFull.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pdfStampPicker.updateSealTheme(initialTheme, initialSignerInfo);
|
||||||
|
pdfStampPicker.loadPdf(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
btnSignPdf.addEventListener('click', async () => {
|
// Sincroniza alteração de tema com o selo sobre o PDF
|
||||||
btnSignPdf.disabled = true;
|
pdfSealThemeSelect.addEventListener('change', () => {
|
||||||
btnSignPdf.innerHTML = '<span class="spinner"></span> Solicitando no Agente...';
|
const themeVal = pdfSealThemeSelect.value;
|
||||||
|
if (themeVal === 'custom') {
|
||||||
|
if (!currentCustomSealConfig && typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) {
|
||||||
|
currentCustomSealConfig = pdfSealDesigner.getConfig();
|
||||||
|
}
|
||||||
|
if (pdfStampPicker && currentCustomSealConfig) {
|
||||||
|
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
pdfStampPicker.updateSealTheme(themeVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assinatura Completa de PDF (Devolve PDF Assinado Base64)
|
||||||
|
btnSignPdfFull.addEventListener('click', async () => {
|
||||||
|
btnSignPdfFull.disabled = true;
|
||||||
|
btnSignPdfFull.innerHTML = '<span class="spinner"></span> Assinando no Agente...';
|
||||||
pdfAlert.style.display = 'block';
|
pdfAlert.style.display = 'block';
|
||||||
pdfAlert.className = 'alert alert-info';
|
pdfAlert.className = 'alert alert-info';
|
||||||
pdfAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer aberta no seu computador...';
|
pdfAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stampConfig = pdfStampPicker ? pdfStampPicker.getStampConfig() : { page: 1, x: 50, y: 50, scale: 1.0 };
|
||||||
|
const selectedTheme = pdfSealThemeSelect.value;
|
||||||
|
|
||||||
|
let effectiveCustomSeal = null;
|
||||||
|
if (selectedTheme === 'custom') {
|
||||||
|
if (currentCustomSealConfig) {
|
||||||
|
effectiveCustomSeal = currentCustomSealConfig;
|
||||||
|
} else if (typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) {
|
||||||
|
effectiveCustomSeal = pdfSealDesigner.getConfig();
|
||||||
|
currentCustomSealConfig = effectiveCustomSeal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signParams = {
|
||||||
|
certificateId: selectedCertificateId,
|
||||||
|
pdf: currentPdfFile || currentPdfBytes,
|
||||||
|
page: stampConfig.page,
|
||||||
|
x: stampConfig.x,
|
||||||
|
y: stampConfig.y,
|
||||||
|
scale: stampConfig.scale,
|
||||||
|
sealCustom: effectiveCustomSeal,
|
||||||
|
seal_custom: effectiveCustomSeal,
|
||||||
|
sealThemeId: (selectedTheme === 'custom' ? null : selectedTheme),
|
||||||
|
seal_theme_id: (selectedTheme === 'custom' ? null : selectedTheme)
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await signer.signPdfDocument(signParams);
|
||||||
|
|
||||||
|
pdfAlert.className = 'alert alert-success';
|
||||||
|
pdfAlert.textContent = '✓ Documento PDF assinado com sucesso via PAdES (ISO 32000)!';
|
||||||
|
|
||||||
|
const signedPdfBase64 = result.signedPdf || result.signed_pdf || '';
|
||||||
|
currentSignedPdfBase64 = signedPdfBase64;
|
||||||
|
pdfResultCard.style.display = 'block';
|
||||||
|
pdfResultSize.textContent = (signedPdfBase64.length * 0.75 / 1024).toFixed(1) + ' KB';
|
||||||
|
pdfResultSha256.textContent = result.sha256 || 'Calculado com sucesso';
|
||||||
|
pdfResultBase64.textContent = signedPdfBase64;
|
||||||
|
pdfResultCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} catch (err) {
|
||||||
|
pdfAlert.className = 'alert alert-error';
|
||||||
|
pdfAlert.textContent = 'Erro ao assinar PDF: ' + err.message;
|
||||||
|
} finally {
|
||||||
|
btnSignPdfFull.disabled = false;
|
||||||
|
btnSignPdfFull.innerHTML = '<span>✍️</span> Assinar PDF Completo no Agente (Devolve Base64)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assinatura via Digest (Fluxo Tradicional)
|
||||||
|
btnSignPdfDigest.addEventListener('click', async () => {
|
||||||
|
btnSignPdfDigest.disabled = true;
|
||||||
|
btnSignPdfDigest.innerHTML = '<span class="spinner"></span> Solicitando...';
|
||||||
|
pdfAlert.style.display = 'block';
|
||||||
|
pdfAlert.className = 'alert alert-info';
|
||||||
|
pdfAlert.textContent = 'Assinando digest do PDF no CyberCore Signer...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await signer.signPdf(currentPdfFile, {
|
const result = await signer.signPdf(currentPdfFile, {
|
||||||
@@ -1038,30 +1333,102 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
pdfAlert.className = 'alert alert-success';
|
pdfAlert.className = 'alert alert-success';
|
||||||
pdfAlert.textContent = `✓ Documento PDF assinado com sucesso via ${result.algorithm}!`;
|
pdfAlert.textContent = `✓ Digest assinado com sucesso! Algoritmo: ${result.algorithm}`;
|
||||||
pdfSigOutput.textContent = result.signature;
|
|
||||||
pdfCertOutput.textContent = result.certificate;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
pdfAlert.className = 'alert alert-error';
|
pdfAlert.className = 'alert alert-error';
|
||||||
pdfAlert.textContent = 'Erro ao assinar PDF: ' + err.message;
|
pdfAlert.textContent = 'Erro ao assinar digest: ' + err.message;
|
||||||
} finally {
|
} finally {
|
||||||
btnSignPdf.disabled = false;
|
btnSignPdfDigest.disabled = false;
|
||||||
btnSignPdf.innerHTML = '<span>✍️</span> Assinar PDF com CyberCore';
|
btnSignPdfDigest.innerHTML = '<span>🛡️</span> Assinar Apenas Digest (PAdES Remoto)';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Download do PDF Assinado
|
||||||
|
btnDownloadPdf.addEventListener('click', () => {
|
||||||
|
if (!currentSignedPdfBase64) return;
|
||||||
|
const blob = signer._base64ToBlob(currentSignedPdfBase64, 'application/pdf');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const origName = currentPdfFile ? currentPdfFile.name.replace(/\.pdf$/i, '') : 'documento';
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${origName}_assinado.pdf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cópia do Base64 do PDF
|
||||||
|
btnCopyPdfBase64.addEventListener('click', async () => {
|
||||||
|
if (!currentSignedPdfBase64) return;
|
||||||
|
await navigator.clipboard.writeText(currentSignedPdfBase64);
|
||||||
|
const origText = btnCopyPdfBase64.innerHTML;
|
||||||
|
btnCopyPdfBase64.innerHTML = '<span>✓</span> Copiado!';
|
||||||
|
setTimeout(() => { btnCopyPdfBase64.innerHTML = origText; }, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// ABA 2: LÓGICA DE XML
|
// ABA 2: DESIGNER DE SELOS VISUAIS
|
||||||
|
// =========================================================================
|
||||||
|
function initSealDesigner() {
|
||||||
|
const mount = document.getElementById('sealDesignerMount');
|
||||||
|
pdfSealDesigner = new PdfSealDesigner({
|
||||||
|
container: mount,
|
||||||
|
initialTheme: {
|
||||||
|
name: 'Selo Customizado',
|
||||||
|
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE',
|
||||||
|
primaryColor: '#0284c7',
|
||||||
|
secondaryColor: '#10b981',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
textColor: '#0f172a',
|
||||||
|
borderColor: '#38bdf8',
|
||||||
|
iconStyle: 'shield',
|
||||||
|
defaultCpfMode: 'MASKED',
|
||||||
|
defaultIncludeDateTime: true
|
||||||
|
},
|
||||||
|
onChange: (cfg) => {
|
||||||
|
currentCustomSealConfig = cfg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Inicializa currentCustomSealConfig com o estado atual do designer
|
||||||
|
currentCustomSealConfig = pdfSealDesigner.getConfig();
|
||||||
|
|
||||||
|
// Botão para aplicar o selo na aba de PDF
|
||||||
|
const applyBtnWrap = document.createElement('div');
|
||||||
|
applyBtnWrap.style.marginTop = '16px';
|
||||||
|
applyBtnWrap.style.display = 'flex';
|
||||||
|
applyBtnWrap.style.gap = '10px';
|
||||||
|
applyBtnWrap.innerHTML = `
|
||||||
|
<button id="btnApplySealToPdf" class="btn btn-primary" style="padding: 10px 18px; font-size: 13px;">
|
||||||
|
<span>🚀</span> Usar este Selo no Posicionador de PDF
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
mount.appendChild(applyBtnWrap);
|
||||||
|
|
||||||
|
document.getElementById('btnApplySealToPdf').addEventListener('click', () => {
|
||||||
|
currentCustomSealConfig = pdfSealDesigner.getConfig();
|
||||||
|
pdfSealThemeSelect.value = 'custom';
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
|
||||||
|
}
|
||||||
|
// Alterna para aba PDF
|
||||||
|
document.querySelector('[data-tab="tab-pdf"]').click();
|
||||||
|
alert('Selo customizado selecionado e aplicado ao posicionador de PDF!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ABA 3: LÓGICA DE XML (XAdES-BES & XMLDSig)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
const xmlContentInput = document.getElementById('xmlContentInput');
|
const xmlContentInput = document.getElementById('xmlContentInput');
|
||||||
const xmlDigestBox = document.getElementById('xmlDigestBox');
|
const xmlProfileSelect = document.getElementById('xmlProfileSelect');
|
||||||
const btnSignXml = document.getElementById('btnSignXml');
|
const btnSignXmlFull = document.getElementById('btnSignXmlFull');
|
||||||
|
const btnSignXmlDigest = document.getElementById('btnSignXmlDigest');
|
||||||
const btnLoadXmlSample = document.getElementById('btnLoadXmlSample');
|
const btnLoadXmlSample = document.getElementById('btnLoadXmlSample');
|
||||||
const xmlAlert = document.getElementById('xmlAlert');
|
const xmlAlert = document.getElementById('xmlAlert');
|
||||||
const xmlSigOutput = document.getElementById('xmlSigOutput');
|
const xmlSigOutput = document.getElementById('xmlSigOutput');
|
||||||
const xmlTagOutput = document.getElementById('xmlTagOutput');
|
const btnDownloadXml = document.getElementById('btnDownloadXml');
|
||||||
|
const btnCopyXml = document.getElementById('btnCopyXml');
|
||||||
let currentXmlDigest = null;
|
|
||||||
|
|
||||||
const sampleXml = `<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
|
const sampleXml = `<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
|
||||||
<NFe>
|
<NFe>
|
||||||
@@ -1083,30 +1450,52 @@
|
|||||||
</NFe>
|
</NFe>
|
||||||
</nfeProc>`;
|
</nfeProc>`;
|
||||||
|
|
||||||
async function updateXmlDigest() {
|
xmlContentInput.addEventListener('input', () => updateButtonStates());
|
||||||
const xml = xmlContentInput.value.trim();
|
|
||||||
if (!xml) {
|
|
||||||
currentXmlDigest = null;
|
|
||||||
xmlDigestBox.textContent = 'Digite ou cole o XML para calcular o digest...';
|
|
||||||
} else {
|
|
||||||
currentXmlDigest = await signer._calculateSha256(xml);
|
|
||||||
xmlDigestBox.textContent = `SHA-256 Digest (Base64): ${currentXmlDigest}\nTamanho: ${xml.length} caracteres`;
|
|
||||||
}
|
|
||||||
updateButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
xmlContentInput.addEventListener('input', updateXmlDigest);
|
|
||||||
btnLoadXmlSample.addEventListener('click', () => {
|
btnLoadXmlSample.addEventListener('click', () => {
|
||||||
xmlContentInput.value = sampleXml;
|
xmlContentInput.value = sampleXml;
|
||||||
updateXmlDigest();
|
updateButtonStates();
|
||||||
});
|
});
|
||||||
|
|
||||||
btnSignXml.addEventListener('click', async () => {
|
// Assinatura Completa de XML
|
||||||
btnSignXml.disabled = true;
|
btnSignXmlFull.addEventListener('click', async () => {
|
||||||
btnSignXml.innerHTML = '<span class="spinner"></span> Solicitando no Agente...';
|
btnSignXmlFull.disabled = true;
|
||||||
|
btnSignXmlFull.innerHTML = '<span class="spinner"></span> Assinando no Agente...';
|
||||||
xmlAlert.style.display = 'block';
|
xmlAlert.style.display = 'block';
|
||||||
xmlAlert.className = 'alert alert-info';
|
xmlAlert.className = 'alert alert-info';
|
||||||
xmlAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer...';
|
xmlAlert.textContent = 'Assinando XML no CyberCore Signer...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xml = xmlContentInput.value.trim();
|
||||||
|
const profile = xmlProfileSelect.value;
|
||||||
|
|
||||||
|
const result = await signer.signXmlDocument({
|
||||||
|
certificateId: selectedCertificateId,
|
||||||
|
xml: xml,
|
||||||
|
profile: profile
|
||||||
|
});
|
||||||
|
|
||||||
|
xmlAlert.className = 'alert alert-success';
|
||||||
|
xmlAlert.textContent = `✓ XML assinado com sucesso via perfil ${result.profile || profile}!`;
|
||||||
|
currentSignedXml = result.signed_xml;
|
||||||
|
xmlSigOutput.textContent = result.signed_xml;
|
||||||
|
btnDownloadXml.style.display = 'inline-flex';
|
||||||
|
btnCopyXml.style.display = 'inline-flex';
|
||||||
|
} catch (err) {
|
||||||
|
xmlAlert.className = 'alert alert-error';
|
||||||
|
xmlAlert.textContent = 'Erro ao assinar XML: ' + err.message;
|
||||||
|
} finally {
|
||||||
|
btnSignXmlFull.disabled = false;
|
||||||
|
btnSignXmlFull.innerHTML = '<span>✍️</span> Assinar XML Completo no Agente';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assinatura via Digest de XML
|
||||||
|
btnSignXmlDigest.addEventListener('click', async () => {
|
||||||
|
btnSignXmlDigest.disabled = true;
|
||||||
|
btnSignXmlDigest.innerHTML = '<span class="spinner"></span> Solicitando...';
|
||||||
|
xmlAlert.style.display = 'block';
|
||||||
|
xmlAlert.className = 'alert alert-info';
|
||||||
|
xmlAlert.textContent = 'Assinando digest do XML no CyberCore Signer...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await signer.signXml(xmlContentInput.value, {
|
const result = await signer.signXml(xmlContentInput.value, {
|
||||||
@@ -1115,33 +1504,40 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
xmlAlert.className = 'alert alert-success';
|
xmlAlert.className = 'alert alert-success';
|
||||||
xmlAlert.textContent = `✓ XML assinado com sucesso via ${result.algorithm}!`;
|
xmlAlert.textContent = `✓ Digest XML assinado com sucesso via ${result.algorithm}!`;
|
||||||
xmlSigOutput.textContent = result.signature;
|
xmlSigOutput.textContent = result.signature;
|
||||||
|
|
||||||
const xmlSigSnippet = `<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
|
|
||||||
<SignedInfo>
|
|
||||||
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
|
||||||
<Reference URI="#NFe35261012345678000199550010000001001234567890">
|
|
||||||
<DigestValue>${result.digest}</DigestValue>
|
|
||||||
</Reference>
|
|
||||||
</SignedInfo>
|
|
||||||
<SignatureValue>${result.signature.slice(0, 48)}...</SignatureValue>
|
|
||||||
<KeyInfo>
|
|
||||||
<X509Data>
|
|
||||||
<X509Certificate>${result.certificate.slice(0, 48)}...</X509Certificate>
|
|
||||||
</X509Data>
|
|
||||||
</KeyInfo>
|
|
||||||
</Signature>`;
|
|
||||||
xmlTagOutput.textContent = xmlSigSnippet;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
xmlAlert.className = 'alert alert-error';
|
xmlAlert.className = 'alert alert-error';
|
||||||
xmlAlert.textContent = 'Erro ao assinar XML: ' + err.message;
|
xmlAlert.textContent = 'Erro ao assinar digest do XML: ' + err.message;
|
||||||
} finally {
|
} finally {
|
||||||
btnSignXml.disabled = false;
|
btnSignXmlDigest.disabled = false;
|
||||||
btnSignXml.innerHTML = '<span>✍️</span> Assinar XML com CyberCore';
|
btnSignXmlDigest.innerHTML = '<span>🛡️</span> Apenas Digest';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Download do XML Assinado
|
||||||
|
btnDownloadXml.addEventListener('click', () => {
|
||||||
|
if (!currentSignedXml) return;
|
||||||
|
const blob = new Blob([currentSignedXml], { type: 'application/xml;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'documento_assinado.xml';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copiar XML
|
||||||
|
btnCopyXml.addEventListener('click', async () => {
|
||||||
|
if (!currentSignedXml) return;
|
||||||
|
await navigator.clipboard.writeText(currentSignedXml);
|
||||||
|
const origText = btnCopyXml.innerHTML;
|
||||||
|
btnCopyXml.innerHTML = '<span>✓</span> Copiado!';
|
||||||
|
setTimeout(() => { btnCopyXml.innerHTML = origText; }, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// ABA 3: LÓGICA DE ARQUIVOS GERAIS
|
// ABA 3: LÓGICA DE ARQUIVOS GERAIS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,705 @@
|
|||||||
|
/**
|
||||||
|
* CyberCore Signer - PdfSealDesigner
|
||||||
|
* Ferramenta visual interativa para criação, personalização e exportação
|
||||||
|
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class PdfSealDesigner {
|
||||||
|
/**
|
||||||
|
* @param {Object} options Configurações iniciais do designer.
|
||||||
|
* @param {HTMLElement|string} options.container Elemento onde renderizar o designer.
|
||||||
|
* @param {Object} [options.initialTheme] Configuração inicial de tema.
|
||||||
|
* @param {Function} [options.onChange] Callback chamado quando o tema for modificado: fn(theme).
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.container = typeof options.container === 'string'
|
||||||
|
? document.querySelector(options.container)
|
||||||
|
: options.container;
|
||||||
|
|
||||||
|
if (!this.container) {
|
||||||
|
throw new Error('Container DOM obrigatório para instanciar PdfSealDesigner.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onChange = options.onChange || options.onConfigChange || null;
|
||||||
|
|
||||||
|
const rawInitial = options.initialTheme || options.initialConfig || {};
|
||||||
|
const normalizedInitial = {
|
||||||
|
name: rawInitial.name,
|
||||||
|
description: rawInitial.description,
|
||||||
|
width: rawInitial.width,
|
||||||
|
height: rawInitial.height,
|
||||||
|
primaryColor: rawInitial.primaryColor || rawInitial.primary_color,
|
||||||
|
secondaryColor: rawInitial.secondaryColor || rawInitial.secondary_color,
|
||||||
|
backgroundColor: rawInitial.backgroundColor || rawInitial.background_color,
|
||||||
|
borderColor: rawInitial.borderColor || rawInitial.border_color,
|
||||||
|
textColor: rawInitial.textColor || rawInitial.text_color,
|
||||||
|
iconStyle: rawInitial.iconStyle || rawInitial.icon_style,
|
||||||
|
customLogoBase64: rawInitial.customLogoBase64 || rawInitial.custom_logo_base64,
|
||||||
|
customHeader: rawInitial.customHeader || rawInitial.custom_header || rawInitial.title,
|
||||||
|
customFooter: rawInitial.customFooter || rawInitial.custom_footer,
|
||||||
|
customFieldLabel: rawInitial.customFieldLabel || rawInitial.custom_field_label,
|
||||||
|
customFieldValue: rawInitial.customFieldValue || rawInitial.custom_field_value,
|
||||||
|
defaultCpfMode: rawInitial.defaultCpfMode || rawInitial.default_cpf_mode,
|
||||||
|
defaultIncludeDateTime: rawInitial.defaultIncludeDateTime !== undefined ? rawInitial.defaultIncludeDateTime : rawInitial.default_include_date_time
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove chaves indefinidas para não sobrescrever os defaults
|
||||||
|
Object.keys(normalizedInitial).forEach(k => normalizedInitial[k] === undefined && delete normalizedInitial[k]);
|
||||||
|
|
||||||
|
// Estado do tema
|
||||||
|
this.theme = {
|
||||||
|
name: 'Meu Selo Personalizado',
|
||||||
|
description: 'Selo visual corporativo customizado via CyberCore Web SDK',
|
||||||
|
width: 320,
|
||||||
|
height: 82,
|
||||||
|
primaryColor: '#0d47a1',
|
||||||
|
secondaryColor: '#0284c7',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#93c5fd',
|
||||||
|
textColor: '#0f172a',
|
||||||
|
iconStyle: 'shield',
|
||||||
|
customLogoBase64: null,
|
||||||
|
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE',
|
||||||
|
customFooter: 'Verifique a autenticidade da assinatura eletrônica em https://validar.iti.gov.br',
|
||||||
|
customFieldLabel: '',
|
||||||
|
customFieldValue: '',
|
||||||
|
defaultCpfMode: 'MASKED',
|
||||||
|
defaultIncludeDateTime: true,
|
||||||
|
...normalizedInitial
|
||||||
|
};
|
||||||
|
|
||||||
|
this._initDom();
|
||||||
|
this._attachEvents();
|
||||||
|
this._renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initDom() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="cyber-designer-root">
|
||||||
|
<!-- Cabeçalho -->
|
||||||
|
<div class="designer-header">
|
||||||
|
<h3>🎨 Criador de Selo Visual (PAdES / ICP-Brasil)</h3>
|
||||||
|
<p>Personalize cores, brasão, logotipo próprio e regras LGPD para a estampa no PDF.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="designer-body">
|
||||||
|
<!-- Coluna 1: Formulário de Configuração -->
|
||||||
|
<div class="designer-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Nome do Selo:</label>
|
||||||
|
<input type="text" class="input-theme-name" value="${this.theme.name}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section-title">Paleta de Cores</div>
|
||||||
|
<div class="colors-grid">
|
||||||
|
<div class="color-picker-item">
|
||||||
|
<label>Primária:</label>
|
||||||
|
<div class="color-input-wrap">
|
||||||
|
<input type="color" class="color-primary" value="${this.theme.primaryColor}" />
|
||||||
|
<span class="color-hex-text">${this.theme.primaryColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-item">
|
||||||
|
<label>Secundária:</label>
|
||||||
|
<div class="color-input-wrap">
|
||||||
|
<input type="color" class="color-secondary" value="${this.theme.secondaryColor}" />
|
||||||
|
<span class="color-hex-text">${this.theme.secondaryColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-item">
|
||||||
|
<label>Fundo:</label>
|
||||||
|
<div class="color-input-wrap">
|
||||||
|
<input type="color" class="color-bg" value="${this.theme.backgroundColor}" />
|
||||||
|
<span class="color-hex-text">${this.theme.backgroundColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-item">
|
||||||
|
<label>Borda:</label>
|
||||||
|
<div class="color-input-wrap">
|
||||||
|
<input type="color" class="color-border" value="${this.theme.borderColor}" />
|
||||||
|
<span class="color-hex-text">${this.theme.borderColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-item">
|
||||||
|
<label>Texto:</label>
|
||||||
|
<div class="color-input-wrap">
|
||||||
|
<input type="color" class="color-text" value="${this.theme.textColor}" />
|
||||||
|
<span class="color-hex-text">${this.theme.textColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section-title">Ícone e Emblema</div>
|
||||||
|
<div class="icon-selection-row">
|
||||||
|
<label class="radio-icon-item">
|
||||||
|
<input type="radio" name="iconStyle" value="shield" ${this.theme.iconStyle === 'shield' ? 'checked' : ''} />
|
||||||
|
<span>🛡️ Escudo ICP</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-icon-item">
|
||||||
|
<input type="radio" name="iconStyle" value="star" ${this.theme.iconStyle === 'star' ? 'checked' : ''} />
|
||||||
|
<span>⭐ Gov.br</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-icon-item">
|
||||||
|
<input type="radio" name="iconStyle" value="crystal" ${this.theme.iconStyle === 'crystal' ? 'checked' : ''} />
|
||||||
|
<span>💎 Modern</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-icon-item">
|
||||||
|
<input type="radio" name="iconStyle" value="notary" ${this.theme.iconStyle === 'notary' ? 'checked' : ''} />
|
||||||
|
<span>📜 Notarial</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row logo-upload-row">
|
||||||
|
<label>Logotipo Próprio (PNG/JPG):</label>
|
||||||
|
<div class="logo-actions">
|
||||||
|
<label class="btn-designer-upload">
|
||||||
|
<span>📷</span> Escolher Imagem
|
||||||
|
<input type="file" accept="image/png,image/jpeg,image/svg+xml" class="designer-logo-file" style="display:none;" />
|
||||||
|
</label>
|
||||||
|
<button type="button" class="btn-remove-logo" style="${this.theme.customLogoBase64 ? 'display:inline-block;' : 'display:none;'}">Remover</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section-title">Identificação & LGPD</div>
|
||||||
|
<div class="form-row-grid">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Rótulo Extra:</label>
|
||||||
|
<input type="text" class="input-field-label" placeholder="Ex: Cargo ou Matrícula" value="${this.theme.customFieldLabel || ''}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Valor Extra:</label>
|
||||||
|
<input type="text" class="input-field-value" placeholder="Ex: Auditor Fiscal" value="${this.theme.customFieldValue || ''}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row-grid">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Exibição do CPF:</label>
|
||||||
|
<select class="select-cpf-mode">
|
||||||
|
<option value="FULL" ${this.theme.defaultCpfMode === 'FULL' ? 'selected' : ''}>Completo (123.456.789-00)</option>
|
||||||
|
<option value="MASKED" ${this.theme.defaultCpfMode === 'MASKED' ? 'selected' : ''}>Mascarado (***.456.789-**)</option>
|
||||||
|
<option value="NONE" ${this.theme.defaultCpfMode === 'NONE' ? 'selected' : ''}>Ocultar CPF</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row checkbox-row">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" class="chk-date-time" ${this.theme.defaultIncludeDateTime ? 'checked' : ''} />
|
||||||
|
<span>Incluir Data e Hora</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Coluna 2: Live Preview e Exportação -->
|
||||||
|
<div class="designer-preview-column">
|
||||||
|
<div class="preview-card">
|
||||||
|
<h4>👁️ Pré-visualização em Alta Resolução (320x82 pt)</h4>
|
||||||
|
<div class="seal-live-preview-box">
|
||||||
|
<!-- SVG Dinâmico -->
|
||||||
|
</div>
|
||||||
|
<small class="preview-note">Renderização visual idêntica à estampada no PDFBox do agente desktop.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="json-export-card">
|
||||||
|
<div class="json-export-header">
|
||||||
|
<h4>📋 Objeto de Configuração (JSON)</h4>
|
||||||
|
<button type="button" class="btn-copy-theme-json">Copiar JSON</button>
|
||||||
|
</div>
|
||||||
|
<pre class="theme-json-box"><code>{}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this._injectStyles();
|
||||||
|
|
||||||
|
// Cache de elementos
|
||||||
|
this.root = this.container.querySelector('.cyber-designer-root');
|
||||||
|
this.previewBox = this.root.querySelector('.seal-live-preview-box');
|
||||||
|
this.jsonBox = this.root.querySelector('.theme-json-box code');
|
||||||
|
this.btnRemoveLogo = this.root.querySelector('.btn-remove-logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
_injectStyles() {
|
||||||
|
if (document.getElementById('cyber-designer-styles')) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'cyber-designer-styles';
|
||||||
|
style.textContent = `
|
||||||
|
.cyber-designer-root {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.designer-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.designer-header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
.designer-header p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.designer-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.designer-body {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #38bdf8;
|
||||||
|
margin: 14px 0 8px 0;
|
||||||
|
border-top: 1px solid #1e293b;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.form-row label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.form-row input[type="text"], .form-row select {
|
||||||
|
background: #090d16;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.form-row input[type="text"]:focus, .form-row select:focus {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.form-row-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.colors-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.color-picker-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.color-picker-item label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.color-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: #090d16;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
.color-input-wrap input[type="color"] {
|
||||||
|
border: none;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.color-hex-text {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.icon-selection-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.radio-icon-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: #090d16;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.logo-upload-row .logo-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.btn-designer-upload {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #38bdf8;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #38bdf8;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-remove-logo {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #f87171;
|
||||||
|
color: #f87171;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.checkbox-row {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.designer-preview-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.preview-card, .json-export-card {
|
||||||
|
background: #131c31;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.preview-card h4, .json-export-card h4 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.seal-live-preview-box {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
|
max-width: 340px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.seal-live-preview-box svg {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.preview-note {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.json-export-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.btn-copy-theme-json {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #24304f;
|
||||||
|
color: #38bdf8;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-copy-theme-json:hover {
|
||||||
|
background: #38bdf8;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
.theme-json-box {
|
||||||
|
background: #090d16;
|
||||||
|
border: 1px solid #1e293b;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #38bdf8;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.theme-json-box code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachEvents() {
|
||||||
|
const bindInput = (selector, key, formatFn = v => v) => {
|
||||||
|
this.root.querySelector(selector).addEventListener('input', e => {
|
||||||
|
this.theme[key] = formatFn(e.target.value);
|
||||||
|
this._onStateChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
bindInput('.input-theme-name', 'name');
|
||||||
|
bindInput('.input-field-label', 'customFieldLabel');
|
||||||
|
bindInput('.input-field-value', 'customFieldValue');
|
||||||
|
|
||||||
|
// Cores
|
||||||
|
const bindColor = (selector, key, labelSelector) => {
|
||||||
|
const el = this.root.querySelector(selector);
|
||||||
|
const label = this.root.querySelector(labelSelector);
|
||||||
|
el.addEventListener('input', e => {
|
||||||
|
this.theme[key] = e.target.value;
|
||||||
|
label.textContent = e.target.value;
|
||||||
|
this._onStateChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
bindColor('.color-primary', 'primaryColor', '.color-primary + .color-hex-text');
|
||||||
|
bindColor('.color-secondary', 'secondaryColor', '.color-secondary + .color-hex-text');
|
||||||
|
bindColor('.color-bg', 'backgroundColor', '.color-bg + .color-hex-text');
|
||||||
|
bindColor('.color-border', 'borderColor', '.color-border + .color-hex-text');
|
||||||
|
bindColor('.color-text', 'textColor', '.color-text + .color-hex-text');
|
||||||
|
|
||||||
|
// Ícones
|
||||||
|
this.root.querySelectorAll('input[name="iconStyle"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', e => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
this.theme.iconStyle = e.target.value;
|
||||||
|
this._onStateChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload de logo
|
||||||
|
const logoInput = this.root.querySelector('.designer-logo-file');
|
||||||
|
logoInput.addEventListener('change', async e => {
|
||||||
|
if (e.target.files.length) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = ev => {
|
||||||
|
this.theme.customLogoBase64 = ev.target.result;
|
||||||
|
this.btnRemoveLogo.style.display = 'inline-block';
|
||||||
|
this._onStateChange();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.btnRemoveLogo.addEventListener('click', () => {
|
||||||
|
this.theme.customLogoBase64 = null;
|
||||||
|
this.btnRemoveLogo.style.display = 'none';
|
||||||
|
logoInput.value = '';
|
||||||
|
this._onStateChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
// LGPD CPF
|
||||||
|
this.root.querySelector('.select-cpf-mode').addEventListener('change', e => {
|
||||||
|
this.theme.defaultCpfMode = e.target.value;
|
||||||
|
this._onStateChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data e hora
|
||||||
|
this.root.querySelector('.chk-date-time').addEventListener('change', e => {
|
||||||
|
this.theme.defaultIncludeDateTime = e.target.checked;
|
||||||
|
this._onStateChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copiar JSON
|
||||||
|
this.root.querySelector('.btn-copy-theme-json').addEventListener('click', () => {
|
||||||
|
const config = this.getThemeConfig();
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||||
|
const btn = this.root.querySelector('.btn-copy-theme-json');
|
||||||
|
btn.textContent = '✓ Copiado!';
|
||||||
|
setTimeout(() => (btn.textContent = 'Copiar JSON'), 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onStateChange() {
|
||||||
|
this._renderPreview();
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(this.getThemeConfig());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderPreview() {
|
||||||
|
const t = this.theme;
|
||||||
|
|
||||||
|
// CPF conforme a escolha
|
||||||
|
let docText = 'CPF: ***.456.789-**';
|
||||||
|
if (t.defaultCpfMode === 'FULL') docText = 'CPF: 123.456.789-00';
|
||||||
|
else if (t.defaultCpfMode === 'NONE') docText = '';
|
||||||
|
|
||||||
|
const dateText = t.defaultIncludeDateTime ? '11/10/2026 18:30:00' : '';
|
||||||
|
let extraText = '';
|
||||||
|
if (t.customFieldLabel || t.customFieldValue) {
|
||||||
|
const label = t.customFieldLabel ? `${t.customFieldLabel}: ` : '';
|
||||||
|
const val = t.customFieldValue || '';
|
||||||
|
extraText = `${label}${val}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconGraphic = '';
|
||||||
|
if (t.customLogoBase64) {
|
||||||
|
iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
|
} else {
|
||||||
|
switch (t.iconStyle) {
|
||||||
|
case 'star':
|
||||||
|
iconGraphic = `
|
||||||
|
<circle cx="28" cy="30" r="15" fill="${t.primaryColor}" opacity="0.12" />
|
||||||
|
<circle cx="28" cy="30" r="15" fill="none" stroke="${t.primaryColor}" stroke-width="1.5" />
|
||||||
|
<path d="M28 18 L31 26 L39 26 L33 31 L35 39 L28 34 L21 39 L23 31 L17 26 L25 26 Z" fill="${t.secondaryColor}" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'crystal':
|
||||||
|
iconGraphic = `
|
||||||
|
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="${t.primaryColor}" opacity="0.15" />
|
||||||
|
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="none" stroke="${t.primaryColor}" stroke-width="1.5" />
|
||||||
|
<line x1="28" y1="14" x2="28" y2="46" stroke="${t.secondaryColor}" stroke-width="1.5" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'notary':
|
||||||
|
iconGraphic = `
|
||||||
|
<circle cx="28" cy="30" r="16" fill="none" stroke="${t.primaryColor}" stroke-width="2" stroke-dasharray="3,2" />
|
||||||
|
<circle cx="28" cy="30" r="12" fill="${t.primaryColor}" opacity="0.1" />
|
||||||
|
<path d="M22 30 L26 34 L34 26" fill="none" stroke="${t.secondaryColor}" stroke-width="2" stroke-linecap="round" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'shield':
|
||||||
|
default:
|
||||||
|
iconGraphic = `
|
||||||
|
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="${t.primaryColor}" opacity="0.12" />
|
||||||
|
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="none" stroke="${t.primaryColor}" stroke-width="1.8" />
|
||||||
|
<rect x="23" y="27" width="10" height="8" rx="2" fill="${t.secondaryColor}" />
|
||||||
|
<path d="M25 27 V23.5 C25 21.8 26.3 20.5 28 20.5 C29.7 20.5 31 21.8 31 23.5 V27" fill="none" stroke="${t.secondaryColor}" stroke-width="1.5" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};">
|
||||||
|
<rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/>
|
||||||
|
<rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/>
|
||||||
|
|
||||||
|
<!-- Emblema / Brasão -->
|
||||||
|
<g id="seal-icon-group">
|
||||||
|
${iconGraphic}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Tipografia do Selo -->
|
||||||
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
|
||||||
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
|
||||||
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
|
||||||
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
|
||||||
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.previewBox.innerHTML = svg;
|
||||||
|
|
||||||
|
// Na visualização do código, trunca se o Base64 for gigante para preservar performance da UI
|
||||||
|
const displayConfig = { ...this.getThemeConfig() };
|
||||||
|
if (displayConfig.customLogoBase64 && displayConfig.customLogoBase64.length > 100) {
|
||||||
|
const lenKb = (displayConfig.customLogoBase64.length / 1024).toFixed(1);
|
||||||
|
displayConfig.customLogoBase64 = `${displayConfig.customLogoBase64.substring(0, 32)}... [${lenKb} KB]`;
|
||||||
|
}
|
||||||
|
this.jsonBox.textContent = JSON.stringify(displayConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna a configuração do tema pronto para uso no payload seal_custom de assinatura.
|
||||||
|
*/
|
||||||
|
getThemeConfig() {
|
||||||
|
return {
|
||||||
|
name: this.theme.name,
|
||||||
|
description: this.theme.description,
|
||||||
|
width: this.theme.width,
|
||||||
|
height: this.theme.height,
|
||||||
|
primaryColor: this.theme.primaryColor,
|
||||||
|
secondaryColor: this.theme.secondaryColor,
|
||||||
|
backgroundColor: this.theme.backgroundColor,
|
||||||
|
borderColor: this.theme.borderColor,
|
||||||
|
textColor: this.theme.textColor,
|
||||||
|
iconStyle: this.theme.iconStyle,
|
||||||
|
customLogoBase64: this.theme.customLogoBase64,
|
||||||
|
customHeader: this.theme.customHeader,
|
||||||
|
customFooter: this.theme.customFooter,
|
||||||
|
customFieldLabel: this.theme.customFieldLabel,
|
||||||
|
customFieldValue: this.theme.customFieldValue,
|
||||||
|
defaultCpfMode: this.theme.defaultCpfMode,
|
||||||
|
defaultIncludeDateTime: this.theme.defaultIncludeDateTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias conveniente para getThemeConfig().
|
||||||
|
*/
|
||||||
|
getConfig() {
|
||||||
|
return this.getThemeConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carrega um tema para edição.
|
||||||
|
*/
|
||||||
|
loadTheme(theme) {
|
||||||
|
this.theme = { ...this.theme, ...theme };
|
||||||
|
this._initDom();
|
||||||
|
this._attachEvents();
|
||||||
|
this._renderPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PdfSealDesigner;
|
||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
ReplayDetectedError
|
ReplayDetectedError
|
||||||
} from './errors.js';
|
} from './errors.js';
|
||||||
|
|
||||||
|
import { PdfStampPicker } from './pdf-stamp-picker.js';
|
||||||
|
import { PdfSealDesigner } from './pdf-seal-designer.js';
|
||||||
|
|
||||||
|
export { PdfStampPicker, PdfSealDesigner };
|
||||||
|
|
||||||
export class CyberCoreSigner {
|
export class CyberCoreSigner {
|
||||||
/**
|
/**
|
||||||
* Inicializa o cliente do SDK.
|
* Inicializa o cliente do SDK.
|
||||||
@@ -284,6 +289,286 @@ export class CyberCoreSigner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtém a lista de temas de selos visuais disponíveis e o selo padrão ativo no agente.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>} Objeto com a lista de selos e default_seal_id.
|
||||||
|
*/
|
||||||
|
async getSealThemes() {
|
||||||
|
const resp = await this._fetch('/ccsp/v1/seals', { method: 'GET' });
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assina um documento PDF completo no padrão PAdES com carimbo/selo visual estilizado.
|
||||||
|
* O PDF original pode ser fornecido como string Base64, File, Blob, ArrayBuffer ou Uint8Array.
|
||||||
|
* O CyberCore Signer aplica a assinatura incremental com estampa gráfica e retorna o PDF assinado completo em Base64.
|
||||||
|
*
|
||||||
|
* @param {Object} params Parâmetros da assinatura do documento PDF.
|
||||||
|
* @param {string|File|Blob|ArrayBuffer|Uint8Array} params.pdf PDF a ser assinado (Base64 ou arquivo binário).
|
||||||
|
* @param {string} params.certificateId Identificador ou hint do certificado digital A1/A3.
|
||||||
|
* @param {string} [params.documentName='documento.pdf'] Nome amigável do documento.
|
||||||
|
* @param {number} [params.page] Número da página (1-indexado) onde o selo deve ser posicionado.
|
||||||
|
* @param {string} [params.pageChoice='LAST_PAGE'] 'LAST_PAGE', 'FIRST_PAGE' ou null se page numérica for informada.
|
||||||
|
* @param {string} [params.position='CUSTOM'] 'BOTTOM_RIGHT', 'BOTTOM_LEFT', 'TOP_RIGHT', 'TOP_LEFT', 'CUSTOM'.
|
||||||
|
* @param {number} [params.x] Coordenada X em pontos tipográficos na página do PDF.
|
||||||
|
* @param {number} [params.y] Coordenada Y em pontos tipográficos na página do PDF.
|
||||||
|
* @param {number} [params.scale=1.0] Escala do selo (ex: 1.0 = 100%, 0.8 = 80%, 1.2 = 120%).
|
||||||
|
* @param {boolean} [params.visibleSeal=true] Se false, a assinatura PAdES não inclui estampa gráfica visível.
|
||||||
|
* @param {string} [params.sealThemeId] ID do tema do selo registrado no agente (ex: 'icp-brasil', 'gov-br').
|
||||||
|
* @param {Object} [params.sealCustom] Objeto de tema customizado inline completo.
|
||||||
|
* @param {string} [params.reason='Assinatura Digital ICP-Brasil'] Razão da assinatura.
|
||||||
|
* @param {string} [params.location='Brasil'] Localidade da assinatura.
|
||||||
|
* @param {string} [params.cpfDisplayMode] 'FULL', 'MASKED' ou 'NONE'.
|
||||||
|
* @param {boolean} [params.includeDateTime] Se deve exibir data/hora no gráfico.
|
||||||
|
* @param {string} [params.customExtraInfo] Texto extra como Cargo ou Matrícula.
|
||||||
|
* @param {number} [params.expiresInSeconds=180] Tempo de validade da requisição em segundos.
|
||||||
|
* @returns {Promise<Object>} Retorna o PDF assinado em Base64 e utilitários para download ou Blob.
|
||||||
|
*/
|
||||||
|
async signPdfDocument(params) {
|
||||||
|
if (!params) {
|
||||||
|
throw new CyberCoreError('Parâmetros de assinatura de PDF são obrigatórios.', 'BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const certHint = params.certificateId || params.certificateHint;
|
||||||
|
if (!certHint) {
|
||||||
|
throw new CertificateNotFoundError('É necessário especificar o certificado digital para assinar o PDF.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let pdfBase64;
|
||||||
|
if (typeof params.pdf === 'string') {
|
||||||
|
// Remove eventual prefixo data:application/pdf;base64,
|
||||||
|
pdfBase64 = params.pdf.includes(',') ? params.pdf.split(',')[1] : params.pdf;
|
||||||
|
} else if (params.pdf) {
|
||||||
|
const buffer = await this._fileToBuffer(params.pdf);
|
||||||
|
pdfBase64 = this._bufferToBase64(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pdfBase64) {
|
||||||
|
throw new CyberCoreError('Nenhum documento PDF fornecido para assinatura.', 'BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = this._generateUuid();
|
||||||
|
const nonce = this._generateUuid();
|
||||||
|
const expiresAt = new Date(Date.now() + (params.expiresInSeconds || 180) * 1000).toISOString();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
version: '1',
|
||||||
|
request_id: requestId,
|
||||||
|
application_id: this.applicationId,
|
||||||
|
operation: 'sign_pdf',
|
||||||
|
pdf: pdfBase64,
|
||||||
|
certificate_hint: certHint,
|
||||||
|
document_name: params.documentName || params.fileName || (params.pdf?.name) || 'documento.pdf',
|
||||||
|
page: params.page !== undefined ? Number(params.page) : null,
|
||||||
|
page_choice: params.pageChoice || null,
|
||||||
|
position: params.position || ((params.x !== undefined && params.y !== undefined) ? 'CUSTOM' : 'BOTTOM_RIGHT'),
|
||||||
|
x: params.x !== undefined ? Number(params.x) : null,
|
||||||
|
y: params.y !== undefined ? Number(params.y) : null,
|
||||||
|
scale: params.scale !== undefined ? Number(params.scale) : 1.0,
|
||||||
|
visible_seal: params.visibleSeal !== undefined ? Boolean(params.visibleSeal) : true,
|
||||||
|
seal_theme_id: params.sealThemeId || params.seal_theme_id || params.sealTheme || null,
|
||||||
|
seal_custom: params.sealCustom || params.seal_custom || null,
|
||||||
|
reason: params.reason || 'Assinatura Digital ICP-Brasil',
|
||||||
|
location: params.location || 'Brasil',
|
||||||
|
cpf_display_mode: params.cpfDisplayMode || params.cpf_display_mode || null,
|
||||||
|
include_date_time: params.includeDateTime !== undefined ? Boolean(params.includeDateTime) : (params.include_date_time !== undefined ? Boolean(params.include_date_time) : null),
|
||||||
|
custom_extra_info: params.customExtraInfo || params.custom_extra_info || null,
|
||||||
|
nonce: nonce,
|
||||||
|
expires_at: expiresAt
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await this._fetch('/ccsp/v1/sign-pdf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status === 'signed') {
|
||||||
|
const signedPdfBase64 = resp.signed_pdf;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: resp.request_id,
|
||||||
|
status: resp.status,
|
||||||
|
signedPdf: signedPdfBase64,
|
||||||
|
signed_pdf: signedPdfBase64,
|
||||||
|
sha256: resp.sha256,
|
||||||
|
signature: resp.signature,
|
||||||
|
certificate: resp.certificate,
|
||||||
|
certificateChain: resp.certificate_chain || [resp.certificate],
|
||||||
|
algorithm: resp.algorithm,
|
||||||
|
subjectName: resp.subject_name || null,
|
||||||
|
subjectCpfCnpj: resp.subject_cpf_cnpj || null,
|
||||||
|
documentName: resp.document_name || payload.document_name,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converte o PDF assinado em Blob binário para upload via FormData ou visualização.
|
||||||
|
* @returns {Blob}
|
||||||
|
*/
|
||||||
|
getSignedPdfBlob() {
|
||||||
|
return self._base64ToBlob(signedPdfBase64, 'application/pdf');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna os bytes puros do PDF assinado em Uint8Array.
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
getSignedPdfBytes() {
|
||||||
|
return self._base64ToBytes(signedPdfBase64);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispara o download automático do PDF assinado no navegador web.
|
||||||
|
* @param {string} [filename] Nome do arquivo para download.
|
||||||
|
*/
|
||||||
|
download(filename) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const blob = self._base64ToBlob(signedPdfBase64, 'application/pdf');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename || resp.document_name || 'documento_assinado.pdf';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._throwMappedError(resp.error_code, resp.message);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof CyberCoreError) throw err;
|
||||||
|
throw new CyberCoreError('Erro durante a assinatura do documento PDF: ' + err.message, 'SIGN_PDF_ERROR', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assina um documento XML completo no padrão XAdES-BES (ICP-Brasil) ou W3C XMLDSig Enveloped.
|
||||||
|
* O XML pode ser fornecido como string de texto puro ou arquivo/bytes.
|
||||||
|
*
|
||||||
|
* @param {Object} params Parâmetros da assinatura XML.
|
||||||
|
* @param {string|File|Blob|ArrayBuffer} params.xml Conteúdo do XML a ser assinado.
|
||||||
|
* @param {string} params.certificateId Identificador do certificado digital.
|
||||||
|
* @param {string} [params.profile='XADES_BES'] Perfil de assinatura: 'XADES_BES' ou 'XMLDSIG'.
|
||||||
|
* @param {string} [params.targetElementTag] Nome da tag alvo (ex: 'infNFe', 'infCte') ou null para o documento todo.
|
||||||
|
* @param {string} [params.targetElementId] ID do elemento alvo (ex: 'NFe3526...').
|
||||||
|
* @param {string} [params.documentName='documento.xml'] Nome amigável do arquivo XML.
|
||||||
|
* @param {string} [params.algorithm] Algoritmo XMLDSig.
|
||||||
|
* @param {number} [params.expiresInSeconds=180] Tempo de validade da requisição em segundos.
|
||||||
|
* @returns {Promise<Object>} Retorna o XML assinado como string de texto e helpers para Blob.
|
||||||
|
*/
|
||||||
|
async signXmlDocument(params) {
|
||||||
|
if (!params) {
|
||||||
|
throw new CyberCoreError('Parâmetros de assinatura XML são obrigatórios.', 'BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const certHint = params.certificateId || params.certificateHint;
|
||||||
|
if (!certHint) {
|
||||||
|
throw new CertificateNotFoundError('É necessário especificar o certificado digital para assinar o XML.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmlText = null;
|
||||||
|
let xmlBase64 = null;
|
||||||
|
|
||||||
|
if (typeof params.xml === 'string') {
|
||||||
|
xmlText = params.xml;
|
||||||
|
} else if (params.xml) {
|
||||||
|
const buffer = await this._fileToBuffer(params.xml);
|
||||||
|
xmlBase64 = this._bufferToBase64(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xmlText && !xmlBase64) {
|
||||||
|
throw new CyberCoreError('Nenhum documento XML fornecido para assinatura.', 'BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = this._generateUuid();
|
||||||
|
const nonce = this._generateUuid();
|
||||||
|
const expiresAt = new Date(Date.now() + (params.expiresInSeconds || 180) * 1000).toISOString();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
version: '1',
|
||||||
|
request_id: requestId,
|
||||||
|
application_id: this.applicationId,
|
||||||
|
operation: 'sign_xml',
|
||||||
|
xml: xmlText,
|
||||||
|
xml_base64: xmlBase64,
|
||||||
|
certificate_hint: certHint,
|
||||||
|
document_name: params.documentName || params.fileName || (params.xml?.name) || 'documento.xml',
|
||||||
|
profile: params.profile || 'XADES_BES',
|
||||||
|
target_element_tag: params.targetElementTag || null,
|
||||||
|
target_element_id: params.targetElementId || null,
|
||||||
|
algorithm: params.algorithm || null,
|
||||||
|
nonce: nonce,
|
||||||
|
expires_at: expiresAt
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await this._fetch('/ccsp/v1/sign-xml', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status === 'signed') {
|
||||||
|
const signedXmlText = resp.signed_xml;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: resp.request_id,
|
||||||
|
status: resp.status,
|
||||||
|
signedXml: signedXmlText,
|
||||||
|
sha256: resp.sha256,
|
||||||
|
signature: resp.signature,
|
||||||
|
certificate: resp.certificate,
|
||||||
|
certificateChain: resp.certificate_chain || [resp.certificate],
|
||||||
|
algorithm: resp.algorithm,
|
||||||
|
subjectName: resp.subject_name || null,
|
||||||
|
subjectCpfCnpj: resp.subject_cpf_cnpj || null,
|
||||||
|
documentName: resp.document_name || payload.document_name,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converte o XML assinado em Blob binário para upload.
|
||||||
|
* @returns {Blob}
|
||||||
|
*/
|
||||||
|
getSignedXmlBlob() {
|
||||||
|
return new Blob([signedXmlText], { type: 'application/xml;charset=utf-8' });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna os bytes UTF-8 do XML assinado em Uint8Array.
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
getSignedXmlBytes() {
|
||||||
|
return new TextEncoder().encode(signedXmlText);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispara o download automático do XML assinado no navegador web.
|
||||||
|
* @param {string} [filename]
|
||||||
|
*/
|
||||||
|
download(filename) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const blob = new Blob([signedXmlText], { type: 'application/xml;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename || resp.document_name || 'documento_assinado.xml';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._throwMappedError(resp.error_code, resp.message);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof CyberCoreError) throw err;
|
||||||
|
throw new CyberCoreError('Erro durante a assinatura do documento XML: ' + err.message, 'SIGN_XML_ERROR', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Realiza login / autenticação segura em sistemas web utilizando Certificado Digital (Challenge-Response ICP-Brasil).
|
* Realiza login / autenticação segura em sistemas web utilizando Certificado Digital (Challenge-Response ICP-Brasil).
|
||||||
* Assina o desafio (challenge) emitido pelo servidor e retorna o certificado e dados decodificados do titular.
|
* Assina o desafio (challenge) emitido pelo servidor e retorna o certificado e dados decodificados do titular.
|
||||||
@@ -407,8 +692,71 @@ export class CyberCoreSigner {
|
|||||||
// Métodos Utilitários Internos
|
// Métodos Utilitários Internos
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa requisições HTTP REST ao servidor local CCSP/1.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint Caminho do endpoint (ex: '/ccsp/v1/status').
|
||||||
|
* @param {Object} [options={}] Opções do fetch (method, body, timeoutMs, etc).
|
||||||
|
* @returns {Promise<Object>} Resposta JSON decodificada.
|
||||||
|
*/
|
||||||
|
async _fetch(endpoint, options = {}) {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const timeout = options.timeoutMs || 15000;
|
||||||
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||||
|
const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CyberCore-AppId': this.applicationId,
|
||||||
|
...(options.headers || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.body && !headers['Content-Type']) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controller) {
|
||||||
|
fetchOptions.signal = controller.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, fetchOptions);
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorCode = data.error_code || `HTTP_${res.status}`;
|
||||||
|
const message = data.message || `Erro retornado pelo agente (${res.status})`;
|
||||||
|
this._throwMappedError(errorCode, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new CyberCoreError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente.`, 'TIMEOUT_ERROR');
|
||||||
|
}
|
||||||
|
if (err instanceof CyberCoreError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new AgentNotRunningError(`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _fileToBuffer(file) {
|
async _fileToBuffer(file) {
|
||||||
if (file instanceof ArrayBuffer) return file;
|
if (file instanceof ArrayBuffer) {
|
||||||
|
if (file.byteLength === 0) {
|
||||||
|
throw new Error('O buffer binário está vazio ou foi desacoplado (detached). Forneça o objeto File ou Blob original.');
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
if (ArrayBuffer.isView(file)) {
|
if (ArrayBuffer.isView(file)) {
|
||||||
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
||||||
}
|
}
|
||||||
@@ -421,6 +769,38 @@ export class CyberCoreSigner {
|
|||||||
throw new Error('Tipo de arquivo não suportado para leitura binária.');
|
throw new Error('Tipo de arquivo não suportado para leitura binária.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_bufferToBase64(buffer) {
|
||||||
|
if (typeof Buffer !== 'undefined') {
|
||||||
|
return Buffer.from(buffer).toString('base64');
|
||||||
|
}
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64ToBytes(base64) {
|
||||||
|
const clean = base64.includes(',') ? base64.split(',')[1] : base64;
|
||||||
|
if (typeof Buffer !== 'undefined') {
|
||||||
|
return new Uint8Array(Buffer.from(clean, 'base64'));
|
||||||
|
}
|
||||||
|
const binary = atob(clean);
|
||||||
|
const len = binary.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64ToBlob(base64, mimeType = 'application/octet-stream') {
|
||||||
|
const bytes = this._base64ToBytes(base64);
|
||||||
|
return new Blob([bytes], { type: mimeType });
|
||||||
|
}
|
||||||
|
|
||||||
async _calculateSha256(data) {
|
async _calculateSha256(data) {
|
||||||
let buffer;
|
let buffer;
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
@@ -492,6 +872,8 @@ export class CyberCoreSigner {
|
|||||||
// Expõe globalmente caso seja carregado diretamente via tag <script>
|
// Expõe globalmente caso seja carregado diretamente via tag <script>
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.CyberCoreSigner = CyberCoreSigner;
|
window.CyberCoreSigner = CyberCoreSigner;
|
||||||
|
window.PdfStampPicker = PdfStampPicker;
|
||||||
|
window.PdfSealDesigner = PdfSealDesigner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CyberCoreSigner;
|
export default CyberCoreSigner;
|
||||||
|
|||||||
@@ -250,4 +250,131 @@ describe('@cybercore/signer-sdk - Testes Unitários do SDK', () => {
|
|||||||
assert.equal(res.subjectCpfCnpj, '98765432100');
|
assert.equal(res.subjectCpfCnpj, '98765432100');
|
||||||
assert.equal(capturedPayload.purpose, 'AUTHENTICATION');
|
assert.equal(capturedPayload.purpose, 'AUTHENTICATION');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deve consultar lista de temas de selos com getSealThemes()', async () => {
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
|
||||||
|
signer._fetch = async (endpoint, options) => {
|
||||||
|
assert.equal(endpoint, '/ccsp/v1/seals');
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
status: 'success',
|
||||||
|
default_seal_id: 'icp-brasil',
|
||||||
|
seals: [
|
||||||
|
{ id: 'icp-brasil', name: 'ICP-Brasil Oficial' },
|
||||||
|
{ id: 'gov-br', name: 'Gov.br Oficial' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await signer.getSealThemes();
|
||||||
|
assert.equal(res.status, 'success');
|
||||||
|
assert.equal(res.default_seal_id, 'icp-brasil');
|
||||||
|
assert.equal(res.seals.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve assinar documento PDF completo em Base64 com signPdfDocument()', async () => {
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
|
||||||
|
let capturedPayload = null;
|
||||||
|
let calledEndpoint = null;
|
||||||
|
const fakeSignedPdfB64 = Buffer.from('%PDF-1.4 Fake Signed PDF Content').toString('base64');
|
||||||
|
|
||||||
|
signer._fetch = async (endpoint, options) => {
|
||||||
|
calledEndpoint = endpoint;
|
||||||
|
capturedPayload = JSON.parse(options.body);
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
request_id: capturedPayload.request_id,
|
||||||
|
status: 'signed',
|
||||||
|
signed_pdf: fakeSignedPdfB64,
|
||||||
|
sha256: 'FAKESHA256HASH12345',
|
||||||
|
certificate: 'FAKECERTBASE64',
|
||||||
|
certificate_chain: ['FAKECERTBASE64'],
|
||||||
|
algorithm: 'SHA256withRSA',
|
||||||
|
subject_name: 'MARIA DA SILVA',
|
||||||
|
subject_cpf_cnpj: '12345678900',
|
||||||
|
document_name: capturedPayload.document_name
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawPdfBuffer = Buffer.from('%PDF-1.4 Document Original');
|
||||||
|
const res = await signer.signPdfDocument({
|
||||||
|
pdf: rawPdfBuffer,
|
||||||
|
certificateId: 'cert-a3-win',
|
||||||
|
documentName: 'contrato.pdf',
|
||||||
|
page: 2,
|
||||||
|
x: 350.5,
|
||||||
|
y: 50.0,
|
||||||
|
scale: 1.1,
|
||||||
|
sealThemeId: 'gov-br',
|
||||||
|
customExtraInfo: 'Matrícula: 1048'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(calledEndpoint, '/ccsp/v1/sign-pdf');
|
||||||
|
assert.equal(res.status, 'signed');
|
||||||
|
assert.equal(res.signedPdf, fakeSignedPdfB64);
|
||||||
|
assert.equal(res.sha256, 'FAKESHA256HASH12345');
|
||||||
|
assert.equal(res.subjectName, 'MARIA DA SILVA');
|
||||||
|
assert.equal(res.subjectCpfCnpj, '12345678900');
|
||||||
|
assert.equal(capturedPayload.page, 2);
|
||||||
|
assert.equal(capturedPayload.x, 350.5);
|
||||||
|
assert.equal(capturedPayload.y, 50.0);
|
||||||
|
assert.equal(capturedPayload.scale, 1.1);
|
||||||
|
assert.equal(capturedPayload.seal_theme_id, 'gov-br');
|
||||||
|
assert.equal(capturedPayload.custom_extra_info, 'Matrícula: 1048');
|
||||||
|
|
||||||
|
// Valida método helper de bytes
|
||||||
|
const signedBytes = res.getSignedPdfBytes();
|
||||||
|
assert.ok(signedBytes instanceof Uint8Array);
|
||||||
|
assert.ok(signedBytes.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve assinar documento XML completo com signXmlDocument()', async () => {
|
||||||
|
const signer = new CyberCoreSigner();
|
||||||
|
|
||||||
|
let capturedPayload = null;
|
||||||
|
let calledEndpoint = null;
|
||||||
|
const fakeSignedXml = '<nfeProc><NFe>...</NFe><ds:Signature>...</ds:Signature></nfeProc>';
|
||||||
|
|
||||||
|
signer._fetch = async (endpoint, options) => {
|
||||||
|
calledEndpoint = endpoint;
|
||||||
|
capturedPayload = JSON.parse(options.body);
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
request_id: capturedPayload.request_id,
|
||||||
|
status: 'signed',
|
||||||
|
signed_xml: fakeSignedXml,
|
||||||
|
sha256: 'XMLSHA256HASH',
|
||||||
|
certificate: 'FAKECERTBASE64',
|
||||||
|
certificate_chain: ['FAKECERTBASE64'],
|
||||||
|
algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
||||||
|
subject_name: 'CYBERCORE LTDA',
|
||||||
|
subject_cpf_cnpj: '12345678000199',
|
||||||
|
document_name: capturedPayload.document_name
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const xmlInput = '<nfeProc><NFe Id="NFe123">Dados</NFe></nfeProc>';
|
||||||
|
const res = await signer.signXmlDocument({
|
||||||
|
xml: xmlInput,
|
||||||
|
certificateId: 'cert-a1-pj',
|
||||||
|
documentName: 'nfe-123.xml',
|
||||||
|
profile: 'XADES_BES',
|
||||||
|
targetElementTag: 'NFe',
|
||||||
|
targetElementId: 'NFe123'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(calledEndpoint, '/ccsp/v1/sign-xml');
|
||||||
|
assert.equal(res.status, 'signed');
|
||||||
|
assert.equal(res.signedXml, fakeSignedXml);
|
||||||
|
assert.equal(res.subjectName, 'CYBERCORE LTDA');
|
||||||
|
assert.equal(capturedPayload.profile, 'XADES_BES');
|
||||||
|
assert.equal(capturedPayload.target_element_tag, 'NFe');
|
||||||
|
assert.equal(capturedPayload.target_element_id, 'NFe123');
|
||||||
|
|
||||||
|
// Helper de bytes
|
||||||
|
const xmlBytes = res.getSignedXmlBytes();
|
||||||
|
assert.ok(xmlBytes instanceof Uint8Array);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||