From ccfa358f97076e010d1e029ce2ddbd8c53f3cc81 Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Sun, 11 Oct 2026 08:20:24 -0300 Subject: [PATCH] feat: initial commit - cybercore signer desktop agent --- .agents/rules/main-rule.md | 250 ++++ .gitignore | 80 + .mvn/wrapper/maven-wrapper.properties | 3 + README.md | 100 ++ assets/README.md | 75 + assets/icons/about.svg | 1 + assets/icons/attached.svg | 12 + assets/icons/batch.svg | 13 + assets/icons/cert.svg | 11 + assets/icons/check.svg | 4 + assets/icons/co-sign.svg | 12 + assets/icons/error.svg | 5 + assets/icons/exit.svg | 8 + assets/icons/file.svg | 13 + assets/icons/logs.svg | 13 + assets/icons/ltv.svg | 11 + assets/icons/moon.svg | 6 + assets/icons/pdf.svg | 13 + assets/icons/seal.svg | 11 + assets/icons/settings.svg | 8 + assets/icons/shield.svg | 6 + assets/icons/star.svg | 4 + assets/icons/sun.svg | 15 + assets/icons/timestamp.svg | 13 + assets/icons/tutorial.svg | 8 + assets/icons/validate-timestamp.svg | 10 + assets/icons/validate.svg | 11 + assets/icons/xml.svg | 13 + assets/images/app-icon.png | Bin 0 -> 26558 bytes assets/images/app-logo.png | Bin 0 -> 10439 bytes assets/images/brand-banner.png | Bin 0 -> 27073 bytes assets/images/default-seal.png | Bin 0 -> 14256 bytes build-jar.sh | 130 ++ config/spotbugs/exclude.xml | 23 + docs/CCSP.md | 208 +++ docs/a1-a3-certificates.md | 46 + docs/architecture.md | 79 + docs/authentication-login.md | 71 + docs/development-mode.md | 48 + docs/diagnostics.md | 34 + docs/errors.md | 28 + docs/image.png | Bin 0 -> 150154 bytes docs/installation.md | 43 + docs/limitations.md | 25 + docs/pades.md | 92 ++ docs/screenshots/01_dashboard.png | Bin 0 -> 48464 bytes docs/screenshots/01_layout_claro.png | Bin 0 -> 60530 bytes docs/screenshots/01_layout_serpro.png | Bin 0 -> 53042 bytes docs/screenshots/02_diagnostico.png | Bin 0 -> 55671 bytes docs/screenshots/02_layout_escuro.png | Bin 0 -> 63533 bytes docs/screenshots/03_configuracoes.png | Bin 0 -> 36088 bytes docs/sdk.md | 117 ++ docs/security.md | 46 + docs/serpro-layout-and-batch.md | 82 ++ docs/xml-xmldsig.md | 56 + modules/cybercore-app/pom.xml | 162 +++ .../app/CyberCoreSignerApplication.java | 144 ++ .../src/main/resources/logback.xml | 14 + .../signer/app/ArchitectureTest.java | 37 + .../app/CyberCoreSignerApplicationTest.java | 20 + modules/cybercore-core/pom.xml | 64 + .../core/config/AgentConfiguration.java | 43 + .../config/AgentConfigurationService.java | 94 ++ .../signer/core/crypto/DigestAlgorithm.java | 66 + .../signer/core/crypto/SigningService.java | 169 +++ .../core/crypto/X509CertificateUtils.java | 230 +++ .../core/diagnostics/DiagnosticReport.java | 51 + .../core/diagnostics/DiagnosticService.java | 87 ++ .../signer/core/domain/CertificateStatus.java | 30 + .../signer/core/domain/CertificateType.java | 32 + .../core/domain/ManagedCertificate.java | 56 + .../signer/core/domain/SigningContext.java | 55 + .../core/domain/SigningKeyMaterial.java | 38 + .../signer/core/domain/SigningResult.java | 63 + .../signer/core/pdf/PdfSealPageChoice.java | 20 + .../signer/core/pdf/PdfSealPosition.java | 22 + .../signer/core/pdf/PdfSignatureOptions.java | 186 +++ .../signer/core/pdf/PdfSigningService.java | 351 +++++ .../signer/core/pdf/seal/CpfDisplayMode.java | 64 + .../core/pdf/seal/CustomSealStorage.java | 175 +++ .../signer/core/pdf/seal/PdfSealData.java | 64 + .../signer/core/pdf/seal/PdfSealRenderer.java | 411 ++++++ .../signer/core/pdf/seal/PdfSealTheme.java | 135 ++ .../core/pdf/seal/PdfSealThemeRegistry.java | 240 +++ .../signer/core/protocol/CcspErrorCode.java | 35 + .../signer/core/protocol/CcspOperation.java | 39 + .../signer/core/protocol/CcspRequest.java | 96 ++ .../signer/core/protocol/CcspResponse.java | 136 ++ .../signer/core/protocol/CcspServer.java | 550 +++++++ .../core/protocol/CcspUserPromptListener.java | 31 + .../core/provider/CertificateManager.java | 194 +++ .../core/provider/CertificateProvider.java | 61 + .../CertificateProviderException.java | 32 + .../provider/Pkcs11CertificateProvider.java | 247 ++++ .../provider/Pkcs12CertificateProvider.java | 318 ++++ .../signer/core/provider/ProviderType.java | 21 + .../WindowsCertificateStoreProvider.java | 307 ++++ .../core/security/ApplicationRegistry.java | 162 +++ .../core/security/AuthorizedApplication.java | 44 + .../signer/core/security/NonceManager.java | 53 + .../signer/core/security/OriginValidator.java | 44 + .../core/security/PinSessionManager.java | 106 ++ .../security/SmartCardSessionManager.java | 69 + .../core/security/SmartCardWatcher.java | 197 +++ .../SignatureValidationService.java | 417 ++++++ .../core/validation/ValidationResult.java | 19 + .../core/validation/ValidationSignerInfo.java | 20 + .../core/validation/ValidationStatus.java | 21 + .../core/validation/ValidationType.java | 28 + .../signer/core/xml/XmlSignatureOptions.java | 32 + .../signer/core/xml/XmlSigningService.java | 290 ++++ .../config/AgentConfigurationServiceTest.java | 47 + .../core/crypto/DigestAlgorithmTest.java | 45 + .../core/crypto/SigningServiceTest.java | 147 ++ .../diagnostics/DiagnosticServiceTest.java | 22 + .../core/pdf/PdfSigningServiceTest.java | 218 +++ .../core/pdf/seal/CpfDisplayModeTest.java | 31 + .../core/pdf/seal/CustomSealStorageTest.java | 137 ++ .../core/pdf/seal/PdfSealRendererTest.java | 140 ++ .../core/protocol/CcspConcurrencyTest.java | 198 +++ .../core/protocol/CcspProtocolTest.java | 87 ++ .../signer/core/protocol/CcspServerTest.java | 323 +++++ .../core/provider/CertificateManagerTest.java | 66 + .../Pkcs12CertificateProviderTest.java | 130 ++ .../WindowsCertificateStoreProviderTest.java | 47 + .../security/ApplicationRegistryTest.java | 47 + .../core/security/NonceManagerTest.java | 31 + .../core/security/OriginValidatorTest.java | 46 + .../core/security/PinSessionManagerTest.java | 61 + .../security/SmartCardSessionManagerTest.java | 18 + .../core/security/SmartCardWatcherTest.java | 51 + .../SignatureValidationServiceTest.java | 183 +++ .../core/xml/XmlSigningServiceTest.java | 152 ++ modules/cybercore-ui/pom.xml | 56 + .../ui/CyberCoreDesktopApplication.java | 496 +++++++ .../signer/ui/CyberCoreUiConstants.java | 17 + .../signer/ui/assets/AssetManager.java | 449 ++++++ .../ui/components/CertificateCardNode.java | 129 ++ .../signer/ui/components/SvgIcons.java | 732 ++++++++++ .../signer/ui/theme/ThemeManager.java | 120 ++ .../signer/ui/tray/SystemTrayManager.java | 169 +++ .../signer/ui/view/A1PasswordDialog.java | 103 ++ .../cybercore/signer/ui/view/AboutView.java | 52 + .../ui/view/ApplicationApprovalDialog.java | 123 ++ .../signer/ui/view/BatchSigningDialog.java | 491 +++++++ .../ui/view/CertificateDetailsDialog.java | 213 +++ .../ui/view/CertificatesManagerDialog.java | 192 +++ .../signer/ui/view/DiagnosticsView.java | 121 ++ .../signer/ui/view/FileSignDialog.java | 260 ++++ .../signer/ui/view/LogsViewerDialog.java | 108 ++ .../cybercore/signer/ui/view/MainAppView.java | 316 ++++ .../signer/ui/view/MainDashboardView.java | 127 ++ .../signer/ui/view/PdfSignDialog.java | 1125 ++++++++++++++ .../signer/ui/view/SealDesignerDialog.java | 643 ++++++++ .../signer/ui/view/SealManagementDialog.java | 498 +++++++ .../signer/ui/view/SettingsView.java | 296 ++++ .../signer/ui/view/SignaturePromptDialog.java | 259 ++++ .../ui/view/SignatureValidationDialog.java | 395 +++++ .../signer/ui/view/TutorialDialog.java | 118 ++ .../signer/ui/view/XmlSignDialog.java | 513 +++++++ .../src/main/resources/assets/README.md | 75 + .../src/main/resources/assets/icons/about.svg | 5 + .../main/resources/assets/icons/attached.svg | 12 + .../src/main/resources/assets/icons/batch.svg | 13 + .../src/main/resources/assets/icons/cert.svg | 11 + .../src/main/resources/assets/icons/check.svg | 4 + .../main/resources/assets/icons/co-sign.svg | 12 + .../src/main/resources/assets/icons/error.svg | 5 + .../src/main/resources/assets/icons/exit.svg | 8 + .../src/main/resources/assets/icons/file.svg | 13 + .../src/main/resources/assets/icons/logs.svg | 13 + .../src/main/resources/assets/icons/ltv.svg | 11 + .../src/main/resources/assets/icons/moon.svg | 6 + .../src/main/resources/assets/icons/pdf.svg | 13 + .../src/main/resources/assets/icons/seal.svg | 11 + .../main/resources/assets/icons/settings.svg | 8 + .../main/resources/assets/icons/shield.svg | 6 + .../src/main/resources/assets/icons/star.svg | 4 + .../src/main/resources/assets/icons/sun.svg | 15 + .../main/resources/assets/icons/timestamp.svg | 13 + .../main/resources/assets/icons/tutorial.svg | 8 + .../assets/icons/validate-timestamp.svg | 10 + .../main/resources/assets/icons/validate.svg | 11 + .../src/main/resources/assets/icons/xml.svg | 13 + .../main/resources/assets/images/app-icon.png | Bin 0 -> 26558 bytes .../main/resources/assets/images/app-logo.png | Bin 0 -> 10439 bytes .../resources/assets/images/brand-banner.png | Bin 0 -> 27073 bytes .../resources/assets/images/default-seal.png | Bin 0 -> 14256 bytes .../src/main/resources/styles/theme.css | 1065 ++++++++++++++ .../signer/ui/CyberCoreUiConstantsTest.java | 22 + .../signer/ui/UiPreviewGenerator.java | 73 + .../signer/ui/UiPreviewLauncher.java | 9 + mvnw | 295 ++++ mvnw.cmd | 189 +++ pom.xml | 400 +++++ run.bat | 18 + run.sh | 22 + scratch/GeneratePngAssets.java | 348 +++++ scripts/package-jpackage.sh | 69 + scripts/package-windows.bat | 45 + sdk/README.md | 64 + sdk/demo/index.html | 1288 +++++++++++++++++ sdk/package.json | 23 + sdk/src/errors.js | 75 + sdk/src/signer-sdk.js | 497 +++++++ sdk/test/signer-sdk.test.js | 253 ++++ 206 files changed, 23952 insertions(+) create mode 100644 .agents/rules/main-rule.md create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 README.md create mode 100644 assets/README.md create mode 100644 assets/icons/about.svg create mode 100644 assets/icons/attached.svg create mode 100644 assets/icons/batch.svg create mode 100644 assets/icons/cert.svg create mode 100644 assets/icons/check.svg create mode 100644 assets/icons/co-sign.svg create mode 100644 assets/icons/error.svg create mode 100644 assets/icons/exit.svg create mode 100644 assets/icons/file.svg create mode 100644 assets/icons/logs.svg create mode 100644 assets/icons/ltv.svg create mode 100644 assets/icons/moon.svg create mode 100644 assets/icons/pdf.svg create mode 100644 assets/icons/seal.svg create mode 100644 assets/icons/settings.svg create mode 100644 assets/icons/shield.svg create mode 100644 assets/icons/star.svg create mode 100644 assets/icons/sun.svg create mode 100644 assets/icons/timestamp.svg create mode 100644 assets/icons/tutorial.svg create mode 100644 assets/icons/validate-timestamp.svg create mode 100644 assets/icons/validate.svg create mode 100644 assets/icons/xml.svg create mode 100644 assets/images/app-icon.png create mode 100644 assets/images/app-logo.png create mode 100644 assets/images/brand-banner.png create mode 100644 assets/images/default-seal.png create mode 100755 build-jar.sh create mode 100644 config/spotbugs/exclude.xml create mode 100644 docs/CCSP.md create mode 100644 docs/a1-a3-certificates.md create mode 100644 docs/architecture.md create mode 100644 docs/authentication-login.md create mode 100644 docs/development-mode.md create mode 100644 docs/diagnostics.md create mode 100644 docs/errors.md create mode 100644 docs/image.png create mode 100644 docs/installation.md create mode 100644 docs/limitations.md create mode 100644 docs/pades.md create mode 100644 docs/screenshots/01_dashboard.png create mode 100644 docs/screenshots/01_layout_claro.png create mode 100644 docs/screenshots/01_layout_serpro.png create mode 100644 docs/screenshots/02_diagnostico.png create mode 100644 docs/screenshots/02_layout_escuro.png create mode 100644 docs/screenshots/03_configuracoes.png create mode 100644 docs/sdk.md create mode 100644 docs/security.md create mode 100644 docs/serpro-layout-and-batch.md create mode 100644 docs/xml-xmldsig.md create mode 100644 modules/cybercore-app/pom.xml create mode 100644 modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java create mode 100644 modules/cybercore-app/src/main/resources/logback.xml create mode 100644 modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java create mode 100644 modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java create mode 100644 modules/cybercore-core/pom.xml create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfiguration.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfigurationService.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/DigestAlgorithm.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/SigningService.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/X509CertificateUtils.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticReport.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticService.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateStatus.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateType.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/ManagedCertificate.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningContext.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningKeyMaterial.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningResult.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPageChoice.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPosition.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSignatureOptions.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayMode.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorage.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealData.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealTheme.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspErrorCode.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspOperation.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspUserPromptListener.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateManager.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProvider.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProviderException.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProvider.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/ProviderType.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProvider.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/ApplicationRegistry.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/AuthorizedApplication.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/NonceManager.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/OriginValidator.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/PinSessionManager.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardSessionManager.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardWatcher.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/SignatureValidationService.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationResult.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationSignerInfo.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationStatus.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationType.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSignatureOptions.java create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSigningService.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/config/AgentConfigurationServiceTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/DigestAlgorithmTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/SigningServiceTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/diagnostics/DiagnosticServiceTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/PdfSigningServiceTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayModeTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorageTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRendererTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspConcurrencyTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspProtocolTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/CertificateManagerTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProviderTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProviderTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/ApplicationRegistryTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/NonceManagerTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/OriginValidatorTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/PinSessionManagerTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardSessionManagerTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardWatcherTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/validation/SignatureValidationServiceTest.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/xml/XmlSigningServiceTest.java create mode 100644 modules/cybercore-ui/pom.xml create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreUiConstants.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/assets/AssetManager.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/A1PasswordDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/AboutView.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/BatchSigningDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/FileSignDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignatureValidationDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java create mode 100644 modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java create mode 100644 modules/cybercore-ui/src/main/resources/assets/README.md create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/about.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/attached.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/batch.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/cert.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/check.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/co-sign.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/error.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/exit.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/file.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/logs.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/ltv.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/moon.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/pdf.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/seal.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/settings.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/shield.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/star.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/sun.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/timestamp.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/tutorial.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/validate-timestamp.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/validate.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/icons/xml.svg create mode 100644 modules/cybercore-ui/src/main/resources/assets/images/app-icon.png create mode 100644 modules/cybercore-ui/src/main/resources/assets/images/app-logo.png create mode 100644 modules/cybercore-ui/src/main/resources/assets/images/brand-banner.png create mode 100644 modules/cybercore-ui/src/main/resources/assets/images/default-seal.png create mode 100644 modules/cybercore-ui/src/main/resources/styles/theme.css create mode 100644 modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/CyberCoreUiConstantsTest.java create mode 100644 modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java create mode 100644 modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewLauncher.java create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 run.bat create mode 100755 run.sh create mode 100644 scratch/GeneratePngAssets.java create mode 100755 scripts/package-jpackage.sh create mode 100644 scripts/package-windows.bat create mode 100644 sdk/README.md create mode 100644 sdk/demo/index.html create mode 100644 sdk/package.json create mode 100644 sdk/src/errors.js create mode 100644 sdk/src/signer-sdk.js create mode 100644 sdk/test/signer-sdk.test.js diff --git a/.agents/rules/main-rule.md b/.agents/rules/main-rule.md new file mode 100644 index 0000000..9697813 --- /dev/null +++ b/.agents/rules/main-rule.md @@ -0,0 +1,250 @@ +--- +trigger: always_on +--- + +PROMPT MESTRE — CYBERCORE SIGNER + +Você é responsável pelo desenvolvimento do CyberCore Signer, um agente desktop Java para Windows que permite aplicações web autorizadas solicitar assinaturas digitais usando certificados instalados/localmente disponíveis na máquina do usuário. + +1. REGRA DE DESENVOLVIMENTO + +Siga sempre: ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR. + +Antes de alterar qualquer código, analise o repositório real, sua estrutura, dependências, configurações, padrões, documentação e testes. Não invente arquivos, classes ou arquitetura existentes. + +Na fase de análise, não altere o projeto. Depois apresente o plano e aguarde autorização explícita para implementar. + +Se durante a implementação surgir mudança relevante de escopo, pare e peça nova autorização. + +Ao finalizar, informe arquivos alterados/criados, dependências, decisões arquiteturais, testes executados, limitações e pendências. + +2. TECNOLOGIA + +Use Java 21+ LTS, Maven ou Gradle conforme o projeto, JavaFX para a interface e JCA/JCE para criptografia. + +Use X.509, KeyStore/PKCS12 e PKCS#11 conforme necessário. Use SunPKCS11 quando apropriado e Bouncy Castle somente quando houver necessidade real. + +O aplicativo deve ser empacotado com runtime próprio, preferencialmente via jpackage, sem exigir Java instalado pelo usuário. + +Windows 10/11 é o alvo inicial; mantenha a arquitetura preparada para macOS/Linux. + +3. ARQUITETURA + +Separe responsabilidades em módulos/componentes coerentes, sem criar módulos artificiais. A arquitetura deve permitir evolução e testes independentes. + +Crie abstrações para descoberta e uso de certificados, por exemplo CertificateProvider, com implementações para A1, Windows Certificate Store e PKCS#11 quando aplicável. + +CertificateManager deve centralizar descoberta, validade, subject, issuer, algoritmo, disponibilidade da chave privada e seleção do provider. + +SigningService deve executar a assinatura por JCA/JCE e providers; nunca implemente criptografia própria. + +Mantenha o núcleo independente de fabricantes. + +4. A1 + +Suporte certificados .PFX/.P12. + +A chave privada permanece exclusivamente na máquina. A senha nunca deve ser enviada ao servidor nem aparecer em logs. + +Trate certificado válido, expirado, inválido, senha incorreta, ausência de chave privada e algoritmos não suportados. + +Se houver persistência de credenciais, use armazenamento seguro do sistema operacional. + +5. A3 / PKCS#11 + +Suporte tokens USB, smart cards, leitores e middleware compatíveis com PKCS#11, além de cenários Windows CSP/CNG/KSP quando necessários. + +Detecte slots, tokens, certificados e chaves disponíveis. + +Trate token ausente/removido, middleware/provider indisponível, PIN requerido/incorreto, timeout e falhas do dispositivo. + +Nunca armazene ou envie PIN. Nunca implemente sua própria camada PKCS#11: utilize APIs/providers existentes. + +Não assuma que todos os A3 funcionam da mesma forma; use adapters/providers sem acoplar o núcleo a um fabricante. + +6. ASSINATURA + +Priorize o modelo em que a aplicação web calcula o digest e o CyberCore assina localmente apenas esse digest. + +Exemplo: documento → SHA-256 → digest → CyberCore → certificado A1/A3 → assinatura. + +A chave privada nunca sai da máquina. + +Valide algoritmo, tamanho/formato do digest, certificado e capacidade do provider antes da operação. + +7. PROTOCOLO CCSP + +Defina o CyberCore Signing Protocol (CCSP), versão inicial CCSP/1. + +Toda requisição deve possuir request_id UUID, application_id, operação, algoritmo, digest, indicação opcional do certificado e expiração. + +Exemplo: + +{ + "version":"1","request_id":"uuid","application_id":"uuid", + "operation":"sign","algorithm":"SHA256withRSA", + "digest":"base64","certificate_hint":"uuid","expires_at":"..." +} + +Resposta: + +{ + "version":"1","request_id":"uuid","status":"signed", + "signature":"base64","certificate":"base64","algorithm":"SHA256withRSA" +} + +Especifique em documentação autenticação, autorização, validação, timeouts, nonce, replay protection, erros e compatibilidade de versões. + +8. COMUNICAÇÃO LOCAL + +A comunicação deve ocorrer somente localmente por padrão. Avalie HTTPS localhost, WebSocket ou outro mecanismo adequado. + +Nunca exponha uma API de assinatura local sem autenticação/autorização. + +Não permita exposição involuntária à rede LAN. + +Use registro/autorização de aplicações, origem permitida, desafio/nonce, timestamp, expiração e proteção contra replay. + +Não confie exclusivamente em Origin ou User-Agent. + +9. SEGURANÇA + +A assinatura deve exigir confirmação explícita do usuário por padrão. + +Mostre aplicação solicitante, certificado, tipo A1/A3, algoritmo e informações relevantes antes de assinar. + +Nunca registre PIN, senha, chave privada ou conteúdo desnecessário do documento. + +Minimize dados trafegados e armazenados. + +Use UUIDs, correlação por request_id e validação rigorosa de entrada. + +10. INTERFACE + +Use JavaFX com aparência profissional e consistente. + +Inclua, conforme o escopo: Dashboard, Certificados, Solicitação de Assinatura, Configurações, Diagnóstico e Sobre. + +Suporte System Tray. + +A interface deve informar claramente estado do agente, certificados disponíveis, token conectado e erros acionáveis. + +11. DIAGNÓSTICO + +Disponibilize diagnóstico de Java/runtime, engine criptográfica, Windows Certificate Store, A1, A3, PKCS#11, comunicação local e SDK. + +Permita diagnóstico completo e teste de assinatura. + +Diferencie claramente falha de configuração, middleware, token, certificado, comunicação e aplicação solicitante. + +12. ERROS + +Utilize erros estruturados e estáveis, incluindo: + +CERTIFICATE_NOT_FOUND, CERTIFICATE_EXPIRED, CERTIFICATE_INVALID, +PRIVATE_KEY_NOT_AVAILABLE, TOKEN_NOT_FOUND, TOKEN_REMOVED, +PIN_REQUIRED, PIN_INCORRECT, PKCS11_ERROR, SIGNATURE_FAILED, +APPLICATION_NOT_AUTHORIZED, REQUEST_EXPIRED, REPLAY_DETECTED, +INVALID_ORIGIN, UNSUPPORTED_ALGORITHM, USER_CANCELLED, +PROVIDER_NOT_AVAILABLE e COMMUNICATION_ERROR. + +Mensagens para o usuário devem ser claras, sem expor segredos ou detalhes internos desnecessários. + +13. LOGS E AUDITORIA + +Registre eventos relevantes: inicialização, descoberta de certificados, provider utilizado, aplicação, request_id, operação, sucesso/falha e remoção de token. + +Nunca registre PIN, senha, chave privada ou dados sensíveis desnecessários. + +Use níveis de log adequados e correlação por request_id. + +14. SDK WEB + +Planeje o pacote JavaScript @cybercore/signer-sdk. + +API mínima: + +connect(), isInstalled(), getVersion(), getStatus(), +getCertificates(), getCapabilities(), sign(), verify(). + +O SDK deve abstrair o CCSP e fornecer erros previsíveis para aplicações web. + +15. MULTIAPLICAÇÃO E CONCORRÊNCIA + +Uma única instalação deve atender múltiplas aplicações web autorizadas. + +Controle concorrência e fila de operações, especialmente em A3, pois tokens/middleware podem não suportar operações simultâneas. + +Nunca misture contexto ou autorização de uma aplicação com outra. + +16. PDF/PAdES + +Não misture a operação criptográfica básica com a montagem/finalização de PDF. + +Para PAdES, avalie bibliotecas maduras como Apache PDFBox, Bouncy Castle ou DSS. + +Nunca implemente manualmente mecanismos criptográficos ou estruturas complexas de assinatura PDF sem necessidade. + +17. TESTES + +Crie testes unitários, integração, segurança, protocolo, providers, comunicação local, SDK e concorrência. + +Cubra A1 válido/expirado/inválido/senha incorreta; A3 sem token, token removido, PIN incorreto, middleware indisponível e cancelamento; assinatura válida; algoritmo incompatível; requisição expirada/repetida/não autorizada; origem inválida; payload malformado e concorrência. + +Testes dependentes de hardware só podem ser declarados executados quando o hardware realmente estiver disponível. + +18. MODO DESENVOLVIMENTO + +Implemente mock quando útil para desenvolvimento e testes, simulando A1/A3, token removido, PIN incorreto, certificado expirado, provider indisponível, timeout e cancelamento. + +O modo deve ser claramente identificado como DESENVOLVIMENTO. + +Nunca apresente assinatura simulada como assinatura real. + +19. INSTALADOR E ATUALIZAÇÃO + +Crie instalador Windows com runtime próprio, instalação/desinstalação, atalhos e opções de inicialização quando aplicável. + +Atualizações devem ser autenticadas e seguras. + +Executáveis e instaladores devem ser preparados para code signing. + +20. PRIVACIDADE + +A chave privada, PIN e senha A1 permanecem na máquina. + +Quando apenas o digest for necessário, o documento não deve ser enviado ao CyberCore. + +Colete, armazene e registre somente o necessário para operação, diagnóstico e auditoria. + +21. DOCUMENTAÇÃO + +Mantenha documentação técnica em docs/. + +Documente especialmente docs/CCSP.md, arquitetura, instalação, certificados A1/A3, PKCS#11, segurança, SDK, erros, diagnóstico, desenvolvimento e limitações. + +Toda mudança relevante de protocolo ou arquitetura deve atualizar a documentação correspondente. + +22. IMPLEMENTAÇÃO + +Ao receber uma tarefa: + +1) ANALISE o projeto real e identifique impacto, dependências e riscos. +2) PLANEJE a solução, apresentando arquivos/componentes afetados e estratégia de testes. +3) PEÇA AUTORIZAÇÃO explícita antes de modificar o código. +4) IMPLEMENTE seguindo os padrões existentes e evitando alterações fora do escopo. +5) VALIDE com build, testes, análise de erros e, quando possível, execução real. + +Não marque uma tarefa como concluída apenas porque o código compila. + +23. REGRA FINAL + +Priorize segurança, interoperabilidade, rastreabilidade, manutenção e compatibilidade. + +Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados. + +Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência. + +Princípio obrigatório: + +ANALISAR → PLANEJAR → AUTORIZAR → IMPLEMENTAR → VALIDAR. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83424a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# ============================================================================== +# CyberCore Signer - .gitignore +# ============================================================================== + +# Compilação e Artefatos Maven +target/ +**/target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Binários gerados e Distribuições Locais +*.jar +*.war +*.ear +*.zip +dist/ +build/ +bin/ + +# Certificados, Chaves e Credenciais Locais (Privacidade e Segurança) +*.pfx +*.p12 +*.jks +*.keystore +*.key +*.pem +*.cer +*.crt +*.der +!assets/ +!docs/ + +# Ambientes e Variáveis Locais +.env +.env.* +!.env.example + +# IDEs e Editores +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +*.code-workspace +.project +.classpath +.settings/ +.factorypath +bin/ + +# Logs e Auditorias de Runtime Locais +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Node / SDK de Testes +node_modules/ +sdk/node_modules/ +coverage/ +.nyc_output/ + +# Sistema Operacional +.DS_Store +Thumbs.db +desktop.ini +$RECYCLE.BIN/ +*.tmp +*~ + +# Arquivos temporários de Scratch locais +scratch/temp/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..bae019f --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# CyberCore Signer + +> **Agente Desktop de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.** + +O **CyberCore Signer** é uma solução desktop em Java 21 LTS com JavaFX que permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e carimbos do tempo utilizando certificados digitais ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) instalados ou conectados na estação de trabalho do usuário. + +A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP/WebSocket localhost autenticado e seguro. + +--- + +## 🏛 Arquitetura do Projeto + +O projeto é estruturado em módulos Maven: + +* **[modules/cybercore-core](modules/cybercore-core)**: Núcleo de regras criptográficas (JCA/JCE, SunPKCS11, X.509, PAdES, XMLDSig, carimbo de tempo TSA, LTV e validação de cadeias). Não possui dependência de interface gráfica. +* **[modules/cybercore-ui](modules/cybercore-ui)**: Interface gráfica com o usuário construída em JavaFX (Dashboard, Gerenciador de Certificados, Diagnóstico Criptográfico, Diálogo de Confirmação de Assinatura, PIN dialog e System Tray). +* **[modules/cybercore-app](modules/cybercore-app)**: Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida e servidor CCSP local. +* **[sdk/](sdk)**: SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração de aplicações web com o agente desktop. +* **[docs/](docs)**: Documentação técnica detalhada de arquitetura, protocolo, segurança e casos de uso. + +--- + +## 🚀 Requisitos + +* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin, Corretto, etc.). +* **Maven:** 3.9+ (ou utilizar o wrapper `./mvnw` incluído no projeto). +* **Sistema Operacional Alvo:** Windows 10/11 ou Linux (x86_64). + +--- + +## 🛠 Compilação e Empacotamento + +### 1. Compilação do JAR Executável + +Para compilar e gerar o JAR consolidado (com todas as dependências sombreadas e assets): + +```bash +./build-jar.sh +``` + +Ou diretamente via Maven: + +```bash +./mvnw clean package -DskipTests +``` + +O artefato consolidado será gerado em: +`modules/cybercore-app/target/cybercore-signer.jar` + +### 2. Execução Local + +Para iniciar o agente em ambiente de desenvolvimento: + +```bash +# No Linux / macOS: +./run.sh + +# No Windows: +run.bat +``` + +### 3. Empacotamento com Runtime Nativo (`jpackage`) + +Para criar a distribuição com Java Runtime embutido (sem exigir JDK pré-instalado pelo usuário final): + +```bash +./scripts/package-jpackage.sh +``` + +--- + +## 🔒 Princípios de Segurança e Privacidade + +1. **Chave Privada Preservada:** A chave privada e os segredos (PIN de token A3 e senha de certificado A1) nunca saem da máquina do usuário nem trafegam pela rede. +2. **Assinatura por Digest:** Por padrão, a aplicação web calcula o hash/digest (ex: SHA-256) do documento e o CyberCore assina localmente apenas o digest, minimizando o tráfego de dados sensíveis. +3. **Confirmação Explícita:** Toda operação de assinatura exige consentimento e autorização visual do usuário antes da aplicação da chave. +4. **Isolamento de Origem e CCSP:** O agente atende apenas origens web autorizadas, protegendo contra abusos via CSRF ou requisições locais não autenticadas. + +--- + +## 📚 Documentação Técnica + +Consulte a pasta [docs/](docs) para especificações completas: + +* [Protocolo CCSP (CyberCore Signing Protocol)](docs/CCSP.md) +* [Arquitetura e Fluxo do Sistema](docs/architecture.md) +* [Certificados A1 e A3 (PKCS#11 / MSCAPI)](docs/a1-a3-certificates.md) +* [Assinatura de Documentos PDF (PAdES / LTV / TSA)](docs/pades.md) +* [Assinatura de Documentos XML (XMLDSig)](docs/xml-xmldsig.md) +* [Autenticação e Sessão Criptográfica](docs/authentication-login.md) +* [Diagnóstico e Resolução de Problemas](docs/diagnostics.md) +* [Segurança e Tratamento de Erros](docs/security.md) +* [SDK Web JavaScript](docs/sdk.md) +* [Guia de Instalação](docs/installation.md) + +--- + +## 📄 Licença + +Propriedade de **CyberCore Systems**. Todos os direitos reservados. diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..d77f68b --- /dev/null +++ b/assets/README.md @@ -0,0 +1,75 @@ +# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer + +Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**. + +--- + +## 📁 Estrutura de Arquivos Criados + +``` +assets/ +├── icons/ # Ícones vetoriais SVG reais e editáveis (W3C padrão) +│ ├── pdf.svg # Ícone Assinar PDF (documento com tag vermelha PDF) +│ ├── xml.svg # Ícone Assinar XML (documento com tag azul ) +│ ├── file.svg # Ícone Assinar Arquivo (documento com caneta) +│ ├── batch.svg # Ícone Assinatura em Lote (pilha de documentos com check) +│ ├── attached.svg # Ícone Assinatura Atachada (documento com clipe de papel) +│ ├── co-sign.svg # Ícone Co-Assinar (documentos duplos com usuários) +│ ├── timestamp.svg # Ícone Carimbo do Tempo (documento com relógio TSA) +│ ├── ltv.svg # Ícone Assinatura LTV (documento com selo perpétuo) +│ ├── validate.svg # Ícone Validar Assinatura (documento com checkmark verde) +│ ├── validate-timestamp.svg # Ícone Validar Carimbo (relógio com checkmark verde) +│ ├── exit.svg # Ícone Sair do Agente (porta com seta vermelha de saída) +│ ├── logs.svg # Ícone Terminal de Logs (janela com prompt ) +│ ├── settings.svg # Ícone Configurações (engrenagem de precisão de 8 dentes) +│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play) +│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro) +│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i') +│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais) +│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo) +│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna) +│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais +│ ├── check.svg # Ícone Checkmark de sucesso e validação +│ ├── error.svg # Ícone de Erro / Alerta em vermelho +│ └── star.svg # Ícone de Estrela notarial / favorita +│ +└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência) + ├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro) + ├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER) + ├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue) + └── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px) +``` + +--- + +## 🎨 Como Customizar Qualquer Ícone ou Imagem + +1. **Ícones:** + - Estão no formato `.svg` padrão. + - Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**. + - Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente. + +2. **Imagens do Sistema:** + - Estão no formato `.png` com canal alfa de 32 bits (fundo transparente). + - Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa. + +3. **Ordem de Prioridade e Hot-Reloading:** + Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem: + 1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar! + 2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional. + 3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`. + 4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído. + +--- + +## 🚀 Como Testar Suas Modificações + +Para compilar e gerar o pacote `.jar` completo com todos os novos assets embutidos: +```bash +./build-jar.sh +``` + +Para rodar a aplicação: +```bash +java -jar ./cybercore-signer.jar +``` diff --git a/assets/icons/about.svg b/assets/icons/about.svg new file mode 100644 index 0000000..f7cc7f1 --- /dev/null +++ b/assets/icons/about.svg @@ -0,0 +1 @@ +info-circle-fill \ No newline at end of file diff --git a/assets/icons/attached.svg b/assets/icons/attached.svg new file mode 100644 index 0000000..7d894e7 --- /dev/null +++ b/assets/icons/attached.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/assets/icons/batch.svg b/assets/icons/batch.svg new file mode 100644 index 0000000..5ced311 --- /dev/null +++ b/assets/icons/batch.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/cert.svg b/assets/icons/cert.svg new file mode 100644 index 0000000..14cc2b4 --- /dev/null +++ b/assets/icons/cert.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000..c379af0 --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/co-sign.svg b/assets/icons/co-sign.svg new file mode 100644 index 0000000..283c24f --- /dev/null +++ b/assets/icons/co-sign.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg new file mode 100644 index 0000000..5a36ff3 --- /dev/null +++ b/assets/icons/error.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg new file mode 100644 index 0000000..d209a6b --- /dev/null +++ b/assets/icons/exit.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/assets/icons/file.svg b/assets/icons/file.svg new file mode 100644 index 0000000..023885f --- /dev/null +++ b/assets/icons/file.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/logs.svg b/assets/icons/logs.svg new file mode 100644 index 0000000..0dd89e3 --- /dev/null +++ b/assets/icons/logs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/ltv.svg b/assets/icons/ltv.svg new file mode 100644 index 0000000..dbc162a --- /dev/null +++ b/assets/icons/ltv.svg @@ -0,0 +1,11 @@ + + + + + + + + + + LTV + diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg new file mode 100644 index 0000000..fd47105 --- /dev/null +++ b/assets/icons/moon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/pdf.svg b/assets/icons/pdf.svg new file mode 100644 index 0000000..3755ded --- /dev/null +++ b/assets/icons/pdf.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + PDF + diff --git a/assets/icons/seal.svg b/assets/icons/seal.svg new file mode 100644 index 0000000..1aa3a5f --- /dev/null +++ b/assets/icons/seal.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg new file mode 100644 index 0000000..e0f4042 --- /dev/null +++ b/assets/icons/settings.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/shield.svg b/assets/icons/shield.svg new file mode 100644 index 0000000..1bc2539 --- /dev/null +++ b/assets/icons/shield.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/star.svg b/assets/icons/star.svg new file mode 100644 index 0000000..ac2213a --- /dev/null +++ b/assets/icons/star.svg @@ -0,0 +1,4 @@ + + + diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg new file mode 100644 index 0000000..0327476 --- /dev/null +++ b/assets/icons/sun.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/timestamp.svg b/assets/icons/timestamp.svg new file mode 100644 index 0000000..16238f4 --- /dev/null +++ b/assets/icons/timestamp.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/tutorial.svg b/assets/icons/tutorial.svg new file mode 100644 index 0000000..32d13d2 --- /dev/null +++ b/assets/icons/tutorial.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/validate-timestamp.svg b/assets/icons/validate-timestamp.svg new file mode 100644 index 0000000..dc0f7c8 --- /dev/null +++ b/assets/icons/validate-timestamp.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/validate.svg b/assets/icons/validate.svg new file mode 100644 index 0000000..f654ce8 --- /dev/null +++ b/assets/icons/validate.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/xml.svg b/assets/icons/xml.svg new file mode 100644 index 0000000..edbf6a9 --- /dev/null +++ b/assets/icons/xml.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + <XML> + diff --git a/assets/images/app-icon.png b/assets/images/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e231bde3db0931e9d540d4362e276ae9722869 GIT binary patch literal 26558 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 b%ds18?VH3((q5oK;E$!5&EYZ=w}}4(I*HpL literal 0 HcmV?d00001 diff --git a/assets/images/app-logo.png b/assets/images/app-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0cea4b98e51e56b5991abf1f90a4ac52eb809a55 GIT binary patch literal 10439 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 AqW}N^ literal 0 HcmV?d00001 diff --git a/assets/images/brand-banner.png b/assets/images/brand-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6bd57c97e8b6858d26200259631c363856a146da GIT binary patch literal 27073 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
k6H?w@CdPIdX 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 Rz)3ui^ecJsLQ(yX{|72h^*jIo literal 0 HcmV?d00001 diff --git a/build-jar.sh b/build-jar.sh new file mode 100755 index 0000000..f42ecd9 --- /dev/null +++ b/build-jar.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# ============================================================================== +# CyberCore Signer - Script de Automação de Build do Fat JAR Executável +# ============================================================================== +set -euo pipefail + +# Garante execução no diretório raiz do projeto +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +COLOR_RESET="\033[0m" +COLOR_BOLD="\033[1m" +COLOR_CYAN="\033[36m" +COLOR_GREEN="\033[32m" +COLOR_YELLOW="\033[33m" +COLOR_RED="\033[31m" + +RUN_AFTER_BUILD=false +RUN_TESTS=false + +# Interpretação de argumentos +for arg in "$@"; do + case "${arg}" in + --with-tests|-t) + RUN_TESTS=true + ;; + --run|-r) + RUN_AFTER_BUILD=true + ;; + --help|-h) + echo -e "${COLOR_BOLD}Uso: ./build-jar.sh [opções]${COLOR_RESET}" + echo "" + echo "Opções disponíveis:" + echo " --with-tests, -t Executa a suíte completa de testes unitários antes de empacotar." + echo " --run, -r Inicia automaticamente o CyberCore Signer após a compilação." + echo " --help, -h Exibe esta mensagem de ajuda." + echo "" + echo "Exemplo:" + echo " ./build-jar.sh # Compilação e empacotamento rápido do JAR" + echo " ./build-jar.sh --run # Compila e já abre a aplicação" + exit 0 + ;; + *) + echo -e "${COLOR_YELLOW}Aviso: Opção desconhecida: ${arg}. Use --help para detalhes.${COLOR_RESET}" + ;; + esac +done + +echo -e "${COLOR_CYAN}${COLOR_BOLD}" +echo "============================================================" +echo " CYBERCORE SIGNER - AUTOMAÇÃO DE BUILD DO JAR" +echo "============================================================" +echo -e "${COLOR_RESET}" + +# 1. Verificação e Auto-Detecção do Java (JDK 21+) +if ! command -v java &>/dev/null; then + POSSIBLE_JDKS=( + "/home/claudecio/.jdks/jdk-25.0.4.1+1" + "/usr/lib/jvm/java-25-openjdk-amd64" + "/usr/lib/jvm/java-21-openjdk-amd64" + "/usr/lib/jvm/default-java" + ) + for candidate in "${POSSIBLE_JDKS[@]}"; do + if [ -x "${candidate}/bin/java" ]; then + export JAVA_HOME="${candidate}" + export PATH="${JAVA_HOME}/bin:${PATH}" + break + fi + done +fi + +if ! command -v java &>/dev/null; then + echo -e "${COLOR_RED}Erro: Java não foi encontrado no PATH nem nos diretórios padrão de JDK.${COLOR_RESET}" + echo -e "${COLOR_YELLOW}Dica: Não é necessário executar com 'sudo'. Tente executar normalmente: ./build-jar.sh${COLOR_RESET}" + exit 1 +fi + +JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | head -n 1) +echo -e "Ambiente Java detectado: ${COLOR_BOLD}${JAVA_VERSION}${COLOR_RESET} (${JAVA_HOME:-$(which java)})" + +# 2. Verificação do Maven Wrapper +if [[ ! -f "./mvnw" ]]; then + echo -e "${COLOR_RED}Erro: Maven Wrapper (./mvnw) não encontrado na raiz do projeto.${COLOR_RESET}" + exit 1 +fi +chmod +x ./mvnw + +# 3. Execução do Build +MVN_ARGS=("clean" "package") +if [[ "${RUN_TESTS}" == "false" ]]; then + echo -e "${COLOR_YELLOW}Modo rápido ativado (pulando testes unitários). Para testar use: ./build-jar.sh --with-tests${COLOR_RESET}" + MVN_ARGS+=("-DskipTests") +else + echo -e "${COLOR_CYAN}Executando testes unitários e de integração...${COLOR_RESET}" +fi + +echo -e "Iniciando compilação e geração do Fat JAR via Maven..." +./mvnw "${MVN_ARGS[@]}" + +SOURCE_JAR="modules/cybercore-app/target/cybercore-signer.jar" +DEST_JAR="./cybercore-signer.jar" + +if [[ ! -f "${SOURCE_JAR}" ]]; then + echo -e "${COLOR_RED}Erro: O artefato ${SOURCE_JAR} não foi gerado pelo build.${COLOR_RESET}" + exit 1 +fi + +# 4. Copia para a raiz para facilitar o acesso +cp -f "${SOURCE_JAR}" "${DEST_JAR}" + +JAR_SIZE=$(ls -lh "${DEST_JAR}" | awk '{print $5}') + +echo "" +echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}" +echo -e "${COLOR_GREEN}${COLOR_BOLD}✓ JAR GERADO COM SUCESSO!${COLOR_RESET}" +echo -e "Arquivo: ${COLOR_BOLD}${DEST_JAR}${COLOR_RESET} (${JAR_SIZE})" +echo -e "Local de origem: ${SOURCE_JAR}" +echo -e "${COLOR_GREEN}${COLOR_BOLD}============================================================${COLOR_RESET}" +echo "" +echo -e "Para testar manualmente, execute:" +echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR}${COLOR_RESET} # Interface gráfica desktop" +echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --dev${COLOR_RESET} # Modo desenvolvimento (logs detalhados)" +echo -e " ${COLOR_CYAN}java -jar ${DEST_JAR} --headless${COLOR_RESET} # Modo console de segundo plano (porta 21325)" +echo "" + +# 5. Execução automática opcional +if [[ "${RUN_AFTER_BUILD}" == "true" ]]; then + echo -e "${COLOR_BOLD}Iniciando aplicação agora...${COLOR_RESET}" + java -jar "${DEST_JAR}" +fi diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml new file mode 100644 index 0000000..e3949df --- /dev/null +++ b/config/spotbugs/exclude.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/docs/CCSP.md b/docs/CCSP.md new file mode 100644 index 0000000..ae4aec0 --- /dev/null +++ b/docs/CCSP.md @@ -0,0 +1,208 @@ +# CyberCore Signing Protocol (CCSP/1) + +O **CyberCore Signing Protocol (CCSP)** é o protocolo padrão para comunicação local entre aplicações web rodando no navegador e o agente desktop **CyberCore Signer**. + +A versão inicial descrita neste documento é a **CCSP/1**. + +--- + +## 1. Princípios de Segurança + +1. **Isolamento de Rede:** O servidor local escuta exclusivamente no endereço de loopback `127.0.0.1` (porta padrão `21325`), impedindo qualquer acesso a partir da rede local (LAN) ou externa (WAN). +2. **Controle Estrito de Origem (CORS):** O agente valida cabeçalhos `Origin` e responde apenas a pré-voos `OPTIONS` com origens autorizadas. +3. **Registro e Autorização de Aplicações:** Nenhuma aplicação desconhecida tem acesso a certificados ou assinaturas. Ao tentar conectar pela primeira vez, uma caixa de diálogo nativa solicita aprovação explícita do usuário. +4. **Confirmação Visual da Assinatura:** Toda operação de assinatura exige confirmação manual na interface gráfica, exibindo a aplicação solicitante, o hash do documento e o certificado selecionado. +5. **Proteção Contra Ataques de Replay:** Toda requisição deve conter um `nonce` UUID único com validação temporal (`expires_at` ou `timestamp`). Requisições com nonces repetidos são sumariamente rejeitadas (`REPLAY_DETECTED`). +6. **Privacidade Criptográfica:** Apenas o **digest (hash)** do documento é enviado ao agente. O documento completo nunca sai da aplicação web e a chave privada nunca sai do dispositivo/computador do usuário. + +--- + +## 2. Endpoints do Protocolo (CCSP/1) + +### 2.1 `GET /ccsp/v1/status` +Verifica se o agente está instalado, em execução e pronto para operações. + +#### Resposta de Sucesso (200 OK) +```json +{ + "version": "1", + "status": "ready", + "agent": "CyberCore Signer", + "agent_version": "0.1.0", + "os": "Windows 11", + "timestamp": "2026-10-08T19:40:00Z" +} +``` + +--- + +### 2.2 `GET /ccsp/v1/certificates` +Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura. + +#### Headers / Parâmetros +- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`). +- `Origin`: Origem web autorizada. + +#### Resposta de Sucesso (200 OK) +```json +{ + "version": "1", + "status": "success", + "certificates": [ + { + "id": "win-4340656a", + "alias": "CLAUDIO SILVA:12345678900", + "common_name": "CLAUDIO SILVA:12345678900", + "subject_dn": "CN=CLAUDIO SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR", + "issuer_dn": "CN=AC SERPRO v5, O=ICP-Brasil, C=BR", + "type": "A3", + "provider_id": "provider-win-store", + "device_name": "Token / Smart Card (Windows CAPI/CNG)", + "valid_from": "2024-05-10T12:00:00Z", + "valid_to": "2027-05-10T12:00:00Z", + "serial_number": "72910482910482019482", + "can_sign": true, + "status": "AVAILABLE" + } + ] +} +``` + +--- + +### 2.3 `POST /ccsp/v1/authorize` +Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário. + +#### Requisição +```json +{ + "version": "1", + "application_id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Portal Assinaturas Jurídicas" +} +``` + +#### Resposta (200 OK) +```json +{ + "version": "1", + "status": "authorized", + "application_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +--- + +### 2.4 `POST /ccsp/v1/sign` +Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquivos gerais). + +#### Requisição +```json +{ + "version": "1", + "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "application_id": "550e8400-e29b-41d4-a716-446655440000", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "certificate_hint": "win-4340656a", + "document_name": "contrato_locacao.pdf", + "document_type": "PDF", + "nonce": "c4ca4238a0b923820dcc509a6f75849b", + "expires_at": "2026-10-08T19:45:00Z" +} +``` + +#### Resposta de Sucesso (200 OK) +```json +{ + "version": "1", + "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "status": "signed", + "signature": "MEUCIQDq...base64...", + "certificate": "MIIFazC...base64...", + "certificate_chain": [ + "MIIFazC...base64...", + "MIIEazC...base64..." + ], + "algorithm": "SHA256withRSA", + "subject_name": "CLAUDIO SILVA", + "subject_cpf_cnpj": "12345678900", + "document_name": "contrato_locacao.pdf", + "document_type": "PDF" +} +``` + +--- + +### 2.5 `POST /ccsp/v1/authenticate` +Executa o login / autenticação digital segura de um usuário em sistemas web via desafio criptográfico (Challenge-Response ICP-Brasil). + +#### Requisição +```json +{ + "version": "1", + "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "application_id": "550e8400-e29b-41d4-a716-446655440000", + "operation": "authenticate", + "challenge": "server-session-challenge-nonce-2026", + "certificate_hint": "win-4340656a", + "purpose": "AUTHENTICATION", + "nonce": "c4ca4238a0b923820dcc509a6f75849b", + "expires_at": "2026-10-08T19:45:00Z" +} +``` + +#### Resposta de Sucesso (200 OK) +```json +{ + "version": "1", + "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "status": "authenticated", + "signature": "MEUCIQDq...base64...", + "certificate": "MIIFazC...base64...", + "certificate_chain": [ + "MIIFazC...base64..." + ], + "algorithm": "SHA256withRSA", + "subject_name": "CLAUDIO SILVA", + "subject_cpf_cnpj": "12345678900", + "document_type": "AUTH_CHALLENGE" +} +``` + +#### Resposta de Erro (400/403/404) +```json +{ + "version": "1", + "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", + "status": "error", + "error_code": "USER_CANCELLED", + "message": "Operação de assinatura cancelada pelo usuário." +} +``` + +--- + +## 3. Tabela de Códigos de Erro Estruturados + +| Código | Significado | +|---|---| +| `CERTIFICATE_NOT_FOUND` | Certificado indicado no hint não foi encontrado. | +| `CERTIFICATE_EXPIRED` | O certificado selecionado está fora do período de vigência. | +| `CERTIFICATE_INVALID` | O certificado é inválido ou corrompido. | +| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível ou ausente. | +| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. | +| `TOKEN_REMOVED` | Dispositivo de hardware foi removido durante a assinatura. | +| `PIN_REQUIRED` | PIN obrigatório para liberar chave privada em PKCS#11. | +| `PIN_INCORRECT` | PIN digitado está incorreto. | +| `PKCS11_ERROR` | Falha no middleware PKCS#11 do fabricante. | +| `SIGNATURE_FAILED` | Erro durante a geração da assinatura digital. | +| `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou recusada pelo usuário. | +| `REQUEST_EXPIRED` | A data/hora de validade da requisição expirou. | +| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | +| `INVALID_ORIGIN` | O cabeçalho Origin não corresponde ao cadastro da aplicação. | +| `UNSUPPORTED_ALGORITHM` | Algoritmo não suportado (ex: diferente de SHA256withRSA). | +| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela de confirmação. | +| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário está inativo no SO. | +| `COMMUNICATION_ERROR` | Erro interno no agente de comunicação local. | diff --git a/docs/a1-a3-certificates.md b/docs/a1-a3-certificates.md new file mode 100644 index 0000000..be529d3 --- /dev/null +++ b/docs/a1-a3-certificates.md @@ -0,0 +1,46 @@ +# Certificados Digitais A1, A3 e PKCS#11 + +O **CyberCore Signer** oferece suporte abrangente aos formatos e repositórios de certificados digitais padrão ICP-Brasil utilizados no Brasil. + +--- + +## 1. Certificados Digitais A1 (Software / PKCS#12) + +Os certificados A1 são arquivos com extensão `.pfx` ou `.p12` protegidos por senha. + +### Fluxo de Uso +1. O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou importa via tela de Dashboard. +2. É solicitada a senha em um diálogo seguro (`A1PasswordDialog`), mantida em `char[]` temporário e sobrescrita com zeros logo após a leitura da chave. +3. O provedor `Pkcs12CertificateProvider` instancia um `KeyStore.getInstance("PKCS12")` e extrai os metadados do certificado X.509 (`subject`, `issuer`, `validade`, `algoritmo`, `tamanho da chave`). +4. Durante a assinatura, a chave privada em memória gera a assinatura digital via `Signature.getInstance("SHA256withRSA")`. + +--- + +## 2. Certificados Digitais A3 no Windows (Windows Certificate Store) + +No Windows 10/11, a maioria dos drivers de tokens USB e Smart Cards (ex: SafeSign, SafeNet Authentication Client, Serpro, Certisign) registra seus certificados diretamente no repositório de certificados do usuário (`Windows-MY`). + +### Vantagens do Provider `WindowsCertificateStoreProvider` +- Acessa certificados físicos via CryptoAPI (CAPI) e Cryptography Next Generation (CNG) através do provedor JDK `SunMSCAPI`. +- A chave privada permanece dentro do chip criptográfico do token. +- Quando o token exige autenticação por PIN, a janela nativa do próprio driver do fabricante é invocada com segurança pelo sistema operacional Windows. + +--- + +## 3. Hardware PKCS#11 Direto (Tokens USB e Smartcards) + +Para ambientes onde se deseja acesso direto ao middleware ou no Linux: +- `Pkcs11CertificateProvider` pesquisa bibliotecas DLL / SO padrão: + - `eps2003csp11.dll` (SafeNet / eToken) + - `aetpkss1.dll` (SafeSign AET) + - `acospkcs11.dll` (ACOS / G&D) + - `opensc-pkcs11.dll` / `opensc-pkcs11.so` (OpenSC) +- Utiliza o provider SunPKCS11 do JDK (`jdk.crypto.cryptoki`). + +--- + +## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`) + +O componente `SmartCardWatcher` monitora os leitores PC/SC em segundo plano: +- **Ao inserir um token:** Atualiza a lista de certificados automaticamente e notifica a interface gráfica. +- **Ao remover o token:** Registra evento de auditoria (`AUDITORIA [TOKEN_REMOVED]`), limpa o cache de PIN de sessão em `PinSessionManager` e força um warm reset via `SmartCardSessionManager`. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..150052c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,79 @@ +# Arquitetura do CyberCore Signer + +O **CyberCore Signer** é um agente desktop nativo desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica JCA/JCE pura, projetado para permitir que aplicações web autorizadas solicitem assinaturas digitais ICP-Brasil padrão **A1** e **A3** diretamente da máquina do usuário. + +--- + +## 1. Visão Geral da Arquitetura em Módulos + +O projeto adota uma arquitetura desacoplada em múltiplos módulos Maven independentes: + +``` +cybercore-signer/ +├── modules/ +│ ├── cybercore-core/ # Núcleo de criptografia, provedores, protocolo e segurança +│ ├── cybercore-ui/ # Interface gráfica JavaFX, diálogos nativos e System Tray +│ └── cybercore-app/ # Ponto de entrada (Main), bootstrap e empacotamento nativo +├── sdk/ # SDK Web em JavaScript (@cybercore/signer-sdk) +├── scripts/ # Scripts de build nativo jpackage para Windows e Linux +└── docs/ # Especificações técnicas e manuais de integração +``` + +### 1.1 `cybercore-core` +- **Domínio:** Modelos imutáveis como `ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType` e `CertificateStatus`. +- **Criptografia:** `SigningService` e `DigestAlgorithm` utilizando exclusivamente os provedores padrão JCA/JCE (sem reimplementação manual de algoritmos). +- **Provedores:** Abstração `CertificateProvider` com implementações: + - `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória protegida. + - `WindowsCertificateStoreProvider`: integração nativa com o repositório de chaves do Windows (`Windows-MY` via SunMSCAPI/CNG). + - `Pkcs11CertificateProvider`: integração com drivers de middleware e tokens USB/Smartcards via `SunPKCS11`. +- **Protocolo & Rede Local:** `CcspServer` escuta estritamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1. +- **Segurança & Configurações:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` (persistência em `~/.cybercore/config.json`). +- **Validação Criptográfica:** `SignatureValidationService` com suporte a Assinaturas Incorporadas (PAdES PDF, CMS/P7M com conteúdo embutido, XMLDSig) e Assinaturas Destacadas (Detached CMS/P7S). +- **Selos Visuais Personalizados:** `CustomSealStorage` (persistência em `~/.cybercore/seals/`), `PdfSealThemeRegistry` dinâmico e `CpfDisplayMode` (privacidade LGPD). + +### 1.2 `cybercore-ui` +- Interface desenvolvida com JavaFX 25, empregando layout moderno e simétrico de grade 3x3 de operações e coluna lateral de utilitários: + - `MainAppView`: grade 3x3 otimizada: + - Linha 0: Assinar PDF, Assinar XML, Assinatura Atachada + - Linha 1: Co-Assinar Documento, Carimbo do Tempo, Gerenciar Selos + - Linha 2: Validar Assinatura, Validar Carimbo, Sair do Agente + - `SealDesignerDialog`: criador e instalador de selos digitais personalizados com pré-visualização ao vivo (Live Preview), seletores de cores, estilos de emblema, obrigatoriedade de nome completo, controle de mascaramento de CPF e estampa de data/hora. + - `SignatureValidationDialog`: diálogo de validação criptográfica real com alternância entre Assinatura Incorporada e Assinatura Destacada, extração de documento embutido e relatório de signatários. + - `PdfSignDialog`: modal de assinatura PAdES com posicionamento por clique na folha, atalho direto para criar/instalar selos e opções dinâmicas de CPF/data-hora. + - `SettingsView`: configurações gerais persistentes, política de PIN e gerenciamento de selos instalados. + +### 1.3 `cybercore-app` +- Main bootstrap (`CyberCoreSignerApplication`). +- Tratamento de parâmetros de inicialização (`--dev`, `--headless`). +- Execução de diagnósticos preliminares de ambiente e engine criptográfica antes do início das threads de escuta. +- Geração de Fat JAR autocontido via `maven-shade-plugin` e preparação para distribuição `jpackage`. + +--- + +## 2. Fluxo de Dados e Princípio de Menor Privilégio + +O CyberCore Signer opera sob o **modelo de assinatura descentralizada por digest**: +1. A aplicação web calcula o hash criptográfico SHA-256 no próprio navegador utilizando a Web Crypto API nativa. +2. Apenas o hash Base64 de 32 bytes trafega localmente para o CyberCore Signer. +3. O documento completo nunca sai do navegador e a chave privada nunca sai do computador ou do hardware do usuário. + +```mermaid +sequenceDiagram + autonumber + actor Usuario as Usuário + participant Web as Aplicação Web (SDK) + participant CCSP as CcspServer (127.0.0.1) + participant UI as CyberCore JavaFX + participant Crypto as SigningService & Providers + + Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce) + CCSP->>CCSP: Valida autorização, nonce anti-replay e digest + CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation) + UI->>Usuario: Exibe Diálogo (Hash, Certificado, Aplicação) + Usuario->>UI: Aprova assinatura (e insere PIN se A3) + UI-->>CCSP: Confirmação concedida + CCSP->>Crypto: sign(SigningContext) [Trava FIFO] + Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign() + Crypto-->>CCSP: SigningResult (signatureBytes, certChain) + CCSP-->>Web: JSON { status: "signed", signature: "...", certificate: "..." } +``` diff --git a/docs/authentication-login.md b/docs/authentication-login.md new file mode 100644 index 0000000..ee7a4b5 --- /dev/null +++ b/docs/authentication-login.md @@ -0,0 +1,71 @@ +# Autenticação e Login Digital via CyberCore Signer + +Este documento descreve o fluxo de **Login e Autenticação de Usuários com Certificado Digital ICP-Brasil (A1 e A3)** através do agente desktop **CyberCore Signer** e o protocolo **CCSP/1**. + +--- + +## 1. Visão Geral do Modelo de Autenticação + +O login por certificado digital no CyberCore Signer adota o padrão de mercado **Challenge-Response Criptográfico**: +1. O backend da sua aplicação web gera um **desafio único (challenge / nonce)** para a sessão atual. +2. A aplicação web solicita a assinatura desse desafio ao **CyberCore Signer** através do SDK JavaScript. +3. O CyberCore Signer apresenta ao usuário na tela nativa do computador uma solicitação visual com destaque de **LOGIN / AUTENTICAÇÃO**: + - Identifica a aplicação solicitante e a origem; + - Mostra o certificado selecionado e o titular; + - Solicita aprovação expressa do usuário (e a digitação do PIN caso seja token ou smart card A3). +4. O CyberCore assina o desafio com a chave privada do certificado local e devolve: + - A **assinatura digital do desafio**; + - O **certificado X.509 completo** (DER Base64) e cadeia; + - O **nome e CPF/CNPJ** do titular extraídos do certificado. +5. O frontend web envia a resposta de volta ao backend, que: + - Valida criptograficamente a assinatura com a chave pública do certificado recebido; + - Confirma a validade e cadeia do certificado junto à ICP-Brasil; + - Localiza o usuário correspondente no banco de dados pelo CPF/CNPJ; + - Cria a sessão autenticada do usuário no sistema. + +--- + +## 2. Exemplo de Integração Frontend com o SDK + +```javascript +import { CyberCoreSigner } from '@cybercore/signer-sdk'; + +const signer = new CyberCoreSigner({ + applicationName: 'Portal Corporativo' +}); + +async function realizarLoginComCertificado(certificateId) { + // 1. Obter desafio único gerado pelo seu backend + const { challenge } = await fetch('/api/auth/challenge').then(r => r.json()); + + // 2. Solicitar autenticação ao CyberCore Signer + const authResult = await signer.authenticate({ + certificateId: certificateId, + challenge: challenge + }); + + // 3. Enviar assinatura e certificado ao backend para validação e abertura de sessão + const sessionResponse = await fetch('/api/auth/login-certificate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + challenge: authResult.challenge, + signature: authResult.signature, + certificate: authResult.certificate + }) + }); + + if (sessionResponse.ok) { + console.log('Usuário autenticado com sucesso:', authResult.subjectName); + window.location.href = '/dashboard'; + } +} +``` + +--- + +## 3. Segurança e Prevenção de Ataques + +- **Prevenção de Ataques de Replay:** Cada desafio deve possuir tempo de expiração curto (ex: 60 a 90 segundos) e ser descartado no servidor após uma única tentativa. +- **Proteção da Chave Privada:** A chave privada e o PIN do usuário permanecem exclusivamente sob custódia do hardware/computador local do usuário. +- **Consentimento Explícito:** O CyberCore Signer nunca assina desafios em segundo plano sem a exibição do diálogo e autorização do usuário na tela nativa. diff --git a/docs/development-mode.md b/docs/development-mode.md new file mode 100644 index 0000000..71d3ce9 --- /dev/null +++ b/docs/development-mode.md @@ -0,0 +1,48 @@ +# Modo de Desenvolvimento e Diagnóstico Detalhado + +O **CyberCore Signer** opera exclusivamente com **certificados digitais e provedores criptográficos reais** (A1 via arquivos `.pfx`/`.p12`, repositório nativo do Windows `Windows-MY` e tokens/smart cards físicos PKCS#11). Em conformidade com a exigência de segurança de produção, **não existem mocks ou certificados sintéticos no agente**. + +--- + +## 1. Ativação do Modo de Desenvolvimento + +Para iniciar o agente em modo de desenvolvimento (com logs detalhados e diagnósticos ampliados), utilize o argumento `--dev`: + +```bash +java -jar cybercore-signer.jar --dev +``` + +Ao iniciar, uma mensagem informativa é registrada no log: +``` +========================================================= + MODO DE DESENVOLVIMENTO ATIVADO (--dev) + Logs detalhados ativos. Operação com certificados reais. +========================================================= +``` + +--- + +## 2. Testando em Ambiente de Desenvolvimento + +Para desenvolvedores testarem fluxos completos de assinatura de PDF, XML, arquivos em geral e login digital: + +1. **Certificados A1 de Teste/Homologação:** + - Adicione um certificado A1 `.pfx` ou `.p12` emitido por uma autoridade de teste ou autoassinado. + - Pela interface desktop, clique no botão **"Importar A1 (.pfx/.p12)"** no Dashboard e informe o arquivo e a senha. + - O certificado ficará disponível imediatamente para todas as operações CCSP/1. + +2. **Repositório Nativo do Windows (Windows-MY):** + - Instale o certificado no repositório de certificados pessoais do Windows (`certmgr.msc`). + - O provedor `WindowsCertificateStoreProvider` detectará o certificado automaticamente. + +3. **Tokens USB / Smart Cards (PKCS#11):** + - Conecte o dispositivo físico com middleware homologado (SafeSign, GD Burti, SafeNet, OpenSC, etc.). + - O provedor `Pkcs11CertificateProvider` detectará o token e seus slots. + +--- + +## 3. Garantia de Operação Real + +1. Toda assinatura é gerada criptograficamente através dos provedores JCA/JCE reais. +2. A resposta do protocolo CCSP/1 contém assinaturas válidas e certificados X.509 autênticos. +3. Não há certificados simulados ou flags artificiais nas respostas do agente. diff --git a/docs/diagnostics.md b/docs/diagnostics.md new file mode 100644 index 0000000..8aeb6f7 --- /dev/null +++ b/docs/diagnostics.md @@ -0,0 +1,34 @@ +# Diagnóstico de Ambiente e Auto-Teste + +O **CyberCore Signer** possui um subsistema nativo de diagnóstico integrado (`DiagnosticService`), permitindo auditoria detalhada de saúde e integridade técnica da máquina do usuário. + +--- + +## 1. Itens Inspecionados no Diagnóstico + +Ao abrir a aba **Diagnóstico** na interface ou invocar `diagnosticService.runDiagnostic()`, o agente avalia: + +1. **Ambiente Java / Runtime:** + - Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS); + - Vendor do JDK (Temurin, Oracle, etc.); + - Arquitetura do processador e sistema operacional. +2. **Engine Criptográfica JCA/JCE:** + - Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.); + - Suporte ao algoritmo de hash `SHA-256`; + - Suporte ao algoritmo de assinatura `SHA256withRSA`. +3. **Repositório de Chaves do Sistema Operacional:** + - Detecção do repositório `Windows-MY` (Windows CryptoAPI / CNG). +4. **Middlewares PKCS#11 e SmartCard:** + - Verificação de bibliotecas de smartcard registradas no sistema (`eps2003csp11.dll`, `aetpkss1.dll`, `opensc-pkcs11.dll`, etc.); + - Disponibilidade do subsistema PC/SC `javax.smartcardio`. + +--- + +## 2. Auto-Teste de Assinatura Local (Self-Test) + +A aba de diagnóstico disponibiliza o botão **"Executar Auto-teste de Assinatura"**: +1. Seleciona o primeiro certificado operacional disponível na máquina. +2. Gera um hash SHA-256 interno de teste: `"Documento_Teste_CyberCore_"`. +3. Abre o diálogo de confirmação visual para validação do usuário. +4. Executa a assinatura real JCA/JCE e afere os bytes gerados. +5. Emite relatório de sucesso com tempo decorrido e tamanho da assinatura. diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..7f2fb9d --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,28 @@ +# Catálogo Estruturado de Erros + +O **CyberCore Signer** adota um sistema unificado e determinístico de códigos de erro (`CcspErrorCode`) compartilhados entre o agente Java desktop, o protocolo CCSP/1 e o SDK Web em JavaScript. + +--- + +## 1. Tabela de Erros Estruturados + +| Código de Erro | Descrição Detalhada | Ação Recomendada para a Aplicação Web | +|---|---|---| +| `CERTIFICATE_NOT_FOUND` | O certificado indicado pelo `certificate_hint` não foi localizado. | Solicite ao usuário selecionar novamente o certificado via `getCertificates()`. | +| `CERTIFICATE_EXPIRED` | O certificado selecionado expirou. | Oriente o usuário a renovar o certificado digital junto à autoridade certificadora. | +| `CERTIFICATE_INVALID` | O certificado possui estrutura corrompida ou ainda não é válido. | Verificar a data e integridade do arquivo/dispositivo. | +| `PRIVATE_KEY_NOT_AVAILABLE` | O certificado está presente mas a chave privada não está acessível. | Em tokens A3, verifique se o middleware/driver está instalado corretamente. | +| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. | Solicite ao usuário conectar o token USB ou inserir o smartcard no leitor. | +| `TOKEN_REMOVED` | O token USB ou cartão foi removido fisicamente durante a assinatura. | Peça ao usuário reconectar o token e reiniciar a operação de assinatura. | +| `PIN_REQUIRED` | O token exige PIN para liberar a chave privada e nenhum PIN foi fornecido. | Exibir campo de solicitação de PIN na tela. | +| `PIN_INCORRECT` | O PIN digitado está incorreto. | Informar o usuário e alertar sobre o risco de bloqueio de PUK após 3 tentativas. | +| `PKCS11_ERROR` | Falha no driver ou biblioteca PKCS#11 do fabricante. | Verificar a instalação do SafeSign, SafeNet ou OpenSC. | +| `SIGNATURE_FAILED` | Erro durante a geração dos bytes da assinatura digital. | Tentar novamente ou reiniciar o agente desktop. | +| `APPLICATION_NOT_AUTHORIZED` | A aplicação web teve autorização recusada pelo usuário. | Informar o usuário que é necessário aceitar a notificação na tela do agente. | +| `REQUEST_EXPIRED` | O timestamp ou `expires_at` da requisição expirou. | Gerar nova requisição de assinatura com timestamp atualizado. | +| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | Sempre gerar um UUID único para cada chamada de assinatura. | +| `INVALID_ORIGIN` | O cabeçalho `Origin` não confere com o cadastro da aplicação. | Conectar-se com a URL e protocolo autorizados. | +| `UNSUPPORTED_ALGORITHM` | Algoritmo solicitado não é suportado pelo agente ou certificado. | Utilizar algoritmo padrão homologado (`SHA256withRSA`). | +| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela do CyberCore. | Tratar como cancelamento voluntário sem mensagem de erro alarmante. | +| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário não está ativo no sistema operacional. | Verificar a integridade do Windows Certificate Store ou Java Runtime. | +| `COMMUNICATION_ERROR` | Erro interno de socket ou timeout de rede local. | Verificar se a porta `21325` não está bloqueada por antivírus local. | diff --git a/docs/image.png b/docs/image.png new file mode 100644 index 0000000000000000000000000000000000000000..b87ee6a2f0fa2cbd55cae3d87fa390afcd73197f GIT binary patch literal 150154 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�R1C>@$JOicE3WYHJX(t;> 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 literal 0 HcmV?d00001 diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..a7559a6 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,43 @@ +# Instalação e Empacotamento Nativo + +O **CyberCore Signer** foi projetado para distribuição sem exigir que o usuário final tenha o Java ou JRE instalado em seu computador. + +--- + +## 1. Requisitos de Sistema + +- **Sistema Operacional Homologado:** Windows 10 (64-bit) ou Windows 11 (64-bit). +- **Compatibilidade Adicional:** Linux x86_64 e macOS (arquitetura preparada). +- **Hardware:** Mínimo de 4 GB RAM, 150 MB de espaço em disco. +- **Porta de Rede Local:** A porta TCP `21325` deve estar livre em `127.0.0.1`. + +--- + +## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage` + +O empacotamento utiliza a ferramenta nativa `jpackage` incluída no JDK 21/25. + +### No Windows: +Execute o script em lote localizado na raiz: +```cmd +scripts\package-windows.bat +``` + +### O que o script realiza: +1. Compila o projeto em modo release: + ```cmd + mvnw clean package -DskipTests + ``` +2. Invoca o `jpackage` criando o instalador Windows: + - Extrai um runtime mínimo customizado contendo apenas os módulos necessários (`java.base`, `java.desktop`, `java.smartcardio`, `jdk.crypto.cryptoki`, etc.). + - Cria atalhos na Área de Trabalho e no Menu Iniciar. + - Gera um instalador padrão MSI ou EXE em `dist/`. + +--- + +## 3. Instalação e Inicialização com o Sistema + +1. Execute o instalador `CyberCore-Signer-0.1.0.msi`. +2. Siga as instruções do assistente de instalação. +3. Ao concluir, o ícone do **CyberCore Signer** aparecerá na Área de Notificação (System Tray) ao lado do relógio do Windows. +4. O serviço local `http://127.0.0.1:21325` estará imediatamente disponível para atender as aplicações web autorizadas. diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..a48553f --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,25 @@ +# Limitações Técnicas e Dependências Declaradas + +Conforme estabelecido na **Regra 23** do projeto (*"Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados. Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência"*), as seguintes limitações são formalmente declaradas: + +--- + +## 1. Dependências de Sistema Operacional e Hardware + +1. **Windows Certificate Store (Windows-MY):** + - Disponível apenas em sistemas operacionais Microsoft Windows (Windows 10/11). + - Em Linux e macOS, o provedor `WindowsCertificateStoreProvider` declara-se automaticamente indisponível (`isAvailable() == false`), operando via arquivos A1 ou PKCS#11 direto. +2. **Tokens A3 e Middlewares Proprietários:** + - O reconhecimento de smartcards e tokens USB depende da instalação prévia dos drivers do fabricante do dispositivo físico (ex: SafeSign Identity Client, SafeNet Authentication Client, drivers de leitoras Gemalto/Omnikey). + - O CyberCore Signer não instala drivers de hardware nem substitui o middleware do fabricante. +3. **Bloqueio de Cartão por Tentativas de PIN:** + - A política de bloqueio por tentativas consecutivas de PIN incorreto (geralmente 3 tentativas) é governada estritamente pelo chip físico do dispositivo A3 e pelo driver do fabricante, estando fora do controle do software do agente. + +--- + +## 2. Limitações de Rede e Concorrência + +1. **Escuta Exclusiva em Loopback:** + - O agente não oferece nem oferece suporte para escuta em endereços de rede externa (IPs locais de LAN ou WAN). Essa restrição é um elemento fundamental do modelo de segurança. +2. **Operações Concorrentes no Mesmo Token Físico:** + - Drivers PKCS#11 e smartcards ICP-Brasil frequentemente operam em modo mono-thread ou canal serial exclusivo. Por essa razão, o CyberCore Signer serializa as assinaturas em fila FIFO (`signLock`) para impedir corrupção de sessão no chip do cartão. diff --git a/docs/pades.md b/docs/pades.md new file mode 100644 index 0000000..b299fba --- /dev/null +++ b/docs/pades.md @@ -0,0 +1,92 @@ +![alt text](image.png)# Assinaturas PDF (PAdES) e Selos Visuais em SVG + +Este documento descreve a arquitetura e o funcionamento da assinatura digital de arquivos PDF no padrão **PAdES (PDF Advanced Electronic Signatures)** no CyberCore Signer, incluindo o subsistema de **selos visuais temáticos baseados em SVG**. + +--- + +## 1. Visão Geral da Arquitetura + +O CyberCore Signer implementa assinatura PAdES em dois modos complementares: + +1. **Modo Desacoplado via Protocolo CCSP/1 (Web):** A aplicação web prepara a estrutura `/ByteRange`, envia apenas o digest SHA-256 e o CyberCore assina localmente com o certificado A1/A3 sem expor a chave privada. +2. **Modo Desktop Completo (Desktop UI):** O usuário seleciona um arquivo PDF localmente, escolhe o certificado ICP-Brasil, customiza o selo visual e o `PdfSigningService` gera o PDF assinado completo com envelope CMS/PKCS#7 e estampa gráfica. + +```mermaid +graph TD + A[Arquivo PDF Original] --> B[PdfSigningService] + C[Tema de Selo SVG] --> D[PdfSealRenderer Java2D] + E[Metadados Signatário] --> D + D -->|Estampa Visual 300 DPI| B + F[Chave Privada A1/A3] --> G[Bouncy Castle CMS Generator] + B -->|PAdES Incremental| H[PDF Assinado Digitalmente] + G --> H +``` + +--- + +## 2. Subsistema de Selos Visuais em SVG e Temas + +O selo visual é uma representação gráfica da assinatura estipulada em conformidade com as recomendações do ITI e MP nº 2.200-2/2001. + +### 2.1 Temas Pré-configurados e Ícones Vetoriais (`PdfSealThemeRegistry` & `PdfSealRenderer`) + +Os brasões e ícones padrão do sistema ficam centralizados em: +- **Catálogo de Temas:** `modules/cybercore-core/.../pdf/seal/PdfSealThemeRegistry.java` (definição de cores, dimensões, textos padrão e tipo de ícone). +- **Desenho Vetorial Java2D:** `modules/cybercore-core/.../pdf/seal/PdfSealRenderer.drawThemeIcon()` (renderização matemática em Java2D dos brasões padrão: `shield`, `star`, `crystal`, `notary`, etc.). + +| ID do Tema | Nome | Descrição | Ícone Vetorial | +| :--- | :--- | :--- | :--- | +| `icp-brasil` | **ICP-Brasil Oficial** | Padrão clássico com brasão e cadeado verde/azul | Escudo e cadeado | +| `cybercore-modern` | **CyberCore Modern** | Estilo tecnológico neon com cristal multifacetado | Cristal vetorial | +| `minimal-clean` | **Minimalista Clean** | Monocromático para relatórios executivos sóbrios | Carimbo notarial com check | +| `gov-br` | **Gov.br Oficial** | Identidade visual Verde-Oliva conforme MP 2.200-2/2001 | Brasão estelar estilizado | + +### 2.2 Renderização Gráfica (`PdfSealRenderer`) + +- O núcleo `cybercore-core` **não depende de JavaFX**, atendendo às regras estritas de arquitetura (`ArchitectureTest`). +- A renderização é efetuada através da API **Java2D (AWT)** nativa com anti-aliasing duplo (`RenderingHints.VALUE_ANTIALIAS_ON`, subpixel text, escala 2x / 300 DPI). +- Exporta tanto `BufferedImage` quanto `byte[]` PNG para inclusão no PDFBox ou pré-visualização dinâmica na interface com `ImageView`. + +### 2.3 Criação, Instalação e Privacidade de Selos Personalizados + +- **Foco Exclusivo no Titular (Sem Emissor/AC no Gráfico):** O selo foi desenhado para ser limpo e elegante. Dados técnicos do certificado e o emissor da Autoridade Certificadora não são exibidos no gráfico do selo, evitando poluição visual. +- **Obrigatoriedade do Nome Completo:** O renderizador calcula dinamicamente a métrica de fontes via `drawFittedText()` para garantir que o nome completo do signatário seja sempre exibido na íntegra, com destaque tipográfico (13.5pt a 14.5pt em negrito), sem cortes ou truncamentos. +- **Upload de Logotipo / Emblema Próprio:** Suporte a upload de imagens de logotipos (PNG, JPG, BMP) armazenadas de forma portável em Base64 dentro do tema, com renderização redimensionada e anti-aliasing de 300 DPI. +- **Campos Customizáveis (Matrícula, Cargo, OAB, etc.):** Permite definir rótulo e valor padrão no tema (ex: `Matrícula: 104829/SP` ou `Cargo: Auditor Fiscal`) ou informar pontualmente durante a assinatura via `customExtraInfo`. A informação é posicionada de forma harmônica na linha de identificação do signatário. +- **Privacidade do CPF (LGPD):** O modelo `CpfDisplayMode` suporta três modalidades: + - `FULL`: exibe o documento completo (`CPF: 123.456.789-00`). + - `MASKED`: mascara dígitos sensíveis (`CPF: ***.456.789-**`). + - `NONE`: oculta totalmente o CPF do gráfico do selo. +- **Data e Hora Opcional:** Parâmetro booleano para incluir ou suprimir data/hora no gráfico. +- **Instalação e Persistência Local:** Através de `CustomSealStorage`, novos selos criados ou importados são salvos em `~/.cybercore/seals/*.json` e registrados dinamicamente no `PdfSealThemeRegistry`. + +### 2.4 Gerenciamento Centralizado de Selos (`SealManagementDialog`) + +- **Lista Completa:** Exibição unificada de selos embutidos do sistema e selos personalizados instalados pelo usuário. +- **Selo Padrão Ativo:** Permite definir qual selo é o padrão ativo para todas as assinaturas do usuário, gravado em `AgentConfiguration` (`defaultSealThemeId`). +- **Pré-visualização em Tempo Real:** Renderização fiel do selo com dados de exemplo ao navegar pela lista. +- **Clonagem e Edição:** Permite duplicar qualquer selo (inclusive os embutidos) e abri-lo no `SealDesignerDialog` para customização. +- **Importação e Exportação:** Exportação de temas em arquivos `.json` e importação com um clique para compartilhamento entre máquinas e equipes. + +--- + +## 3. Posicionamento e Pré-Visualização Interativa + +A posição e a página da estampa são parametrizáveis via `PdfSignatureOptions`: + +- **Posições Automáticas:** `BOTTOM_RIGHT`, `BOTTOM_LEFT`, `TOP_RIGHT`, `TOP_LEFT`. +- **Posição Interativa Personalizada (`CUSTOM`):** O usuário clica ou arrasta o selo diretamente sobre a página do PDF na interface gráfica. As coordenadas na tela são matematicamente mapeadas para as coordenadas de mídia do PDFBox: + $$X_{pdf} = \frac{X_{clique}}{Largura_{view}} \times Largura_{pdf}$$ + $$Y_{pdf} = Altura_{pdf} - \left(\frac{Y_{clique}}{Altura_{view}} \times Altura_{pdf}\right) - Altura_{selo}$$ +- **Páginas:** `LAST_PAGE` (recomendado), `FIRST_PAGE`, `ALL_PAGES`, ou página específica numérica (`targetPageNumber`). +- **Dimensões padrão:** 200 pt de largura por 55 pt de altura, com margens de segurança e clamping contra extravasamento. +- **Renderização da Folha:** O `PdfSigningService` fornece `renderPagePngBytes(pdfBytes, pageIndex, dpi)` via PDFBox `PDFRenderer`, permitindo que o diálogo da UI JavaFX renderize a folha de papel em background thread com paginação e sobreposição visual do selo. + +--- + +## 4. Estrutura Criptográfica PAdES + +- **Motor:** Apache PDFBox 3.0.4 + Bouncy Castle 1.80 (`bcpkix-jdk18on`). +- **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`. +- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256. +- **Salva Incremental:** Preserva a integridade de assinaturas anteriores e do documento original sem quebrar certificações prévias. diff --git a/docs/screenshots/01_dashboard.png b/docs/screenshots/01_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..a22786b5c5f486eecafe5bb7ccb63091ff1de294 GIT binary patch literal 48464 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/01_layout_claro.png b/docs/screenshots/01_layout_claro.png new file mode 100644 index 0000000000000000000000000000000000000000..3e2c1e98d8b792a0722a1d11ea05ca0ddcc7f761 GIT binary patch literal 60530 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 z +`Cd^vNTD>;epqY;p|2skmi2Y0(wMqw>bDdnbMxWO{#@!=54AH-z@iR(WKx;E8`CD;AkfXDP-|n#-9_QiM05RNLFpa~OZSNWpCA++k`?V3&Tg<)w3p8?E z5q;^7rt?#vZ!o^zPQXyf7$0m*LCjmd z!CH2-=E8S@0#W73{84J$BV}B@*iqKg28FwmxWD(w(`(2^oZs?G+9;~xJ6q16l~Bp( zEyG6sEt=7b?~17zEXyAZmTH6raBXf{Fq|t1ubgYwY@ngt-i=8&m~*NsH5If z2&KV}jOr}0*6&M#7((q-5OyFZj+)FeO`jh4S#%nTN zUv$(8{1oy5YPP=E7N~a_x#{LY^*V=Haq$ntu{dnml&OwbY>;Vpk2k=N=i399E~agU ztGjO?quQUm1g>#cHF15*)R`-Hm+9&FVhX+3zYK0*({X+D!=Q}MY~5lq7033P#|*3r z4T0=q_?W{!T$$ddo2FS$v^+`gMPLHTU6rY*=ai*!8>%l^&P#h2>&4en-x4{>Vv8y| z$EBkANWaqz?6(qGD+Vqn^Y3FNm+4u$>Z^&e-yGuxXVSt!o#lgwn?7L#-5~ce_IX4m zMXvPF$G!5|quB5P@ts{|lq_;nntVTEKz%>cvGZxdIon1={Y&0&!U=m3{KJaf1zWucwBEXLQn>3 z(y&V`QC%M#W@?r2%eJ;rJ5E)#6r8)UN5h;wiL%$ztodO0QH=lp^ez?E~aMpLyE*hKCxRcl9Upsnu}S z`F2$=UE!2k0@pEyFPlJ0KppB>yoR-1T%J}d#w2lKuD#qS$eG zPxaQhh6^jE1G1=5*NH9SIYM70ECb*EsSvI*p2} zXcIYw%Ah_nJqLwggWS0Dn`ymKO;}%$?L)J*DpG3z4CF2$O{;^84Kvsct9P4jYS*7M z+Zk)l@BuPao@%X1j2w+0y0H5?#j)JkB4qv}MiU>Z~&G9md zjX#WF+@s#32=7?`4$FX$hA)N09{5`du=_H{^&*6O6Za5<2SYn&YTt-JhNE$7P`wj_ zvzIU(;m1ntJ>jBlm>9%lEcLGW6o*)>hOXTRfpbNFopV(UUE3khmV<0>pf%5|mT@tT z2t*M{)GZF{26_c^Umm20D)ic^(KEY~KH9O1JCrSzfzs*#W0SHnq4C7Rr*Mv=+TQY{ z05@!qu^+1N94^Qy2Q|-R)_IOp8ZHU!yD?(ia%rgD!rde7ns0&$82f(p)Q#>pi)kZ! zsXRA0Xdex?M%`Cn?JjepnsMK`ad^*B@L-Nts!H}n`z2`+7E?DNsm_ zP$f{!B4b0~e`Gfr?Hath&;@0*R)UHtDsIp`zdT{&O63VeL{rBTG59vshf?EPHrJO} zuCHdmy&UdoEL_3tpeGs{7L%<)T?nqDQ^)8;>x5%7T4_OM$prc*W7g9NJyZDf#n8@~~^t+f;8_zP3o6}ee! zFti{`Y*$gP@)M|9y1&pVvtrU66^UZSjRIy1u`5M~s;Y#~?x~5a_}S#s10OoFtq-BY zGQB!Sw<9vltOsuNQGrLsmO!1YYb(ytt6Vtj7E9Vh{qq3z3CWQ)W}&5A;|zE?NgO;$ z{e03}2YufnFUl&?eb(il5zv#&=2xri{QVQyWAgz@fWr{;NN=%q4?`2bUF#kT($_f< zrka98ALSETX~7OV8=dfk;MhDL+V9lnSb(r9^B3}Eq=zoEJ!6lXYD53kl;F69gi!sZ zz~@kq#bY0qM&&p>C3}QmzZ1-j(&9F}#Cv=k)SAI7Z3)rKF*TT_W4XK~9x1#lA0XAJ zKVmI^BThnb_^_J+D_G!S8s8my*bn=QI z*IYTNm^*$4xZdYbt1%Fm<`GyQvSai4(Ckh8^ZEs#9CSIGKMekDy8e!r@17F8)PdJafipzx^a|s?!hM z56k!T=W>r_PAeXp&C684-(!;&O{otAoYuA(d5Fv6h0{n!$@QSjtpL8H5)RwKw(`RY!Wb8U8cz?%>2-frPdkS*nLB4T35gzE{Gi|rJOM}sqRUsKqtM}Scd z2ArBf$LPf9-8ZoQ@8z@?&n@`%1lge4SI$O&@@$liEW$fmCw<4_rSZP;KyfcGKl9n* z_LXZU=#V%GGlY!9$a+2gA&rk)2qb6--R)$NmzH0NQ0BtIZ+S;xcFT^}Lg-@k%sUb# zSK?fp-72x@?tG;>T{h#jC1$9l^orR&Y6I<3WkhR_lCG1`p_Z2P6x}UkA!9Re)Kqxk zhmW#^Bwt9D^dp3$=pMga`w(dys#7*L3n+g#XX(nSL#YiYhdJgn(}%LBeOBQqXhGRc z7f~W(kh?E?Z(Y?D%ZJ~f!6!V&V~CTtJa6kaE;UM%xfBu$l0KLkBfxAruE4YFkU@(#Y>37r8tk~$%I3AmAnvf)HTyIA8 z-#$~`eq%yGoIRjmDV2MDJ-a`bM}4ZV;Y1?z z!T#t;M#N}I!y`Mn;UqNV(PPH(N*6CUYWHEmWYz0A!~vMb8oR?FSf+|G z^of|K|9kd@7`Lhk@g|spw0V$kZJ}7m0LtZ(CR5xRVJZ4f$tW8ICEovA4$E+{XLLVx9Et_0Q+@Y#os;(nzDTQBOyJ|Ib3c zN(LeOFbl-z3mF;n`ah zA=ZCdzk?cz`~hUjdxo*hW%A0m(SN$@65f?_!mR4_#_JaRq+g<3-S@YwLnoKC`AjZC zPSG#(vFrQW_(JrNxqS3tX-te@|Jh;fhpz%huenDW{crakw{k?2CPhJZne@n?ZxiOu zf+}toO;#=NIQ#iY6-*F1L(jq3-c*g6;1zI+lAV2tKsF>sQdN@1 zsCGmVq@1L!&HEcOd6#7shwmXz#jp?aLQZH4lZ(}r@V5{Xo@5jU7g7rMyRJu_H?g6= z=t}rt#sB_NSK3+ct_1u_7@M(TU=e*|0oxv=oi{4ma zjYAxbWc|aU>Yd#o?#K#(uZme+g=4$@6_)~(Knbk*?s>iDuQpdZM?gS5+UOMq2|o*h z`|C)9-bOgv_RS#W6fPur9I|b&*s88;(2lN?$^+|Ab+!E)(fl8k^Sf)6P+bOdXLIiZ zczQlhCFliU4C(S_eXV}I6PaVHb8P8s{*%q`UG3f$NvQRJ-EXb?rR!xy=0iz%(kRaY_KVz5hl~0#`d!$i@I~;qi7(@#9Cy%*n#{6gXo#i24g-jZa~k#YB0=SIg5*vUMhs$wFNHjWeyX$I1=pY%+v1Ti^J z87wQ1;$RHC=k{5+$Hw z-O6|Vw2zToZ2`YLn=brS67E2wkYt63r2xO{T1%1cLy;@EH*kfA$$5ss<<5*YecbR% zS;m@g9Z$W{bCYf|mnATf+JvS~Gv7W}XEAE9hQna8X*8M1hNA^70?3bcEgXhvlDyT1 zhQ}zfc?Xam(QSr5IpPHwY@q6lsadG0`mT2`F&S(chU+%LN#6vLE)^zRhdxAor7(Wu zW;BbB+~E4FGdmAb$)}@JcWgi7N-_(`*ZO%nr#J3)Q995esqLs>W8TrAf=Rrajm&=9 z8r(7Bwu`BiP}9l&vpnwMQR`RLwSm-;CGCgxW>XxsHHC*dRe=Q;YH3&b!wMr+#n(6G z8bRikx1F9(YCA7UF;?J`LpP2qe5#?htb7lws|~e*6p<)cf~%^oW;-j6`BzjruC83M z(+;SpnUO<%ebG=|V%UhO4eThsETMX$!8&sxxNNpnjk3%(G(=x1PS&_>gdvQ`*|OfF zFh6V~@O+xgayNZfbLSoBah_CFaDFsiztp-rI@MAHVEAbVHM75xL}2L6V!-^IAoFz{2fasf)6+eEkWfCq2UkDpwAchdyx^HS{aKV zS~w0cmzqAK!l*$WG5(R+afdlhp>{$M3C~|(mYZ&ULSLoRE+(6BeHX%z{D(}*Iv$e?005<04H}|GX?Z-${8t`Y8TN(U~2u^fT zQdqJyJjm&g_zhG`<*VIscwTa?1+rQwj&2Jejvby<_t=~LL6Xlget1|Y{$>SJl|@x~ zc2;w4>A^YF9yHiqJ@om^b*eNX+?E{5hjP( z?IH%UllSsOxpjtSgGXUBk*gHb_1y3#pSbE0K>3IsoSD#7)^+l6Tv-Z|5uI0R9hzCZ zkHHzvD*t%A;V$n_?HE&TsPgvL#T!V4MXas*K&8d8(nD z3aRQ69b0l_051M=7?igRFgXhVZ|dEti#rdYRJwA_zD=5;RyLn_g?NiN!|zK)6BV!r z)#Xgme_?@D+UtfC0---H=eMiSKEQ|=&qB_KE7-oTi-h3-W7ywNy7`3aqGYrR+wAXC zMs4a2LAL+XkOEay)vGO~m|sDIIjnHE>9GZ!Y8+O@K9DNx94v5eOTto>)skkKJdI+3 z2qtAvcR%A_1sJcvIEpZzxcr@fgC!<4BOtdy;g?Pu;n8c1OTB^!k)KCY$0{)d{unHC z9oBnXX-G0cVntUrxSWS4@iM)!*R0jlzOBv5ABDik7ji?*)Tf0k1+rSwkd*usPQH(n z(}xWdCnxbuyRzo`d>8qU`acJctG8pYy$cJkwIN-Juoi_iihmD2A*?X#bt(NN!ET^f3tvw=uDEhC3Jr=+xBrozprjn)pNW#xFao{=EOaE$z&)QXGk&7mz%+FH$P#T-%8ZvHi~uzo zOCdwPyz1aKE%NzB2VKA7j&aO51Ze~01QvMVC@H#US&BfmY$k=#1NlpmZ}tYXxVSv= zb2#3VOvr|LbEp%T6X|_9PIsxk8>CgvuNc7avvq>+A!X>OZ%L)qWk|~ zt??;ww8lVGcQPvEeXMSJ%5CE=J|^3fzC=N@hYNDH>D_lNK+!@Gh7(_@iycKV7H_7M z9~Uh6OTJX1$)Clw3V~X#g5?L9e=YvS*kCi|>5rDMKN3F(2TJ3{)rAZXGIm7_p!blO zaqXbNLDQ3Jb7~7}OKMNknQTa4qkHkn?6C^KG+ZBLM4b%7b>=V>hvDxNcf7nyr{9fs zk4YoEkM~3LgUuZ#THeWf)ye)#P*MZx6u}OO9I#$6!D32GQ`U}!Jfk=F?H)>!E1b01 zaqnA4@poub_p_z02@@s z0a%9=6tk|OrNHF|3=K4mLb>!3y5OF`L+bdt;aoYkG*=XLRij*aww52VY$G*KGFCX8 z%JYihIz+iC>BUO^Cz@e4znE+nzlky4sh&#PfC8q(8dF&Vruv@#6xMe&^$Bn}Y@G&R zmZ{e6SXKr%#<3}1=*@|aACXgk7F0~2s3P)^byJBn{Gr*PDS+>?eto{|D-6=*>RE%q zy5^nxMB4!z4CS2;W?0#y=jQ&=Oa3hm7bk-}RHxZ3arMfH6mk$Xbss35--9R9{XmTs ziwzZ>Xwfe<7fAk+;%HoLRBc>sQZ1x5^B*Pb)DAA0uZqaA*#>Uf8}HF3jL?YvDtqlu zzMa({?tlcTe-v2F#o?e#uJ=A{dZoLYrtm*vi4$H!F^SHFhEHD7&e1g{bDqJAZ{0kO zc4&CVRx*znFrRKE(6PXy()bYcrsU<$P{?#ba%UMZvQ`N%t>*USg2tO{o{6~*Q6~Xa zk<#hQ)G{!+`i=}7&l(+>0HxopcQ?F}@eI4KOKv>|#2WMGIA)Mo!g1JrnEf@u4oYEz z#S9tJ(r!y6YjGmn`MF;EO`@5~k5DUZLKG#eK@_8Bn{@v(FY4#pqJFD4~1R zw?Vg&d5lOpu%QqvyIp~sl1*!?3TbN;6;z~-9f7315nt)5t0dC_%953#|6d~75qm4z zuIzS$PYDW~y3fa)DNt_*PPoJW7*p3 zZ>O2@cyDb}ZK^Ws}B(x^(!;yfyw zLO;z;%J$ASasQ?R_8%gmBjdnS&w(-xxuHV={B!(MHT$O9qon)l0sFU!j=`TrFt)2? zaMkMoa*Azq?3fH?-<~TH5%L{UPImY;y0&?#ktH{ey3I5#3hIBThl$nF1i1{UJ`TBp zG6-C?aF#cBU&eielUbJFmPUA_o6@XA^im6&_f!6%Pc9HkqkS>o{U-cbi^EO(ARpF7 z< z4euua<~0u1e5akWN5(i{6ZbGGL@sW?aPLeoofSV}ggixBkGE&rhxXrJlwlOqM}FO? zmpco*XZ{tu49XlRvxH_Yj6Z7q1gUa^AENN=f&5ERPNiBwK$**`8zhFK{8*D?W1L;# zH(j7eVe%CgaWHL*eM2huH`@@B;P$Y|-MFe#Noz>;HF_T8EVkW_?dl~<=#~{}m!FER zC6krZzKCuilONO``eYKrEV*!X5XW}3RtuEDee(b7KXx>fAi5hMCsVY;&FP7oXEn-* z2P0r(kz zGY6czsj$P|s7rtj4NG`x1|cPxzWoR5+k0<^2}DRPxw+J0Y|u8jtJJCzfckH}5qNH0 zN&d%7)~ZP}T{k|=GC`Nq{b0sKLRv5Kr14Xv6-Ih46^M)aIxH9c5d+N+10^O;Z14?8 z7x$uu@9W&rt+ZBb{(qEbdXwmCi4+*^p-*d)8pjLeZz!wr!lPmFB|>OWBx(6=bYqd zi@D>E|FNsf>{ETvG$EXt>iObZJ*eH8>7)|4Zf>zZ!Hc!Q5H$eb zA%#hf>wm23p@eM}ILfJZjq(&uYq#uD&@`!ipZ&^+dA<~hVef8H;@IbZVaR5^!-mS? z*a~U1dZ?95<@gUOFcLe5Z4+IN-NPbhh}%pu*QiQS{+LyDny;%{ZXluc^u_F46X@`7 z8Zk_`@wCM-+P*{-SQD4a)6FCzt41tro{iw>8*6vId|WL%nAPS#6*Q)LCB|n&EeRTG zW#HU%5xpyNt$)iWz~_Brk1B*f?2jT8Tyn-DTxE4Pjmr6(~sXOHEZ(Gb)UFXou`c zphK0NCsW6`diu`qD3nUS7>I`2Pylj(aACBbDQjAql(* zRww&!cTNn?dVbFc6w#IAsX3R`$aEU>^?&rKJXs(&B!;YWDYo(<){Eik$w?+J_urez z2}gb2-o`3vzE*^`k{p~)ooaKx7ynnAX@Q73m+z%#PEG8N@f;Z?ADXY+{||9*8CTWX z^^MY9(v5(GbPA||(w!0#A|Mjd-QA6}bc1w*)S^?6P8W*?>25d^)P1}6y`Sft-+4d0 z-xiBC=QZjY*NFd^bK~}xQafnAp(mUEP-xw4R0N8iEz&J{zwXK6C159mP$s!?u5~N+ zUHRh;%k>)`OJ;*bqu&I5LoBX`r}^z!`IIn1l zK(yS^N>oLq_n$e}X*;GR1fy?guLq+f{?Y|evH|4X((1Z>hs^SuUG{hMT2-DMbs0nAe&I@&_4S zj6`*fi`G}W6NFk43`I7Bic&!^VZK%l>%$@IW*`!* zN$s%=YF8Y*^6>E}*m32%z}Z_6)4(&rpd+PiSMwOm)zCA13y&ij0kUdj@o>{FPw4rq zwL0Otf`kDjeT=JGR?*uxI*a2DE|6n6oux{PRlW9ODq<1DKZ%9Nk<|I!5II_l(%(er z&Ab8Q!t<=0p3RLsolFVjhNR(D#+k59JWWlu=on#4G^vssanyTF|6WI;)W$=f%LIa^ zYkB)Q&$^ied^3Y?gU<|Fqi|4m-wi2spv!#7ie%oja%1jj1JaU7dkTJXB%04uJ;h@D^-YDXK2%u(_^{S zo_*^_WS{&dm(}Ivwf3|NQY~hUk;4u9ov1Tw!n*qbq=jWGnFT{j%aeEfC#je(60W4F1GfP$JHtJ-38sLIwW31?#9)F+yVaRT0|W3 ztQ{oSW}D6A@hH_xuLY#X0ySobCfX+_Ni#Mp21qjsNxVi!7)3R-;07-*KsJMzjw*b& zeLebtrE~`pTkU^rQ}2m)5GLe069HN9io<3^HGRmJ#p5Ay zeaN2Vds_Xo2_(XbEB5N7BBxD1^HB%g$!0Rb@rHl{7fjchAGFp7FzhYyH3&P~UWe=| z6Ck&@)d(y{Y`JyYAp2cZ-c1kq%lI&rytmC9KX^YHK}g&FMu-W7$w^ljE;m)W&}J?X z0B_|!(V+4#X>Z$PBkYePvnL%zz*NXG`B8t`+yz>359I1$i81I2d+tXhjJR-odXah9 z6E@YNANADlAcwV!=`xI9Rb?LRd@h=?H3-any_E6Gmvk>%dRL)HajQEo$k7{KJ)is z!1A*nX;2sl+g(oznJT*Mx+ zZWxZtld@Y-Nk;!>0B(@Wl8Q*C-?oNB`FjsC<)F}AtRVh133&F_&%hOJ@QEevipgI* z!TxXY$2XAT>6hQ3O+Rqbe&75*zt9J0h5`HsOZP`6YWl{Y`F#2tKe5@rV8k8@-hbJhd_Oo!}|uM1Qjngb-A}4|)#XevsrN5rol=X+QeMoa02cIE$t~ zMY<;OS1?(3r(}*4St=?ni)DMNYuSG_fge>*Hld$TGKc+l)bmasxx|Uu0p?#Ko~E2+ z0m&Vxf0$gZS0%YRj&;MI=luVVFs9DiD_WKTLwtq0^KFq$I9BT)4hTk#&2k4i@l4Rh z=bO8LDXk zkGjPuA!5AdT?SNnb*st8O|luZm@9mUDcw8TmymKh3VXzN8TpUko8P#UTP5+Z zaiU!C;u_bdEps#BP?D!=n+;dmN8?m3iF=5Vtf5P$t8X-~Y%joKI3te)<5BD|XdKvT z_vxb*N-@yrNcXtOv9-&>mezmrofVx1-yg)20oC%_xpIOV+wH>+9RgRouE=ughNC98 zs@pfaOvES7Wd7Y3c4tYlFo6|0UilT8OHaVty{ zu$SBLjH}yell#hK`4K&4ej!$($o_hT{@v);P zy1$Vx%WM6P69MgvoWGk)$S6pur0oS0dG;KqC1vAcb&uLhkm`|2$p-fdZ>DtYhv4?l z{Z%*Fn5*a%-aG;c-rH8SmZtl-M_YVQ7R~dptR8zbPdys23a^xO6@EOnW9~!i-Z7{g zF1#d=(ss!Z9x6|8eT3}S$JblfY#7CoE4MR*)OjL0I=o#e;JDJ%d}b6$GUulJYLsD3 zI*e~`z8Ux0#Vtr}e%ovEnOoRUb7BbkWD>!)?cbtsB3`1pe*Z^b*fm}F3kSKqUBw`2 z%(+VD4T;KY#4d_qL*sUiYMB6~BVnoXN#^VGG#}r&Cf9h^H*l4&qz;`&< zIG@aC(}W{`^-<=aK;L#u2AfPvZ0Tc=B2KJT7;&vgG+||8ryCE;a6aK_-Gil)Ko|xS z1(BBz@bGZ4;NE>mpZ@ZCxAbQG`_wB3I|n-zW%MhLNjn`WGkLpH?c?VT{PHRbizmD6 z=8owGGj&c0d@gu3I;_He-?93td1Tq}=-!SKYMApn%V~u>Nw4beC3&q=6`l4?xdrB3 z9!Qly8gAMITJ~+zq%$Y7lF9}Ol`)oB{2C7&HwD5(CU3Ha9olLM1)cCPas@he_(ICx z9#U=zIKs?Mu(m>)+iEU`x^{3Wi{c*X_9JF7zqDC?K5%J;i2oID;`_%yY!n@u(|0W) zj}b=|m8>a}@n7y#j zO=R(zTnP2bRimVS7etuwhNy|8TujrGBuKL5efleP*4sG~xNleNE?8<}{Azf-4Zhy8 z55;!nL=(*}KSHV5K9*oAJWQ`fVof&~kifXXFXcnE6k5OH>(ur{?I;5}E28Y7N=S@6 zRL6s|BB>E=1%V0!6!KnHxbKr0JQqg9jxr7qS=ytv^1N!Q=0A2}JqI4fW#V+rJuI){!?PYhkODn5EG zSa|KQ-}7X?*I~F|p5dxuo+FJMj(WTh$D@;((o+rhd;3Wu&i-dRzI`utufy{F&(JHe zq>x_>sWasr#6yWwzNigI!0puCXOos6pY1P!lyh?hG4zT`$-y5C7K=HcJolQl%o*cg z6|^p%3K=t)vezm<2E9&u^w^(e^P7wTbhtC5h+j+Cz3Rl0Z^Mf*taT1rX#EE#EXj_7 zQ*0YY+#8kvD8pKLyj7`pX<8=8fzduzO%m#ZF+oifqn1Na39j@w^-%yrI|w59Qm|S% zd-W?VA%nuTR2*#_x3T4#58`6{8+zqq6Fx;yZMxYWDa6wioLX(@J+f!M)Y^h~b6Yje zPW+Fa)a6!Kq2=Z$kzP?TyK;lG!CaJD$4NB^^A^)SDvWA+S9_;>1|sZz2l{LtyncD^}e%O&}<&e-+}R9cLCp zo7&N+a=9-(WuO&U@2@ZD&U`}Nd}F}N!TXn0st?|AdrRPy6ls5s)tvIQ-2IFK{$sPU zD)pIMJ)tYgj=z2@B2MQU7AyCb#|e!BD7`f3!U;1_;-e_hgL=c_$`8Kn@}s5J!*-vP zJ_t}~e!4&~hwXw?7)ex(M?Ien;YQOIXYQ`~D&6^pX%e~wWu^19++_lH-{zZ}h4AXN zHy-EuwZjAyovW&_^`p30(ybRr0L0 zx99byRwM?JbbAYB!+P}90~F!pyTCNwGny+boMI6Q|IUxmW2_pI8WsR+DDpWkvVQ*tjMSA@tcUaKEt-e9e%WwMZ|JXvh41jZ2@Q5PM7*zSQrPp}1jA6_Wb`zyU$A?n2J)r%tN!C6K>2#oVLoG;f3 zYU&=5-r|pYb5MPHMG1=1tL)pGEMz(b9bc|w@R}+SPe|Nejq(o!_7v&ZpZJ*um~pF2 zdw-3!iyq!ik>3O79kRJ^IqvIHX!|pDG-sof!i48J6A$`YCJ_5*w+56u&RizAKUYJm zY-Z978*{FG4Oe$j_}qYYKl>#ELqdw6c;L6kV7C2rd>zD8)265xJZWp@jPt99c5xAW}y)z%`7eBxmz0t^!9LJ%*R*ai( zM47H-eY+;F$!4)n>{@Gz>OM!|N5U7U8~JF%S)&*1uyT%Rx%!R}L-f(Z>g5gFji)yc ztHEXm(1Z3u^cCbiwjc8g3!~jt{U11J53IPB`xHWD0Ubp7vEVvgc6tM z%d{FtTS8wZ8x|%fhueB4#yI5?`sVHug&+Kei%{Jr7Zwr|n}Lgy*Tj zO*@3f{l0NVhl};EUutuAN75ZQdE#J#`b;(q^$8;$?m9X@1ZZZ~1?X)B>mLaPoi0B* z&aVCT<>n&LN%au_Tn?azR5&$X5eD$F25g}`kq*!Ku>jKlw!%l}5ls#oX8yjBF7A+( z4^g{49^lLq!UGWjbV~$%J~jGK2Gdw%r{3>gpl1Cl-=^+$c$bJ_R2pz}v??@9Z5i4T z@SMe^Pn`$VJr2u)77I|_RgN*QXaSa`|6+NOpU)T%BE`rMm1rmekD2w3JF~}P`Po5Q zt)-{F#&anK96;MNcbdMtgnmCYxeCRSo4dM)l|fkU<>LTe)vQ2QF?Z_usM|(ASklo& zMWw`l@}8~YnRPlcEUc2xF3V5FRTQ=xq<=27%y>X>9(pC#tb8aSi(x4fA}(pr_TPMj zr;BlID+iTnf1wK^XL>pNuDlBOJorjffV!ms*#gci8dx;*-xf9Sqz2Y&pJpf|bCFYL zDi*n+N-IFOfJ5ZX?aa#N(4A-3Xakhv4T#MTd$(6|6RMj{7X5!91P8b(ryY7QeB{Gq zBo1zN5Q?{t;s*}NkhfP@G_gUft`6tAk5$E%jWW064-^$wu9d?Qx)GK{q>TPW1WY zHuMczZ!kCua=g}$!!=6MQg-?Y#o1+utTxyL>ndLgM|!&$Z+46#>bx|#y zW^00)tV~1}!i2R(Uq~7(qRvml%zd$`*Rx0_R&-p#nza2k>rw+%%k@-kD@>Uh#e#J4 z6UN^<*>sFn*s zVjE1I3hjx&iZ3m8f#re_XB;Db{>A6R0ypKqjDg-O6d|0LKy)q=>+dhnDuiL_ekb|g z!U%|i4u@b(aw#Y!p0zn>Ancr1w01=!d zw?z9nAufLVFb;opyq3pq?NUTb?Ec&bHkl&x51B56K#89{>KG@se8vGoC)EDHnh!n4 zWFFZ_3bdM7$#z^QT&K*OGTjK6AwYRElBKV_5b33yGO!pc4Cz8NRyTMQx2eex)2~+W zvb6Zqhn|dAW-z2K^2#xe@^Yk*gg6XD-e<0O5Ts&ZEjuYTu<<;)$7KX-#yI2rqN7(I zg3`Ic02v#cHX`DJMy&1oL$Q0Mo-=?RXaw+{q|~f$^XtI#3T@skgbyqf!3^qrv_{2T zD^e|R&8IO6b@%<9UzmU{DJj8N$(35hi=*V6q&JW598vnU6LaxrXc2iCo6#(NxjM#G zoLp8*#1?Z;_q39rY}aQfhH5s#kOOSq>g2p4R>E?lMGJ`VTRJ-DeUyzl9~He(%Mpro znmOZnsiyycEGbxMCi45dfAI51T1ef)W$M|4#q%~P$o0L*b&e-!;biEY`+N4bUr?)8> zt+))Cr~Qnkv!k@2^3y6Yz{O@8cor5Hi@HZ)%^if&Gq5A0Z!skJx&7fVqISnX(1RE{ zh`ioHT775xdVD!*B@%`%{^w6ro5snCpUQNfe0$h9Ttf1O&;e^>qBMPVUs7fV-&1w_ zBHbo}Xa6JH?exppsK`~`reg@1L^dUrYp1INS1%p=Rr|uJnJS!x$tq4`KFG0?a z_YCK92irgMGOd$)8a_MeE^I=8bk_D7WM#wRK4;_OEUm!0q&;lQasYQ zIfMd?&k%nSUA96wj%LS}ni@thd(`o5`!U-nt}^t_yo{)G{?bvlFBk5xC{#U4&h`r# zunh@OLDX4Inu1)zu)0g0ltiYV<53wDUfY-I@%PJOB`}Yq;5S*>xy}aLzlZ4RW3Yr*~axTCYQH35nS$E(Yi)#mulHVEc43tB|b22^^(PNJf zM35<$nLGbB!T=h!eDdSL!d{n&Ow|0Z6U{JS=IzSkAAOJ8JI{UgiWJ!4c4WkK%c0}d z!e4AY`93VT-t5>}{B#9MtWk*WaK{LT4MsSZrR`I6TLmU*-YwPz6pbzPixQ!Y3Zyj# z->`%h10 z;uD*f&lupgh6*q<^Hy}K=H*9Js~>&3yXP=>?0kY!Q&RRKSj=0XSOSZ`>eaUTWDgzQ zw1#@O`8D9{=Gr){)RU^bzUrN8{5p9n+qKI+4-CY}9iTX@)MXByku+~z9Ko5FPQZF& z(Fu^`V603F313^XO%$tpW_*X;lWDM;N_e%w)&%4zNKz$nr3$7VlXwefba5yKtty-- zLd`EpGN?A#40tkka_*k{aZ$DDzvh1vdO7GHc*YJK^EzjP+GTxgMu+ggL(cHFp3SG% zp3cD+R_RUZWV$)p;4RL@E}@*HTHI~Fd}PZkilWY4J^w|_a{WlXz6W_g5HaP_{PLF- zB1-S^TQKp9G*jO?p#rXL4xaIoyi!Pm^>TsF!kB3lf!s1H0!w#|8)A7^;-v@Mq8 zt@&1O#+CeL0)O<@sJf4lq1l&KQ@j~Gsm9Mp8N<_T6TqLfAF9&Omnu|Xe(gXtgZXBB zQ5x9f0(;q#QTS1|Qs}4m-SNizN>|XS&390?>*@yv+Yl6Kp@-SxgO`Ppf%O5Upxh~? z7TR15?8M#}b+U4+DyW?Vc*sLMsYW`wVi?lm4~q-YOk^U>%6F9(Re<&b)VasWwFZXWNbqNtOq-7}ZAOH_T{nEHNHig^va> zg^KuBqP)0p@IlGJw(~1j=lhu2L&G#`3@4z`40S!j^A$z(e&^Sts@9w{wx{r9^8@5p z0Ob4MlW%&scD?#MvRAV5jz}WloF=z`(W-b%;$dS}MF{Q^)S7>rN_%Hfoc9)mz-!$8 zsndk1;Ftw)=Xdgu|1nG8@WxF=CD2AKoOVnlV=OYDs7Q;jZv$=j{TH6GPl{)c5aCT~ zvp9bnLgxAHmoxIz%afYVSZgJuvXO`JbG8;nw9V9jjW@g%JxrT+B=BhZq3RZ>Obp(d zjA9hWS$k#OA>r@u0-GktYuf$dxZ0Q=fyndjQ4 zxo^+Wvj`WRx7{FEQ*(YiZQ6V?OIfw2TjiLRiLvl?n)~WsBwz(Rjw%VkHwQmF7Mx!P z+uAQ7@`^pU(=&!;(JGMYtUwux5*Nt~$M3)UrrGo3!s4^YY`R%*Z2Nl1$G#4N=1NS} zJaXcF4<4|c5upp?48EDN2F4Nv;CLNc`|u$`6()OKUg8)x74ce*)V~QN*MtQ3e+3D4 z%M+y0C#t7XYRfF!4x?Ws9yix=Pt#>rit>WadH#!vR5VozuxaMsS06GVy!%e7U-t?Y zI0jkNf!Hcvc^ad+BEE1>td23j*cDI-e+|fPLlPymUVsR9Fxjt;F4k|+_S0?JDSm%5 zTs&;D$Gr=E1`^FCWWN-uS?yyjw51Ojcg>k-AcYi3_{1{1uAq4kbfu$b2CFi)M|;8k z$ius>!}mj7qi(n*kpmvuDxdO5E}k)9nBZs3M^Usz(y5c&-&Vb4eRaBvvo{(BL;6K0 zl$~xwQT25Qks(K?!O1)`_N}|_D8JPksi^4;E7^~6?PrL;G`QpJ`~6R?Q3#!bVzzd(Cmj2o zIZQ_h=R2Q4al@apxt@mc7C+y@{k$_TZYGI*0QM+tJy^+;%iLPm#dT$m1_C@F2LJ*M z)7Bp&K1xhsgPkkg28%&gr;ONGr`e>sk7?*R#?#0BMPzsrp@l`ccL);pw7zLSe_V`R zyT2%Z+Tp}!pmL1Y+Oq59dAO02fNx%(<_nvA3xxQ9*L-uSHGe1u{K)&xmVwiIn%XTe zs1obcZGwG&S1^LepR+SAM9JFH^U&gM&)#4|+@)DxD!KcaxMA-nz-pn!{AO+h>U6mn znqH$>Ly?JzumFOgGxlTSg-rfF6wa=>cb1SC*We`6_}L z*ddd+SDnFm=*zm|BOM0rb#ZhzvXt9$smS8d!B*Ubq#>-DV9YfpP}I#?yO|7HwN`6$ zn*Jgb@K~2^ezn%VC-QP0S}x*OY8S!KhII@9!$1sf7{LX4a6-95u}BL!RfFxk9g>>Y zug$gjZP6acmKyLZ#kJig#7g(>GrLua7-AA8W;NjEelXvSr5p3lrG-aELB!Eaw z@yXwsV3x9)ybiB+ZZhI4%FpdASXDlMgKHYZiY33y=*uR43Q~-i`!eNv6{n~{0WlKb zpSY%&`2L_4NH2Zszo?Ey3LEQTY9}$DlJzM{$=Va&m-albQg6q#1ZAyahZ;g^3zt zZuk*Q@{G+}JBBBSCoc-)^Yjcq*R%wbDgvot^%0ceXReHgJQRuY%dsDdaN`Q?)h8!8 zkh`}!g~iLLdC`WaRWJ@ydXwz_6gThXi}%35jIs~04|GneOVs$`yg~frQ&6=D51LIo z`NQ)abkbyglw!ZG{X_i|RJ>%f8-;giB$DF{fG6( z?D=-$rz?}4s0iZ)t&XP#pSqysf`J$TzbL=<6!U}mGsnPXSx4&R*sM^KN9N9$L)FMH z0!ps;O_RV0mb{3^4@!RLXDGr>80N{S?LnLM`33BYx-yT1+DfT<6~ea~tm;UkJ!AA* z%U8ALf*wrNr($Foz2&UHF}`7b&QP8Tw_U5LpTx>l!NiF4ic)-PHHUHEQC-AE8D3b5 zluFKjQNjp!kwYruM(qIo&cM~6!_%=G1}s&czQ#9$)#46TUb1>}gd3h3F4l$b^Uc01hI${2=ObE2MEmWCVzlpIC^v_lCI zDg@~H>Bg#(@u#nH4;-0X40)fPo+E8y5ByW%2mO;synGu;S!$U$$8E>2Bc?dI&tJfo zl#(&~HxlHfh?GnXM#8lQ6l9VMlt1ax#JZ1~N_rc`(w|^v=wM(I5AlC1AmPq*StzF1 zO>gcsI2`MbW?yMD@58pU*3m_@Q-F2n{2QS1 za4(@w11NSJDIhmr0NV!<#|^{8G&VV3VzZniwUzs{1*W#jIq@_AFt`4=DTVM)sr;aV#v`Mj8S{dM9#v*Uu~3A)?Vx7x zH(Tlhi|tOM)`k(N$uDWg$?H!*(A&wtmW)adiKdR$)A!fv@ zgE^Ajwfq;CsS{?x!UHg`tOJw4;L7fJ=p73`sM=@a4g00B$VuPC&t%gmjybXWe<-Ao zgv@Z?tG+~;LPtS>*fBr)DpIf1>4aF_)^)9}p7=h{VNWb0UM z3{oXa0R@~7c%8U0a$fr@$8~JCyWj}6uRASBu(*U$qYAb|v0tGJgUX1ITyHr;rya>e zR~cdi>iwD+*Yu|v^!J?S%L&=6kX4foocyv&)f7GkRyp!6rrR)cH$|pfX+r$wZS&jRdfIwKECVZ0bU8nBrMm@!RxEtfbYN^l{@SLY)g?)je~!$ZxQ&CY>Zn zJ8lRxUqmC+K__AD>Uw6qiCRiK!Xx?mQqm9U*4eY{ioQsWjgs2&X=*KN>0n@F;uwzp ze&a1y8uMMT2&gg6n1Au{&`6_C*{0;4n16*N0!_VtvUkdEbY|2j<&Nck$gS4S3|&%* z^^L)|Q&#cJd(|%4Pak1nvgAv+AAdfgDSr6KypmQbteL^H)6wocX@yNo%;e2$J0@Yl zJ57^9o+DXy8M#jCAQ@V?hNWN@L$jFhe?ivW9)BF*7kLY}Kob8J7e4Pz`>^4nkn0l{ zzUCPR#Llh;q)%K@$zYd01}4o)Y-+?2>s{R+8E7#x0W2AFCD-CvyS^6ex_u_3GWBmL zx+cI}t*@ejzuVyJx2_6Lx$VK#6zh1my7^uI5F~L?V!rHzmg578zzTRpxapshFQRXd zm>QIzgfkRfi8Ge2@?wRwH{7ncZqUowzuOs;c7%Az#eFECFOYFvcy`>{$2k_KoI+$$ z4>RY45@)XA>1qFcZw_P^SpFUJ9R;MKP?5A=0?mcB*#nM!i@H*t%$yeDX+HH3GfSbd=YM?Td2j%ZagIy@!I1!&_q(Xj{lE1X%9&Nq<){d&xpUPf3D-!35o{E}1w zpLy~Tw^$BnxuXq~fz;b3QE9n{!~L@=pQwO{UA9cg`*dZj)zak2Ol?NM8q?p3QU2wA z?09l8$&r&vr}~bF#BEQAP%(5~te;G;gTwtBU&D~fuqTX$In(*;&BS4CX$z30%#6&J zz$CvACB<)g3Erl?IB$ZTUd;FVbkt7xJ+UvzTU0y~>p>%un+`sV9>_J>egZa&^DaKp zgj~2fgr0d+|EPv!(|VQBE+KWWJA+YshAEPU|33RSx)@2yI5y}L&?bP1Ip`G?$4yZ~eSjc537 z;6zXIF<817zVVB6JPgae|2WDz)Hf67i&(W!pdFhJp={k;hy~t|d6U4~_c#1UUnr_5 zd(Fuv*%Vh=jR6(gbvlvn%z7s9y7ege)ibppu4=nqpoCmNO+u|f%gtnse2%u`RYC>A zAt#EWm<2n&YA4?3VdPlDh|2PWsRDLNd&~FLHYNK+5_J6_eb1}_9vZTQu+*_0^HCh_ zyps0Gtke_Gurl55?j>ny@G0}L*lyy>GupDeODHm)7RBagyHSS^L9rcg zcc3Wgj}yJn9q8YxQ1P9SG)_DO;UPhX`%8s)Ad1@*L6=}Vt*K@`gD(M@)hU@wzj0sI zILbC|uc7X0MjRyml8jh~@-Bb{uN_kO6SdQIhuVq2>aXH|to(&}W6iAOXTJRko)Ml) zl`qurr!(92iRBRszj5mQj}pT;hdlQ(Rv0MMOBKS1M&o$5C?_G2D+ZVBZ&EM z8q=)78Ej({nWFQ)UJw}T0gAo2$g}eO%)?sT)x@G}Ul?MRkz0(-A4qLmyf#8>+>cJZ z+=RZXWC3}9T52!ut;Hz8TE(7Z#}dM5_9ttGc3q93Cd?{K%N5LWCu~`YaQ8HGv2VF5 zg!`*(``oa{K%EecpqYa8B-}2o4yDk2Cctg-KrxQO=e*t6aoGq>#}NuKe5X5JpWZeM zRF;OVxctbr7rrieFBT~sa=L?U8rB#bnIgSMd%SGrsl0txuO((w=l+ZThAtGd=#rJp-<{ZA9hl^a5iU^TS4YxncLE!(U+ zC3Vlt4<6a#2&+8o+L0A5sKS$mFxp>Yo-p zUbJ6tuBAM5Zw)4j(@i3Uj5z^`d+aYth<;`Z_r$b2hJ(Q$ir$W^uheyginAM9Q5{j4 zpAW;59SK?NvNIFh_3XgVEP2`3lIM8nRf6RT&xgV=B}>!9nyk3CYmcz^cYTwsul56G zjVjMm+b^=MD-CyL4@GKTsJ!QT$W2Y*kJ9kXFxHmr@|fwfi0%$fcJ3Nd+BEi=Rx#uF z=!$Gl|Dc$LIZ@xEOUOper>vh92FYXy>jVRZ zHDF)+EVRCGl9 zt?kb)xB(%9)w!;5R)?D}?7)uL3gJgXTO-m9|HEBL0S?kcVOf*d|1|5Vnw zwGYRNRjWVlt{A7U!3AjUpJ<3HW)KYlFL-D*s3MGc&5Mjl{J!j-EBY@G1AubUkQD&+ z6aPP87CNBA$4RsAO0o&~VITW^-MWpC?+_h#n3=y-i@U3g{sqVZG`_hJ|L5f;`=KU3 z>OA^7BES6XuJHZuHvt+s95!LVo5%UMN?tSRgt&^|9OC~EsEA)M6Hyx4fd3#=MoeLn z>p$vX^*bUub&vix6U8uE#-cPFhoSip-IyXVT%bJ(ED`tq0i@!9NgHwx%{hpqi$%T+ zAIAQT7P`|3&?@`%Nn@EHmNG9rMwqlvQ3Sw%+=F69m|(P0r{LdEUYnvVJy<6DpDzFM zjRm7-{-<$N=&DFI7{&z z^ynVSQ*hsAr8Y`iZ*IiaZLpQc=2!Qw>D4oSe1LZz0w7_OT`+RYo@_vgKhrV)TqH=z z77F;N2LMHgFaSd+e=+0?Fa$sxh)21Siz|SaLi%=&NG-D-B+l%K0d%OnQesI(TW^1r z;AASWM6`@|faVnNe7W}?>Ernu>0@!%uYx6Q=i>fk>q$L4sLb;`g;1*R>mp(%<8zH{ z&0HrT@WY5mwH{7DCRk3rtBeKnf8wJux?HR??RY`#%!#xl5Q!St1UWePPJ_h+rKju) zr_Y1T@r&40f-IgHmiM48BPR*o;lB>TKbD_uJ&RMi!=v4!Uv(#@N9(j4l)>ujiED=Vr&jwRnJuD8l^SAR5l{ z41k?V=e>sGHH<}R(r?XeY*WH7^rA~vc_f7abwVZ`H08q7)B)DtpkZ#Ji*3T>1E`Lo zrGQ8bGT>=;w&_V490tq;1KUoC;Wy$=l5tnktp_^XxEIihctzPyp0NaNKQeI8en1fcmN>l{R=@( z4YWLueHHYd=B4A}#g1cynd3`;__-;#`K|tgQI-|cfafJ16yv8N1*?ZWJ|H;{zglDd z`~Ux~nXUf|w#`ih_?GonY}X+YY0RJd))Y4R2w8$Dt|{AnSZc&tov9!@^}jzTGS1}U zLx)dr@9#;*VNlH%9Gh37SS|)&iBK?1Dg+ljcPC})r#gUf@@{-dK0LAqNSg64>ukOQ z7|6|(y)P%0Hffp#JjJz2Do3>A$iv~Y5ftUv1nbvKz@=@gmETaf1&+;S(2N3y^XoS(ZLgdKQ{RR01w||L723UY?a;;(nK2e zJX@xuL4*R#9QC)EgYV7!zIXZm1^y_x3q=s6*Rh2x28FLLL+DpJ#G*>JdHwhsyZ`Ak7p#qnmAp-Z8W<#k}kI-Wc$#z=Vz}N~a~7}VG0PyubFG$S+hpw~5;zCG#Z3D5NfRJFO%+ce&YPNV-(pPsnR7g|jmcg#9v%h-Bh z>vHK9I_pF%y%iT7mmx9&d9N%vAiqxSI-%CJBkWp#FtmYrp9#o>05GwCA}WEdI|}>z zM`UjT!Fra&A-a_gdfge(Q}t$y%?Z3jI%({|tj!+jvv1{&P4g~}AAT^vX%AGv z6(5M0o*!|-$!Ua|8B&$&bp@=0Z%JHFzsjIFMtd)?!J#F2fldY6r_xF2KkK+7+s~Kc zdcT2|E4{*oMI3QQYv0^;eZ+K$r+R}Q@s*RQfBTE!Mge1gLx7JUOXoVs%CTwJ=!oc; zd(|Hph?x>6{^WEK;6%u6Lgjoy;WddM1A=T6mOo}75z}?|tH<_}Tq`@v!t}j%!SoTF z^@@IWYMluNP^bC?NTNJ^z{))@sXE1=!aaDn_8ob&0+Ud7CHuNGwaT5_Z84uZqB8*23W8obu$44=kz;%3b~c~mheVY@9z>_+Dr zvM;o*(Wr+k7Bm`=HPqpEW_n^hfFg(6?9huPW%Gc={FZ#lA2ArZr2E&zly;Lw%iK&7 zZGu#E`HIhQTqKV*`Ra8YY{)8#ftjQTGJIk1tF6>ZLewV|Z96`6WoFmdXgiFqH8+*z z&qR?jJCW7-{rd>RQ*q+;TPyq=Pm5pJ0=NvCQ!K6^flbS!)C!dTiJ}e(cJ=0ff&h*V zYpErVu30zW{VVMK`}%uUTT{)jHZ5n^hl98-c9`$YXL1_g4{5_X&T>yQjHdzeJ|GgVWyW?p(DG~lmDyVp-lfd+O#|UbF^tH@Q=|Z_GMF1 zbtU2*T0s>(6M^7lI1YA&#K#~-dXgZ_@YQ@YOT3;4E$SWdcn6+`6*miXHBf3nTilMPsF2$ZGoR)^4?aj%8$bKXW=Zz?DX614&i_Govx(qbJ__dZw-`zT$}?1P)DE-?96;2J+&oh^UOXa5X9P0 zt9Mh{CV&LZLgz{Y)8SbQr%7E(^==J&bFBidu40bfmB)92!cX_`l==)}E>jD3FeDLr zt!RmNxKVjR9JYXp2>}gXpIM{%fgTW^C{v+qkk@rY0}m+|bVqIoMP;9~-0Cjvq>L-| zAPPsru*3j{8_u}@CEfj`#whAB5bS@35q%F<`}#M`x(g|9ex$L|K_gW90QX~cK|fB zUcqNkVipj|M{2*r4tPG}t$uywt(BcC_!0moqrI-N%bA_f7;&rK5`R+Rd!7C5$d=+) z!KMAW@|F}YM^s{{NA%6>b~AzR2nE=F_42+P){)8wwilxY1M~y^qBB`C*#hjDz7%>? zg|q~awoE-NUK`ME~ULzj>s}|p^uR-qY5-}(_QrKp~b!Zw_ zh17Ld!NYBIuU$v{dSrYCZ~P!M?GD&}H*DA{jcWJ(P+=~$id6OR-tAOU-Lie}0Pz32(S+aF|DSav{qm}xFu{+}7Fg}G-srQNBu{Yb4p2k(G`D0)!+9eZ_iGhZ z-%x6gGoQ@rU!mS3TIdY)ZOIYLUGftNZ-H)`G1>hP0RW-OAmJeSK7SGv;!bM9XQ@LI!d_@ zy9N6}pX{#T0Hoi~nS=9dzRw$DwZ1>+Bi64Q<$O zHbr?K5c%cTB_Ux1y@2FZvb(i1jEyvig=XK@Aq(bGEbyPci3?kab~ucXySwR|?khYKMIA+9#6<)$XQ!`A>6 zhEI5l;fX1g^)^BOsd7nn^>Qx*>ReRJ$4f7nD+LB7jy|jtKrVt>FHzEKgoYm&*9R$q z5NNhghl}`MJp%#r<#_ERBGo4hnGVjFVKwm2XpRKdrpOiLk7ShPB9}P|$^wBDmgvlK8RACY3UunTfdhifxQ%kv`*!t%kkS%W7u_ zQwE&hrlF1Ok;Z86^JDBQ3a68*Uv3|AXci1FNx$9Cx`>Rm1l8`dIh;yI1OR*L9Mq<5 zXcD$dY0CvfvVq=I6%shs5_|qOw}z7Wh%*PYGzSxC-Cx?kw_nHLF_*#32)ogr6Q1s} zb_EqOwYuQMFvYK6Sk|qr>=~D%>B8Sl#(V@JKSA5oKY1r}eD26EKsMfeVQ2R_kt|4P z@>5;LY|B8B%NGC1;@1;j=3>IxDgES7z$PXb4n5 zSNL*2gdMgBeVC8xl{AMqyWzbC2U~CtbK>YZc)Ko&5`NPSzJL(;hlS|Szt-Y$b|aw^ z5>8IWmq}Qrhz-|w+j+U)-n~50Oo(?B2*~m{5=*9?3h&hxDVKr`lUU+AytTU0bAj?{ z`eu)Bgg#OI?W;uE7e{AeUv7`Hob?OW9Q3RI@*Tdtx}1pqeDSP{euX6?kc-&= zw%k7Nb8&~gb@1ZFuh(~emg!L(DGS)BsMGGZ6B~X8Hmu}=ejS&6HXRe#toMkyfY|+) zMRf{vz@!*hQ;(5%OMM0)Ae=Y^vcOp-X zmtqLDlrATh0q^kJ-(CV+E8@M}#7J317m&V>L2!c!JZ-Y`^>srY9PFhWS6IxbL_YuK z_|DVYS#I4fxkLNHf4AG(MG_cL&3-TAO$bCLIS=gCZ%D{Xdt2u6G(D@L`WEuZ$wDLo zQ9d*C>U+?I@y@Y0De7{J~dPAH0g^vkBnI=X`^*Q59?{H?;`A zQH~|c)L7bEuJ>2cWXal&tI*yLo_szA22U2P-m40ZJEjfR3l&8$$gdUGUeYsQ|1p%B zZZ+tML4wHRUJV-!NbxkJqEbOeACf;lsr(+;f=_j2nKv2I)n-G~n)`!z;2jep&v_){A^rcB>4KCs9}@AKQ^-!e~!-az*Nv z84y1JbF81nQ<-Ytdwge*15FF&$zbn=bhCi$WqBAn6f~R);HqYCTy2N99>rP2bah8l zahR`>2U@+2Fnkw-KaaQ#r?JM(MaAK47OKzxL@R+P2ytPLhB?@;pihB8QyY+@MqzRS zCnBgHq!l#BHcav*R*@lJY@y#19k0+1DLal&RWhGgX8-hWyZIZOg&h(7-@+pj!k=<2 zkslf3ZEoB6ATyl}2(RIV0E*e<<4PWU>0U7>V0ck)-pYK+4!!2Rlbv4sn}yVAU@o&# zK6ukfcO0GAj{)K5yJ|9@AS}nFnDUmQ#k2Q58+nuP{`xB<(#t~)R5%i+Nijl8FCN6F z-};Z&a2DPYRnwcOB#qoneVFDRi_Z^fA9OF^rw(ypDj=_Z#wwq(q)oR#XqsIa9`tJ; ztI3r$)r`z2{{xJYpN6mO=Vz`aJSCva9b?109Yk7;Wsj!)X2 z713B)Y_B|-0>+$|2xk8dRo>@3Q28k5*TY9iZ;wxvgCY7CCOwV>n9G^AN=Sb1mebp^ zVDj0+g-nuPx3K1E1|gcQG;ZRzQHMT5{Kbl;9m~&b6{u=vx42JOxO~F7IyQuH#VA_T zD8>I1yd@eGhjI4l9PZc*q>}W^Day_SOANNdupLz>*#UwG)n3j8US0}x7n@N38Pj+I_Ld{sWt_6vql z58pfjwEUH-MV-lp%T%1~GJ4;sc7Jom<>FUW9ouN*0<9*x#cS|qvUm+{f5<`|;=K%} z84~qphkmZVLsK)ELIW}Rwk};9g*F3W2ar7WeZk=`^!$R45*zOQN=4!|?owKw;5^wc z7Ev3E78h&fVDU^Yafr(n;oYQy^21=U!vAW}E{n-JzyTn@D}hG3qR?WeG+=0q)TEWfV#> z$0AVJI>8UxEv3Eqf`8M;?>Md#+t)QnTy^vnfU@mRF5M{2VcvOTptG$RzkmG<#ne<% zA>vdgz)NG$0Y#Z_X)uKg+0kx zN(y(3GaAHead3kX^S)6kV%ZOO7HZBq7IugMn6~ufrUCe+o zce$ENET;XtSq}7;2TNtBnr$}{UN{y4ixU2Y^ro)GC4i_f#0MP>hRPZ1Ix+88kK(?KQ%CCaj7oU~Iem@3$`C<(zM0G9zw| zq2UP+r+rfSgNPNSeB&>k{{`en^%nJSV&u6gAD=(d4oHD47-33u5elq;_WWK#s)+nx zM3Q#!0I2OZ#_@gVlS-hu-817#a_cltdeB5=m2B56ss-gS4+7?10CA@_Z7%sr9)KJ( zVl~{tBqk|g6As-RpcSRg$d>hxdn;+A`K*M`_XYnb`H!mM)M4>98wG#l|ADkV5p66F z0(fo}ceA!%{EJpuAw>0CojCdB9e?nSVH7A}h58W8N0W_e3tZ?i{;j1#2M?(^;u}JP zR-8bK=eHL)(5wELvz=8e2tO+L26#T8uSgqdi*sc0y|&N56^Bxl;g+9eO*e*Vl!vZC z{pOVc9O}Ch5sRv*DS&46s})N0^tUBK1&QuLC-34iy#D!HSI!iH)a#iwg#$qWhXyUI zQ_Gf|x&97uAJ_skHz5Q%Q;G#u(bHrCEPHJs6yg$}EX zlGmH)yPpn37I-L=ztp#cheYW>i*bn*eSYh!9@#u?uAF^r;db&=tQNb~hr!Jmom$qT z@Ux2ahtm7%&1#40kA@DRiY7q6O{AlyUCqU)(lXd<@8erH0{k;Y_ZaQvhmpY8L@=H(GUlq)RJbfpfhK* z%kmWY+FX=Zf|v(YC=CqO?KW{Q3N(MZ9dX)oK!2x+_lqbHK0qg_yeoeSZny14X%(Pp z9HWN?MIEskZWuvsfOtnkpj5>FubNUt{qy6ydQxM)RhD$yY9r~VlS9RA$ua2+C_o|be6mm?l3z$c1o^W~TElbte+h2Q8*ggR$ zeO(8Huh-CM1j@&d!h$K(Z+eM)0jM_(UTzmX!yshMk@f5KTHo`lI!!l9cugLbCtMe| zqckyRV>^P2c4Wa#9W-bQdktp~#2596%w_V4Lkcg_ZraAaZOGq_A78a%geN38QhUJE;d5IM9_P(A~G$TjjOm;wiw%WU^m&Sfq zf2y{^q7z;D?_w27tlDeZpYPr*XDM)59r7jbF8>JRFvXismN@zfCMOne(F@fpbzQTe zhE3%&iw&)maM^Am?yjzla5hG^Dm#_~MXCFO?&Mx%%SyDmkg!y=ECBOT@j5Tcdif93D+!GMi*Oi1#fVJ9Oif>^`mVT${%bpe(b>2>p~6;uH)UYi-ZGq_Lti6aP=xPFYEvRK)cz>|yc3(= zAR9=$np!|I52#_14v^{dd9NYc&MV>aX|Lpvc-j}aqsVv5egxe4m~R$}0)#;MUzD^Q z2;fdFoA!+Q8=;r>e38%Ah^kqbt1{8&645%eUHKk{wFLXMP}8kncNb6CI}DELx#fh9 z^yYU#0yNx?8pYc^rPhb@i9R+EcoG?HnNrC`k4mYYu9%ER}tBY-e{;^C<%JKhZaXcHGC3*a*0H14;Ru$O#dEj_EL-`9O+<=MWOHX62XMZ}9%M&t<*q)GO)o%8B2wU*J-d)h?g!YQ<^nAQ7s zKUM2wWt57b`wrWyq+F-k9Zd}x50kB;Mz~C#5j_4@vqZZlaX$3Si@2$G}&q)z6pq6YGff)3P z*}$&^l;ds(Z#0K`E15c|r2CyvLQmvu*f?s3h4(jP8G}+DtN0}Qp0B@Ii=pdKmy{{E z-`wygejvzfdMiu|tvPvM_f27ceo%j8#1Yy)%GRiKx40z&LlO-~Bq&xM}HGT2p-BGtmWv{w~I~2Mh~^QaalJz^}IFXXY@qahs_J zVLyFKD<7xI)#;mgmXi4Xbh4zRe2qDGT5>F%TPGa!acqiXZrpcw1b?#uJ845~d2TXb zuwdZS9OdQtiFMs=7ysQhBC|P+As`6}*n1SWKc#h}W&V|K3Ph>5_+=mBLFDJ>bE3@Yt$T(j!hQLtfpu==ky z6r&q=b@oRB(Ll%rfuEsGW*;dTQ6X~ZNXpvKFb-;mtdT9Vzhv`TP}FVUEOOb% zl+*4PC0B>nMsYu}eH4eR`i^fc>S<<=s7W|XLTaou7Iv=|c0d$k`?f3bC2Gd}rweME zuL@uj=sjGK-pPMzb~r+F1DYF)ZI3}PZ6?6^4C^Rqs(W;b)2U**kdh@LG*#eH79KbT zwhU9R?3WT}bjHE9Lys^{76rz+)rjeiF21RTBfaG}p!PbeKYQM)LsB%rb_YWKv!BXf zfDMc>X;IL_<6Tb!XGfhd0i$k|i$JQe#BrKau<6UYjpJFH%~n4F<%c_w!9+uv zseeW;yu9wYNzOzT3}g53$Et*v6MOiuDHP>P=W`+amhi{TI_NF54+GJkJ%kf^78BOT zXVYNkl2am7GBmR>dTpbu{OeMYKkdC;!R`PEI_Kyl#Z_EX<|sU^}$iFeCd|!(J@NjB?B0v%fF~o#}M;|oNWR^Xp2YN zmbA(!uuE4SF4@v|HR;WFTXjjx_xcKlkKY-Y6b}Y9D7`%4j4%Nul7C9X+Ji1lA*6X% z;K#<-y$Mh<%)>euBkq#;%rPrqPegY88Bd#LZ9ypC$)_d#*|7sK8GI&3aO||`(sntk zy3WL#m?(e_Mz!-xEQ=GR>rc8-sFKCL#URC0*G<29T$Ex^SRkm_)2H>Wt?L0ff5;eQ z&$V_f^QOFq1fS}ZalHuNj{p*lo-|!>MEv5Dy0(RX==Qn1tvig*EXe z8(l1RF!(;CL&PW|wQg%}*Y-=zy$E8NDU^@y21VcIiw>)lHP1hj z3))s!C(r0MKY5{9?|$6MG75?zBd#Wmiue{m!Y-=ze$!h#gEUL<$L5`(pc|n{_OWm1htrG^F8?wYzm%ecS(4Q}+tFTnl8mlCR z3lGy=yZqD7yw6{;ZL*K(uCl`3OT{^i-+WiaAaR3Xbzd3v#1Q#F^h7gYV_cL16lE%f z=I^d?S^MEK3=1!~(z9yPd&RyyQ@XaSG9+ZinJTZ=l>av3knnF&bjEgS(iS$JzfoQo`dY$= zhj`lCn9^l%JK!sNW;twYj&7;pWA~&c1R=($SR?*bxgIAMLLWAu55O&}MTn&?5Y$z6n+b~K7wrry5Y>3UtCq7h zq3k`c_aY6cVihEJ?L|J8Mf_e4x4L&=%M4C-^IEiKDClMZOQb((D0w!VA=NfqC(<4c zGih|*u2IOzRq42H$lPH!&`(|6lvzLTxZQ=u<^c*4=Z;p<*<`aoQ z66f2X%Qjp*k3Yf@av;5361N%H^DDpsK_|G?(d|+97{EK1^FP&iL&0F88`jFG?>U@H zNF!Jgk1sET+qSp86myZ$hmNK>cbVD)xf8QpUqR8Bf9;9QGl*M;>!HKQNlmn&?Vni1 z6A`uPDiFnr*FLDhOPeELFbq$er)6g5Ta*q-WtofiiOh=Z3Qb^kz^y&v_Szzqk6h~! z?IBuhJg<+g9J407aJrln@8LuB8QQcCaq(cizGP_F@)op-4xS;%C^87=(oc*2B(~Tq zYRx)7=tT8_acTFUEc(m<3Ug`ocjy;=XI|W>)S_gc?q{en-&IbqPNnQT2@@Lg(_Zix zoa9`;Ap@KL6~piBfx_#@M%F29if9cKzswcj*U;Yjc_*2|-P8 zA2mkW>d#!G(a#i`1Xk)*I`YYp9mmwd4T~QmRyX>sbxB;HE$1#@ zLN%ip)@$oNVjn4A@)=J_AZdfm;rG~?7#XvK>lu0uBy`-KVQf1sI<-yN*$#bS0?X~` zp3(p#UrY&)_Lrwcu4Z4B!^8f~_p?fk#zdaJwxkQJN0)t}L@I+AlDmt2q191=uD1{( zy%$dP;Y#zZyXJZU&$@{$q(uQPi`;zig714Oj~Ml|I0_HDj~;urSIo7lL0Qq`Tdlc) zAzo6?+|2Rg!tKdoID?f)M-8kFj|Ze=%=gA#Gv|#1OMrDa@=AmQw7|uv$BSN^x%jY~ zoC%(JrOGN|t&2{`q^xyxf%>++@g=WQ5>8HI=mDQ=2by~-EcGG5MAcpZ9MxQ#L_HFH ztv1D*(=8BONeP~O5!)MUel!GP%88@2T(~F&)%4w8>Do={ucrN{{jT7VkmArvae|>& z_TtyKvey+-V?8Y3Rt7QZFz{ONI8sRoO?$d*fYS3nj<} zuZP=1%8CR$h6;(;0>%68_IhRjtvw{62qoe@UnVGxbOG@ixToYb$(D4(iJJ$7{`9P( zWYazMReR~w%6NBkMsVqCd{bMsdEY!%wwRZ8BGh*`H42IL0vuOy8gAuK*GKbeSvR>F z&>}RyHV}Hg{U!ShdcX^btqi$=j3oT8&o%dhxBOsAeskEHCPo7qV%VkJ&4KcjB_C@YUW z-|8`vB<8`ysPjphx!p`69K6M;C5rVbe&DZSc<#XHY|BM)QSVnEpG}CviCei(Xlko2 z>YgJHC-&~8@AhM!{}L({c4cS{h=X3wv83B{WX-{~V2=qbRK$3F7+YMtO(8(UFGL6G z*FUc99WP}d-5Sel$%V}}rRK#jf^lDtY=5k8ocU?jrHwrh+$U)L^R^kF3k?w(oSXla=WT% zedyK!+0o`9?q1!Oxa^OL;qf^v;IKk4>|qad%jTH)3@*Xd4rP(OYqUcpJY#XnE<>la z^89R9mIAloh{qB6dr)shwp?VGe{Ui*a6vdxJnA9K{7=6RMT_eeT&h=az!3t-U%`z` zdUsCw4B<&ANodj=YOK}ZPEX*pGy%%^5{MMaJ>g*5f;9t~z8ff|jedq3QFGxDX$`s? zoeP`0VVd63Z8b<^3pJWFU*pu$wG!SifvKX3^rk1D{xLWtk^VVmRUA~>N z&&eJGe7>_v_k8|-&u8OP=5Ul6N#%$I+Y1m^sU49~!-4d<-YsoPl~vXp)xLQq z1$#n{X_q%?bb|9S^<6GcT)nT)Cz-O@PryIx;Kq#MBB^Wk z6y%h=h*)9!sp2FdgDkMa__5n@O=fvoEhG{d+wY2hn0tK zb#{G`|Jc?5o#M6bNZENwLBIE+sLbiBKPoq8i$G}&7R@EeaVV;u{M@6RAFh!3Ta}A3 z<`xXHE(IL?w3DN%SIkkQIn3RiNXf3&XLpK<*RhTGnaUgMzQAYD^9P63*d>SgXPYkH ztZUhh9OZ<9SDrAZ&9T3xL~EP&Dt<5AYe}1!^afeo9m^k&g`*|FJt?(T;@YE?lW)4t z<4njv*g(WU)Ie+or*R=cvQ%T^sC^oP;Lf1m(VYYs^+i@AyvjGsl<1$_kv7@jsAvub z8$;J&n;p^h1|;=2wkWe>!P>F-9wUc@ipz=ZaSO>mf|~Y2Ds-pvne3stmD`Q)kkw=S zo4sc!eHIQdkoHGUxt!Uzm~6T~F7JcDr2|JIGzswePgWcX3hku@%a8H078 z#E~pSc@bVgMcYOdaJS+joy4SD0zt6}Dic1lc!_gdbWMT76U#^e+;$B$J zCrKp@g%3qIncuj`easvB(ysN{U(UN{d&7bbUYcuVe@85hQMOJ0q&wx5b!5BijPaa( z8@_KU^_$A){^@3UiM?*m3cuFQr*2g#Vg{#~YHUs;jD|)s1;(htnh3uN6p56!jqoM? zutFBfhPEw!pKQli#Zo^n`MQq+LhB$L@O1}<}U z$8waWpY-{tK*X0>chch-8751Exf=Y00+97LfQACMk!m3i- zy8waQy^1%blU@~5UZb38t8B?sHey^lhpOzRl#7-@7L-)tWRsop!}XDDwn`#_+Ge7U zeBDKlXSOOe5^-5|l2L^cE|&ydTDbh=#XkYm-QN;Swa4)GZC@*A_Dp2=q^R)`W1@$;vc&=+YGNEBEUL==>4kNT(Opx*3G;oqq`_&58( z!A`}9K+hUTgbh3USPcDAdXqb2fZX?vJm2XGf4H%ICKSW&}QF04|q!v4#gg-#<)L47%>R8LO?7lXJ8m2U$R^&9*7wXkx_QX z@*Pvj%hidZ?Z)YN;fM8My@FbvFq=Y!LYu;v0+bXSs}~c=3`TkM_Ih4cPATP<2;smV zupp8z=MdTKUSJFTBuz8=2?_E1(gTq(0`3-JQpBrnW3j|U6W%4i{9&1K$_9hb`--)-&*|OszD-edR~mR&iMt1*Zo5O+)UCS@b8=5JcIB^3w?QOKr?^_2gJ%i zu68d(3M_)91c5Nm@gP}2VfS%HcNT0uQvj3*Z`FWRetSH?O6%!g|*!xOr@ogYY z`mF|kN$#$K!~ib-TE|E6fBwdH;oILoSqsQf!z<+8NQ|yqQK~I6MHh3$^^1x;XrJl)+xmpW8gs_Kxo+IT<4g0=oJc04_`^fRX@Tr+q*p%3cBf&O=LG| z6OZ2i1!w*q5ABl+AkyY|UjBwzu3%^`S@6OKo^0uhQ-q%BD~ftwC$a#$IxjTNpr#{j zTRG5WNE*f&wJs{6g!OR%0?I10`Mn-aOfMzqj7DnWpPu@7V^c}<^kEvr@wiECaq69T zv3TQ}m+Qn8uQN>`M|l$WmZv`;vnzN3<6BJ_Vc z32Ss)iGqOYDu~6``0}pmX}q1I9s=7UCopQKvWO;a)%^8GyZTo;Sj`65*Le)!r2`UA zxRB7zS}xws`!!0xReL8$lfwl!FB#9^C6~MsqsloE%#Hu%SL54xxpd zzPqY&;k>H2T`NE-C#ofEI49ZPJiuQAu&VfGUiwM@h{-dpk?9IYp+ZVLV*=WdVC{5b zhv2&z4jO=kT7H7HBF5$1A`U2h^E8%3aJBk#R@{ zQfa2z&D9RgXL>Wa!OjJtegr=$sB6KRPb?TXb1A(juVve^Tr@9Pc59dua-yo`z|luT z`eqwYpt33T=l3NN>91>*0d!m5*@oM(k4nH}kUBY&qm}bm@q^t-@=bl{*#^>evWIsS z658+A@2Fh+nY7f932!;kzH<@#9mm*~QET@{pp%Dys8ISN!!-EH_{czh){OlcTJV$h zG$B@ecpm9s{CrmTc=U*&IX}sQ=-dKOhE_9N`k|iylIXJ?QE?K81&hZ9<}Fd73x zUwlHx6;>N~Qzez(RT?uOr(0}#B(rL2hz@RGs;6y@cwu&Y3Yn|7hRMx}Ud z_gTFVUw5RUTNoR5#$Icy<;-Ati@$gupY zDgJI(;oP;W@_w)r4KDH78XlEwGw4NOS~$cTJ)hNeFM!iEXG1XF?K36@ja$fTcLqjB zUdL|6C+FZdB%{r0@Z*=+B$4qpP5#3y4T#A;^Y$5SxlZ zOUZ%@oDKShZs{WqU9|^ye$9$TI)Q$lVGbA8Hla>~;bdFq z6fj%Wdy(-9)tZLn-s88t-aT*Dc4v@y*XSi3|2?C1G@Kq<(=oS6-#%;Cia$=YWV=G% z0Z9M03gf0j$j;q*(Xt2oM1aJI)H?BcEXHT2zz>e|fkKGlbVY0Azdac(Sj9Sj0m`yI zTDY}*8(pCRjd~8DVA&PsOWygFguHDnW8zVcrN&F0!!Q<4ZP6CP9?Di#ciG zWn5@5^2R1AxIF%^)<&apsym{_1jnN~9{L8eIV4RQS1&X%Gj*ZtYv!pfPs4|Ij;|}e z@?8bM*j;K)dLwt$AJ}CGTQu2f0dd0wekM-|E!ADl^lc`p+U$;H%jrQK+!{_#f(@-%~F=pPlDPDfrZ`7*SR}WvUq`9rANeH8Y9N4kFD_ zeAsn0SK#$0TK56^AWgW;5ED0*G3igVAdrRgQ=7i(A5r4XKnMzu;-o){1w?Htm;PfX zwcVBAHS?6_blC890O8^3On(mHj4RWcN>vp0^C6(JDACKNv6Mh#gZeZZG^adMWkRGC zzn_1hy<=D3qgf)x1UB_kuh-B-HJSK3cpfJ2`ZYmh>_M-wD1yBxG&9+C9IeEXoIL3g zDEqRTviGimpixD1-L`aQz88Mk70X5NeoxfpIOf<0yi_VII5a9FcW-NT z4ITIKgk@Eg$H7EM8r^Mxmkgdp(E9l6QGjjzglx5l36}9UY_&tBMcc%=q+K?;-J_dw z*r+Dm2@GSFu6|SCZPy^ne?ETsdjHW-g1yj*dBN;E$J&59M%>iyS2R>1X=F%us+CDJ zd>U&0`d8Gy=01K%-4$EoDZ0?mubtHpAj6R`J|a6?tC<-KpTDAKz=j~c`&i+ZfDf80 zzK!<5p~_-#Dv6?uGKD2kgJrGbf~5;&xc2i}_{aIv_@G~cOsoWdn=Xn0{rSfz>bv2G z)WLp~Ok{uCUY^smZ4fOaq&HB+xl7<$laQQwWCjv>i)2&MSf+7*mS!?)X4{aof$`h% z9qJEpmG$al=QE>oL8`2n6I2z15gamDt~05)mDXcn$Ny%3{j<`>qFOZ|oBLzuwa}@g z&E2sWI+J_O86XLGo4cbfLsVLQ$7lMBj0Q&`0M2!4o7kA+}>UvHC3vC z(+00kn5VfB^c&W5Ykx^x4`YViXGVA0#sWtNW(55b{pY~7vV!%H;uu&#)d!x&FdgV;O z9YOBN5U(xAsV`^5*hN38hA1^F!Q? z&`eHn?y3D}eFMD8$*n>AQ#lxX#FUteFaJ5YR(;78ml2BN6HTAoMiw?^T7_75B}&KS z9eu1KX5n2YN+Q6!aamWe>E>7C>|XVz1o=lB`)#om@O}d&eEM9&*$|wV&U|G03;ueT zAbk}^8O`tP=D465%KO@>>$W2C6ied74mDiX3U5FZqR#Q#u2@eWc{jjCl~@@bMRI!T z*1!JZ^`GSDfgMP(G9tJ@c$=1pRq*+^S#z~+Pp9m}eexe2ytx#x=cXn~P>`@`vWepg zcZ;5RT6p`8P2Q63R!qzFRz1DN<>A02Txz_42bM9LEG_Qxf%RKaW_ed0H%$a%@QmE; z-uC~H5eQxD4W-r6tm(5}`;mUS?8NVxb|LJLNqgRsG@TvpACM8gl28YKkaCD*kVef= zaqAsRm4BI7!AG=)g{HGNK|FHqRF8hAbnJHUY25fnqQf6 zfUOplss^e~A|&p)Xx}QfXH^A{>R-@$Q zMWd904l`LWKf%+b#UgeAgr6~KUt#%P-oI6c7*@Mb(C9a0%SxX;(WUB?U8R7;&nr`jz|hW?SG&wEv@2Dod|4ey59N|K6AZLyg> z2>x7N4ck0SRHUC~UuEU=X@ZtVI8hLE&a(4ks{c&?u{wKcB5$292IZ4%^^cxVk|I*P z)+EK(KclBocuCi}&_iIPo<5|wFc;Dz8SECd+n}WK2(jy5hoPaW-{p~vrHQq6e$*!1 zbcZs2Pq_frdI;`iL z6kQ)5owWbqMc3OdGv)!oAYD^eO?NE`Eoaxu>5*A5?Eb>`PMYr&W;& zhAITC>jq#RhV~%v-bJF*kB&GjT5KeX&UGYwo>Hi=7@4CHfi(7LMzflmeqvCIeu6p* znFAq9zKq4{%#YeXF%pm+-5|#OpypM-&DkGpbq<)U@r~tBq_f{W$k6OZz&{m{DRnth zW8_MaOwZSg`*=#oL$9w_)A@FNKrY6EH@LmjlQGlD^)lA1o3K9(>S+HsGOquqp$9kk zpPX?WDazPy__$WqPGLO?4-u!q*=x6}@j6PycF}f}%K0Fit%{EabxsRQxB@BontlIk z@gnc%UK>TfImK1VoiXHTf6&z>10ClN*Z1o=jr@H0OVCQU9>C&rHC{8#N8v@Rt}wRt znw@9Ll5XYZ2}v*NeOAyK76!Fa*5p44&ebklCNRWwIyQ86z2LAkUiqQwAfMM*MpOv5 z*4lldG(QPzQaHi_uMSZGTQW1k4d(izmeOSKo9+`$1Xm!pE^F=ebtSC`7$S-&N$0Z# z>Kq{bhV3sz+$I-oPu-Ec4#?;yHoTvTn>Qu5VG6=-^jzFJ{ zr1QgsZ;_myh814rU8zEaz~hF`kzc-BBeR#=ts;VT-F+7+l=hV8fOky!)%Oq3hOnSG zb}7aZFUr3PiG5FJ5$`0XomoRy%f!Of1^(POyeYBiYqLSi$lQ29MLC%7f`+USn!Zb| z=Hx;o=8qt9u6emjdC=49aKlB}hE&ocYTH<6i*N2btIy{cQSY7rXqfF zIVN!I650mH)ad(n(ANItYQ+dE^c_2pR20*65d#E5NhMSf^*6 z;SRc9E+%3Wd?0>-JC}{}=2c-fCC#T2OvnLu1vNdj7^>EpK`~1 z0XRa*uk5}4nYz||rEDSS38~VPT^}*VX1myOeGl*wPc3_;507pSzgqBQH^Q0mXA0@` z)Fp|JY>^GqPXQyzg}dixc6?bL5-1xYW%kN%9r_EWGM6MQ0wI-`M{3faJ*rQ)`}09w zs<^xLs!sRgqGUdFN`VRgUe=7SiK%sBs_Jl&^om-1C=McVh9)XUgHbSEa-n z(9p>1vS1xsmyWl49?bv$$Y9m)k-?D}|EB0?D+2${j>8+0E{V7C)DQmTEvuw3t3#ww zIL?bYwKLs*R~wbCH?`5Onhlxg&&=y>jM{Xkm%8we#$N{Bno~yh6BdxhrUQ9*R27y! zWm{AvFyQ)*eu!DS^}usSbzk>YUqF?-Ajc+BoF#M6ECh2yC05l4N&KH+c^Nz3>77~v zJ02^@H;nhVt+fuG)VL|TZPaFIw8TKPhcEcck=ec?C+s~Lz%L6j7fj*R8{e&;LD1Wt z4!_Rh@-KO}YMfb>i2XW0X(3|FMPK+aLCoI+9)>t#bFGZ7kX}B~zku11P0t(%Z!7il z$$6DZ?JYn~#?uAX*&X21L)J${+&uuc_rmvu@rmQQN(Tw|{2W!awkJ=9-Vk2)i7joH zrF@DE_&m0{IC9}&pC835y!r4lzC*7WElPF}uuMxX5%TlNvn|^);Sto3ZxKFE=YLlD zoz;YSengdsu{sF7Rl=9f#;gzi7V^8 zaKOPN|q!h_% z&wsYwNCdvLoAAZ-w%dH+@8tH#J-m6veS77#U(S;>`2W}5c?UJUwR!x$AS&(B3_+zx z=n#SsX-ZL~2q>4=0T0R@a89i)X8q~1$JED4AZO6Z_eDN2C- z0Rf%&-I<-)+1WC)`7d*RInSxj?>XoDIXQ$X52*tzqrzgltAMl-}>*W~`E4W>%w`B!tFHLgi8VY`tyF*}cx zHMIoB`r=xYFD!|D9P#l8%4spXPaYG5xS#)xW9gPuaIUqlVu_1aS@e?_DnWSDq`l7? z(%ON8V3Ec@en<4} zmOfJ6-V+4!-DQ?uWJauw#W!$U@}%(e@Em3v*^+$^?~7+>p+RqUv`}NPT88($1H4uM z_=E5~vi3Q@rY7pCFa=!|U431o?zOGT>L%{;Lm><)l{FNzBSHUB(ZE{HRV;9DA(BLP zi3|%m3pKFrXa0mf5;u&iEI12H%T=jLgD>C>Mr^a4ghY{w6zDr{5Z+5!ih$3!h;|1V-fczs= zN&pmuWRSC!U$m^;j@EvY%xH}*b^jaAtcFos0|b`>QiIp0&T-F}XkY%XZhKAuR?^l)nx}pr;lgy9|m^L7~<#c8YRc z(yT=Z;BnKO%UNQ!P!eo~7Vs4i(ivn)+>X$_1D_oJ|D4cr(}`|Nj{8}(w=Ak}&P#^Qz@J)4U*H}}Mh zzBc4Qe{=~Ss8+%X#cXp)WTlERrWJA=f-WK=qL+I3;rp>B<{OJa(rEXacU>0yVPz?` z%&7jJ2imo-Ky@o%j4|%Q-pJdsoE?jm9{u_V(a(!rsNh1qXwB@rKAt`JKdVH!`FUo9Y_>xosZI>5arE`sLw^_9(EJCavLapCe5@G zO&%VepgUhbzl_+NRe|$3{Wvu(W87>&g>b*WE^1;vjpj&yWEcNS@y)?ix&~Rcl6)UM zxP6BzZqgEJjP6>;YS&gEWiUOhY&IRruHN`mL<~f60@%`-LW&v*5vmd%-3%J7QR)5aAoyN^$j?JxBg_Z>NO4R?mN@kg^HJqr5l6X!_xu@g2w=Venr^k~p1E;p*#6WS#fq-B>hs(*x!RqK{lA}+K_1_`31_41I z_1YI~^v-RJfxd-DoiN{&`x>7|J9$06o-7rGmo+7i>4W!0o``5ql z!Lg_%u#BK-oKL(|@M)yl6*A5o9^9a_B#i)H{ zcfEX@=G_9*UL8#;jWErjQv>abt!2=8^KJ8a?^gbd+ZYPKWJnMB-!?aTH>}_n%b_6O znoSOZgWq#H^3OK)7=s^`&7g=yhNcM%%ITL9 zk1CHi3f9}QwNZqUGoh-PB)PD9%W)?H*!|i-ws+TnONrXA^QV$-1lpLr7@N`2YyEDl z5PT?XMsffhP2L%fOyu22ykH4dU{iG ziKF73FUL6!t;8OQNT9JEtr+ybcON*YcymEH<9A{nDO#6!F)+wyB8?Oj;%uAH*aiB? zmC)F8gWy?3CI3?ek}gl`#I9=KYf)zHz57?@-T1m?Szw$l@gIOm%^T?F$#Sx=ChVz4 z6ezBGpz?|X7~ia+#Y951&zus@&n-nzQKeHfHjc;$-A%5sq4oN>33|I^ShS6-u->F; zxOW+!dd+|i7%#nqTZ1c2ZxUCYkA_7@p2c1BgCJUF;_OUmw(o;j& z-0#D|zWp@(i?N1PvUC}F2#CTKc16I{Gwfy zSQ(g7$|@Eht8$y38bJzz+jCe{p4VIU=kY+In+lU}_|;5Y;7Icnkf3RKrhHPebEWdu zh~H@et`9yz7K*h68FUT=HH%~^P7~g?ch)JQ0)=lXc?~Yjw3i;Z&|3Xe0kYnXR`b6B z*GettA%;hLM@J-XcAs!vj-7JJCG|b%ZXWijkHY)LLfogJ9AESMhyxsguy<)TdU6jg z)bU3G%o@YPxhP-~JYgCu@ywguee0ZUHS6wus;k3tq4rFIlRuzn!iH8M0&j8`m(T?+vlinHN{KQM8^aB^f z-16`_{5@?JH0t$gF{MWT#<(!p`thmz0yca{+3OC;gG7oZ>pGIA>@Rc9NXoZ8e@$~u zpe>*KCU}zb=v#0V3Ew@cGH(S1*gVGjsi3ZbHNwS4VCfZe;QF{TJv%&)B9GoYx%u11 zx~ZsAyZ9HTiF_SHJC)K!V^a!7D?n9w(nWE3{@k~xYjCorL>)~hLF_;e3uwh;d;oJ< z*=TWw=4K+=p37N_)GjjABT(bApsK_}|3K@qFPE2L_IyX#*&seL!De;C3bbDks`E62 z>CHtMJf@;t}ll}!-R*6C!aLudGfv7qib&@F=dOwx@^tCt+W?I&}Q^n zCQqFve_P?H8KE_OE5W_F{JEfkCldf0x5F$e{T=X-;kvE@u;%|zQq{F*6}&7gzhfy= z=((7WlOdNkjWzct8T#!FBQbUxHTMj9tD#^0#3{e(lSklSRLe%Hc{{ppP__x}-a9u> zS5O>Ur-TvAvxx?p`x6bBf`8Vt>YaPI{I%-+5*3*g%ILA>Mark!dWWERZ(H?yvGp23 zz5UfGc}EB-mQ~icaH=tJ(RMHj8ozJf-k^YDGHA_Iv7?IcWtFnJ!?qIO62CKokKq@u z4AnAMJ*S%b@}-U$3}fn_3W-mbmULeX$|I?x{8E|~_S(L*AvEUIAXLlR`(RQY<*6yg zK~5{`@6$cyO{;^X_`g0>C&0mL^>qGTrP?HTf+U*_>NrU}VDH@+$6~_t79nU~!#1s8 z9G5^F6>+%_@5O~td3)kH|69eq& zJxesC?}Z>@>^O1RA_uBeZ;AWj5$O3h53Pt~z8JsAXy=3(zL@6{eW!-P^4KgbKEz$h zm$U0|`OhU~)dN6xZKgm87i4y?vwZlgdI`UiDbC;w$>&RA z3$R1f3qLlActxigP#F(%TE_705>Oks7 z1urUTT{gfKevGzn%B={Vwj9wQ3F49O;ly-8*L5vi>CPF057*v%gmznJWR*$eb-Hwl0cva3{D!0t9z=NRR-*C0MW!ELd=Nx8UyX?u|?E;BF1U-Tifwy?^KI zd+vDSjyLXGe|FcJHRY>0znWDmSV3MA6^RfD3JMBUT1r9*3JMkm3JL}R0S^4jvVx`= z6qFZ~w1lXNlkUMXqLYeoGV@8(wfvZ@<0JxIz5F6yy`|pa?(U<*BKzb}?cfA?cF^Gp zWoRS$f&~nckJmu9m)8JcJOBX^x(h4*jUNC@iu{cc1;3HrFJR2WvGZ6_OmXTPl{}@7 zQje?Kw}K+YO56N#vp0`px1&X#-@Y|NQ$n6IhUYI2Fqn|%(j5gx0`iasiFyTrp8;4_ z1PsVO&g5`^N2B+Ui$F$fdgB8iBZ34le4!vCqTvlN$Y11%{GSQ=5n5zraTFH@dPAAL z5)PiP5YJqAKHhUxL`U4NA~!N}-79EkrUN|P)R2b!10&5s-6i$ls`*|DABCRdE09CV z6zeW%A0G`LDJaN=3anqYZVx_`kwMv~vy%N}71g@r!Q>ArGtEhCaP9(kHy4#(2#_vB zzAWicQreQe4DVorno5qS+>i0JpQw;+rjM0&bhh`+#+)r7R54b)M7o_M*<0+RF#K8d z3*WfZg(b>;d=!_16$1nFk_}?y_ZcRD1_0`Ovt2RGDM4)}CpVk4cgvnu?TJC@iGi*{ za{ZF8A$&{0lxg<(?qKgG*A>J$GjWn({etUWArdjFJn+ z25G7qz7=9;r%TO74+bl5m+n0{fcHnvZjNWX?_UG&<=-5++%p`x?4umHC`+uBb9b@s zgiqpgu%h@tYX5n)%y>^U1?o6wI2aQ(I*le5ASog0+fFjf*-}zYg(}yHPZd@fq ztX<)ugRbz<)~~i$_Tx#Dl}|HzHFNc+KQ*wnk0RJ( zC>WSc1Yl%E6KOEP{|vT?GbM;37EO4|r5c?(hvO({rbam z`#}#fvY1U><>zIZ#{;i}5|6K5`d&c;?k}De(ZhPP|7VM~i4dl^4nsj9-_I+&!2@AO z*dlPxKT>?!U8&n^*T-Pq065S&R_BIVHDLOW?zdXu{h-#9kkq))hiFL&21bQ8# z%;$dw^6M^hKMs2VWu@ACX}iJd?n4$#Qz9v#_N}Pg-kme}JsDjb(sa9`PySFgt2;W4 zKXq2XUc1t~T5gqqEVF@Wh<4htEh2NRZ=}9AyldDPrZ4KH<0TsJ7xOaP;bK_Dt~km$ zC^@iO#L99x{0t4c2QR?UUWj&LeKpY4vLB8*GxXF#&HbmH{tV>`5y0HNdt#%KT9%ww zpEsFzOvfSri+D=B2`fA^XD$(;RQiV84{g^^V}JUZ82}I0VJyAx68xY}$A^G>LW96T zp$1|4%w_qh8xQcxK4*d06y;Gs-sJzaX_ra#Zr*Du70GS&JsB?7*I&HagM|v=w;Y4J z6q&1)miRmdEzOa4n$TcPdFjXJ9N;NH_wT_9cS97Wj34sZjBzkFl~r_3YyPeq5lf>- z?hSmSR}x zC#`CkaI*Kk-ip4ui$r z!;1}Zl(AeiK%30^lFvZ-BMBi)x(;D@+cwg=Cs1mufNm;v66aes!&Wyl8yV2#>}0rK z9}O9LmGJ1r0S7e)Ev@i(+kZXqUq1Ei*(~R^{qz|2Kxb^L9TEEOEgB-UBhU>H!=)YY z$tX+O`%fto*$SU=^(wVkLJ1-sZ(`#aF&xUQkH{yn7q#Qu2NIy#R{(rox%Yw*#!4)?LcDt-XMe1EwW#UKRg zGA}r_ts#$!DtZO&D&8@+D(v6utzNY!fu=^%5F4)A&q427uGtt8Cuy&#;Vh9*#LK{@ zqDRc8)yPdCOhEKS9pPpMfe~Q<$Oc(}WDwhtYwuOo%Y8&-f^{g?uF2ASH^gU8=A$jJ zZ(BA_R7WUu;;9vk41#%R**M@On!mRHnS3-Qaob4nS(Ou-IK(&^!PJ^|uf+v8AuED| z{{@07@03^GwM$5L7&{*j8g&q%sp5OqVmi!kHrG^R7#J|%Wv@(gW@ifS zQc)t1*JbJydW$lAA>B1a5}dYZdga)A&yRKTFoBRFCUQ^SktYf+3w@B_!qme-Yl(Te zY0R~||G*U1jEN8~m+zY^Q)ii{L5*os=Hyo%cF$4iClJ;`4FIjqe%)A|HCl_FHoe&j0ifumc_GTsCN?r^g5q+M@uDJP zeF)!ds!&~-y&W}CA*bBp6N{-e2cY%iQjq6E0Tple;Deq0jX`aPgXWfD-A>E-JYmXD zsg+NZUvIVhnq$y=94^vx88^s_C2@B@b~Dva>WCKhR;CuRoe(1-CYB~B>f zCY7*9R{s;#uFlaQ0Sn$Z-%FYbeR~C5HR92pjYm_BM&?(@59MpVg1-`Lu95!GiGC3k zDJFyyLY3{F=-$<^;ee%&G~T#pKC0*a5A@h!RI&n4+`bFLMa)OS3j>457*)kOOoDIL ze>w@&@}LQ8J0S{dWP+2P5YX!vkl>_EA?z^v)tuXBEtt)`hbD>}!WrV0kg+(ZN(n@$ zkLbA@AB}CFw@zgl<`P0oTYj?GHDJNH#l*2#zdDbrU{p!3x%omU#Ge}QPK()|9`JCk ztOcPuE|`B)f*`^iDGZztkypzq((ibfq*0hcinSOGM!;|jSc8y0&x)Ylt3ok1lJvmo z&Up>OA2=DrQvEd0rW*OOd+j`_JJaK;9`(I^apu@OZp(oTBcvG-#92wGxTJ|e=B z-RX2LxxY(n7k-6|p1<*ljQCg$0g~|cbY2Ko#~%2?c{793J6JTZ1ri>v39;X$uys-6yQC z;3brx{@#KAKTu(gvGW}P|Kvh<>GDny;`~fFFVFjvR$Q2`X}#Ab>7CaFl4~ow>o=WY z)~&4+@`%vC%n&XhufosrM&I=5%|6Lh=ume!=w?Y?Vg^v@USk%Qjc$b9v!O8OE zk-%gM%KEt0`eFZGzj3=wxzR)h!Q-LR^g&s=id9G6~~ALD{jRzrQi;ZB#bgFh75=Ij-J z2m15kYcO?Ow#4cS{10pJ@X@%Rtx%=BQZZk@xFx;l2If8*D1!Hre4cqM>;Y|-pL z{Ew8I=qoY_W&&y8{ezwZo#%C*juo+a`)esNuU@50{?%9vYZY)t?kDTA5<=@g21~g2 zH#w1fi}G+=BPCG9fhC@h(QOT$xk(8TP%;LZ=VW!V&`)G5Pjd+LvJz;Um%;c3!HzA( zztKAf-fp2UX6Z?kNKc&Vyf=`1$r1!GkU$tmoYY z3h6xqR^%vZ@1f`A(QjLFK|n1!3=X75egB8ZNcurdk6YfDC%oFTAW@-XWOaSv_Q_G7mAjA%#zIZR&DGgK@bs1t(m>;Wz^*F1}qj-;TP~&=t zP^-6#9v7~hP{N70VS!)$*7U*QW8;5)EzdLV;Hn@Q2*rt@T}28AJ0`-!Kj z9(rCK68CFE2n3?*$rUDp_NP&4=@Q5lmT>`0`LPD$J8h%L&>>c+*InE=tAsVkt8Ty1 z9Y67U^Y_EiVtXw+hf%1u3hT_gDfr?u#uq#lS+!=#JCvBEShvX_!!*6+ z92lq&+nBaB{;BOl_IQ}7Ebf(OB~%Cd*mtv*LNyHCp-CaK#J?i>@2x&30`0MXE6v%; zfUA+)D!75i`?BOEsGI6gM=V7%8?#B9dSx)8*kRNwsJQB0`HoOX#o$Q znv~PPdGEhHfFa~&(;ZY@QE0p1gzwHZtM;tr5{ z3KA$d>Y-MjORO@Gp&Qzp<>njQ?8%Jvj5CZ^w*;fE#a6JLt(^81{@JrKB5)MNtb`d+{ZGo|rnsDxp&Hr)(`Woiil1h|eXo57u|YOCtVg-hr!s{UAAP2}G)h;B` zu8^2P@sB5SS9k|D_@KdUj3s3~VR~y`@fNi;SuqOv-am$(PPqByp$_>BG7NqxoqgT@tffObS0o>jx0JaLgz~K>iw0pFcJBEF*CB6r@Ki1b!bpf>PxWw z84}>@+|xyzbrvEwfNtNLi#Mu8<)k`qOG0VlA;;LLO%U2x*GypcC)RT7XY7&6-RwA)HEa zeh{FY;#U&uNv8C4h;p`Ixv2QWxERkmpkXA-#zl}5rrO6~I4dU7)ck3WQGD7Qb`RAA z_U0D)pY~o~4a}MM=W1PGW~rJESB$EHF%s8zF?qFll?3k9 zA;T_Q!q{GD$4?A2yEF}`MU)P0evYWl_{bopq7==yD^&8J6Tc>*%~YBB3a+2sO1%Ev z|8Az=Vj^kHk$Y)AW+NuB5NIy>LQsmj&?VyQ96WA6spGd<*u;<(eR)CP9JhyQpQJJ* zcw_7X?l3dIi#eQAVI}*j+7yXd==1j_M-ogY$ zlJ(GnEAQENX=>2um!${`t+b1TZ~yB1vwbCK@xT?il17KVdtw9pyBwuxGGYI;Kj*~7 z!yJ0^IU;}PkjvPB@uP*&Pe>ELi?LxNmg@M3d{K?&F`xC= z@F7YYq!cMKk29eROb~>MkT=Y81j1NC-hQtc$93v}ei+mq1%C75S9{nz8`OXhD|qg| zoq)C=WAbq;R=Oe63x!O0SWegDfwx(_Acl4KcSsx4?8tp~x-;Cg%wsV#fPwJhU8 z7&}=adFhPfGm8xcY45FtKn`{6_;)u9{iX0w0Z?F83R&}ocR417JL$;-x!i&NcumCZ>)x<5RUVYReU*3-z`+}z9;8#kL-T@Et5I%+t*x5_Wi zLR2-*lWJEB>)Pu>i*$Sstnh9AY()LF^Z5%yyj4WkLq*QX{3wr8pfSkS#(nXcuz!(c z8u9ndvtbEiv)|-a-pPuMS;Qn&#ghD zC%a*7M|>d7C`e;~;O3A&j9J~?zu~JzueZf{^!AZ}UY}YDng4tC_>J)J1cg#~=*ijI zs>7~e7|>jD6iGEKc0qZEI^Kw6ez?WFuS924>A!Mbm4>;ZtD7$akiAi3Ko%-e#0403CVrt%5~e;+$RM@4q#T zaHhDiIF`3M4w^IaN6hUX_M1L5IebW;%^}b9OYSa#J!xa2Zat)Kr;0HA6l@9Flc_+5 zVe$|Lm#tt?uK`#B8b!9~QN+tWqDf#+g3wbUnO)aj4K)SD3gu_#U{uMdi^dKm6l}wHYkUrC3K<( zzo3SHikx(+-Q>??m;ZhZAf&@+a%b$Lzvs7eB`#NMcwT8Ph5=aV6tt2Ju_({OKZZ$| z+%I3bbM_+4SATFHX_7o!WGrHP(&nccs}V-vAa}VJT{2|djr(=`sUXmBD~p3Tyg4AV z^0mbRZbU+#b51)fHL#5zq4DVv%s8pRBoW;xRbqM;Z>=L_3%{KlA)(yl%i@D^VkIR^ zQovgVdsJ0~)mCCqeuMDdYF@zJcR{_ruc;VX9+nd`v;a%(;8XSnipu5vc?^KXlB7Pd z@2L$UPm{?*EK8Qa_Bs2Wd7=5KvM=k$+XcV3ol>fWIBG8T$(|# z1IxMM`Lu(9KAc3|r!x;Ar`>Ms#9B%ub06Dx;>8L@JHR=K$ItfYF%lC1U^n^d}aQZv1_e>m=mKed$O zOkLjl<0>MN{xAFxauQ@EvRe`e&3WRehNDU?J#$jaE+VXkUF{$46PjV{Uh?+IY59)W zaO!;OS(tLzzh&bB`OTe~$FyQ8COae9nJ{#}t^kB0CSH<%*%v|r;fF<*nAugT#|y}< zmdfw!#|EpnuJTY+^!& zkqcS|qQlBbw%@<=^f>mF$aTa@WdNf&o0bv<9i7#EHEKDLP;>UlS%!>6V>1VsFJRI^ zZ(`Hjd``~d;>sqJv@xmA=+Pbxok*fMzn*4dbd6 zUScx|%s&Ja?t^6nHy&s8yDsEThNh^o*-js<@7QFo$1EK6%NbDVLeBb)n&upj94vTA zBj<*dVhNa1InDQG&W$bl5UaLk-yo=PsZf~p6Nahx?WdG~+9-Pky?CvN-#>nODFMrL zYxdCr7*r}@>5hq3#sTjeF~(gYjoK%hG*=NS>a9=4|Gfr!S5()`IuXc>JdImn_>weC z)G@vlcFYhgGEztJvkJ|*Bq}FJO>Y|hZnU(-9)ritr5rQJ*c+T+*)?@HXhD5$s}n&q zNs}9Fv+l1vT-daaML!~2pT;Gf>dz1g8PWVhwQm1*_M}te@OA+(Pt<;UZzFqigWz#p zd+T~HZ1*~aZP>&xY|(9P{wik2!`GdKs(85kT6{Ht54gb~d%wtaeiJJUAF&Xw2i)`( zjbBuzzN#PwuHf2Q5Uk27V^m%Xl2eX>kJ2~27vxmvlq*fk^^?MYVPB6ZdNh{GKIp-F zvW^oSkFn_dpm&5$zJ2?r6wzy}NX3XKq2~wa-zYT0P)Jvg!47`Up0IIvi6@IKOD~#9 zbWa;*xO3uC{J|)>+*89k8-cSf>Ezakpl@W(H%bX)7hd6s78*C3QOSW)ba{Af({)D0 zgNqZ$(pwZ{cXGZGa2-3&avI~yqdett8~At8>xgDDk)7*JQ=jko+0J>meb@GwKs?Oe zrOv$}>(aS1y7-kdmu&7KuP=pgG)qS0fKf78Hyd~OS>C0t|B>ZZFArCY!}{dBrej6xa~($ECWCvzmma7+znOEN7Pm~Lmo2>mc2_z6_e4q zcaN%aQCs_BpZp$ zDy^~__%LxCN2ElWcLn1Q2_rpL?`>pSY7j5u)K48lZBU&!k)qECQ0-scT-cvWJVsn^ zSm!NQr@{G^%}>_%6s{_FBGB$P${lic+jO0};r7lhETxL%esV}rL+N)uxgareT<~s+UVrhB z`sVH7QQWFLtV}tz!FBR9n2QmtTmuanOClAw#}cXtXx8KP%#mTjZ7ZIUHCQE`SBj;K zrdQt}{IDaNo~?g}z_C`{?jY9@5`;8Gi`@o0lt90DD)vQQ{W#`>-np>Nh_n5OXUn55 zQE;q}(k5;!IXycLja&U1qcIVCYs`WUEkl^_?C8Mx0TK*b5eMV3B2BpXV--)9AwDbH zwUl7j%#mfv9GcT3akg1(|HnA5@p^`DG`gt8X2>ShIv|&utd@_9dhp{4!D%&&@jGEW z-fhO!OSCu@ku0C$%XYaLpoWxduX7%`8rElt&K4uG8xGYifjY;5Ks`2K6)uQ~lYCHno;?BIsUZB@SyXb;(;y?gsZi!sDp2GkT zBH38`vz=yKaXJ(X^>%RvoobeO=ynws4yKfnSc!SuFtug@GK-#)_L84RuNGU=Z2erz zI-QJ#+e1vBoBJ}%O*Wk!UO^jsx@{jSL=Ns_PbxxhkuZ&NRv$#~O@%rcB<|h^9(0Bk z@@>MygM=PAB?BqPHwNl0X7K^(iuLnHUFOExuEHz&>BDH~KA)nim$QvpIa-Ng*0MY_ zoSLf6EswY@l$R^$T2|2A+rz*vq9Nc7SSbz@u+(MClf8Lvt!a!QAHThsV!iq=pw$NZ z0{b5A7%sxa6n1XWB5n?Q7L^k~piCHhF#|?~so8vk$<#c2?rjW%=0@_HuP@N*0Yi%n zu!-;uX6DzzvoQsN@QfzL&XdsKH9`Nl{!+-##_mLl2Bb$0z^}Tq6^;#;C5qMxH_M4V zTJ3#0gy}z^j%_S*U{{2ZWog=B1DFH-ku=<8FxJX=w^-eX^OwP=>xhcn0*5=umn@SL zL!d!97~gNEzU4RwPADKAqdOMIn4J3hCsgSJoi)clsp*O*^A< znWw=%zY*3^sLVq-o1|jAv0TrNs*BITNhpeEhq)3r2a_A`SU)l<+#=!uylMp}Jk5wt zD50m}}%iqM@O55V5(nd7eqpCQdMpJ{j`~otH`iEpzghK2`(Bes7 z?HH?hu$w{x1;Sia2g)39DvG{oX8Zn0PjRx?n|z5Q1C50@-wKvvdc`=o1Z z@I+<@vaX+Ne3OsZtEpb~-T5^dp-@@V&9Lc%Mtj^&B}rrdkDkvFAI2ihJ@?h-zXx8P z7>;e!_KuZr$b{W8`0~xSg>h)YSNKJc)Qm_T+(#mcibS8fPfe++l2bmpEY9O=Q7`Nj zm@S@q0OFHq2$K=h%CZ^wE6nemJ$6_S3SOcpS9 zzdokVZz=zgkIO2gXVA*Fz8tMm!Q>fy@!31ruHj3Z6_30`!P-O|Lr*Wwn$wz)8;@%d zAMeWR5@!9P{eK7$9~^YjK!L%&k8wV-`64{D>y;uf-^!{hyX^!G#lM2b8f&rjvGQB= zuGsc`N)>B$rKP}K^ht8|8w<^5F_NL-OB(A7ZKRE#>=-0C{v`V|>!@3_8h4An+e0M$mdjv+?W;p0x5b*WG+3|YHWnDei* ztCSU|tsrM5ZG0O-;c4-}vsCyM#QuO2w;tm-3f?k|_4QvGHAetaq8buMW*POd5BLr9 z(-K^d6OD2hU(GH+HgB1rn^tncY!9y5M zqJSrrkftYwP-n2zJ5GR2Lj+g4+S6l4BQJ1JHT+VoMzU zwYsW$&Y|6Bt_j;gg_&M^*uGiYH_nggub}1L>!Q_r*u!{wj6UeATE@&$@SlcWi=FH$ zZf$=yM}Zl9)BFZC=7Sc%J6d!uefIW#+-5>6q|p!)AAIC0+o3=vP%nUqx3KInnVUr_ zto)(39w`_6*)<$JzXQ>aFu#}8X6j&PGZcLEog+E|9-}vFPa;O3qEav99$`q{Q(~osr%Wp zKc1$jJFL4}4t;JtX)Cgk`q7Q`4%}XY+yX5`x6{L*?Eo{aj2?RP$^HO?V@^;R7XhTP z3pp+>M2|sq(ELh?mL4XVx|@DAI>l9GRVWeUi4t$6e$&%Q&w)-5(DIgmKo8Qs4+Xyg ze*XWwzWsmY4)}i)(j2AyIZ#um$mycn4xeNFb#%A1m2{+Z1;PE;JfyuH6vT&Vv&Bc6 zjO9&(tJ4v%rtJs5lnGRm-(;Ckp3s^woG_oT>l69`X_0^*--Dl@?G3n090`-HYJGZb zzd~e5k4YaB-2c}6D-Y%Cp-e9b*$^K_gNc5ZbC-8l&`_8Ua&ZTI=e4uK zp8;=O?so2V*y9cZB zDJj{$V0>wMx%vQ(X_%HgFsMUwc6v&bv7u zSKv=f#LmNKkhoyQj7yKdb|2iclhb?kjet`dwF%_oScymIovbBlNN=GJzrY4K!jas8w{f9ym{0RzXM;!_GKSv@@Sca;8tb=@8A zDY7A@?@-~h>*v38myBId5A9n|1{MSYyuZKxi<^{74usMWrp`8Vtkd?mU(Y#yyl z__xy}1wC1_#iyvo6MAa14jMXT8w+1c+Z3|X1*2vgw}&L$a5blnc)8-@$j3j&`0`d* zDAPQ*r+Z?$ipNwLU47?#jgpEHp6Ig0i<6Wn(D%`O^wV}~e|@)2BI~@CyPO~bNALS} z;Q-pz^ACUrW?XVQYUHP&xp4+gU z_cVP*Wkz9Ahl{)P+}NjQ?La>E#D6OZ;ry+Q zU$=Pm8}-u9HxX|V%*RaSTfBtPY7k(86>sve0C&Qk_%&`=mDfMGn6|iC06%K}F1n@C z0@2LoQ@GZRp_86+6np6?hUX+F4#~7REv9qlFFIIi{js%=TJhNa#_FI$!={^~J->RE zs$C@lI--Nd?J+rD5&96H2>qxtjpu}HjJ&Z0qH2~~XhR8>`GN_66R5%Sbfbdu zWV<~t{TNg++@F1F8}5Zin&g1H4`aZ3s6%#1=-ooyPbvCSTpUxawWzHxGXaM6yFSK~ z-G##v1$5VIK=LGr)kgFU5Jk4LGe{P2z!$71gMp9Z(>;v>QxCbc4NiV3yz~efGh^HZ zYccHpea!szvZ5k-<%UJ8L(x2|fT?YxFHb0|_54o*Khsx-gZqAooku?fJjgPnf+FER z9CT1w1$?nQc`ra?f*NqxKX`$=G$7wK*Y0d6vS{w4zqSLL=y1kI$Pm!o?ELTLOz$J| zT@`yT#dQegPHjqEg@U~R#yM`}n$0z)+C}AWz6W2F>9CdjmwfO?DMKWXZ!f@S zmS0g|7##l%BUXgUCB^@*FF1_lvPk5s;cuHV#4CSvk#`xJiyVdAn~;z&I@#A$oWoBw zPc5j@kZ1f~%G7~Qk&~M~3b@D-ZkvJMm2r{#&$px0)glA)CBBRvaRflePkwZf{a;6n zPq~TfR4`)K{8wF-?1{6b42(|yx$=`VMmHusW`+GKrv`G6iiv5Teft3!X^4o$T%C~g z@g&uS8;?>l=3fKZud>+xI?7%&Vzj=tHoGov$X8?Z-7e!Ut9m-(_}@SOuYOOK&9oUi zqaXi?4y+9^oE*>lP3ymWpzq0!>m}0J@)Fuj5nwN@1;59uq~O6@|1}#|5r@qib`g6K zf01~Re34p(o(AszLr4>OORid4;>dhjz#pQrFIB0Uf4RkkYoEst+`q{2)of8Zz3IXa zfQldbA0GcFUM2sVcnQXIVI+Ol|KG*Zdue2AVww&l5Z-HR_P$TCz0y5DZam9d|y2lbq&zYDo_PF4tTugUBdO@SwhEK?7tH zIPaMmHR7RCF=@-1Q;PUN^$b*)hr%PEp5UXC#g)`5^A*C-=PF5!nc@OL1A6dq)T+Fj z)sGt|G{-+c;tc2&LPUTY#pQHG!CUb7Ad}awe#T{UR1D_RF{D*-IX)fJAs@8qip}q@ zf0fcbxY>me0-l^r1!lBqMJ<&>?=(E@u}8OO>je|GoTp8Ep@fSU?FLsMhB(PXW(^VS zs#d$Y3MwPU1Z>1Yos?~z?&W&!q@%Q2uNB`p<^;~r7@Aq@F4_sBF|z<)4~Z9#!GXQe zupQe*7CAzCnraYPS&9SE>EIAk3tXuO?4dm8l2-I5tU^6CaU)}^;Xog&1v2j}Fm`r5 z6-u=V4lE6>g0}xL!`LcvuDGDBmcOXA8_>@G;b?>dXvAGi@cv!W+^!wzIWVlFwXAB+ zg^p~#4o#PB)|$jyOYCG=>;9yr>gne3?8j@)_%F*So(~QdP=DTY6SVsp=cO*segNl@ zz=Srr;s2pxZ32)>2V*ITto)N&0^;Lr1jhr&goF&wK~G9s4{p`?-g@i{vm0o8Xz@b% z$^CjmR)r|T%CU}|ehmZeUB#m3g@>$$J1!a7faH5a%y{{^;Fb?JUj_-_ypQ5*?iaut zkFAsHhTwUsbmarg_&SJt;@SiaO46@>GN698Dc5KtpR@I8-?5_KzgH*?so*&BT?Hr`|IVZ4fi0g2&hB%@qBavoZ^V~eJcdOnZ>Pe?~OSN6-d98RdmMXiuG*Ih^H(xWS3VH2FBkPLW9!%iw4xvwUnp~ zzdyIdPirgtZ^jzr#&PD<QdpSAL-l(Z~_$y~116y-rNLr^A;UQg7$2luW4gPf!kg$cYEg$rimB6tryikwC`d#SMR36G23yS zb4W7E^Kh{5#!>HnjMj=%o`LyIR8-}4fPm$~N~NHajUmqmG<99OLgDy4TI}0=3t6JK zizl52$O}B5Lw=s53Ya*Na~U;ggqg$ZuHffH>}!3p=+9MtJICi9$Om`syXgG43ptmhEjUJ8e$6%PfLioR}0vZQ9JkQ-)^`!e$>7H1v+6g6)})vcoK3h;7bC6%9J&Bu|vyPRlgm* zL8w(TGC0F$%zXGU>TJPJ97qt;R%iX20^y)5HFROF1r4S;QV7457AvDvFx(!m0+f6F z{y@MU6>?PzC(f@3yP;&!)j&PT1vjUT8qZE{)%BTwUBi=-i4bIR=ll) z9uYVr66;=ZvAXuyy(AaNDyVLdwAc#LwYA46~UUhdlMoAb0kz_N6@e(qo-y zRCmtEEk#%GO^6Tv>mKueVrbU%$}MYn4`H5oN@K`bjJSnD>Tm(|>+0sV;|`U2MMRu9 zK>~4p;Vn!Cy;t94T~{Shxg8m$B%994>b7MmOnVd$Xb&FRc2TzufLsO`jW{KBh|atI zLDCEoN;d4oZs=a5FDb?53pjP^s4^d;w#`v}FV@Nx&V^(@-r@x9CXPa0UcyMUxSYKe zC;rx~m<&A#xi-bS0lxO9b$53Y$E7a$7~(*?Iy?29?fH+XWytF1xClflqm84$-r!|K zm@-;HXEDK)@#iLX9>ayNA5GbG4=hZ_qjYW*&|&<8@s~|8=DdRohVN&U;)C1Ui%AaK zw|k*bJcThlJruR*ICgSgzh0@FJI13NxnD*ens>MCAzzDCB4lD97N>r-_Q?vccG{E> zeK3%3&eU7XT_+(heC@K0*WFn4l9^9=1!=Da7`4-viCn5Ao9V6sbs^=!W#QxZCXGhX zB--|Jxu9bg8p3UQ#%vKg!JiZKiUcGPWT^2}AkjG;Gd?~~MzroJ=_WGVUq7?aVw{aV ziw{v@1}mA;dOcBoQ(x$r483W`!BJdam|gG@LHimW&r*$T6)<#{tEQ5U+#e~oYJvju zXCM8uBCeYfDj}?HxP(y~AA+&Pf-Mar0@Nidrjv$ynfP4djO_K7v;!732A5V0DVS*!qV#{)!G^z*aQpPHEb*CIq5>f(GG| z7eRs70lEj#Od*fIUrHg^)}to(Lbi7i!98TgUF8=8B+SG;H_W47X3k=qTuQl*bQ2>6 zo81@)EByT>^V52py)FZj`Jx(lAQX&wj}^Zb?_GGm*1fNk#uLGtfD6lZ*56VNAras=k|`2(9A4S7f62<*DV2zFx7qb1M0{morhx z%!5jil&@?UP@%eIWH`1Gs_YmpQcDf*wgAj`++ z%14bPCY_0pvqtEm z#g?KX>1x)fLx1i-K#D&tZy&!nU{z1`^QWvW-u(ld^=TiYjK{sQA~^Z5&R3v7EHVg{Kez`aNSlmGq(2 zb>34TiyQL_YKg;~#2BO+R!Wxc-V08^yL4~VdGK%ZkP;O=etL|qTfQfwcFnCZ@YIO^ zhbY{2k6xf*yJ-!)>U(EgEs$>lF512aOGrla71QJEcuw?t^u#pklL5gOW;ixUb3VNFP6^(^3y3s-T%2w-OT<& zA>=gEKAabG8o&Pvfyee0QT$OiI{5U`k*kawQnPpBkzl5`$8xNf`2uM#Penhrfjl2B z2bt&fr0vl8z3Mb&9=?}}ceou2wOljHN+zA9G~e#MMNza$szdhOPU~ALOIUN~BU3l8 zZT$Y?jrhX{Bf0XKxe?==GsZ*b_QlVIq?SeW1HQ-Qw8PqjK8WX^HtHO&zh#Kr7$HO? zM$lf?g0rvW#X!=983VuW4n_)W6tPXM>N^cw zj;7K~-<`P>mP?_QJ8sTx>83W|Jx?tJ z^Lx222IlN8Z{B1bPROKyi&NDL$*4)#yz@3pQoUNHjGZok)b}4B;k*Zx3&POtTa3<* z=UB!=s>i=9(X?L#z#C}FBK~BPkqfDG_eP(}VLI-mdMk*=+(zaT>Aq2Q?nHNWm(~Ze z?VO4dOvIZF`!k#(yt2zYF$(<3W1vic?Jc?4MZ9INyOQj;{A6-Eg@2n4Wc?L0;oDKw zBLfm&W=dM(!ro?m(nI6oV?p;k8S1^KcqPbo zXAO<}gC3z$n2#P(7$T0|;tstKM`sI+DBExN--^7CB^N%Ah_YkVg_bWA%0<&JBpiCUgGBfX?ptAE?WKal}e9=`uoI+cEh>nlmg2m5)I)E z1}dUwAt#SJ1213`efnL3#?hPhGN~@-cG)hiQ1_X+W8N=1eI0Qj%cuwkQSw78k2SY@ zNN?4j8k3X+ltWC2)bzI0w4hAbTAO>~XOYs%y0+R3X??whqOxkpw_A-V2j`HY2qPP8 z-Yf}#*Z=##j9bUT=7s9&Aw&Wh{?*4oU+cKqFRiuxhYZ_5E^@R7X%)dfV;bc|^49P| zSxt7sVYG~N3T8&GY9<3UY0Y@uKhE%emh8=+(tTQnUe)<4uoAwX3JZPp)XT7m|Kl%q zFhHX^_&X_EWu$+8R>0p`1b!Zu5$Sli-iLi8+Dh2c|S$%cw1E{$fAy87PQJ#-SsC!uy!U9J-bAr}&Ej3;g{<$9+XR@Qd57FnPIb>l67X z8E<|hV8TaVmH726Y&z|4S%ELzCDdn8Ubt8o8O|o~tt#^mVg^?OHkG}AmCC5qW5;V) zc_bQ%0$!8~XgWvmzJgV2c_f7XrhjkuQR*<^jLQUI++ug?Knj-Hu7-F2)l%BYd0(5> zrg3rq!AUHtff6j)XwK_d`?yq45UvkjG}B60^~VeW&YJ-B>)2Ybcxg$i_Ao-ecJ+w7 zzy9GAm*~T`rau3N-*b3Vwji&=;O@;0*#AAdu=BLjwK*UlF|+Wjj;I4J5^*Jo0;BV< zU~PVMgt*~a7p4ma56&Ns0cuptV6MzZtq!lmh4w%1%Se*BRew$UpSGIJ7Ziku#z?87 znL2>>oxd5Hhw$rL`K4?9NPPIMv4{a&%u_~M$ss&i)Zpr^9^{h}2ye02NLBQ|Ynu1` zxR0(oJxPA@W{Ip6hdy8bL!Uu3$wfa z_!CR;h`7s{quBdbG#~JH|B#^nOCzWVXD$leKDCDx*4lr~sGgFsAH~Q!^V@KArmBu) zW*D#^prtZFfn$el?3*$!HgJuKUjH|ez5ME;FSk5XWM3MgPnQb1#!K~FoXzm#C1yIN zIYk`-Eg9KhNtTZGJCC&I1o_NPv|HZr`1@=En6Ml9gNWfJy-i1nmf2&ob?+jWSkCP) zFFZ2$ab#-SPv|SQ0{J>;`>qmxMP1tK;#yKEC9}EbsX?EX?Rj*3dqOQRl{S1Pt{vD5oQh) z#Kj_SEoF^P?a>BHB+T5X>kdPEXZwF<@X48>Z<%=0!WTPhca0RVs5J2`a|1vUee^lI znt`Fz)XGuCxmx{D2GHfVI1>a1&)4Aho>uhcPKSqo^*FSla&5Cs+;0GFoyvBLDjD{t z@uc+qpT&4};+UjvpI0UExc-{xFM>4yZ2hBHXI|Ed@A|pG0pA+d((ayB4e+O`y0o(5 zCxZev%!<1qzSst~X9IRi*|)6^s1WevcC6OMC4%2 z{cfZn4WNY@LX$q>RoCRzeW@=p<~YE&dTYNFq|@RIZYQ-q^8dKqxAyb5JCkn3eQlb{ z*Y<<>`&I*+D6Hst1`^QTXu5Fc!1}<=^20vF72UP8tj@qb>At@(ZDciKn}b^ap-{!; z%VgA)E9@UY;T9JBtQ~S(F1N?zTAk1m-;!s?yG;8ZMe>6|KbAW-+thf^eR9i0d}3=4 z9p3#ZBT@$Ve`L!p3_X?+Ni{dRU*AneU%z%UHBsRvEYH{LnmqHMdO_dpw^Q;a;2N}^ zNwF~Nh6nOd-TKuZH9>tBhMjAm8STy`iH?j&qZM#Xg$zU>F~%Wsa*DN&l{UrBaMC5i z90G4kv$vj~w#&{w@ksX`yRwb)7qV~#PRXfyS+C&N@?*NPqYt2A_IqQzZM*mmu^h57 zyWy6$IVbZ3$#nn3s`?3T<5WityKHg9yMJC?dAhm0!{}@M-a#{?85eMD3d(9t9xx8y z^iQmenAtsZUJkXz2d>tWj{KyCqf8UX;oTOHmR6=_tZsKaeuB+9U$d7~=^uze_I)1p z-T7)>v+3R6#Z@Hm8m0Yn$Z`dudP=VBfyO=-lXw0CAS$nx$iyVMA`x!T&36;tDKhLj?Jk~Gu98LENQk-OQlwh! zm#v7pFWJw%@@_p0Q8}wNncl}IqpyV2cTvgKWl^%Jqubo2^F(~oyo&W{vF5uVxhEvJwQmg}IEqS!5D@$mVZ&9G0g-3%SWXCrRK6gvjL1cR zkZRT2nWCJ}8tKO^+bUFXG17Zeh3s#Sf8Ov5epb#OY45t3a$niL<&-#_lc4#`DXn0R z91+!-LiDE6R+-j#BpT&d7<*(*sI^fCz4iso8Yz#nB{2rOW~dU6chFs?a7=T!5V~r6 zX$LrCv1H6PH=s*4JeP9vdVUG4W7dQ6dn}{vo6n$SS8&VL#;JR`tgu}EU7D#DD}u4< z#;9~}GgcIZK{rzgvRR@66pcgFA9k2fqt_DS|NJEkiV3!oRxRazCEPUAVAxX$(Pi;Z zhSvkJ{oD>|x4`fOl@e+__U?}jsi6GtHmnZ$mK~4DUr+{rU zI_FdkbS(P{NCx68f@0h<-l z_u2Z*`_jgSN!n$|N1IV>o#GiMCtU9bQq_Q-3Ds|W3=t5gj}YAm7absvI?unj5Tx<> zf%eij+v%Wl$qm(>`WNxTvH|^7Fq16L44Q!;AV3P3L}5&EK|dV` ze9cQ-MBN^$qu*4T7JQ2j6@LPQ3*zx$)#4IhL4cY_g#9ESeXr#QyS#?`0;sbQLO#ce zXhDk^*8X61{UX)sGbzrTzXc3#XF$3?8)uQ3rXnQ>e!io`Mk0obpkWcLUaMyTY8%KL zNS+5XC4GC16bor!HO3)q*LZcmbTwU6W)Ro@K*EEHIu@TgwRxL|zCYez{-lUTUKhFs zE07yG8XLGQbZmH2Bx+=U3_ENNd1z~D4o-2{iFX;fmZ{x1Ryi=4t%K%eJbg~*sBWjd zpwp2KAzx{|N6Jh6l#cL2t-cy!3*R;&>HVbXS8bciW&Qj{`&q8|VP?D2K{uM5ehb^V z!8c})G^LMADZlV$$Q@ikrVag{8sEK~xy;UtBrM6L)Z%j9Ny)E#@ts8=)nZqvf)KtM zWBiLucLY+qExz&$)yD5xGnXh4C>*wQR+Pa2cqnHw`lezh^rl$qaS6J^d~_|4f{a@N z$#guVZe6|1)T4fWx19!ODEnKHsRxA&OZVl)1ai6XVei-VtnVg78JN7gB!=fekfm0B zh4>Ymn4HpEnbb5{$^Q*LZ89D!DzfAo={9gW_W7_cO%T$rCShdA6t#)7siBNMdL7kl zmXb+_ni>1mFy<3rgbv(I}hI-=eOliJgytR_ze{!x(7k4B?ZWk$;HB^m>zv~=Ig|nf|dTP{o??#?S_}I#8+HtHTTE4pPKkDCi zudX;D;gJY|UOf7NJ-zEDRCewGpd7i*%>tC$fY*3UJ)e*kR_`7K6CXrI2-0a6Ktz;|9ka@?@NTwO$G;d zvnbS44$p~Z>yRn+V>z3Cxllf7R_E%{4z5WSA+}J(Y~(08E5e;%{RP?M#9S;0AQr@r z?9aD89(JX{d?3^JpMNeNd`zDFnYoASJ?w2oQUus-+Dbl@jIj`$m|)yKG*w z)B_-IwcW2?};j}FUlYAK2`NS zdK|xPBDfyK!KM~_;873XYYP|39L0MU(I->pVy~Wy;cX=05?CVOYa;^ENi4+d zk1HEOb&&xv6f^&ra)oxRby_~7OO5cKQ3-%iac8>jvd0`wP1Jr*Rc%_c4&8I2Y$CuOVELkBQG^cq_;arK`yHDgeg7U6>XG3 zzUK_O^)i=X&1jpSf0HRa$q{E$AhI{M)@;tB4#nt@WBF5SnOXO{S6uTu-dgeh>RU^T z*O<-3crDjCaITdotTxHbLA<;aNmBs(oYM41s;R*lQy{2Z{&w`&?2{sd!9Hl z>t<#B(u-Ez>;%XvPWHlwxz#ng2$y1^OF-{v0Dy+VN=fU@HJ0SFrM40#tX8eCt#!Wk zmF>{q+G->2TbYrIl)_>sv^4aYQhoCs8E62LDK1A!g#`y@YVisze0DnVFqIOp9@l?? zG1Bb3z)f6MQKB)i+GwB;c{>X0z}rJ{*v@Psp-As<3-8jU7;#pR>i(@t;q?Q<@IWA( zqN}nKdsJ5^u25|WSx&a2lG>t#*G_LAeu-UVK`YiNWm#RH9bx&Hh({0^wPi(^+4tNj<&5#Hyc z45e{(W!(e2l}}MD(X&%QytwRK_Dm3O{CWxLv11)kjMCRMg7u9g5vp|#Lgm-SYup96 zmAq_OO+wDk%4!TIVs$SJL+!!oOK&32O2V#w;;&yuN4wm}hSiKTQyt|53PX37m|ARe z-gyi!xnQ$WPn=?nD^@@hF#ty?tH~}e7CtDXw?u39llzE*rBfoxRK|5o7jeaa<5hn% z9rZ5iRwhvWUMVqAGpNOx)O`~bLZTDur@HG$lC06wD}B7`(STLB<#s+EJDRBql_sdr z(7$5mE4AQ8&Vjs45!AVzSEf&5>9vCSWVEOH-4yB)K_Xp&E6>uAGr!S=aQo`%pVni_ zlwI;AgbPLHq5q0_#?FA$zC@)`8oZndvGBb=T6uG7qHr2`$oZBCo{&<-U4S6!jjtdJ z0;WQimd@qc`c_~;&C!LS0m!TEC44!5JF>IfkdSfy(|U{;mR0M z=__3C3mh{YgzX5Qr*VaaS3g|}^Ye>2O?$M%@5_iCy$EF#08~2FhM}ad+W){@><30DsZzE#SWY*Y4sy#ApogRyS!Q$!5oIVm??5WGa0NgFZ0XxAkvKLY`< zaf^r1wkUmWz~Y=mUyerfT*D0{&=Rh!_SNo4n*Hk$j=_%?ZV%dg&ox4%OG&9-y}IIW z^D!CJ zM2BFlW(Bt3EQkv;#&})zgJncVz0nw+R;RL`F!6={izA{& z&7Tb|T80|6a(kkQ4X07kkaxVZHgl#)mCT zf;p_TRr^%qFHe`llN+kvu#iH0qb7K7xM#gHReOXpArO(&QK}6!Ha1h07hb=s9J0M^ zr3Ai~IA(tXJK(24ICR`XB?2F=DWPbVAO!HA;-nj)zi;!U93@SnMnHoM&(-e2n|9*# z^;Z=z!=20KQdz;n4Pkd!$B#Rx2$P{De$X#nE^0AO>7NO=;75olGY=s}8u*Cb&d z9R})7d$f-sbWQ#|rNBz}n9rY7e3!swmumOvsg}1~x2p!@#7d(VMlB@iSM>_-D;*|9+`I3SYp(~<^f(p|8|w(O0M9`PU-J|T=(owl3yC>McFDHfiG2HLA6UzB>m7$e`<&I zJQI0!8;P9Pj>t3+J+-Ev;U1o?`yJ^T$uRvwMbvZBU~_R|975(g%Ih#mv)^NdyOOhSV$lbd+*RBNFGc9PzBa4H z?{OfUugJ5R){L3v<_kxL`xKNWP@Y!Al@oXOi9EP`?jfx7A91rxV)%89cDHXvk3Q6q z_4_6Vw?F;412b0Jtu@F(EXb>E5wq0;hvu(Zm@<3KE z$^Yc?{~@hUde~KX3|4O$-bDzx<;oMs@6ZYc7aKycK>r%&CBuRhHV@TibY{b4;LBU< zAFo<2MM+=JEbv__yU7XqX+V(MV6ra-Jyku51WjepR97;#Z}E2aD~!q; z;=>aV6TovLNALtQ+RuZpSkVA`T>La$;`~}A(G!ft#{`Avty;LX_NdG6B#h4da(tlx zBkd?6_Se5lWZP1|nWx}mlP8Sl+KEGz#5R}l&wIAm#uvVx-K>T|$i{&(Nw$?8@EDmX@F&scOK;Xw&MysL?G@yT9SktZ)jgnQ09T!hF`93UHY%Ubu7+ z>Tw}h1g!t#l7qWl%kZgf=~5rRp>o=>72-=^+n7L~M*Fs_8h_?JwpukqGg6KAo zn2UcSd`emY!;gHF)enI4++vLnO}5T(z@07C`8}gEgLoM`^xZAnrqf+u`WDK#rM%?# zEn>w4k=#Kf^=xxM|6c5uaCz?fn|y-O-o`{$Pn%h!q$!J&wGte$1dXrGDP4E9S_Ce? zad)=~Lbh5_-3(Zryy*!d%bWscy{oTySTyL3eItA{|3B20+qI1GzA2p&iWMTRmTM;h z1P&_(7X9)yEt#mUJF+}o(H?e7p7%jjKR(dArA2;RJ2g_XBWK3cVF<6HkM<-`D;UZs zw#|S049cW$M5t;h*EvIn3rG@!XLnOh$ybO|$XOJfo*ovJH@=P0SshJSXyWt#OCn%E z7L+eq)drv*DbU3>cxPgC9iH~2zkgjZ=DcS`JuYH7LWMB~kCCvolGrm>;KOW^=h`XC0zpgA$JV^ka$joA-w!{8-eD1lj^+*x}9#3zGG zxn+ycR_y;=xc;MHG5yMEu*$Y>mMBygCU;GY2mdJ9R z?U#7NdHOir{0vSr>63M$H^-f=<$b!}ko!;0i{m5C1n7j&t%CKoJ$(aw*b$-_CHu>+ zP=2otfrS)PU#)yV=c4>S7j&lq=xe1eCGOF>3uj>8S>m_{^OJTg&h{EYUyM|Bqa^y4 zY?K*=F)!{Xm`YI{bLwK#GE{AuY}na@V&Oc&aolzM2vuFJvv#a(r*`<%?~ltkCqB)* zP7jEcn^1&uAf#u`hIe!gi11ZYA|hpHJ+UA_#KF-CW$o20gO@Oj03oYG{~|*tF0g8w zfnCj4(x)bLf^sEY$aefugTJjUUCRv~D(mN1VLsG5UN2fX4oWB0Tlk+E=ujHyT7beE zTK0X^KVf?P#y9p#2_21863@rjlb_)5$128~Mf0=1<47(7ZSU2npM>}kA32jm^p z7xNKm9_6)t3zgn|4PCQtS_XH7_kBYPfA0PyK*W1PiJmVp@+p51($k0Iw^ZsZ$K*%Z zo96*u?{OqspCDXbUZ#YmY!ipt}--*H<%A0J@Ldnx`=dU(_Jm{Z}|AF0|57P zJ)SnL=Ml{X{2U0RfC7c`J6}d_XEPpE)0OL*cszM?M))atb%1y+hwBaBtFr&C&)4su zvVdNhNXE4u0Xag*pczu1%;Uzy(=J8-BR}=!J`b!F(Sg484b6SJ+G=F;>}m&;_osX7 z7gILZzP03YtCz*5FwaprIGnEVi#)v8SkBC<`7wS>A>44&LW{1_WT$yAP^T}jy2Sj) z1NLD2cw%_#gT2pt1!D>Db6noMf7OO#;(8GGnf+PAqa4xfT@GT-W@1kZ`PIUG_+`0V zP`&G^QFFC;o#)AJ9$cksT@GCA>}Q!_w&WQRaM4T6QnuB`_E-xoa7eu|8+(!YtR>MM zphx^IfdoMtl%bJ3*3eIO5;S<1-Syk^BsVs^2^|YEC(ZnUXG2T~#PBwtL|N*XK|_|i zv?C6LDI#*RDC8_OuE9>WI)wvhtllW*+e*YmD011(Cap zir%=5p&*}Ki`U`wJ*;#rx=hJiwS@lbkWVv`_Bs5Gr2OLLM80R&9Q462dt$;ELk%(# zeT745fXC>f8mj^Cq}Cb-)s>>c%=W#MCdzpi*$tCp08<^FRn<NDpbtWLsi3u_M&-Y5&U`Y0LGE4mQp6Fu0^~FghbyPuar>TfzHYqCbVl3y7UFJ}J zEx$iJQzHgHaC+GH3E#}?m%1dYuz&EP6W)YPpX8ZXw^(uwNn-UW<-O+*I}Uhwc|dJR z8*+B}S9BzlJ+3df)?`WT;?5mr3|YD@jpdm65GU}=zNPTPs{--kTEaNo7RYb%O`@me$hQjf~W)rRkf`TUeT5i%pyV@dLyw1D}{ zZlaSGXdx!BKjTv@f)HYe6c4cN^C+$^MK(XM;+T}FT+$LI`APtgoU$Q*N<;FJfD{nQ zC4(&rVEfy4*`Y!P87?2&)_fMUPSv5Eh`EH7BzP)|bJS(@_D3+!@~mhAmmDCny*jGv z_Ww>89rHia$3#44YALDj{ydi16-6D!QRkEq6d|NIZVq~RgyTH{4|oeh><m5a!CEvpogaitmIlkJL-D&dDLeFlyhJosV>!ngjo@cl9M zc{v`u1Xkg!fPkqrBH(({fO$z{zdsHt_}_zaAau$B?ku7BSpr~E9{}I9N2Y9Q z?k7-_p0&bVW*6f@^zQ`Uw;SL@`8isM{Dgs8(9Iih$<^#B^-^1Ggx2`GtMwKnx8Fmg zJ`3OZY=M*)Pnzx704zhPxck0V;-tTj+2wj)AIChKvkRDb^3>t1gtY-}g6pWi&@r_VIF+wA4Iq%ILWLt6?N?T^?@vTgGE zAuyZXCh_i7MXq=Q6)ET19O00&I+v5C2{S?|16Knp!K511X4ne`wu^9|H2d#gdUXIH z5ZaJ`J*9H(d7JlxQ}-%Wg|GPx#i>t?oT^-yYi7W(DI(|9ADLmvG5cdNK)DRpcSavh zR4pOdrsQ%yt$2!bGj~B-vzTh|%zEYdvkw*Qe2A-J#`+2eaxb3OWetBmx%OOMid;IU zBo1X15ed=Wi;p9*c*WS=hNxD#t_%Nqsd5Dlt$?AhV{nf6Ah;>iF;lJbP@Va;%1$$O z<3HS5v;#K5*^28y1Y=KT01FPAg&lw3!b_NW;8wCoGk3fbwvDDh7z)7sBEE&9ZD)!> zEkOg|9Xsw^tgs&%ZQZP+1}2siT?Rw8jb-T11CMQ8EFqCwySbUdAD|gl$99o0CP*@G zE!PWe(UHXlMXP5K5_Z;4Fo=gb>(|vP&psAkQ8(e6Snp9)Q(y@1xh_|Av3S&9($0Lb za7vlig_2V!9t#I*)H)|x12zZPRCyXa%gb3kdJot zpE{1x<9!=qOl-?fey*v#iC&^k)0KCRwj0xxP}lpPU3qPgOx| zp$M0x=c2j$o|w{S!F2Dj-UXYe8df~jbwSTz|0j4vM+kZElyoM)dc$%nBgW6e9_V6$ zuzW(vwC#8PJ%^Q*q@A=8U?!zRtNMJLN->Ojw}J(ff>^RQXK!S>^t)1RalWLz?*E;` z7M_tWA`&fdYRVdJ%wIJj{YfcL9QCknej=cubcU!8*KjiMTiVMZy6(Qf_M2cFA0nilsMx#C4oLZ-P=#K&=B^NzS49XE4 zeG}=J#UB)kss1Zq5P$q)6=o|EV8btI>iagp;9%gTHoORY7);4@D4iX-xcK zWS#MvB88ULqTg7|N;W;|LNg&}iR%}R;ly&wb`ZIP|pMYfuB{D-#ZZr)Of z3%|F1pcJCcWpw5xO}IYPH4~DbRAe4$Gy2Ta3i~w|?=~(^c})lh~_L zt(D7cF#T>lTKbUDSw}+g+)HWVS5isrL#z1!jJ)Gm*xKha-v-KYT6B0#7xjOA$Lm^% zsj0QFhX&F`TDYi@T-vP>W;XU9Q8rWJ=Y=c$mPQI(RL)%eAE}bNa{;VeiZ*!tLdK*D zo%S{XVNVr3{3vgEzenJt=FuPS{PK#i1;Y+!CHuqWu8t7RJa(`JRYY}^gI_Vep&;`^ z&hORl+k4S+c?$pPX;ei2_)J;os@JnRW`uusMGOPUrYjKMfXe7JOVuOUrJrHRNqlnu z+I+6SLUhhVPhS9^WEAM)`8igTG?6f@UX4m`L1MX)s%m5@J1xJP{mmlc9QefwZTCbDs`YyMKKI#B zv6#=Q{}LPkx41>)&-~YEAL*gH8E{Wt&SoyAKzGhOs<>CfF@k_X<0f9+4oO18j~UHG z*1}6J4LalqaCeTu6u)_$Y!Q2=7Srn|N$*_tsiY&<6XPZ*-Vb!{V|=!qtt6GY!Z63) zV3KVic(DXzyohQLVF%h)?*oChRlW|PuSMe%{nLkuk20qN zS&~aPuJrd_)4AH5hP5S@nexN;xkGWp?u3)6(V-GB=~-7R=hse;ArJ#IT%@jS~o={HiWF+p3bqsO2cWe?M^34cH;KY5C z1#u>VcV6>vkUrHYONOV7x2P)L*sVPGT=aL>5saKo=+~PvG^78OaoS&5Ic7*uWYCrS z=?DN_s;7xfZEy)G2RNH3;X=KQ_AZ}3e6(#vHc9P_WqfvmVQ0j10o-Ue5~#vwImp0x z__Y`F=C$s(oTDWmU+Ewr$6jk90N=VR`no91{@<0S(6&NJ=o@8F7RKxs1LpXM&gKvgz^p@SHOQ~Pk|A#1*p77U^6HPwR z8B_>zzNhCW^_p}UEt z*a8SBA|UtVpX9NoJ!BDZog~IWw29~g+zGyFt1A|)hEUL@LPUQu8^c}?&G|%Qa?ytp zbr*1V>FT=Lgl*oqJTuTHau7T{{uMCaWUgqznl;lG%#WmR?aB(g_^rtvl?cAyb{-AX z$^UX0T?+OH%;q&cx(z4_`x^4?tO(PNgySIk#oJb{@P3VkS$4LKrLFy7ftcVL3(kz` z+QHR5th!ly#&2Ww=1NNQ!5d#y+4i2p89xbM7d;n-AwLVjLqf z7cWtPez0s*469tlZH^e}P*nE&t<0cc7fAGdREIYpe1;3Z0R2GquXN3GI01S03-qp)Y3SMDTt? z#vQM?n)29bF+adRMq!m~Dx~hWkKEaH8bq=^oq^ETKOegZ<#MMd%;i@$mDbNE z!OD20V%Db&zZY);x5MGO@&1adcLndHD+WnH zPwZK%+oHBIq{{rN3g!b84iZ`+k4g-ZWyZ^=R75C{>t(iq@%p_l! zWr!%+jt{;)b6pgmLMMmfX-+OObVVsn*^pC^xZPR>q2$SUyeTMhJTqBt;OJ)&E{otX zbNyAVS>3Kq%?59a*Z0DKB+9wdWV5I5YzQC@Zk|s;75i2u580F zF$$9bHHMlG3h^*69)F51fb*X`4u?A=5PEXIMbx;Fu=PBQvMJn`fl?tAH%e!C@Yt>e z=$D>gOogf!!oHs*K?Wo{@9>}}dV;L04rWsE(}MDp-y=P+zwlp8&(Q9mlx_Ap=MHxC zw9^m6g!8M=1AB(J=N`>}M*wd=w2~*%zN(z<_NEf}7L#DhW{oI4qN?7X_j=3dx9%>? z+I(+9E0JwBXIGzx_J;t}B>ubF)*G)9W8IOYBJD%g>2b1%$Nh@;EkrI`I|57fhZeV2 z`&13?-6*$bHxcTe90=zfr_bsgI?a&;1l;BCpNUfaW4XGD8Dh`qGbDeCkB2XUC$1oqDFsGVKzRi_pGPQ zVrO`2XkbsJA@kE*xWAk~aqCJsq@ulWET$Q`4Pwff$n@K^@akHW_pFk&7$VL9i{1Oi z@6uR@c-;SXoyMY?$y5t3>!S=ta|Iuy3yi*())cTA;Wx^ zhW)_Lr*>ZpQBK*v5}mCeNEm`R{#Wx!wFtr;i|;#CVs6PD8 zyGwtX-i_uBj1&q1q*x=>Ov#s~JOPvsN*XrqDr{}vDq}|$SUj#n4Jp$XG^6DG;fEOx zO)cvxYOzU5zjEL_`?AZs@Mv(1aGzEx5Mn1-U9Q8Tf5@(SdTQU8!9>KBh;%OgBN^%w z@1@dojoKg2U2kL^&uO5xOoEy(?-Mr4Y}z|MPq}!sbEP*(G;3OpE3Wph6sK z37-$4kz>*eO1wJMlDU7Ub=UIYIt-GL4#xtId7%E$9yhlY%_p^(F>lQV#Cr1aByA^_ z!FXJU7pQdqrc70VQ0N-Hpk&3Dp;+OD{O>p$%{%l5IQU7szm2loI~6~cei7RYsn@SK zXPMRW>;w?NUWmZASVZaJ?iWV%mwQ_czP3G6)mdiu@Nc_#L`3k%LZ!iR=3*EUu(j^t zU47xxd1r!+c1~-K0R>2A9~f)ltBu}WQMQYr<>lm!@aBa1hI#Rvx=D<#maT4PtbZpjD`hUfjXoxzllNSBU^mKcX+xjYb z8bR>@4XAFQ)M{i=*_mB49Gv{2l$fwe5%p4$nW#nnoudy=twwA-XQu3AW>fGH(cfTm z2~=C@YzWO|GOh5uEJW^0rrBjG`bR5h->3H7mjkvuXgBh>bbNjgV5PDn{4%0SWX*MF zZTcc75Fpq#-AX`mxJKvwh1GJZZf$MEP*x`{obhwbSc75#axo+$q*G;V(w5w{a+ORb zIpt)m>=%BM87^9$_48FVUNp!5K*X1DpGtac1GC=x=uCXtBN=o+J-!~V6bgIr< zqs7^L#6h?ic)LP>S7KJ}N^H!LG5DcWmtp!Z_MAVNH9+i^w=wWqUPQ^#mSZx4+VeBaAC-pggL%!qfP4_{`{-y2IA(2uvS zo=$2MXmn*=ZmazU{dMZTz-x?c$cUUm9jS$cl>F1WK&|e28rtZ|7cDDU;$M?2OXfXE zcmpvNjhEjxe1wdxsR|d)^(A|XOVVScl^zp4XgEZgKYuaN}UFpT=u8AN$|DsUgdB8#dz;=7f@#DzU z?lcs+jaE2&t5L;780lUTQ&d~BW1M=GzvPVSU*o3eK@{kD{M*A#_l(EXF9MMKEh>YUat(q;s zBps*Sa+F{;p-nx44?lcJEL&j-yIdT8+hDN0>xM{xXoc$JVM((ZF5t6kqDjT7tDGZv zu}g4k23!+}NqSBFoNi0uRMVpJjAVDN+=Q&(M#32r*WJ_(NtI#Vq`F9jt53{as5p0_ z?sR7<#>FFi7)6aSy)RN^5N+T=UTp^xw)U`m&wkHfDN#G&)L$L(4AuXA@&m;|U6p!2Hw~^y?bW}a6Arl& zq!8a|{4im+&s`KDWP+|5b~7HZKCLXWA}QbD=_ilW?ne6RFy6i?)M1Z3xFa~u>`H%# z+Y)$#FQ#_g_aZ8SU@lKY)NL_Iy4hbW2+`^eZ})_4EkQgzahI_1ukNG0o5AYmM4wKV zg_)7>^gRgE z4`g|){4oY4@J0p7Gwf#gKrpHvvtB;02SZf?RYQUfYZSn&w=d=UokgFD4T?N=H4ax3XUtgH$w0U0!PbU zaU#@&J6+|o@v#uwq~o%cmG?}{?lvKxij#v?{`d$ourF@UP%mXT_UNGsgm)Ka+N>OM z7rmRZJ_{P2!IoYRoh8V`%hv1DzS(ADM0q4WWUOn>Uzr&s%*xn#Y{c~^-|D?ByY zkuN=!{1)Rc*=wdYiv_~r(r$`}-xfe3%}`jGMV2t3#K%38Sd{C>yd;c=p3L{ek?fb2 zo3olZm+W;(pK2c(xZpjPa+ z`@;o-bcg<|S6irDHig4-zkVlL+L(UQrN`5Z5OTzz=3znT`nCUTqxZebXxDojE&1MZ zQ1^$+X0>Rd7J-hv4!$VlkGGDI7GBjH+$Uf-S}y98Wckk zFGQ2Cs^c!GG0Eu0LuZ$P9s1gn1W-jroYhecj_jMsbe^eSz#*tVRG`BOqO!s1MbQh{ zn*!d|u>k!=Jzj;67ERP=7d9Z^Mmg=>_k?+D=>kIoi0Tf#FWc%5Wc>_5eN>y0m{2Ug z{H_wKL_1!Nk*asv2wCq_m+12a@qN9QTM4?Iy(WtX)VlniaA!g0+}K<9-8^dzo^!x- zKO{g3I?XG6Vu*axFJFl;EfxP;{hS~}b*&vM$c6)|gYh`y4qi`~q!#?9beZk1@?7gk~o~{2GX`={eh;hHz0GJvV?&hp3~WXMEqI{abZ@B7z6@ zLr|^ibUu(0QR6-HjK08;Y?x@hh5iScRs^Akds6NQb1UJcFd=z&eh`^PS&F#-&Uz}u z)Y7U0X=vpc+mx|^{72wzQ%K+6mvv23r-*0{a3c2(b{pdmAB}K+fa*49MM0su{Kgg_ zJRK^i?omJZpmQ_r_Rq4W*;dW&dG*xZKx2tFOgS z?3CN;w9?7)mFKiHf3gp_FPla6d@)xSuEHns+WEUcOC9fXrSa!X>*Bje1TP1t-~J5< z!1mV3nVjG!J#)YFD+Mz_-1US#AziA}+NE7;>$9gF-iO`G4+Nq1v$+h5^Z&;6M?Qko zow2N_f0(PdjHEDkxK({(!QPP~ne1e8xDhx-q&>@?#)2RMuN{{b@^mH+h|TMaVc5)@ zVXXL==U*~M-$+WqeslE1^bA~sdcHAb?!H3_wC#$_?iMI*wwP6t7CG#$T zd_*1m8^Fo)%aFXAjVl`rPU;6uP8Gs_IN)A8d$N%{7+frBdC=YR-dYcDtJ@xszg{Wq zc&ODExL`)A}Fysb$}*#zy%3DepKC*m6E{fZTO8waY0)?(Dk% zuj<}1tjca{8O4gAVMJoBD=0s4KX@yF! z_RHImoVjn`OpW8W6ijp`7lV#1Jp+z8h@BDV7_8ePzYXr+qRriC-gJS?d3g@E_GK?p zer+sKIWB_H{l?L(WMy$8{r;Sfa-tq`vg-9j$N$0}ObZa6WH_%?^W8f|ey`#;ujpKX zyzBY-vD*3rZz5F)p^^8()}YJ9F{#F2zXO1Xz(LoA4HjK?Ao22Y(y;tK!?C3`EX@<_ zLa9cAaoSDQ*SRXqx&J1wto>@(7Y3`8in?_3*Ej+BUNW}Orh>fR z96XTf)F#{+!-E=>u#d7`^*W-rLIrOdROe5b1ta`QB7 z#(+Bbzslp5@E|860Ijh3J?x{78@6aop8*@qtHWIYBkT$o;~m?ZkMekGs6ZX-)8pD7 zbbU=VFRXh#v~m zh!g_>)$=fp-EFgu4E>;|kr(ZtT3BFKE;2 zDRKlbABfLRYZ~>ig#QDG^v}B<4%j;kM=ILz!k8H4$T=-I#P;5&RTOI3;DB*ZZMf7I z^A0i&SQjXkBmf`{?h4hCtIWskBXP5*!L9f#N&fddTA%mlOUkwN5;D8J^FT}2bbxc` z|Hl1KZeG)#^nU9TI?C|}6}?bi5TBv7Ubw?OU@@N60#FY)2gGHn)img5*t8SF2{!l~A^aNEBs>vdx zjC1!0C(b-DQpoOsSXTz#g83ig+Hs~a!$Ltl!n|9e%fspxh37#2(}c2jn)NUIjWNz& z)HZwSFK7tGmHh<)=y`NoRwK`77>KpoOZ8X%Nji_~1cSYwleavLe0<#lBNJ(Dcdwu-2)!^P*ZUZybA)?r zO87U%_0$rwBfEe1dvX0i1b&$fxw!Su2CHwRqg#U$?0-vf zE0s)S%G3_?&e;7H%d?X*LN=BC)SW2ECJ0xKAY1F)fM^!A4r!{J1%>?ZZddejsVCWt z9bpRapb-l+94W_Z@jTAv;Kr1QWKsd@19K+uH<2n!oGQ4hd!ZvE74lv>A#p2Wl--m| zli4{xHXVQM47ZB4T8g!j8`w~{k8Ktc{peZO{v#0)(q*TW*=h*S6L{Stb97gZ|LcQV zmQta(q!fyeJP(oU)90#pF0OWr5B{N+={wf1qF%qYEnmzz0)ONYB8#X>s>$p;0JBG4 zY4&xMSt>s}TECB;c6S!5LM9p0ZZXVoES$Ugo*VYon#=hO($+}qk>izjKeH8MMQo)t zN6r-hnz4}h^!mLl>x=it*Zk~0oN_AG4rC!j2Khh+2>%_NFD3QqWm2?2sCA93660%2 zel_`o=AD))l@D4Ru&=G*PZN~vTHI{8qR46K&x4|*=+fq_;ggHcqZNk}>#upL_kVRx z^x{O|l52ce`J++1$WQG+^`MMKfkh8M+}6NR?KJgUw*L=iwZFh{=vt&AfsCY!7uv$y z$FL76O3aROIZjsXguA7&8!)FP@FajjIb9&x8%+!Pt8#lT8U7~@Q)uW!Rg2%Be=bK} z^nNy=?&iQr4Ab5EphyXjZmh-q@g$G#<*6`+ucV)1j&7dxJRBX*%{PT$jfrKx;BNa4 zt4V^Pd4TRDm}o#sq!Rva=T`W_yLXBSZKusnCd8%^?KQd#6xcSZkJDqRY;d;w^VJlg zo_|~;V$KF9OOg%5BCJyMKS6GOb${?SWPWq+fA1ma2OA{1RZ2>H|C{T!^KG5f_Cb}v zDB|slNE7-i_wBv;oi{0gufhd72EV;#fe5XNNHe_cnfZuTB&ZBm><0Gix#HaycmVmy zLeTywWW}_y!wL6nu3TSyN=O+cux7C5!-kTk6~3bV_2F1}5QHf<<=Y z&7BCIwS}UZGQ=-^kk0n|eeNV`4kvkPGPbiP-Hg($MY0IJ2TqO4ID{}n8x<#z0&#;O zM`ky(SzND%YuK;9WIKJPjhCAv{a|QPD7q}sZi}9<$3K^DivHs|=M5l$k>bS&N&tv0 z>@pnL7g>uTAn$ChIeoruByUvL~V|@P;P;(q2d$ zeL^cJ4Bh*LEk5vVcd#Qp{|FvrXu6=S8k4R=)$E1Q=#MufeCeUi+AD{reLdx)3wFxj zzZb&wWBOpSjA`P4&qg2t%)jx~-@#=_v3S>lv7dH^LEf2pcyVTJo&LDdqOL@-}j0HhUHz`wD}L9;;jgGUVXV-VoC4Eo;y*&qP7{Xm6y zh<>x5jj8(j2VED`DZH)oCxv%y_E)l+gB*Cz`t)5RTNFl1UOd@WQ~6*14};muJau{ zkv0HHQ3l8nqq-QpTF|0Z-XjK2#+5wvcAk)6R9Lq;%g&ekR2l@nbdHzn=iL~^h{ z9wALN#Bft0`HMjE$b^W+h$8?N(qFm?kcDq|@U)|^?9A!0Q3hbn5X5XrnO%T*;Z~&M zr%1B+vh!h*@jbdU04+zpzy0%y|h$$06 z6cx(srxOYNH?m}k(z^a(%cyRi>~y8XFaTjlMQZ2g>j_<$NRtsJ+|YI4=2fj`Wg-nP zPK6nX=O5&j>N7-YP3DjvPE-X*0p~gTZo~@<CAIvAhV&TB8A56t$!baT%3OQ8W zm6epZQT_h1VGw||C3TbwFhblVutwGYrazgI=PW;;56V=g!`m(>|Ewa!f`n(iDt?qx z>q6|eK%~#oB>Feg>P*;!M+PBl9A`?23NHrG#~rP>d|mg+*V2DXf{L0D&h(iAnfA-k zgwr_i7F%*TV-2{Jl)me5&;26UnNwusms5mpwj)6s@;?Y(R6ttpgMFt-eQ|$Q*zY5` zIaC+11P5J{2hJAxXqs57F~@3b;5S`_7RZR_R#U_haKZG*4}<=*P$Y60JKTT%El?s- zc&%KzvJjt8=u|sD=D(S3`^RHIsvZ%-g>?u(J^tNgXNCMH8rmaq&%HnGg(W4^wp=7r zQWU^5V%i?yd$U*f4xT6CWw0Y`FZ>qz%SMl!%1KML5%w*u-}*u(F3(+d?0|KI*|4EVf2XC-~?@a0k@NU;D zLu9@}fZZVD!5dxM0owg#xvM(XcNv}Ha44_sa3nZs3g{B=M89buwD3jQO@EE-=0b1s zY-#9{J73{9{R4yA6od!>e*19%(x*r(k^gs|w2Lks994KY#&Fz^nYEA3?9t_201V1U zd0uX>9^}ag%$zIxyeh0xGPtdO@3^fcmz@n8t~dSuH3V`fCBl9DEm_f{0rAJtG56$+xH7+JEOqeO>4fTsICpW!~PaRp}_|*eI&rZt~7tTVD)Y^ zY!ioLn$3_Uv-+($BoC2W)nlFi>FWoQEGh-+QGXbkTt5uPKU`!0T#0$$4=0far)#?g zb|vr+cHKCq-C9?92fw8yC+vwm+5_Ntg1IG&`NOXn5{I=?!?{P#4G%#DoBBflhm-3S z!fb_M467o+M%hwbJU5j332QWT0HMF{ZbUKoCHS-b5{AqNe(Sc*a`>zgb{Ey(TDUL4Q^KRe$FXMtS6~ zh0MuzW)Dl4Po)Vf!Hk$IHp_F%dN^OSwy|=b2?4f;b}IU4)`SE{{qJLr59rkNaum!R z4Ufr}CJx&U0x^xL8}>~09QKvZpTOs9w771($f-%rlokv|%6B6t4;GgF#Lc8;vRcQi zB;UTN*-gd|YlOijM6zo%LgAp8?^Y~ct@38kSq?-t`!)(M1?hI)I<<3>CO1SHIO?(` z-BKeur&zi2mvv!Y!A^i`S5dnN2p;y_VPSt73vWrc{%ZV=hl`xyoJ2yqL6l`@RvxlXxB;UGCJ_^aGzQt zu_9afUr7%cIu2G+^4CjyI;s;NBll&|5PHL>*xD!fK|!NX!kQNNA$WSn-fql8u?d7b zc_$&fm1`$I+Ph72@+QAbxTx89(g?We(jMngRb}5gppX&0mftV}t1^B*fTI4${1uVP zS$e9)pTYZv<|n)ap-y64YWxOYj91e-omPBT@C+J4Oo|Q-3bd2-+4o;cNxE}>#U5_Qa3qzev8BC6Jbb=+bAgx6WC<8+%JI;Vj4H zQ?tdgQnV93Qv#~w#U5S!RKjWA@6!m@d2jk#0)XRzhScZV)x$AX^5Xf?w{C@BlD5BU zu*sI{%D~Hi03-ZW*-L!jBL4Heoy*M?Mv!en@idd1v_m$9I1OmUsj{u8S@WK{pxUy| zq-ZH9Ed>5FeLq0p%gf}H`SlyJYH>9&nA)H43Xk3f)mp{;bcpx*?3v&HpSE)J-k%i4 z3v1P-JQyKvkNqfG6PR4HzSj~Fa*y{GSJm347y)&?GTRwmpk#iX>Z8+u2^3a~z^Je) z_|^Y8<$@Qn(?vhghM))8Uk$d*^hZ{{~a$ zL8HKqItq->I!VD9Ak1{!dMkGL0N(>@dCE-L$0Bt5gs&_1jKG?fJ0rXw%ND+4FIJ)r z!FvRksUH$*+r4vTF)fwsp2XM6mE~PKJPD954nZmr)4(4DT7&^xmZa7_aG4J9AAGsF za@mB}4#Y<@!%ArQD0jXZuELbJGCf=DJi*}9M-whl%0P=t6N~6wo!J9A2uaW!KWFth z0a!I!?h9Js|4QNucAk25(ZAO#%h>+qf}(TLswUM^NLB86b2wIs{dcqv1N^80+uLOU zXI3R?3R&WgnuHW*dR=Fy`z;-$3a?iDi@*~f93M@9H;zLSWJxAUj?1shy$}Yb&xegIOa^)!cb=xK_K_M_7dJMZF zxr+L%(Ni3ruIeS=ddXFvcV@9(^tsV0VX!MA0~Q&2un5AVMT9oNLCSzd4CtA? zU>@#GfCft)5%H&vZiyVj1m5u4HmJFAn!$dnUZB3YggKGK^D@JNlN|S-?zET}TmX*I(A~A~T>4`cL5qa4asQ zIN=2Vh$qBIoDN%*>g7^kZ4;UPH=52k{M@YFHjDDaavtOZZ!aO**6!|A^FmF zk0tyZ7{PRtc)yZ{9YG-#dyyd{7xSYF{-uEVmHyIJFq$c@ebha$s`C)x0fL4U0hXY% z?%Jeros+BDqU&$X5J&AA>jj}@Q1i=ryXnwq<%lj0Ct^|AjPsknp^&r4HBzNAWUh>c zEh-HtrQf6=%U&h}N=Fppw8Q<8qKQ|jDsz$u8K`^iPk;-*Z(=tvMW`l^%ECPdWVijN z?942k=D`x)d?G~j>Sn6QLcAWe=(*r7^@cpPYLlE5Fzs02YU$pfsLCmVjGH1Y+BJQv zF@l~MP<@e0Sl~Pz7}a>{KuA6qiIb3B>3v*S-*n&7|i&eTFrE|$1b{byYNUW9qc*KG%$WjrDvN&7g7HwOosQT+zSfRW_mp* z9(`kyZkv9fnHNimV_TJvE@t4w%7b(KftW2Wyq4O_ zZz6C8M8y0=du53`if^Yu;S0pyXSMXd6$y(rp+~W{^NTzYlovOrAM|kSRK`(D7k}G* zu0bM*%i1BnG_ESlBJ>#LaEqG8&mvAjWX8y2T$_>FaCq{$d9IyRspkq?fwhVGdz{?z?COFRMZjapA-a39sa1)VGW9wsF<|8%B!}>1;eGv0WTAh56&#LLbuE;@e?Pwr_HPxF zpNvt{PH}3kCP?Hz7iI8~p%?GYB{J46d4WO@qfAjF5*7Luo14l(J}H{NYbvretm1-t z85>RahH6)!NxTjuYy>sMn%XUT|2&Hv(b&D69oS|}Z;$2TU@&QCJF&vO=yW09$l#|c zD=dkYW1vLj0?&#xiGaP-JNOAPDq1sU4`z?-sHvG5MODi#D*F!vXVbq%Ho*wVz4S3g z6q6!@G$&7W>GiXM*>3BOnaV*k)#;||d%8`D_ANkeW$ZP}4h7+_WVoPr< zqp%8wwZw0bZ6p0GIuKLI*+8m0ldk~UKS>cpKhXv!3;sE>*eKEH5j@)NX!oLrZZc2zQD5^7cRs%~%fJL5JvZ$&|!z(19fj zS+GLnNwzE(q`-v$G??^{5_9KV_A@K1AMeylkhI=A2TBNbu)(VSdC6Df zkW+~4#%@?u^5;tvjN(alPF__QRWAlXIh-EScWiUZR)6~K#6Pk;Zz|}{3_LSCf+|&y zl`Rgm)k3t$?{zPpYK2$ClYlW%u_IeQ6LaezUNke!BHfHa7GfL>dq4HWxLjshE_6fs za-0{73Ke{cddiZ*n0=E|u~#sE+rHfIRJQF|Z3ElLAB|%JcF?=TYRL6ud!OVS01XYm2fWVk$y-6-b2g5*kD(C z1pEDGI4`|1c69@v6>%jwG%YT8UU@2~J+=}?YDaSZ+oE}QgaXmXA;)O? zhZ9Ron)Ha%%K~2H)QxpWDeQ8!Gb)&%(R*s;-4<^V-kM3m%8K<7>VfaT(dl`&W-y2G zWOg)PW~y@_^^+@a$TJ&c9_JG$b?n3Qt!Z;h+tEe=?iqU;7ySKd%&YzM^!Jb-!H0gg zoBpLH6got^)0X+UPxWM496MXLOMc{iv{cSGu#YaDtrWaleUeV#xY+~;+^u{nn77X{ zw9Ips5~mY;VW@UxU3XYBuHQi?-?!VbfpU8t0Nw5^tX`COJ@VUZek~q5PY2c4TnH~- zc{?t>QrETOAzLut) zVrgB-xQ@fhOEuYqJoNMmOkr{ED232<{AuF;+*!K>NDm@fboUggsYHTc!{$iv4}uN1 zj5uj*42o}^+&SjrqNXp?y_y}bhQOjahwO%kjBD z$*elZr}$=yVc3BOyM>scTA1z5r@b#jQm;Dt+Jy`pjZAz}zM}z?C9k_v4gQFDxcccx zRt#YzEA}Y!REOfQwLf;sz8;4-p;I~!bB_HF{u+&d(b5HuSB7$=G(LmT8BX~fh_A5- zE30%jpQatnBHD=ZuCe}!yGd2=bV5+uic{W4>x&Ego&{k+Vd&YTB<_V=F22Z}Ihk!W zOtJp0D!3^L|CPtbh9g=ftjp1AtC)+WaF%eMi*I9;1JJLl^TF*Ql$ zE?*nxe60#TosF^0=L@}g%dz1b8)P9Jt$$4CUV7-qY$4&Y_idki5+tL04c|O-*=%{p zfOx+$C}^1CI*GQ+Q=!9yD?!J~Xprs{_R)dxOIC?OGZy!)_Ii&?XnZOfAH`ktUS#W> z)q(Fz)tcy87awC$=69*H-9~a;(I651A@Kcb=mK7=OMzoYkfBoUqV%X|3%>VXn#Ry+e!*7QV6-Ic@U~D zbx|ED#V8ZkEv%iVPW`30Jq%ezLAx-Hjw{VHEJSVzv5 z!02q<9X7x72{e)>FNu0;jsBEsljPGU%qDP4PURHZ@8CZ<8hs|8ePQ*&oq&dj_xk>N zl)0vU-e_XK@63L7uw#{URx_|Cwbt?7M^>sCo3+&!!=OM)!)b&|tA#5g3kL|(OHy$j zcGws88SFiyiz#x>`Ira~C#!co7%L{#wRIfr z#$Xw1_m{L9!^n$HxDG`6$civ*vqmF-F$n9O|7hsy}I zqr1GrQS4qx5c}JH+E^MmxU(W3N+{%jTe*C4Uw*?lQi;5yC#otenb6kK5Ldyh1X&uz z4zgyyu#fWkHr|?`Tvm|ZwHHT7EOkPwI5p?51$dOaJ-XuV4yPKiR_=i&@CBb98!V|V zkw33^dFOfmbfqxSGqlvWNaeG2t>f>^)Rb@PUrmI0^>CKQt*lP2`FANZk0&F7{$9cv z(5V}?e(Ak9=I14;rrGrd>pxS+n01ORzM|ltRTCugqgNVbQCQoPo5V2SEJ7@C_|7MC zYzPaldPS`S~STcv+rB7zFkYcaob5+T89=Qtdj$ifd}; z)X}kDsUyU0Ghh=Y8~f;HgsrjW45b7O*(%7(h^gZ)q zlWVt`K?*)uOd$xS+VAz*x^qz`&|DvcvsE3fkjo(yj}6#l{Z1het^lC!be$p6zv+ zrK>)Lq)ipIT$5EBHTR)xSK7^0s>rFi>ZCFQ<`>n&9TrIlE zK+;X(N*K%kIxojS+Una)@N0X3GU{^y211um%E)HACa%FJ#xaF2l?!Pl2?N<5*gWY|G31> zNd#M9hUJJ?i~YYNwlN}qT?<%|%5jK^BT>3~sv(cvE=D7vW*Ex0tB%pY5nFLIbR)kW z1%>!u&-Zoc!Ic@W!xm56X9CnkH+uv?_gj0@S;u1Bz!J3gJ*&c*#rcQ3YPE$ zP1w*0T0@?-HxVibFv-yK6-R>;ALh0Z5|XBw!ep>+lvsUdvD+6ZWUXwH0Mnu*`A2jc zh`o4*BG8{(!FoJ@7%*9A3|u84BN-)Rz;gMl~LgFB!)fkhJJ<-yI{@8R*1x4+Ua9qLF)+F<7yf3B2WVH0e z)n;V!NWXKTg$P(x@y4!Y?7SQ?eQT#I2fS?jhrF{T(-vMtqi94~=?2Ai$7l-`c%y8i zYJ-c5agpSDma}`+b6)$nc8Yed;C<0Lz~~&pdw4@+pY2Gv8hk5>A@ZLTd>fcQkCdYv z=w<(9>6?Qp8Ywg)ZZU_i6v>?Isl^7iB2V%DJdyenr~g7uCJ||$g?u&goXgR$)eDOr3{$Z zp9}@6=4qWiWXKv?XSgkiVyZ`fh#3P)z#z7k?D>ES!VgnPT7mrzJwqkggqPEXTF;|& zYqI9=uCi90waRh-S6sdGIEu|+^2-c5@+7r|= znu%uFSZPHC-#oAz%N3p$skEkT(np6Sk_nM@!@a>*67=sxr1Soq-a{84pe2gQ>v2|J z!CR~bg`F#tklptU#fgcAF-Qm1DH`lLXcyIn{TSE!i)xVv3!jnvq8YgY&C>Dv#Fx$& z6S8w3iU>6;E&O;hKl~I!6F>Vj=&`|Rg*)19hM*&ODjzaF2OpnR+8$hgeYtumxgROS zOUuG~w03gLQerL0hX~>PysYLsbs-Q0E$GcT2LfPxU?cU2wJsiJ137>@WQEzlNWg{k zm5`GQy%ui~SAuFpsuC(u5r+oxZ#&~#hB<3;t_6O6eEh4nO9&vDX;VU6_W9Q)>f`o> zo8ra@y%N<`b1M`dd+<@teJQ!#?5YpY~i4B_d@cjo&PHW_UX_*gUR4xKX-qzIt(pDm`?{wT2B zksPBm=yjK)%{javP6A=d&sYQOIycYl(@M6k9{@VX>(@4*v7GWHk8dv3lF~ zR-E7t>a(rjZ_sPRmeAE9`t6gpp6}o(nE4jwwkd{PfKM7mB_IX2q@@QC>XCO`pFCX` z(|4jho5Q&CZhzEV*n?<4p?Vp-TjXjb9D~G~nujlYqX>*@-_S7f|J1)KGaJRNXpViksFd{vGDc0`0<6Ti(Tv^=|P$oRyc)k+Z?w&0rmQaRm0_pcSR|_m*7=DH?mH> zY5t(D{|QsrJ#oX@V-b3Eyv*MLb(B6FN70dfLFTmeew>STXm{dN`bz!WURRvI@(^i= zVWxhUbJ8JWO2T!5<^*4$Ya1$)=6wc$iyI{6IC`D^HR~_!TYI@8<`aVvqWJ6<1jbw{ za26S+{e#814j6DkebZxt4jpB6_X*Z5><%oFYqq^#K|PIrDFkg1TD{|d)#@-66&2;T z(U^&I^a^p&=>J1qViowc=KKga6|P02rvAE17}z28)1n9Zb&r1;n;RL}7P{qyj^(2$ zP9X5t`$YhQS=^PxZI}vOa}Dtb=yCHe&$kg`53Mp9kTT0X?g2mWAs{3P(ha@j2y*V^ zdhmGUe_bA!MiEdwh#;B(uR^QB0KVb>NWi3JWGfu&NQ|mN-VML$X$RJ0{_lo;r*!#h&(EET69>Y{FTaX%xO+@8HPgs% z8VcThUR^phXz2OMQ3&0!li&-Lc*s~;6afTy^I@}rr%@bPeK>isQCCnkGp{W|_Gtg{ zc+P$B0#Kst;2F6GY(Z`3Q4BV(s*>D13Duk1Z0_xfE?DoaRe9gvSB10I>4Gz>Y^7fA z-Q|KetDOY*E)jC8p6L)MfxaVg-^KF=#|_W{R9)?&GOl$b`Sy8`_gRHgmEa#tL(i~B zp+Bt;UTsL+fH1fdt2GT0|^ec0|DP<`{G zEzPakcG)#A$RWT97A)dHxe;p8`TgfPwma}iLSRAiF|pyUV@{On(QX*t*~)mKWgTL?Z-Ea}0v=NXO|lG#4Zy63b2J|V#Gr+T#MMK;A7 zt$?G>;gpn@uy7#u`*EHBC>k>D?m1=dhn(l@u$G-0BkvR9zM#J!lD2nyHz9D7G5IuS za{SMLPW*XJ9+AJ07pD@?)%#&y(NFPVULjpJKRr+_`BiXMjT6%z@KlHmoE#^wLg{W zU;RXDw8+4Zw`(4?kS4!9tf*{uR38|*?gBs2Yff>T7u;(Juvj#f$Y`LrqO+OcaM!6} zT>K&NFHMB=_I|Upo_}^Lb?}z^F`Zko&hUB6C5bWcpqX;O*V`i5a%QPOa-=eY@#lbl z1^8ldb@gZ}F0EwJFq3{GS}%^a2`}&b#`(-=doyqEo=5(`p5b6__Wn9FRKS~aITs-~ z*Tm<M_CY| z`kn3Z4UCm{!Ii)|KF6IVw!M0&Ji_v7^|)*+k7Rq3eU`CygBNKQzqHSDzpG{CPb)a) zf@9e$;b5)OPFXJRJj1>ry)EJ3EbW#z6B~)r2YK%qyC^mk zzMP(9>@FI)Zv2ru4(m@_IPfViXI!NJo!PkGGan__Z$GEAJ+FFP^(1}bpDU~)uLCr~ zB%?Wajo&F+w-QHW53>iy*y&Wy`m0cSCNikbM~4y^)5oGv%aS?CVA(t4tR3T=A~q7a!rN%@x|#o6;Y1jOpdOETr_KT z&enMek)duVOW+%oEjg`K8RkNlXyxE}D9m>*vxQp*9>cQT)B_S@UhbBZTH)y0c%hq{ z&TCIaL-PFa%N3CSl;y$Qd?!2NW~qy>NbBUm%jQWV+XJTo{7F`>Qq{w1o2mn2%17sT zRhI6RnK=4~7`?Ye7p`Kqm?y1so6B_?X*^nPjzK4`F!g}aX1;*Li8s{ow(AevxbvML zTYj$<_GlN0k-3quPY}fmD_0sDUc`Ob4Zb3sX8dXOio>q& zIBVSUixzZZYn#jxb;G|6_@hjA;cg#FeBEJI8IylcWMsit8RJb0UkG0~L26AF3xq6# z^6iJqv5<)rH=HWcZ(7%`*o$?|{1J8p5L6$?~XT@ReF|u6O3th=2R8#=RP8gpDa+=M0rDjHXB^t} zweh~l2{u3A++gf%ANY%vFXyQbAH8l&C%cBNh1kN`pF|EpJS5rTk;<2r;j3yH z#7FBv=5!k61I~rNObd}c)l1vH@)a8K19k4X5X^+4u739~iFM|r0yoE4gB;GfYDF~- z&a^GW|2F#iyQj%?`7GmK#L#||j>h#}eWRL?Z2o^cqPP>3``zK1oA@E`3GkB?l@lp> IqaX190Va;lr~m)} literal 0 HcmV?d00001 diff --git a/docs/screenshots/02_diagnostico.png b/docs/screenshots/02_diagnostico.png new file mode 100644 index 0000000000000000000000000000000000000000..8233ae70bdd10093a30d657bf201b766f94a1d67 GIT binary patch literal 55671 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 GLK6UFSM2`) literal 0 HcmV?d00001 diff --git a/docs/screenshots/02_layout_escuro.png b/docs/screenshots/02_layout_escuro.png new file mode 100644 index 0000000000000000000000000000000000000000..583fa6e7b2ca067d6a5033108a6642c641d5e1df GIT binary patch literal 63533 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�nCLM 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 E27orZAOHXW literal 0 HcmV?d00001 diff --git a/docs/screenshots/03_configuracoes.png b/docs/screenshots/03_configuracoes.png new file mode 100644 index 0000000000000000000000000000000000000000..ecd705318086261e0f18aa6129430f893f112016 GIT binary patch literal 36088 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 z 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�-smy5;A7inhVK>7p5v|P zd1YqgzU`uPBzbqKBUrFV!>Dp}Q}UGy2O6+WjI<}8w5z7(qHV&QWp4>Tg3ZhcUN`b6 zoE;UcQFP~G2tN2o_an((f56Sp8IPIVvs)rc;ge+8>qHNIFREux$hI5~Bp!OfM>a!* z?~&g*D{`H_EQWng8Wt;fgxMWvMKheIi0C8wB38B`6M%0I2dyVFf~8X?}Hw#im*t6O!Lc@OO@`jWUvSS_hV58s!G(wO$b-Q=9r&zKlqq&z7MbN8+>E|xmn2GEl*AJiAfjgm~++1xLZHA+qx zC{r^rEYeDy7uy++7(u8Oeb}x(^2m5CfJ~vXm{uDrw_mkL=&4wWw1)MjReviqTU<}T zU8TeKAWIvC6gc);dS^VXj1vmd*sX(!KJ{PD8X2G^}CLc-OLPGk_@MkoM>S zS?7Va3n+3R8`AUJ&O801t|E`ZcH)LFFr{t~l!6h~(faFZHX1cMhA>d zYt-5lc}XD+lJlKnGw+PalC~m{JN@Hh9-z>ykLML}HE)rlw;tC@ZfG{Dxj;dyW@6ur zPyaw=kPHdwkmF+c?b^WIc_y)PljT2k^tMB7DBC#K?Dy%|j*O4ezH?HdL?rVZ(K0E& z5#Pi3H4KoGqNJjb)H$&cadSkmhZAY0%8vW}1b4x6b$%-%&_v<8T2svR=~NH75iZ1E z=zF)EcRtAo@(tJCkc!>Dn?c+;LG5>UBCg6Q_JsfFx0(Oe+&8IL)!MWVu6s894!f@` zZRB?e1zo(9j(Cd*EF)r;_)G$9VwSceJVnd;Lu)YhJ4Z`GCt_wwY*zCRC!5Cn&T>Fa;P$tj!vN}~4}9;myHlwB)l9%494u@#W?hrU486}S ze4I(blJdg4x8Cs68np`Y1nzw|w~jpFuU^0?`E3s07X$u&$u93ws$sDM4Jn1XNK(SnoL?ej6>;YG=!r_{NCzFsg{S1=`Upw>t4g_v=ZQrI?dXMD?9- z$s86(SL#}lzK-27B?flhMOV|hI9*GR&M6kA

<4K^EEWK0&V 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 +Testa rapidamente se o agente desktop está ativo na porta local `21325`. + +### `connect(): Promise` +Realiza o handshake inicial de autorização. Se for a primeira vez que a aplicação tenta se conectar, abre uma caixa de diálogo nativa no desktop do usuário para concessão de acesso. + +### `getCertificates(): Promise>` +Retorna todos os certificados A1 e A3 disponíveis e operacionais na máquina do usuário. + +```javascript +const certs = await signer.getCertificates(); +certs.forEach(cert => { + console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`); +}); +``` + +### `sign(options): Promise` +Solicita a assinatura digital local de um digest ou conteúdo bruto. + +### `signPdf(pdfFile, options): Promise` +Assina um documento PDF (PAdES / Assinatura desacoplada). Recebe `File`, `Blob`, `ArrayBuffer` ou `Uint8Array`. +Calcula o SHA-256 no próprio navegador e transmite metadados do documento (`PDF`) ao agente para exibição clara na janela de confirmação. + +```javascript +const pdfResult = await signer.signPdf(fileInput.files[0], { + certificateId: certs[0].id +}); +``` + +### `signXml(xmlContent, options): Promise` +Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`. + +```javascript +const xmlResult = await signer.signXml(xmlString, { + certificateId: certs[0].id, + documentName: 'nfe-3524.xml' +}); +``` + +### `signFile(file, options): Promise` +Assina qualquer arquivo (ZIP, DOCX, imagens, binários, executáveis) gerando pacote de assinatura detached / CAdES. + +```javascript +const fileResult = await signer.signFile(fileInput.files[0], { + certificateId: certs[0].id +}); +``` + +### `authenticate(options): Promise` (ou `login(options)`) +Executa login seguro com certificado digital via desafio criptográfico (Challenge-Response). + +```javascript +const auth = await signer.authenticate({ + certificateId: certs[0].id, + challenge: 'server-nonce-12345' +}); + +console.log('Bem-vindo,', auth.subjectName); +console.log('CPF/CNPJ:', auth.subjectCpfCnpj); +console.log('Assinatura do Desafio:', auth.signature); +``` + +### `extractSubjectInfo(certificateBase64): { name, cpfCnpj }` +Decodifica o certificado X.509 e extrai os dados do titular ICP-Brasil (Nome, CPF ou CNPJ). + +### `verify(options): Promise` +Validação sintática e criptográfica da integridade da assinatura gerada. + +--- + +## 3. Tratamento Estruturado de Erros + +O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`: + +```javascript +import { + UserCancelledError, + CertificateNotFoundError, + PinIncorrectError, + TokenNotFoundError +} from '@cybercore/signer-sdk/errors'; + +try { + await signer.sign({ certificateId: 'cert-1', data: 'Contrato' }); +} catch (err) { + if (err instanceof UserCancelledError) { + alert('Você cancelou a assinatura no CyberCore Signer.'); + } else if (err instanceof PinIncorrectError) { + alert('O PIN do token informado está incorreto.'); + } else if (err instanceof TokenNotFoundError) { + alert('Conecte o seu token USB na máquina para assinar.'); + } else { + console.error('Erro na assinatura:', err); + } +} +``` diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..adb637a --- /dev/null +++ b/docs/security.md @@ -0,0 +1,46 @@ +# Modelo de Segurança e Privacidade + +O **CyberCore Signer** foi projetado seguindo as diretrizes rigorosas da ICP-Brasil e os princípios de *Security by Design* e *Least Privilege*. + +--- + +## 1. Isolamento de Rede + +1. **Loopback Estrito:** O socket do servidor HTTP local vincula-se exclusivamente ao endereço IP `127.0.0.1`. Nenhuma interface de rede externa (LAN ou WAN) escuta a porta de serviço `21325`. +2. **Prevenção contra Exposição Acidental:** Testes unitários e de arquitetura garantem que conexões fora de `127.0.0.1` sejam bloqueadas imediatamente no nível de bind do socket. + +--- + +## 2. Autenticação e Autorização de Aplicações + +Nenhuma aplicação desconhecida tem autorização para obter certificados ou assinar documentos sem consentimento expresso: + +1. **Handshake de Autorização (`POST /ccsp/v1/authorize`):** + - A aplicação web identifica-se enviando seu `application_id` (UUID estável) e seu nome amigável. + - O CyberCore Signer suspende a requisição e abre uma janela modal nativa na tela principal do sistema operacional perguntando se o usuário autoriza aquele domínio web. +2. **Armazenamento Seguro das Autorizações:** + - As aplicações autorizadas são gravadas em `~/.cybercore/authorized-apps.json`. + - O usuário pode visualizar e **revogar a qualquer momento** qualquer permissão através da aba **Configurações** na interface gráfica. + +--- + +## 3. Proteção Contra Ataques de Replay + +Cada solicitação de assinatura enviada pelo SDK deve incluir: +- Um `nonce` (UUID criptograficamente aleatório); +- Um campo `expires_at` (ou `timestamp`). + +O componente `NonceManager`: +- Armazena os nonces recentes em memória protegida com expiração temporal (TTL); +- Rejeita com código estruturado `REPLAY_DETECTED` caso o mesmo nonce seja submetido mais de uma vez; +- Rejeita com código `REQUEST_EXPIRED` requisições cuja data/hora de validade já tenha sido ultrapassada. + +--- + +## 4. Confirmação Visual Obrigatória e Proteção de PIN/Chaves + +1. **Confirmação Manual do Usuário:** Toda e qualquer assinatura exige intervenção física e visual do usuário. Não é possível assinar "silenciosamente" por chamadas de API em segundo plano. +2. **Inviolabilidade da Chave Privada:** A chave privada nunca é exportada nem manipulada em forma de bytes puros. Todas as operações utilizam handles JCA/JCE ou comandos de hardware nativo. +3. **Proteção de PIN e Senhas:** + - O PIN de smartcards e a senha de arquivos A1 nunca são transmitidos para a aplicação web e nunca aparecem em logs de auditoria. + - Em caso de remoção do hardware, o `SmartCardWatcher` limpa e zera os buffers de memória de PIN imediatamente. diff --git a/docs/serpro-layout-and-batch.md b/docs/serpro-layout-and-batch.md new file mode 100644 index 0000000..cd5cb43 --- /dev/null +++ b/docs/serpro-layout-and-batch.md @@ -0,0 +1,82 @@ +# Layout CyberCore Signer — Design Dual-Theme & Assinatura em Lote + +O **CyberCore Signer** adota uma interface gráfica que une a ergonomia de hub consolidada no mercado nacional com uma identidade visual moderna, autoral e com suporte dinâmico a **Tema Claro (Light)** e **Tema Escuro (Dark)**. + +--- + +## 1. Identidade Visual e Filosofia de Design + +Em conformidade com os requisitos do projeto, a interface **não é uma cópia datada**, mas sim uma evolução: +1. **Identidade CyberCore:** Logotipo central em cristal tridimensional e escudo criptográfico vetorial (`SvgIcons`), subtítulo "Agente Criptográfico Local • ICP-Brasil" e indicador de segurança em tempo real. +2. **Badge de Status Ativo:** Exibe o status da porta local (`● Agente Ativo • 127.0.0.1:21325`) com verde esmeralda no modo claro e verde neon no modo escuro. +3. **Alternador Dinâmico de Temas:** Botão pílula localizado no cabeçalho superior que permite ao usuário alternar instantaneamente entre **Modo Claro** e **Modo Escuro**: + - Ícone do Sol ☀️ para comutar para o tema Claro. + - Ícone da Lua 🌙 para comutar para o tema Escuro. + - Classe gerenciadora centralizada: [`ThemeManager.java`](file:///var/www/cybercore/cybercore-signer/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java), que replica a escolha de tema para todos os diálogos secundários. + +--- + +## 2. Paleta de Cores dos Temas + +| Componente | Tema Claro (Light) | Tema Escuro (Dark) | +|---|---|---| +| **Fundo da Janela** | `#f1f5f9` (Slate suave) | `#0b0f17` (Dark profundo) | +| **Header Superior** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` | +| **Cartões de Ação** | `#ffffff`, borda `#e2e8f0`, hover `#0284c7` | `#131c2e`, borda `#1e293b`, hover `#38bdf8` | +| **Texto dos Botões** | `#1e293b` (Cinza escuro de alto contraste) | `#f8fafc` (Branco gelo nítido) | +| **Painel Lateral** | `#ffffff`, borda `#e2e8f0` | `#121824`, borda `#1e293b` | +| **Badge de Status** | Fundo `#ecfdf5`, texto `#047857`, borda `#10b981` | Fundo `rgba(16, 185, 129, 0.15)`, texto `#34d399` | +| **Alternador de Tema** | Fundo `#ffffff`, borda `#cbd5e1`, hover `#0284c7` | Fundo `#1e293b`, borda `#334155`, hover `#38bdf8` | + +--- + +## 3. Grade Central de Ações (Hub 4x3) + +A interface principal organiza as operações em 12 cartões modernos com cantos arredondados (8px), sombras suaves e ícones vetoriais SVG nítidos: + +| Posição | Operação | Descrição | +|---|---|---| +| (0, 0) | **Assinar PDF** | Assinatura digital de documentos PDF com cálculo de hash SHA-256 e gravação de assinatura. | +| (1, 0) | **Assinar XML** | Assinatura de documentos XML (estruturas fiscais, NF-e, XMLDSig). | +| (2, 0) | **Assinar Arquivo** | Assinatura genérica de qualquer arquivo ou binário do sistema. | +| (3, 0) | **Assinar em Lote** | Processamento simultâneo de múltiplos arquivos com barra de progresso em tempo real. | +| (0, 1) | **Assinatura Atachada** | Assinatura embutida/anexada ao documento original (CAdES enveloped). | +| (1, 1) | **Co-Assinar Documento** | Adição de nova assinatura a um documento previamente assinado por outro titular. | +| (2, 1) | **Carimbo do Tempo** | Integração com protocolo RFC 3161 TSP para prova de anterioridade temporal. | +| (3, 1) | **Assinatura LTV** | Perfil de Longa Duração com inclusão de cadeia de certificados e carimbos para validade permanente. | +| (0, 2) | **Validar Assinatura** | Verificação de integridade criptográfica e conformidade do certificado digital. | +| (1, 2) | **Validar Carimbo** | Verificação da autenticidade e validade temporal de tokens TSR/TSP. | +| (2, 2) | *(Reservado)* | Espaço reservado para expansões futuras do agente. | +| (3, 2) | **Sair do Agente** | Encerramento seguro da sessão do agente. | + +--- + +## 4. Painel Lateral de Utilitários + +Na coluna lateral direita, o usuário dispõe de atalhos rápidos para operações do sistema: + +1. **` Logs`:** Visualizador de auditoria em tempo real, monitorando requisições locais CCSP/1 e eventos criptográficos. +2. **`⚙ Configurações`:** Gerenciador de origens web autorizadas, revogação de acessos e opções de PIN. +3. **`💻 Tutorial`:** Guia passo a passo interativo de uso do assinador e integração web. +4. **`🗂 Certificados`:** Lista de certificados ICP-Brasil instalados (Windows-MY, A1 e A3) e ferramenta para importação de certificados A1 (`.pfx` / `.p12`). +5. **`ℹ Sobre`:** Informações de versão, engine JCA/JCE e garantias de privacidade local. + +--- + +## 5. Assinatura Digital em Lote (`BatchSigningDialog`) + +A funcionalidade de assinatura em lote permite: +- Seleção de múltiplos arquivos simultâneos via diálogo de arquivos ou pastas. +- Visualização de tabela com nome, tamanho e status de cada item em tempo real. +- Seleção do certificado digital ICP-Brasil desejado (com exibição de titular, CPF/CNPJ, tipo A1/A3 e validade). +- Escolha da pasta de destino para os arquivos assinados (salvando arquivos `.p7s` correspondentes). +- Execução assíncrona em thread isolada para manter a interface responsiva durante todo o lote. +- Compatibilidade automática com o tema ativo (Claro ou Escuro). + +--- + +## 6. Ícones Vetoriais SVG Nativos (`SvgIcons`) + +Todos os ícones e o logotipo central em cristal foram construídos via `SVGPath` e primitivas vetoriais JavaFX nativas: +- **Resolução Infinita:** Perfeita nitidez em monitores Full HD, 2K, 4K e configurações de escala de DPI do Windows (100%, 125%, 150%, 200%). +- **Sem Dependência de Imagens Rasterizadas:** Sem risco de imagens ausentes ou corrompidas no runtime empacotado. diff --git a/docs/xml-xmldsig.md b/docs/xml-xmldsig.md new file mode 100644 index 0000000..2b3c005 --- /dev/null +++ b/docs/xml-xmldsig.md @@ -0,0 +1,56 @@ +# Assinaturas XML (XMLDSig e ICP-Brasil) + +Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer. + +--- + +## 1. Visão Geral + +O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`). + +### 1.1 Casos de Uso Suportados + +- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML. +- **Assinatura de Elementos Específicos por ID:** + - **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `` + - **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `` + - **MDF-e (Manifesto Eletrônico de Documentos Fiscais):** Assinatura da tag `` + - **eSocial / EFD-Reinf:** Assinatura de eventos com tags `` + +--- + +## 2. Padrões Criptográficos + +- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários). +- **Transformações:** + - `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest). + - C14N padrão. +- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`). +- **Algoritmo de Assinatura:** RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`). +- **KeyInfo:** Informações do certificado X.509 embutidas em ``. + +--- + +## 3. Segurança e Mitigação contra XXE + +O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades: + +```java +DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); +dbf.setNamespaceAware(true); +dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); +dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); +dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); +dbf.setXIncludeAware(false); +dbf.setExpandEntityReferences(false); +``` + +--- + +## 4. Validação Pós-Assinatura + +O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando: + +1. A integridade criptográfica da assinatura (`SignedInfo`). +2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`). +3. O certificado X.509 público extraído da chave. diff --git a/modules/cybercore-app/pom.xml b/modules/cybercore-app/pom.xml new file mode 100644 index 0000000..5cb16cd --- /dev/null +++ b/modules/cybercore-app/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + + br.com.cybercore.signer + cybercore-signer-parent + 0.1.0-SNAPSHOT + ../../pom.xml + + + cybercore-app + jar + + CyberCore Signer :: Application + Módulo executável desktop, ciclo de vida da aplicação, shutdown hooks e empacotamento nativo. + + + + br.com.cybercore.signer + cybercore-core + + + br.com.cybercore.signer + cybercore-ui + + + + + org.openjfx + javafx-controls + + + org.openjfx + javafx-graphics + + + org.openjfx + javafx-base + + + + + org.openjfx + javafx-controls + ${javafx.version} + win + + + org.openjfx + javafx-graphics + ${javafx.version} + win + + + org.openjfx + javafx-base + ${javafx.version} + win + + + + + org.openjfx + javafx-controls + ${javafx.version} + linux + + + org.openjfx + javafx-graphics + ${javafx.version} + linux + + + org.openjfx + javafx-base + ${javafx.version} + linux + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + + + com.tngtech.archunit + archunit-junit5 + test + + + org.mockito + mockito-core + test + + + + + + + org.openjfx + javafx-maven-plugin + + br.com.cybercore.signer.app.CyberCoreSignerApplication + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + cybercore-signer + false + + + br.com.cybercore.signer.app.CyberCoreSignerApplication + + CyberCore Signer + ${project.version} + CyberCore + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + META-INF/versions/**/module-info.class + + + + + + + + + + diff --git a/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java new file mode 100644 index 0000000..8817b83 --- /dev/null +++ b/modules/cybercore-app/src/main/java/br/com/cybercore/signer/app/CyberCoreSignerApplication.java @@ -0,0 +1,144 @@ +package br.com.cybercore.signer.app; + +import br.com.cybercore.signer.core.diagnostics.DiagnosticReport; +import br.com.cybercore.signer.core.diagnostics.DiagnosticService; +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.protocol.CcspServer; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.ui.CyberCoreDesktopApplication; +import javafx.application.Application; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.GraphicsEnvironment; + +/** + * Ponto de entrada (Main Class) e bootstrap do agente CyberCore Signer. + */ +public class CyberCoreSignerApplication { + + private static final Logger logger = LoggerFactory.getLogger(CyberCoreSignerApplication.class); + + private static boolean developmentMode = false; + private static boolean headlessMode = false; + private static boolean noWaitMode = false; + private static volatile CcspServer runningServer = null; + + public static CcspServer getRunningServer() { + return runningServer; + } + + public static void stopServer() { + if (runningServer != null) { + runningServer.stop(); + runningServer = null; + } + } + + public static void main(String[] args) { + parseArguments(args); + printBanner(); + + logger.info("Inicializando CyberCore Signer v0.1.0..."); + if (developmentMode) { + logger.warn("========================================================="); + logger.warn(" MODO DE DESENVOLVIMENTO ATIVADO (--dev)"); + logger.warn(" Logs detalhados ativos. Operação com certificados reais."); + logger.warn("========================================================="); + } + + DiagnosticService diagnosticService = new DiagnosticService(); + DiagnosticReport report = diagnosticService.runDiagnostic(); + logger.info("Ambiente verificado: OS={} {} ({}), Java={} ({})", + report.osName(), report.osVersion(), report.osArch(), + report.javaVersion(), report.javaVendor()); + + if (!report.cryptoEngineOk()) { + logger.error("ALERTA CRÍTICO: Engine criptográfica JCA/JCE não está totalmente operacional!"); + } + + // Se o ambiente tiver suporte gráfico e não for headless forçado, abre a interface JavaFX + boolean isHeadless = headlessMode || GraphicsEnvironment.isHeadless(); + + if (isHeadless) { + logger.info("Executando em modo console / headless (sem interface gráfica)."); + startHeadlessServer(); + } else { + try { + logger.info("Iniciando interface gráfica desktop..."); + CyberCoreDesktopApplication.setDevelopmentMode(developmentMode); + Application.launch(CyberCoreDesktopApplication.class, args); + } catch (Throwable t) { + logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage()); + startHeadlessServer(); + } + } + + logger.info("CyberCore Signer finalizado."); + } + + private static void startHeadlessServer() { + try { + CertificateManager certManager = CertificateManager.createDefault(); + if (developmentMode) { + logger.info("Modo de desenvolvimento ativo (com certificados reais do sistema)."); + } + SigningService signingService = new SigningService(certManager); + runningServer = new CcspServer(certManager, signingService, null); + runningServer.start(); + logger.info("Servidor local CCSP/1 pronto em http://127.0.0.1:{}", runningServer.getPort()); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logger.info("Encerrando servidor local CCSP/1..."); + stopServer(); + })); + + // Mantém processo vivo para atender requisições web, a menos que em modo no-wait + if (!noWaitMode) { + Thread.currentThread().join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + logger.error("Falha ao inicializar servidor local CCSP/1 no modo console: {}", e.getMessage(), e); + } + } + + private static void parseArguments(String[] args) { + if (args != null) { + for (String arg : args) { + if ("--dev".equalsIgnoreCase(arg) || "-dev".equalsIgnoreCase(arg)) { + developmentMode = true; + } else if ("--headless".equalsIgnoreCase(arg) || "-headless".equalsIgnoreCase(arg) + || "--cli".equalsIgnoreCase(arg)) { + headlessMode = true; + } else if ("--no-wait".equalsIgnoreCase(arg) || "-no-wait".equalsIgnoreCase(arg) + || "--test".equalsIgnoreCase(arg)) { + noWaitMode = true; + } + } + } + } + + public static boolean isDevelopmentMode() { + return developmentMode; + } + + public static boolean isHeadlessMode() { + return headlessMode; + } + + private static void printBanner() { + System.out.println(""" + ============================================================ + ____ _ ____ + / ___| _ _ __| | ___ _ __ / ___|___ _ __ ___ + | | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\ + | |___ | |_| | (_| || __/| | | |__| (_) || | | __/ + \\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___| + |___/ + SIGNER AGENT v0.1.0 + ============================================================ + """); + } +} diff --git a/modules/cybercore-app/src/main/resources/logback.xml b/modules/cybercore-app/src/main/resources/logback.xml new file mode 100644 index 0000000..3626ce9 --- /dev/null +++ b/modules/cybercore-app/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java new file mode 100644 index 0000000..4f6be7d --- /dev/null +++ b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/ArchitectureTest.java @@ -0,0 +1,37 @@ +package br.com.cybercore.signer.app; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchRule; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +class ArchitectureTest { + + private final JavaClasses importedClasses = new ClassFileImporter() + .importPackages("br.com.cybercore.signer"); + + @Test + @DisplayName("O módulo Core não deve depender de classes dos pacotes UI ou App") + void coreShouldNotDependOnUiOrApp() { + ArchRule rule = noClasses() + .that().resideInAPackage("br.com.cybercore.signer.core..") + .should().dependOnClassesThat() + .resideInAnyPackage("br.com.cybercore.signer.ui..", "br.com.cybercore.signer.app.."); + + rule.check(importedClasses); + } + + @Test + @DisplayName("O módulo Core não deve conter dependências de bibliotecas JavaFX") + void coreShouldNotDependOnJavaFx() { + ArchRule rule = noClasses() + .that().resideInAPackage("br.com.cybercore.signer.core..") + .should().dependOnClassesThat() + .resideInAPackage("javafx.."); + + rule.check(importedClasses); + } +} diff --git a/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java new file mode 100644 index 0000000..0398c33 --- /dev/null +++ b/modules/cybercore-app/src/test/java/br/com/cybercore/signer/app/CyberCoreSignerApplicationTest.java @@ -0,0 +1,20 @@ +package br.com.cybercore.signer.app; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class CyberCoreSignerApplicationTest { + + @Test + @DisplayName("Deve inicializar a aplicação com argumentos sem erros") + void shouldBootstrapWithoutErrors() { + try { + assertThatCode(() -> CyberCoreSignerApplication.main(new String[]{"--dev", "--headless", "--no-wait"})) + .doesNotThrowAnyException(); + } finally { + CyberCoreSignerApplication.stopServer(); + } + } +} diff --git a/modules/cybercore-core/pom.xml b/modules/cybercore-core/pom.xml new file mode 100644 index 0000000..b4388fc --- /dev/null +++ b/modules/cybercore-core/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + br.com.cybercore.signer + cybercore-signer-parent + 0.1.0-SNAPSHOT + ../../pom.xml + + + cybercore-core + jar + + CyberCore Signer :: Core + Núcleo de domínio, abstrações criptográficas, provedores A1/A3, protocolo CCSP e servidor local. + + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + org.apache.pdfbox + pdfbox + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcutil-jdk18on + + + + + org.mockito + mockito-core + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfiguration.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfiguration.java new file mode 100644 index 0000000..c8494b5 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfiguration.java @@ -0,0 +1,43 @@ +package br.com.cybercore.signer.core.config; + +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Modelo de configurações gerais e de segurança do CyberCore Signer. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record AgentConfiguration( + boolean requireConfirmation, + boolean alwaysPromptPin, + boolean autoStart, + String defaultSealThemeId, + CpfDisplayMode defaultCpfDisplayMode, + boolean defaultIncludeDateTime, + int serverPort +) { + + public AgentConfiguration { + if (defaultSealThemeId == null || defaultSealThemeId.isBlank()) { + defaultSealThemeId = "icp-brasil"; + } + if (defaultCpfDisplayMode == null) { + defaultCpfDisplayMode = CpfDisplayMode.FULL; + } + if (serverPort <= 0) { + serverPort = 18420; + } + } + + public static AgentConfiguration defaultConfiguration() { + return new AgentConfiguration( + true, + false, + false, + "icp-brasil", + CpfDisplayMode.FULL, + true, + 18420 + ); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfigurationService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfigurationService.java new file mode 100644 index 0000000..ef1b8f0 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/config/AgentConfigurationService.java @@ -0,0 +1,94 @@ +package br.com.cybercore.signer.core.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +/** + * Serviço de gerenciamento e persistência das configurações do CyberCore Signer. + * Salva e recupera as preferências do usuário no arquivo ~/.cybercore/config.json. + */ +public final class AgentConfigurationService { + + private static final Logger logger = LoggerFactory.getLogger(AgentConfigurationService.class); + private static final String CONFIG_FILE_NAME = "config.json"; + + private static final AgentConfigurationService INSTANCE = new AgentConfigurationService(); + + private final Path configPath; + private final ObjectMapper objectMapper; + private AgentConfiguration currentConfig; + + public static AgentConfigurationService getInstance() { + return INSTANCE; + } + + public AgentConfigurationService() { + this(resolveDefaultConfigPath()); + } + + public AgentConfigurationService(Path configPath) { + this.configPath = Objects.requireNonNull(configPath); + this.objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + this.currentConfig = load(); + } + + private static Path resolveDefaultConfigPath() { + String userHome = System.getProperty("user.home", "."); + return Path.of(userHome, ".cybercore", CONFIG_FILE_NAME); + } + + public synchronized AgentConfiguration getConfig() { + return currentConfig; + } + + public synchronized AgentConfiguration loadConfiguration() { + return getConfig(); + } + + public synchronized void updateConfig(AgentConfiguration newConfig) { + Objects.requireNonNull(newConfig, "Configuração não pode ser nula."); + this.currentConfig = newConfig; + save(); + } + + public synchronized void saveConfiguration(AgentConfiguration newConfig) { + updateConfig(newConfig); + } + + private AgentConfiguration load() { + try { + if (Files.exists(configPath)) { + AgentConfiguration cfg = objectMapper.readValue(configPath.toFile(), AgentConfiguration.class); + logger.info("Configurações do agente carregadas de: {}", configPath); + return cfg; + } + } catch (Exception e) { + logger.warn("Falha ao ler {}. Usando configurações padrão: {}", configPath, e.getMessage()); + } + return AgentConfiguration.defaultConfiguration(); + } + + private void save() { + try { + if (configPath.getParent() != null && !Files.exists(configPath.getParent())) { + Files.createDirectories(configPath.getParent()); + } + objectMapper.writeValue(configPath.toFile(), currentConfig); + logger.info("Configurações do agente salvas com sucesso em: {}", configPath); + } catch (IOException e) { + logger.error("Erro ao salvar configurações em {}: {}", configPath, e.getMessage(), e); + } + } + + public Path getConfigPath() { + return configPath; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/DigestAlgorithm.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/DigestAlgorithm.java new file mode 100644 index 0000000..522a1ed --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/DigestAlgorithm.java @@ -0,0 +1,66 @@ +package br.com.cybercore.signer.core.crypto; + +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateProviderException; + +/** + * Algoritmos criptográficos de assinatura suportados pelo CyberCore Signer. + */ +public enum DigestAlgorithm { + SHA256_WITH_RSA("SHA256withRSA", "SHA-256", 32), + SHA384_WITH_RSA("SHA384withRSA", "SHA-384", 48), + SHA512_WITH_RSA("SHA512withRSA", "SHA-512", 64), + SHA256_WITH_ECDSA("SHA256withECDSA", "SHA-256", 32), + SHA384_WITH_ECDSA("SHA384withECDSA", "SHA-384", 48), + SHA512_WITH_ECDSA("SHA512withECDSA", "SHA-512", 64); + + private final String jcaName; + private final String digestName; + private final int expectedDigestLengthBytes; + + DigestAlgorithm(String jcaName, String digestName, int expectedDigestLengthBytes) { + this.jcaName = jcaName; + this.digestName = digestName; + this.expectedDigestLengthBytes = expectedDigestLengthBytes; + } + + public String getJcaName() { + return jcaName; + } + + public String getDigestName() { + return digestName; + } + + public int getExpectedDigestLengthBytes() { + return expectedDigestLengthBytes; + } + + public static DigestAlgorithm fromName(String name) throws CertificateProviderException { + if (name == null || name.isBlank()) { + throw new CertificateProviderException(CcspErrorCode.UNSUPPORTED_ALGORITHM, "Algoritmo não especificado."); + } + for (DigestAlgorithm alg : values()) { + if (alg.jcaName.equalsIgnoreCase(name.trim())) { + return alg; + } + } + throw new CertificateProviderException( + CcspErrorCode.UNSUPPORTED_ALGORITHM, + "Algoritmo não suportado pelo CyberCore Signer: " + name + ); + } + + public void validateDigestLength(byte[] digest) throws CertificateProviderException { + if (digest == null) { + throw new CertificateProviderException(CcspErrorCode.SIGNATURE_FAILED, "Digest não pode ser nulo."); + } + if (digest.length != expectedDigestLengthBytes) { + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + String.format("Tamanho inválido para digest %s: esperado %d bytes, recebido %d bytes.", + digestName, expectedDigestLengthBytes, digest.length) + ); + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/SigningService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/SigningService.java new file mode 100644 index 0000000..73b6496 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/SigningService.java @@ -0,0 +1,169 @@ +package br.com.cybercore.signer.core.crypto; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProvider; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Serviço central de assinatura digital do CyberCore Signer. + * Orquestra validações rigorosas de protocolo e algoritmos, resolução de certificados + * e delegação aos provedores criptográficos JCA/JCE (Windows-MY, A1, PKCS#11). + * Nunca implementa algoritmos criptográficos manuais e nunca expõe a chave privada. + */ +public class SigningService { + + private static final Logger logger = LoggerFactory.getLogger(SigningService.class); + + private final CertificateManager certificateManager; + + public SigningService(CertificateManager certificateManager) { + this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager must not be null"); + } + + /** + * Executa a assinatura digital local de um digest fornecido por uma aplicação autorizada. + * + * @param context Contexto da requisição contendo digest, algoritmo, ID da requisição e certificado indicado. + * @return Resultado imutável da assinatura com os bytes assinados e certificados em Base64. + * @throws CertificateProviderException Se qualquer validação, dispositivo ou operação criptográfica falhar. + */ + public SigningResult sign(SigningContext context) throws CertificateProviderException { + Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo."); + + if (context.requestId() == null || context.requestId().isBlank()) { + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "O campo 'request_id' é obrigatório para correlação de assinatura." + ); + } + + // 1. Validação de algoritmo suportado e tamanho do digest + DigestAlgorithm algorithm = DigestAlgorithm.fromName(context.algorithm()); + algorithm.validateDigestLength(context.digest()); + + // 2. Resolução do certificado alvo + ManagedCertificate targetCertificate = resolveCertificate(context.certificateHint()); + + // 3. Validação do ciclo de vida e disponibilidade de chave privada + validateCertificateState(targetCertificate); + + // 4. Obtenção do provedor responsável + CertificateProvider provider = certificateManager.getProviderForCertificate(targetCertificate.id()); + if (provider == null || !provider.isAvailable()) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "O provedor para o certificado '" + targetCertificate.alias() + "' não está disponível no sistema." + ); + } + + logger.info("Assinando digest para requestId='{}', appId='{}', cert='{}', provider='{}', alg='{}'", + context.requestId(), context.applicationId(), targetCertificate.alias(), + provider.getProviderId(), algorithm.getJcaName()); + + // Se o contexto não tiver hint explícito, utiliza o ID do certificado resolvido + SigningContext effectiveContext = (context.certificateHint() == null || context.certificateHint().isBlank()) + ? new SigningContext(context.requestId(), context.applicationId(), context.digest(), + context.algorithm(), targetCertificate.id(), context.requestedAt()) + : context; + + // 5. Execução no provedor JCA/JCE + SigningResult result = provider.sign(effectiveContext); + + if (result == null || result.signatureBytes() == null || result.signatureBytes().length == 0) { + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "O provedor retornou uma assinatura nula ou vazia." + ); + } + + logger.info("Assinatura digital concluída com sucesso para requestId='{}'. Tamanho assinatura: {} bytes", + context.requestId(), result.signatureBytes().length); + + return result; + } + + private ManagedCertificate resolveCertificate(String hint) throws CertificateProviderException { + if (hint != null && !hint.isBlank()) { + Optional found = certificateManager.findByHint(hint); + if (found.isEmpty()) { + // Tenta rediscovery caso um token tenha acabado de ser conectado + certificateManager.discoverCertificates(); + found = certificateManager.findByHint(hint); + } + if (found.isPresent()) { + return found.get(); + } + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Nenhum certificado encontrado para o identificador ou hint: " + hint + ); + } + + // Sem hint: seleciona automaticamente se houver apenas um certificado operacional disponível + List available = certificateManager.getAvailableCertificates(); + if (available.isEmpty()) { + // Tenta redescobrir + certificateManager.discoverCertificates(); + available = certificateManager.getAvailableCertificates(); + } + + if (available.isEmpty()) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Nenhum certificado com chave privada disponível foi encontrado na máquina." + ); + } + + if (available.size() == 1) { + return available.get(0); + } + + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Existem múltiplos certificados disponíveis (" + available.size() + + "). Especifique o 'certificate_hint' para selecionar o certificado desejado." + ); + } + + private void validateCertificateState(ManagedCertificate cert) throws CertificateProviderException { + if (cert.status() == CertificateStatus.EXPIRED) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_EXPIRED, + "O certificado '" + cert.alias() + "' está expirado desde " + cert.notAfter() + ); + } + if (cert.status() == CertificateStatus.NOT_YET_VALID) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_INVALID, + "O certificado '" + cert.alias() + "' ainda não é válido. Início da validade: " + cert.notBefore() + ); + } + if (cert.status() == CertificateStatus.NO_PRIVATE_KEY) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "O certificado '" + cert.alias() + "' não possui chave privada acessível para assinatura." + ); + } + if (cert.status() == CertificateStatus.REVOKED) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_INVALID, + "O certificado '" + cert.alias() + "' consta como revogado." + ); + } + } + + public CertificateManager getCertificateManager() { + return certificateManager; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/X509CertificateUtils.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/X509CertificateUtils.java new file mode 100644 index 0000000..d0d90ee --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/crypto/X509CertificateUtils.java @@ -0,0 +1,230 @@ +package br.com.cybercore.signer.core.crypto; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.Collection; +import java.util.HexFormat; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utilitários para parsing, inspeção e extração de metadados de certificados X.509 + * com suporte nativo a extensões da Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil). + */ +public final class X509CertificateUtils { + + private static final Logger logger = LoggerFactory.getLogger(X509CertificateUtils.class); + + private static final Pattern CN_PATTERN = Pattern.compile("CN=([^,]+)", Pattern.CASE_INSENSITIVE); + private static final Pattern CPF_PATTERN = Pattern.compile("(\\d{11})"); + private static final Pattern CNPJ_PATTERN = Pattern.compile("(\\d{14})"); + + private X509CertificateUtils() { + // Utilitário estático + } + + /** + * Decodifica um certificado X.509 a partir de bytes codificados (DER ou PEM). + */ + public static X509Certificate parseCertificate(byte[] encoded) throws CertificateException { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(encoded)); + } + + /** + * Converte um X509Certificate em um ManagedCertificate imutável de domínio. + */ + public static ManagedCertificate toManagedCertificate( + String id, + String alias, + X509Certificate cert, + CertificateType type, + boolean hasPrivateKey, + String providerId, + String hardwareDeviceName + ) { + String subject = cert.getSubjectX500Principal().getName(); + String issuer = cert.getIssuerX500Principal().getName(); + String serial = cert.getSerialNumber().toString(16).toUpperCase(); + + Instant notBefore = cert.getNotBefore().toInstant(); + Instant notAfter = cert.getNotAfter().toInstant(); + Instant now = Instant.now(); + + CertificateStatus status; + if (!hasPrivateKey) { + status = CertificateStatus.NO_PRIVATE_KEY; + } else if (now.isBefore(notBefore)) { + status = CertificateStatus.NOT_YET_VALID; + } else if (now.isAfter(notAfter)) { + status = CertificateStatus.EXPIRED; + } else { + status = CertificateStatus.AVAILABLE; + } + + String cn = extractCommonName(subject); + String sha1 = calculateFingerprint(cert, "SHA-1"); + String sha256 = calculateFingerprint(cert, "SHA-256"); + + String cpf = extractCpf(cert, cn); + String cnpj = extractCnpj(cert, cn); + + int keySize = extractKeySize(cert); + String alg = cert.getPublicKey().getAlgorithm(); + + return new ManagedCertificate( + id, + alias, + type, + status, + subject, + issuer, + serial, + sha1, + sha256, + notBefore, + notAfter, + alg, + keySize, + cn, + cpf, + cnpj, + providerId, + hardwareDeviceName + ); + } + + public static String extractCommonName(String dn) { + if (dn == null) return "Desconhecido"; + Matcher matcher = CN_PATTERN.matcher(dn); + if (matcher.find()) { + return matcher.group(1).trim(); + } + return dn; + } + + public static String calculateFingerprint(X509Certificate cert, String algorithm) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] digest = md.digest(cert.getEncoded()); + return HexFormat.ofDelimiter(":").formatHex(digest).toUpperCase(); + } catch (NoSuchAlgorithmException | CertificateEncodingException e) { + logger.debug("Não foi possível calcular fingerprint {} do certificado: {}", algorithm, e.getMessage()); + return ""; + } + } + + private static int extractKeySize(X509Certificate cert) { + try { + String alg = cert.getPublicKey().getAlgorithm(); + if ("RSA".equalsIgnoreCase(alg)) { + java.security.interfaces.RSAPublicKey rsa = (java.security.interfaces.RSAPublicKey) cert.getPublicKey(); + return rsa.getModulus().bitLength(); + } else if ("EC".equalsIgnoreCase(alg) || "ECDSA".equalsIgnoreCase(alg)) { + java.security.interfaces.ECPublicKey ec = (java.security.interfaces.ECPublicKey) cert.getPublicKey(); + return ec.getParams().getCurve().getField().getFieldSize(); + } + } catch (Exception ignored) { + // Se falhar em extrair tamanho específico da chave, retorna 0 + } + return 0; + } + + /** + * Tenta extrair CPF do certificado (via ICP-Brasil SAN ou padrão comum no CN: "NOME:12345678901"). + */ + public static String extractCpf(X509Certificate cert, String commonName) { + // Tenta padrão CN ICP-Brasil comum ("FULANO DE TAL:00011122233") + if (commonName != null && commonName.contains(":")) { + String[] parts = commonName.split(":"); + if (parts.length > 1) { + String candidate = parts[parts.length - 1].trim(); + if (CPF_PATTERN.matcher(candidate).matches()) { + return candidate; + } + } + } + + // Tenta SubjectAlternativeNames + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans != null) { + for (List item : sans) { + if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) { + String content = new String(bytes, StandardCharsets.ISO_8859_1); + Matcher m = CPF_PATTERN.matcher(content); + if (m.find()) { + return m.group(1); + } + } + } + } + } catch (Exception ignored) { + // SAN não disponível ou inacessível + } + return null; + } + + /** + * Tenta extrair CNPJ do certificado para pessoas jurídicas. + */ + public static String extractCnpj(X509Certificate cert, String commonName) { + if (commonName != null && commonName.contains(":")) { + String[] parts = commonName.split(":"); + if (parts.length > 1) { + String candidate = parts[parts.length - 1].trim(); + if (CNPJ_PATTERN.matcher(candidate).matches()) { + return candidate; + } + } + } + + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans != null) { + for (List item : sans) { + if (item.size() >= 2 && item.get(1) instanceof byte[] bytes) { + String content = new String(bytes, StandardCharsets.ISO_8859_1); + Matcher m = CNPJ_PATTERN.matcher(content); + if (m.find()) { + return m.group(1); + } + } + } + } + } catch (Exception ignored) { + // SAN não disponível ou inacessível + } + return null; + } + + /** + * Extrai o nome amigável do titular (removendo sufixo :CPF ou :CNPJ comum em ICP-Brasil). + */ + public static String extractHolderName(String commonName) { + if (commonName == null || commonName.isBlank()) { + return "Titular do Certificado"; + } + if (commonName.contains(":")) { + String[] parts = commonName.split(":"); + if (parts.length > 0 && !parts[0].isBlank()) { + return parts[0].trim(); + } + } + return commonName.trim(); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticReport.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticReport.java new file mode 100644 index 0000000..63e60ab --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticReport.java @@ -0,0 +1,51 @@ +package br.com.cybercore.signer.core.diagnostics; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Relatório consolidado de integridade e diagnósticos do CyberCore Signer. + */ +public record DiagnosticReport( + Instant generatedAt, + String osName, + String osVersion, + String osArch, + String javaVersion, + String javaVendor, + String javaRuntime, + boolean isWindows, + boolean cryptoEngineOk, + List registeredSecurityProviders, + boolean windowsStoreSupported, + boolean pkcs11ModuleAvailable, + int totalCertificatesFound, + int a1CertificatesCount, + int a3CertificatesCount, + boolean localServerRunning, + int localServerPort, + Map additionalDetails +) implements Serializable { + + public DiagnosticReport { + registeredSecurityProviders = registeredSecurityProviders != null + ? List.copyOf(registeredSecurityProviders) + : Collections.emptyList(); + additionalDetails = additionalDetails != null + ? Map.copyOf(additionalDetails) + : Collections.emptyMap(); + } + + @Override + public List registeredSecurityProviders() { + return Collections.unmodifiableList(registeredSecurityProviders); + } + + @Override + public Map additionalDetails() { + return Collections.unmodifiableMap(additionalDetails); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticService.java new file mode 100644 index 0000000..48a6934 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/diagnostics/DiagnosticService.java @@ -0,0 +1,87 @@ +package br.com.cybercore.signer.core.diagnostics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Provider; +import java.security.Security; +import java.security.Signature; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Serviço responsável por verificar as capacidades do sistema, JVM e engines criptográficas. + */ +public class DiagnosticService { + + private static final Logger logger = LoggerFactory.getLogger(DiagnosticService.class); + + public DiagnosticReport runDiagnostic() { + logger.info("Iniciando diagnóstico do ambiente e engine criptográfica..."); + + String osName = System.getProperty("os.name", "Desconhecido"); + String osVersion = System.getProperty("os.version", "Desconhecido"); + String osArch = System.getProperty("os.arch", "Desconhecido"); + boolean isWindows = osName.toLowerCase().contains("win"); + + String javaVersion = System.getProperty("java.version", "Desconhecido"); + String javaVendor = System.getProperty("java.vendor", "Desconhecido"); + String javaRuntime = System.getProperty("java.runtime.name", "Desconhecido"); + + List providersList = new ArrayList<>(); + boolean windowsStoreSupported = false; + boolean pkcs11ModuleAvailable = false; + + for (Provider p : Security.getProviders()) { + providersList.add(p.getName() + " v" + p.getVersionStr()); + if ("SunMSCAPI".equalsIgnoreCase(p.getName())) { + windowsStoreSupported = true; + } + if ("SunPKCS11".equalsIgnoreCase(p.getName())) { + pkcs11ModuleAvailable = true; + } + } + + boolean cryptoEngineOk = verifyCryptoAlgorithms(); + + Map details = new HashMap<>(); + details.put("VirtualThreadsSupported", "true"); + details.put("DefaultEncoding", System.getProperty("file.encoding", "UTF-8")); + + return new DiagnosticReport( + Instant.now(), + osName, + osVersion, + osArch, + javaVersion, + javaVendor, + javaRuntime, + isWindows, + cryptoEngineOk, + providersList, + windowsStoreSupported, + pkcs11ModuleAvailable, + 0, + 0, + 0, + false, + 39741, + details + ); + } + + private boolean verifyCryptoAlgorithms() { + try { + Signature.getInstance("SHA256withRSA"); + Signature.getInstance("SHA384withRSA"); + Signature.getInstance("SHA512withRSA"); + return true; + } catch (Exception e) { + logger.error("Falha ao inicializar algoritmos padrão JCA/JCE: {}", e.getMessage()); + return false; + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateStatus.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateStatus.java new file mode 100644 index 0000000..3d5f83f --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateStatus.java @@ -0,0 +1,30 @@ +package br.com.cybercore.signer.core.domain; + +/** + * Estado de validade e disponibilidade de um certificado. + */ +public enum CertificateStatus { + AVAILABLE("Disponível", true), + EXPIRED("Expirado", false), + NOT_YET_VALID("Ainda não válido", false), + NO_PRIVATE_KEY("Sem chave privada", false), + TOKEN_DISCONNECTED("Token desconectado", false), + REVOKED("Revogado", false), + UNKNOWN("Desconhecido", false); + + private final String description; + private final boolean usableForSigning; + + CertificateStatus(String description, boolean usableForSigning) { + this.description = description; + this.usableForSigning = usableForSigning; + } + + public String getDescription() { + return description; + } + + public boolean isUsableForSigning() { + return usableForSigning; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateType.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateType.java new file mode 100644 index 0000000..6e6f1b2 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/CertificateType.java @@ -0,0 +1,32 @@ +package br.com.cybercore.signer.core.domain; + +/** + * Tipo de certificado digital suportado pelo CyberCore Signer. + */ +public enum CertificateType { + /** + * Certificado em software (.pfx / .p12). + */ + A1("A1", "Certificado Local"), + + /** + * Certificado em hardware (Smart Card / Token USB / PKCS#11 / Windows Store). + */ + A3("A3", "Token / Smart Card"); + + private final String code; + private final String label; + + CertificateType(String code, String label) { + this.code = code; + this.label = label; + } + + public String getCode() { + return code; + } + + public String getLabel() { + return label; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/ManagedCertificate.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/ManagedCertificate.java new file mode 100644 index 0000000..303d158 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/ManagedCertificate.java @@ -0,0 +1,56 @@ +package br.com.cybercore.signer.core.domain; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; + +/** + * Representação imutável de um certificado digital gerenciado pelo CyberCore Signer. + * Contém apenas metadados públicos e nunca expõe referências diretas de chave privada. + */ +public record ManagedCertificate( + String id, + String alias, + CertificateType type, + CertificateStatus status, + String subjectDn, + String issuerDn, + String serialNumber, + String sha1Fingerprint, + String sha256Fingerprint, + Instant notBefore, + Instant notAfter, + String algorithm, + int keySize, + String commonName, + String cpf, + String cnpj, + String providerId, + String hardwareDeviceName +) implements Serializable { + + public ManagedCertificate { + Objects.requireNonNull(id, "id must not be null"); + Objects.requireNonNull(alias, "alias must not be null"); + Objects.requireNonNull(type, "type must not be null"); + Objects.requireNonNull(status, "status must not be null"); + Objects.requireNonNull(subjectDn, "subjectDn must not be null"); + Objects.requireNonNull(issuerDn, "issuerDn must not be null"); + } + + /** + * Verifica se o certificado está atualmente dentro do período de validade temporal. + */ + public boolean isCurrentlyValid() { + Instant now = Instant.now(); + return (notBefore == null || !now.isBefore(notBefore)) + && (notAfter == null || !now.isAfter(notAfter)); + } + + /** + * Verifica se o certificado está apto a assinar. + */ + public boolean canSign() { + return status.isUsableForSigning() && isCurrentlyValid(); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningContext.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningContext.java new file mode 100644 index 0000000..0ced061 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningContext.java @@ -0,0 +1,55 @@ +package br.com.cybercore.signer.core.domain; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Contexto da operação de assinatura a ser processada pelo SigningService / CertificateProvider. + */ +public record SigningContext( + String requestId, + String applicationId, + byte[] digest, + String algorithm, + String certificateHint, + Instant requestedAt +) { + public SigningContext { + Objects.requireNonNull(requestId, "requestId must not be null"); + Objects.requireNonNull(applicationId, "applicationId must not be null"); + Objects.requireNonNull(digest, "digest must not be null"); + Objects.requireNonNull(algorithm, "algorithm must not be null"); + digest = digest.clone(); + if (requestedAt == null) { + requestedAt = Instant.now(); + } + } + + @Override + public byte[] digest() { + return digest.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SigningContext that = (SigningContext) o; + return Objects.equals(requestId, that.requestId) + && Objects.equals(applicationId, that.applicationId) + && Arrays.equals(digest, that.digest) + && Objects.equals(algorithm, that.algorithm) + && Objects.equals(certificateHint, that.certificateHint) + && Objects.equals(requestedAt, that.requestedAt); + } + + @Override + public int hashCode() { + int result = Objects.hash(requestId, applicationId, algorithm, certificateHint, requestedAt); + result = 31 * result + Arrays.hashCode(digest); + return result; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningKeyMaterial.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningKeyMaterial.java new file mode 100644 index 0000000..66a0496 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningKeyMaterial.java @@ -0,0 +1,38 @@ +package br.com.cybercore.signer.core.domain; + +import java.security.PrivateKey; +import java.security.Provider; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Objects; + +/** + * Contêiner seguro para material criptográfico de assinatura (chave privada e cadeia X.509). + * Permite que serviços de alto nível (PDF/PAdES e XMLDSig) realizem assinaturas estruturadas. + * A chave privada e a cadeia permanecem estritamente na máquina local do usuário. + */ +public record SigningKeyMaterial( + PrivateKey privateKey, + X509Certificate[] certificateChain, + Provider jcaProvider +) { + + public SigningKeyMaterial { + Objects.requireNonNull(privateKey, "Chave privada não pode ser nula."); + Objects.requireNonNull(certificateChain, "Cadeia de certificados não pode ser nula."); + if (certificateChain.length == 0) { + throw new IllegalArgumentException("A cadeia de certificados deve conter ao menos um certificado."); + } + // Cópia defensiva + certificateChain = Arrays.copyOf(certificateChain, certificateChain.length); + } + + public X509Certificate signingCertificate() { + return certificateChain[0]; + } + + @Override + public X509Certificate[] certificateChain() { + return Arrays.copyOf(certificateChain, certificateChain.length); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningResult.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningResult.java new file mode 100644 index 0000000..4ff73c3 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/domain/SigningResult.java @@ -0,0 +1,63 @@ +package br.com.cybercore.signer.core.domain; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Resultado da operação de assinatura digital. + */ +public record SigningResult( + String requestId, + byte[] signatureBytes, + String certificateBase64, + List certificateChainBase64, + String algorithm, + Instant signedAt +) { + public SigningResult { + Objects.requireNonNull(requestId, "requestId must not be null"); + Objects.requireNonNull(signatureBytes, "signatureBytes must not be null"); + Objects.requireNonNull(certificateBase64, "certificateBase64 must not be null"); + Objects.requireNonNull(algorithm, "algorithm must not be null"); + signatureBytes = signatureBytes.clone(); + certificateChainBase64 = certificateChainBase64 != null + ? List.copyOf(certificateChainBase64) + : Collections.singletonList(certificateBase64); + if (signedAt == null) { + signedAt = Instant.now(); + } + } + + @Override + public byte[] signatureBytes() { + return signatureBytes.clone(); + } + + @Override + public List certificateChainBase64() { + return Collections.unmodifiableList(certificateChainBase64); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SigningResult that = (SigningResult) o; + return Objects.equals(requestId, that.requestId) + && Arrays.equals(signatureBytes, that.signatureBytes) + && Objects.equals(certificateBase64, that.certificateBase64) + && Objects.equals(certificateChainBase64, that.certificateChainBase64) + && Objects.equals(algorithm, that.algorithm) + && Objects.equals(signedAt, that.signedAt); + } + + @Override + public int hashCode() { + int result = Objects.hash(requestId, certificateBase64, certificateChainBase64, algorithm, signedAt); + result = 31 * result + Arrays.hashCode(signatureBytes); + return result; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPageChoice.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPageChoice.java new file mode 100644 index 0000000..930803a --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPageChoice.java @@ -0,0 +1,20 @@ +package br.com.cybercore.signer.core.pdf; + +/** + * Seleção da página do PDF na qual o selo visual da assinatura digital será carimbado. + */ +public enum PdfSealPageChoice { + LAST_PAGE("Última Página (Recomendado)"), + FIRST_PAGE("Primeira Página"), + ALL_PAGES("Todas as Páginas"); + + private final String description; + + PdfSealPageChoice(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPosition.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPosition.java new file mode 100644 index 0000000..15f70d8 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSealPosition.java @@ -0,0 +1,22 @@ +package br.com.cybercore.signer.core.pdf; + +/** + * Posição do selo visual de assinatura na página do documento PDF. + */ +public enum PdfSealPosition { + BOTTOM_RIGHT("Canto Inferior Direito"), + BOTTOM_LEFT("Canto Inferior Esquerdo"), + TOP_RIGHT("Canto Superior Direito"), + TOP_LEFT("Canto Superior Esquerdo"), + CUSTOM("Posição Personalizada por Clique na Página"); + + private final String description; + + PdfSealPosition(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSignatureOptions.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSignatureOptions.java new file mode 100644 index 0000000..a133c18 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSignatureOptions.java @@ -0,0 +1,186 @@ +package br.com.cybercore.signer.core.pdf; + +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; + +import java.util.Objects; + +/** + * Parâmetros de configuração para a operação de assinatura digital PAdES com selo visual em PDF, + * incluindo suporte a posicionamento predefinido ou coordenadas personalizadas na página, + * e preferências granulares para exibição de CPF e data/hora. + */ +public record PdfSignatureOptions( + String certificateId, + char[] pinOrPassword, + boolean visibleSeal, + PdfSealTheme sealTheme, + PdfSealPosition position, + PdfSealPageChoice pageChoice, + String reason, + String location, + Float customX, + Float customY, + Integer targetPageNumber, + CpfDisplayMode cpfDisplayMode, + Boolean includeDateTime, + String customExtraInfo +) { + + public PdfSignatureOptions { + Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo."); + if (sealTheme == null) { + sealTheme = PdfSealThemeRegistry.getDefaultTheme(); + } + if (position == null) { + position = (customX != null && customY != null) ? PdfSealPosition.CUSTOM : PdfSealPosition.BOTTOM_RIGHT; + } + if (pageChoice == null) { + pageChoice = PdfSealPageChoice.LAST_PAGE; + } + if (reason == null || reason.isBlank()) { + reason = "Assinatura Digital de Documento"; + } + if (location == null || location.isBlank()) { + location = "Brasil"; + } + if (cpfDisplayMode == null) { + cpfDisplayMode = (sealTheme.defaultCpfMode() != null) ? sealTheme.defaultCpfMode() : CpfDisplayMode.FULL; + } + if (includeDateTime == null) { + includeDateTime = sealTheme.defaultIncludeDateTime(); + } + } + + /** + * Construtor para compatibilidade com assinaturas anteriores a customExtraInfo. + */ + public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, + PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, + String reason, String location, Float customX, Float customY, + Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) { + this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, + customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null); + } + + /** + * Construtor de compatibilidade para assinatura com posições pré-fixadas por enum. + */ + public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, + PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, + String reason, String location) { + this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null); + } + + public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, + PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, + String reason, String location, Float customX, Float customY, Integer targetPageNumber) { + this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, customX, customY, targetPageNumber, null, null, null); + } + + public static Builder builder(String certificateId) { + return new Builder(certificateId); + } + + public static final class Builder { + private final String certificateId; + private char[] pinOrPassword; + private boolean visibleSeal = true; + private PdfSealTheme sealTheme; + private PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT; + private PdfSealPageChoice pageChoice = PdfSealPageChoice.LAST_PAGE; + private String reason = "Assinatura Digital ICP-Brasil"; + private String location = "Brasil"; + private Float customX; + private Float customY; + private Integer targetPageNumber; + private CpfDisplayMode cpfDisplayMode; + private Boolean includeDateTime; + + public Builder(String certificateId) { + this.certificateId = Objects.requireNonNull(certificateId); + } + + public Builder pinOrPassword(char[] pin) { + this.pinOrPassword = pin; + return this; + } + + public Builder visibleSeal(boolean visible) { + this.visibleSeal = visible; + return this; + } + + public Builder sealTheme(PdfSealTheme theme) { + this.sealTheme = theme; + return this; + } + + public Builder position(PdfSealPosition pos) { + this.position = pos; + return this; + } + + public Builder pageChoice(PdfSealPageChoice choice) { + this.pageChoice = choice; + return this; + } + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public Builder location(String location) { + this.location = location; + return this; + } + + public Builder cpfDisplayMode(CpfDisplayMode mode) { + this.cpfDisplayMode = mode; + return this; + } + + private String customExtraInfo; + + public Builder includeDateTime(boolean include) { + this.includeDateTime = include; + return this; + } + + public Builder customExtraInfo(String info) { + this.customExtraInfo = info; + return this; + } + + public Builder customPosition(Float x, Float y, Integer pageNumber) { + this.customX = x; + this.customY = y; + this.targetPageNumber = pageNumber; + if (x != null && y != null) { + this.position = PdfSealPosition.CUSTOM; + } + return this; + } + + public PdfSignatureOptions build() { + return new PdfSignatureOptions( + certificateId, + pinOrPassword, + visibleSeal, + sealTheme, + position, + pageChoice, + reason, + location, + customX, + customY, + targetPageNumber, + cpfDisplayMode, + includeDateTime, + customExtraInfo + ); + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java new file mode 100644 index 0000000..d895faa --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java @@ -0,0 +1,351 @@ +package br.com.cybercore.signer.core.pdf; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningKeyMaterial; +import br.com.cybercore.signer.core.pdf.seal.PdfSealData; +import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Serviço de alto nível para Assinatura Digital de Documentos PDF no padrão PAdES (PDF Advanced Electronic Signatures), + * com suporte à aplicação de Selos Visuais estilizados por Temas SVG, posicionamento livre por clique na página e renderização de previews. + */ +public class PdfSigningService { + + private static final Logger logger = LoggerFactory.getLogger(PdfSigningService.class); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss") + .withZone(ZoneId.systemDefault()); + + private final CertificateManager certificateManager; + + public PdfSigningService(CertificateManager certificateManager) { + this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo."); + } + + /** + * Retorna a quantidade total de páginas contidas no documento PDF. + */ + public static int getPageCount(byte[] pdfBytes) throws IOException { + Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos."); + try (PDDocument doc = Loader.loadPDF(pdfBytes)) { + return doc.getNumberOfPages(); + } + } + + /** + * Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada. + */ + public static BufferedImage renderPageImage(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException { + Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos."); + try (PDDocument doc = Loader.loadPDF(pdfBytes)) { + if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) { + throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages()); + } + PDFRenderer renderer = new PDFRenderer(doc); + return renderer.renderImageWithDPI(pageIndex0Based, dpi); + } + } + + /** + * Renderiza uma página específica do PDF diretamente para bytes de imagem PNG. + */ + public static byte[] renderPagePngBytes(byte[] pdfBytes, int pageIndex0Based, float dpi) throws IOException { + BufferedImage image = renderPageImage(pdfBytes, pageIndex0Based, dpi); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + return baos.toByteArray(); + } + + /** + * Assina digitalmente um arquivo PDF do sistema de arquivos e grava o resultado no arquivo de destino. + */ + public void signPdfFile(File sourcePdf, File targetPdf, PdfSignatureOptions options) throws Exception { + Objects.requireNonNull(sourcePdf, "Arquivo PDF de origem não pode ser nulo."); + Objects.requireNonNull(targetPdf, "Arquivo PDF de destino não pode ser nulo."); + Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas."); + + byte[] inputBytes = Files.readAllBytes(sourcePdf.toPath()); + byte[] signedBytes = signPdf(inputBytes, options); + + if (targetPdf.getParentFile() != null) { + targetPdf.getParentFile().mkdirs(); + } + try (FileOutputStream fos = new FileOutputStream(targetPdf)) { + fos.write(signedBytes); + } + logger.info("PDF assinado com sucesso e salvo em: {}", targetPdf.getAbsolutePath()); + } + + /** + * Assina digitalmente os bytes de um documento PDF, aplicando o selo visual configurado + * e retornando o PDF assinado completo com envelope PAdES. + */ + public byte[] signPdf(byte[] pdfBytes, PdfSignatureOptions options) throws Exception { + Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos."); + Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas."); + + // 1. Localiza o certificado e o material criptográfico (chave e cadeia) + Optional certOpt = certificateManager.findById(options.certificateId()); + if (certOpt.isEmpty()) { + certOpt = certificateManager.findByHint(options.certificateId()); + } + if (certOpt.isEmpty()) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Certificado não encontrado no gerenciador para ID: " + options.certificateId() + ); + } + + ManagedCertificate cert = certOpt.get(); + Optional keyMaterialOpt = certificateManager.getKeyMaterial(cert.id(), options.pinOrPassword()); + if (keyMaterialOpt.isEmpty()) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "Chave privada inacessível para o certificado '" + cert.alias() + "'." + ); + } + SigningKeyMaterial keyMaterial = keyMaterialOpt.get(); + + // 2. Prepara dados do signatário e resumo para o selo visual + String signerDoc = cert.cpf() != null ? "CPF: " + cert.cpf() : (cert.cnpj() != null ? "CNPJ: " + cert.cnpj() : ""); + String issuerCn = extractIssuerCn(cert.issuerDn()); + String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now()); + + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + byte[] pdfDigest = sha256.digest(pdfBytes); + String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase(); + + PdfSealTheme theme = options.sealTheme(); + String effectiveExtraInfo = (options.customExtraInfo() != null && !options.customExtraInfo().isBlank()) + ? options.customExtraInfo().trim() + : theme.formatCustomField(); + + // 3. Monta dados do selo visual + PdfSealData sealData = new PdfSealData( + cert.commonName() != null ? cert.commonName() : cert.alias(), + signerDoc, + signingTimeStr, + issuerCn, + hashShort, + options.reason(), + options.location(), + options.cpfDisplayMode(), + options.includeDateTime(), + effectiveExtraInfo + ); + + // 4. Carrega o PDF e estampa o selo se solicitado + byte[] intermediatePdf; + if (options.visibleSeal()) { + byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData); + intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options); + } else { + intermediatePdf = pdfBytes; + } + + // 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle + ByteArrayOutputStream finalOut = new ByteArrayOutputStream(); + try (PDDocument doc = Loader.loadPDF(intermediatePdf)) { + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(cert.commonName()); + signature.setLocation(options.location()); + signature.setReason(options.reason()); + signature.setSignDate(Calendar.getInstance()); + + SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial); + doc.addSignature(signature, signer); + doc.saveIncremental(finalOut); + } + + logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size()); + return finalOut.toByteArray(); + } + + private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme, + PdfSignatureOptions options) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PDDocument doc = Loader.loadPDF(pdfBytes)) { + int totalPages = doc.getNumberOfPages(); + if (totalPages == 0) { + return pdfBytes; + } + + PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal"); + + float sealWidth = theme.width() * 0.72f; + float sealHeight = theme.height() * 0.72f; + float margin = 20f; + + // Se coordenadas customizadas foram informadas (clique ou drag & drop na página) + if (options.customX() != null && options.customY() != null) { + int targetPageIdx = totalPages - 1; + if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) { + targetPageIdx = options.targetPageNumber() - 1; + } + + PDPage page = doc.getPage(targetPageIdx); + PDRectangle mediaBox = page.getMediaBox(); + float pageW = mediaBox.getWidth(); + float pageH = mediaBox.getHeight(); + + float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth)); + float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight)); + + try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { + cs.drawImage(pdImage, x, y, sealWidth, sealHeight); + } + + doc.save(baos); + return baos.toByteArray(); + } + + // Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.) + int startPage = 0; + int endPage = totalPages - 1; + + if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) { + endPage = 0; + } else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) { + startPage = totalPages - 1; + } + + for (int i = startPage; i <= endPage; i++) { + PDPage page = doc.getPage(i); + PDRectangle mediaBox = page.getMediaBox(); + float pageW = mediaBox.getWidth(); + float pageH = mediaBox.getHeight(); + + float x; + float y; + + switch (options.position()) { + case BOTTOM_LEFT -> { + x = margin; + y = margin; + } + case TOP_RIGHT -> { + x = pageW - sealWidth - margin; + y = pageH - sealHeight - margin; + } + case TOP_LEFT -> { + x = margin; + y = pageH - sealHeight - margin; + } + default -> { // BOTTOM_RIGHT + x = pageW - sealWidth - margin; + y = margin; + } + } + + try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { + cs.drawImage(pdImage, x, y, sealWidth, sealHeight); + } + } + + doc.save(baos); + } + return baos.toByteArray(); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + private static String extractIssuerCn(String dn) { + if (dn == null) return "ICP-Brasil"; + int idx = dn.indexOf("CN="); + if (idx >= 0) { + int end = dn.indexOf(',', idx); + return end > idx ? dn.substring(idx + 3, end).trim() : dn.substring(idx + 3).trim(); + } + return dn; + } + + /** + * Assinador PAdES CMS/PKCS#7 desacoplado via Bouncy Castle. + */ + private static class PadesBouncyCastleSigner implements SignatureInterface { + private final SigningKeyMaterial keyMaterial; + + public PadesBouncyCastleSigner(SigningKeyMaterial keyMaterial) { + this.keyMaterial = Objects.requireNonNull(keyMaterial); + } + + @Override + public byte[] sign(InputStream content) throws IOException { + try { + byte[] messageBytes = content.readAllBytes(); + + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA") + .build(keyMaterial.privateKey()); + + X509Certificate userCert = keyMaterial.certificateChain()[0]; + + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build()) + .build(contentSigner, userCert)); + + gen.addCertificates(new JcaCertStore(Arrays.asList(keyMaterial.certificateChain()))); + + CMSTypedData msg = new CMSProcessableByteArray(messageBytes); + CMSSignedData signedData = gen.generate(msg, false); + + return signedData.getEncoded(); + } catch (Exception e) { + logger.error("Falha ao gerar assinatura CMS/PKCS#7 para PDF", e); + throw new IOException("Falha criptográfica no Bouncy Castle: " + e.getMessage(), e); + } + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayMode.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayMode.java new file mode 100644 index 0000000..4a0ac64 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayMode.java @@ -0,0 +1,64 @@ +package br.com.cybercore.signer.core.pdf.seal; + +/** + * Modo de exibição do CPF do signatário no selo visual de assinatura digital. + * Permite balancear autenticidade documental e privacidade de dados (LGPD). + */ +public enum CpfDisplayMode { + + /** + * O CPF não é exibido no selo visual. + */ + NONE("Ocultar CPF"), + + /** + * O CPF é exibido parcialmente mascarado (ex: ***.456.789-**). + */ + MASKED("Mascarado (***.456.789-**)"), + + /** + * O CPF é exibido integralmente (ex: 123.456.789-00). + */ + FULL("Completo (123.456.789-00)"); + + private final String description; + + CpfDisplayMode(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + /** + * Formata um CPF bruto ou previamente formatado de acordo com este modo. + * + * @param rawCpf CPF a ser formatado (com ou sem pontuação) + * @return String formatada conforme o modo escolhido, ou vazio se não houver CPF + */ + public String formatCpf(String rawCpf) { + if (this == NONE || rawCpf == null || rawCpf.isBlank()) { + return ""; + } + + String digits = rawCpf.replaceAll("\\D", ""); + if (digits.length() != 11) { + // Se não for um CPF de 11 dígitos, retorna mascarado ou o próprio texto + return this == MASKED ? "***.***.***-**" : rawCpf; + } + + if (this == MASKED) { + // Mascara os 3 primeiros e os 2 dígitos verificadores: ***.456.789-** + String part2 = digits.substring(3, 6); + String part3 = digits.substring(6, 9); + return "***." + part2 + "." + part3 + "-**"; + } + + // FULL: 123.456.789-00 + return digits.substring(0, 3) + "." + + digits.substring(3, 6) + "." + + digits.substring(6, 9) + "-" + + digits.substring(9, 11); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorage.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorage.java new file mode 100644 index 0000000..29cbfaa --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorage.java @@ -0,0 +1,175 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Gerenciador de armazenamento em disco para Selos Visuais Personalizados criados ou instalados pelo usuário. + * Persiste cada selo como um arquivo JSON no diretório ~/.cybercore/seals/. + */ +public final class CustomSealStorage { + + private static final Logger logger = LoggerFactory.getLogger(CustomSealStorage.class); + private static final String SEALS_DIR_NAME = "seals"; + + private final Path storageDir; + private final ObjectMapper objectMapper; + + public CustomSealStorage() { + this(resolveDefaultStorageDir()); + } + + public CustomSealStorage(Path storageDir) { + this.storageDir = Objects.requireNonNull(storageDir); + this.objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + ensureDirectoryExists(); + } + + private static Path resolveDefaultStorageDir() { + String userHome = System.getProperty("user.home", "."); + return Path.of(userHome, ".cybercore", SEALS_DIR_NAME); + } + + private void ensureDirectoryExists() { + try { + if (!Files.exists(storageDir)) { + Files.createDirectories(storageDir); + } + } catch (IOException e) { + logger.error("Falha ao criar diretório de selos personalizados: {}", storageDir, e); + } + } + + public Path getStorageDir() { + return storageDir; + } + + /** + * Salva ou atualiza um selo personalizado em disco. + */ + public void saveSeal(PdfSealTheme seal) throws IOException { + Objects.requireNonNull(seal, "Selo não pode ser nulo."); + ensureDirectoryExists(); + + String safeFileName = sanitizeFileName(seal.id()) + ".json"; + Path targetPath = storageDir.resolve(safeFileName); + + // Marca como personalizado + PdfSealTheme customSeal = new PdfSealTheme( + seal.id(), + seal.name(), + seal.description(), + seal.width(), + seal.height(), + seal.primaryColor(), + seal.secondaryColor(), + seal.backgroundColor(), + seal.borderColor(), + seal.textColor(), + seal.svgTemplate(), + seal.customHeader(), + seal.customFooter(), + seal.iconStyle(), + seal.defaultCpfMode(), + seal.defaultIncludeDateTime(), + true, + seal.customLogoBase64(), + seal.customFieldLabel(), + seal.customFieldValue() + ); + + objectMapper.writeValue(targetPath.toFile(), customSeal); + logger.info("Selo personalizado '{}' instalado com sucesso em: {}", seal.id(), targetPath); + } + + /** + * Carrega todos os selos personalizados instalados no diretório do usuário. + */ + public List loadAllCustomSeals() { + List seals = new ArrayList<>(); + if (!Files.exists(storageDir)) { + return seals; + } + + try (Stream stream = Files.list(storageDir)) { + stream.filter(p -> p.toString().toLowerCase().endsWith(".json")) + .forEach(file -> { + try { + PdfSealTheme seal = objectMapper.readValue(file.toFile(), PdfSealTheme.class); + seals.add(seal); + } catch (Exception e) { + logger.warn("Falha ao carregar arquivo de selo {}: {}", file.getFileName(), e.getMessage()); + } + }); + } catch (IOException e) { + logger.error("Erro ao listar diretório de selos {}", storageDir, e); + } + + return seals; + } + + /** + * Remove um selo personalizado do disco. + */ + public boolean deleteSeal(String sealId) { + if (sealId == null || sealId.isBlank()) return false; + String safeFileName = sanitizeFileName(sealId) + ".json"; + Path targetPath = storageDir.resolve(safeFileName); + + try { + boolean deleted = Files.deleteIfExists(targetPath); + if (deleted) { + logger.info("Selo personalizado '{}' removido do disco.", sealId); + } + return deleted; + } catch (IOException e) { + logger.error("Erro ao excluir selo {}: {}", sealId, e.getMessage()); + return false; + } + } + + /** + * Importa um selo a partir de um arquivo externo (.json) e o instala no diretório do usuário. + */ + public PdfSealTheme importSeal(Path externalFile) throws IOException { + Objects.requireNonNull(externalFile, "Arquivo de importação não pode ser nulo."); + if (!Files.exists(externalFile)) { + throw new IllegalArgumentException("O arquivo de selo especificado não existe: " + externalFile); + } + + PdfSealTheme seal = objectMapper.readValue(externalFile.toFile(), PdfSealTheme.class); + saveSeal(seal); + return seal; + } + + /** + * Exporta um selo para um arquivo externo de destino. + */ + public void exportSeal(PdfSealTheme seal, Path destinationFile) throws IOException { + Objects.requireNonNull(seal, "Selo não pode ser nulo."); + Objects.requireNonNull(destinationFile, "Destino não pode ser nulo."); + + if (destinationFile.getParent() != null && !Files.exists(destinationFile.getParent())) { + Files.createDirectories(destinationFile.getParent()); + } + + objectMapper.writeValue(destinationFile.toFile(), seal); + logger.info("Selo '{}' exportado com sucesso para: {}", seal.id(), destinationFile); + } + + private static String sanitizeFileName(String id) { + return id.replaceAll("[^a-zA-Z0-9._-]", "_"); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealData.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealData.java new file mode 100644 index 0000000..cbb20ad --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealData.java @@ -0,0 +1,64 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Metadados e dados do signatário a serem gravados e representados no selo visual da assinatura PDF. + */ +public record PdfSealData( + String signerName, + String identifier, + String signingDate, + String issuer, + String hashSummary, + String reason, + String location, + CpfDisplayMode cpfDisplayMode, + boolean includeDateTime, + String customExtraInfo +) { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss (z)") + .withZone(ZoneId.systemDefault()); + + public PdfSealData { + signerName = (signerName != null && !signerName.isBlank()) ? signerName : "Signatário ICP-Brasil"; + identifier = (identifier != null && !identifier.isBlank()) ? identifier : ""; + signingDate = (signingDate != null && !signingDate.isBlank()) ? signingDate : FORMATTER.format(Instant.now()); + issuer = (issuer != null && !issuer.isBlank()) ? issuer : "Autoridade Certificadora ICP-Brasil"; + hashSummary = (hashSummary != null && !hashSummary.isBlank()) ? hashSummary : ""; + reason = (reason != null && !reason.isBlank()) ? reason : "Assinatura Digital de Documento"; + location = (location != null && !location.isBlank()) ? location : "Brasil"; + if (cpfDisplayMode == null) { + cpfDisplayMode = CpfDisplayMode.FULL; + } + } + + /** + * Construtor para compatibilidade com assinaturas anteriores a customExtraInfo. + */ + public PdfSealData(String signerName, String identifier, String signingDate, String issuer, + String hashSummary, String reason, String location, + CpfDisplayMode cpfDisplayMode, boolean includeDateTime) { + this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, cpfDisplayMode, includeDateTime, null); + } + + /** + * Construtor de compatibilidade para código que não especifica cpfDisplayMode e includeDateTime. + */ + public PdfSealData(String signerName, String identifier, String signingDate, String issuer, + String hashSummary, String reason, String location) { + this(signerName, identifier, signingDate, issuer, hashSummary, reason, location, CpfDisplayMode.FULL, true, null); + } + + public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary) { + return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, CpfDisplayMode.FULL, true); + } + + public static PdfSealData of(String signerName, String identifier, String issuer, String hashSummary, + CpfDisplayMode cpfDisplayMode, boolean includeDateTime) { + return new PdfSealData(signerName, identifier, null, issuer, hashSummary, null, null, cpfDisplayMode, includeDateTime); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java new file mode 100644 index 0000000..baf143d --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRenderer.java @@ -0,0 +1,411 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import javax.imageio.ImageIO; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Objects; + +/** + * Renderizador de alta fidelidade para Selos Visuais de Assinatura PDF. + * Gera imagens nítidas (300 DPI equivalente) baseadas nos Temas e vetores SVG, + * utilizando exclusivamente a API gráfica padrão Java2D (AWT), sem dependência de JavaFX. + * + * Garante que o nome completo do signatário seja sempre exibido na íntegra, + * além de respeitar as opções de CPF (completo, mascarado ou omitido) e data/hora. + */ +public final class PdfSealRenderer { + + private PdfSealRenderer() { + } + + /** + * Renderiza o selo visual baseado no tema e dados fornecidos para um BufferedImage em escala 2x (alta nitidez). + */ + public static BufferedImage renderToImage(PdfSealTheme theme, PdfSealData data) { + Objects.requireNonNull(theme, "Tema não pode ser nulo."); + Objects.requireNonNull(data, "Dados da assinatura não podem ser nulos."); + + int baseWidth = theme.width(); + int baseHeight = theme.height(); + // Escala 2x para renderização super-nítida no documento PDF e na interface + int scale = 2; + int imgWidth = baseWidth * scale; + int imgHeight = baseHeight * scale; + + BufferedImage image = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + + try { + // Configuração de anti-aliasing e renderização de alta qualidade + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + + g2d.scale(scale, scale); + + Color primaryColor = parseColor(theme.primaryColor(), new Color(13, 71, 161)); + Color secondaryColor = parseColor(theme.secondaryColor(), new Color(2, 132, 199)); + Color bgColor = parseColor(theme.backgroundColor(), Color.WHITE); + Color borderColor = parseColor(theme.borderColor(), new Color(203, 213, 225)); + Color textColor = parseColor(theme.textColor(), new Color(15, 23, 42)); + + // 1. Fundo do Selo com cantos arredondados + g2d.setColor(bgColor); + g2d.fill(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8)); + + // 2. Borda externa + g2d.setColor(borderColor); + g2d.setStroke(new BasicStroke(1.2f)); + g2d.draw(new RoundRectangle2D.Float(1, 1, baseWidth - 2, baseHeight - 2, 8, 8)); + + // 3. Tarja/Acento lateral do tema + g2d.setColor(primaryColor); + g2d.fill(new RoundRectangle2D.Float(1, 1, 5, baseHeight - 2, 3, 3)); + + if ("gov-br".equalsIgnoreCase(theme.id()) || "star".equalsIgnoreCase(theme.iconStyle())) { + g2d.setColor(secondaryColor); + g2d.fillRect(6, 1, 3, baseHeight - 2); + } + + // 4. Emblema à Esquerda (Logo Customizada em Imagem ou Ícone Vetorial) + if (theme.customLogoBase64() != null && !theme.customLogoBase64().isBlank()) { + drawCustomLogo(g2d, theme.customLogoBase64(), 10, 14, 36, baseHeight - 28); + } else { + String effectiveIcon = (theme.iconStyle() != null && !theme.iconStyle().isBlank()) ? theme.iconStyle() : theme.id(); + drawThemeIcon(g2d, effectiveIcon, primaryColor, secondaryColor); + } + + // 5. Informações Textuais da Assinatura (Focado no Titular, sem dados técnicos/emissor) + int textX = 52; + int textWidthAvailable = baseWidth - textX - 12; + + String formattedDoc = formatSignerDoc(data); + String customInfo = (data.customExtraInfo() != null && !data.customExtraInfo().isBlank()) + ? data.customExtraInfo().trim() + : theme.formatCustomField(); + + String identityLine; + if (!formattedDoc.isBlank() && !customInfo.isBlank()) { + identityLine = formattedDoc + " • " + customInfo; + } else if (!formattedDoc.isBlank()) { + identityLine = formattedDoc; + } else { + identityLine = customInfo; + } + + boolean hasIdentity = !identityLine.isBlank(); + boolean hasDate = data.includeDateTime(); + + String fullName = data.signerName() != null ? data.signerName().trim() : "Signatário ICP-Brasil"; + String headerText = resolveHeaderText(theme); + String footerText = resolveFooterText(theme, null); + + if (hasIdentity && hasDate) { + // Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé + g2d.setColor(primaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9)); + g2d.drawString(headerText, textX, 16); + + g2d.setColor(textColor); + drawFullSignerName(g2d, fullName, textX, 33, textWidthAvailable, 13.0f); + + g2d.setColor(new Color(51, 65, 85)); + g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10)); + drawFittedText(g2d, identityLine, textX, 47, textWidthAvailable, 10.0f); + + g2d.drawString("Data: " + data.signingDate(), textX, 60); + + g2d.setColor(secondaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); + g2d.drawString(footerText, textX, 73); + + } else if (hasIdentity || hasDate) { + // Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé + g2d.setColor(primaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10)); + g2d.drawString(headerText, textX, 17); + + g2d.setColor(textColor); + drawFullSignerName(g2d, fullName, textX, 36, textWidthAvailable, 13.5f); + + g2d.setColor(new Color(51, 65, 85)); + g2d.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 10)); + String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate()); + drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f); + + g2d.setColor(secondaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); + g2d.drawString(footerText, textX, 71); + + } else { + // Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé + g2d.setColor(primaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 10)); + g2d.drawString(headerText, textX, 19); + + g2d.setColor(textColor); + drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f); + + g2d.setColor(secondaryColor); + g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9)); + g2d.drawString(footerText, textX, 68); + } + + } finally { + g2d.dispose(); + } + + return image; + } + + /** + * Desenha o logotipo personalizado fornecido em Base64, preservando o aspecto proporcional + * e aplicando anti-aliasing e interpolação bilinear de alta fidelidade. + */ + private static void drawCustomLogo(Graphics2D g2d, String base64Data, int x, int y, int maxWidth, int maxHeight) { + try { + String cleanBase64 = base64Data; + int commaIdx = cleanBase64.indexOf(","); + if (commaIdx >= 0 && cleanBase64.substring(0, commaIdx).contains("base64")) { + cleanBase64 = cleanBase64.substring(commaIdx + 1); + } + byte[] bytes = java.util.Base64.getDecoder().decode(cleanBase64.trim()); + BufferedImage logoImg = ImageIO.read(new java.io.ByteArrayInputStream(bytes)); + if (logoImg != null) { + int originalW = logoImg.getWidth(); + int originalH = logoImg.getHeight(); + if (originalW > 0 && originalH > 0) { + double scaleW = (double) maxWidth / originalW; + double scaleH = (double) maxHeight / originalH; + double scale = Math.min(scaleW, scaleH); + int targetW = Math.max(1, (int) Math.round(originalW * scale)); + int targetH = Math.max(1, (int) Math.round(originalH * scale)); + + int posX = x + (maxWidth - targetW) / 2; + int posY = y + (maxHeight - targetH) / 2; + + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2d.drawImage(logoImg, posX, posY, targetW, targetH, null); + return; + } + } + } catch (Exception ignored) { + // Se houver falha na leitura do logo, recorre ao escudo padrão + } + drawThemeIcon(g2d, "shield", new Color(13, 71, 161), new Color(2, 132, 199)); + } + + /** + * Desenha o nome completo do signatário sem cortes ou truncamentos. + * Ajusta a fonte gradativamente para caber perfeitamente na largura do selo. + */ + private static void drawFullSignerName(Graphics2D g2d, String fullName, int x, int y, int maxWidth, float preferredSize) { + float fontSize = preferredSize; + Font font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize)); + FontMetrics fm = g2d.getFontMetrics(font); + + // Se o nome for longo, reduz o tamanho da fonte até caber perfeitamente + while (fm.stringWidth(fullName) > maxWidth && fontSize > 8.0f) { + fontSize -= 0.5f; + font = new Font(Font.SANS_SERIF, Font.BOLD, Math.round(fontSize)); + fm = g2d.getFontMetrics(font); + } + + g2d.setFont(font); + g2d.drawString(fullName, x, y); + } + + /** + * Desenha texto informativo secundário ajustando suavemente a fonte caso exceda a largura. + */ + private static void drawFittedText(Graphics2D g2d, String text, int x, int y, int maxWidth, float baseSize) { + if (text == null || text.isBlank()) return; + float fontSize = baseSize; + Font font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize)); + FontMetrics fm = g2d.getFontMetrics(font); + + while (fm.stringWidth(text) > maxWidth && fontSize > 7.5f) { + fontSize -= 0.5f; + font = new Font(Font.SANS_SERIF, Font.PLAIN, Math.round(fontSize)); + fm = g2d.getFontMetrics(font); + } + + g2d.setFont(font); + g2d.drawString(text, x, y); + } + + /** + * Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado. + */ + private static String formatSignerDoc(PdfSealData data) { + CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL; + String rawDoc = data.identifier() != null ? data.identifier() : ""; + + if (mode == CpfDisplayMode.NONE) { + // Se for CPF, oculta. Se for CNPJ, pode manter ou formatar. + if (rawDoc.toUpperCase().contains("CPF")) { + return ""; + } + return rawDoc; + } + + if (rawDoc.toUpperCase().contains("CPF")) { + String digits = rawDoc.replaceAll("\\D", ""); + String formatted = mode.formatCpf(digits); + return formatted.isBlank() ? "" : "CPF: " + formatted; + } + + return rawDoc; + } + + /** + * Renderiza o selo visual e o exporta diretamente como array de bytes PNG. + */ + public static byte[] renderToPngBytes(PdfSealTheme theme, PdfSealData data) throws IOException { + BufferedImage img = renderToImage(theme, data); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(img, "PNG", baos); + return baos.toByteArray(); + } + + private static void drawThemeIcon(Graphics2D g2d, String iconStyleOrId, Color primary, Color secondary) { + g2d.translate(14, 18); + + String style = (iconStyleOrId != null) ? iconStyleOrId.toLowerCase() : "shield"; + + if ("cybercore-modern".equals(style) || "crystal".equals(style)) { + // Cristal CyberCore Vetorial + Path2D.Float crystal = new Path2D.Float(); + crystal.moveTo(16, 2); + crystal.lineTo(28, 9); + crystal.lineTo(28, 25); + crystal.lineTo(16, 32); + crystal.lineTo(4, 25); + crystal.lineTo(4, 9); + crystal.closePath(); + + g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35)); + g2d.fill(crystal); + g2d.setColor(primary); + g2d.setStroke(new BasicStroke(1.5f)); + g2d.draw(crystal); + + g2d.setColor(secondary); + g2d.drawLine(16, 2, 16, 32); + g2d.setColor(primary); + g2d.drawLine(4, 17, 28, 17); + + } else if ("minimal-clean".equals(style) || "notary".equals(style) || "cartorio".equals(style)) { + // Carimbo notarial sóbrio + g2d.setColor(primary); + g2d.setStroke(new BasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Float(2, 2, 28, 28)); + g2d.setStroke(new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f)); + g2d.draw(new Ellipse2D.Float(5, 5, 22, 22)); + + // Checkmark + g2d.setStroke(new BasicStroke(1.8f)); + g2d.drawLine(10, 16, 14, 20); + g2d.drawLine(14, 20, 22, 12); + + } else if ("gov-br".equals(style) || "star".equals(style) || "governo".equals(style)) { + // Brasão estelar estilizado + g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 30)); + g2d.fill(new Ellipse2D.Float(2, 2, 28, 28)); + g2d.setColor(primary); + g2d.setStroke(new BasicStroke(1.4f)); + g2d.draw(new Ellipse2D.Float(2, 2, 28, 28)); + + // Estrela interna + Path2D.Float star = new Path2D.Float(); + star.moveTo(16, 6); + star.lineTo(19, 13); + star.lineTo(26, 13); + star.lineTo(20, 17); + star.lineTo(22, 24); + star.lineTo(16, 20); + star.lineTo(10, 24); + star.lineTo(12, 17); + star.lineTo(6, 13); + star.lineTo(13, 13); + star.closePath(); + g2d.setColor(secondary); + g2d.fill(star); + + } else { + // Padrão: Escudo Criptográfico Oficial com Cadeado + Path2D.Float shield = new Path2D.Float(); + shield.moveTo(16, 2); + shield.lineTo(28, 6); + shield.lineTo(28, 16); + shield.curveTo(28, 25, 22, 31, 16, 34); + shield.curveTo(10, 31, 4, 25, 4, 16); + shield.lineTo(4, 6); + shield.closePath(); + + g2d.setColor(new Color(primary.getRed(), primary.getGreen(), primary.getBlue(), 35)); + g2d.fill(shield); + g2d.setColor(primary); + g2d.setStroke(new BasicStroke(1.5f)); + g2d.draw(shield); + + // Cadeado + g2d.setColor(primary); + g2d.fillRect(11, 15, 10, 8); + g2d.setStroke(new BasicStroke(1.3f)); + g2d.drawArc(12, 9, 8, 8, 0, 180); + g2d.setColor(Color.WHITE); + g2d.fillOval(15, 17, 2, 2); + } + + g2d.translate(-14, -18); + } + + private static String resolveHeaderText(PdfSealTheme theme) { + if (theme.customHeader() != null && !theme.customHeader().isBlank()) { + return theme.customHeader().toUpperCase(); + } + return switch (theme.id().toLowerCase()) { + case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL"; + case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE"; + case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001"; + default -> "ASSINADO DIGITALMENTE • ICP-BRASIL"; + }; + } + + private static String resolveFooterText(PdfSealTheme theme, String hash) { + if (theme.customFooter() != null && !theme.customFooter().isBlank()) { + return theme.customFooter(); + } + if (hash != null && !hash.isBlank()) { + return "Hash Criptográfico: " + hash; + } + return switch (theme.id().toLowerCase()) { + case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local"; + case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil"; + case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001"; + default -> "Conformidade ICP-Brasil e validade jurídica plena"; + }; + } + + private static Color parseColor(String hex, Color def) { + if (hex == null || !hex.startsWith("#")) return def; + try { + return Color.decode(hex); + } catch (Exception e) { + return def; + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealTheme.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealTheme.java new file mode 100644 index 0000000..9911a25 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealTheme.java @@ -0,0 +1,135 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Objects; + +/** + * Modelo de configuração e identidade visual de um Tema de Selo de Assinatura PDF. + * Permite definir paleta de cores, dimensões, templates, textos personalizados e preferências de exibição. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record PdfSealTheme( + String id, + String name, + String description, + int width, + int height, + String primaryColor, + String secondaryColor, + String backgroundColor, + String borderColor, + String textColor, + String svgTemplate, + String customHeader, + String customFooter, + String iconStyle, + CpfDisplayMode defaultCpfMode, + boolean defaultIncludeDateTime, + boolean isCustom, + String customLogoBase64, + String customFieldLabel, + String customFieldValue +) { + + public PdfSealTheme { + Objects.requireNonNull(id, "ID do tema não pode ser nulo."); + Objects.requireNonNull(name, "Nome do tema não pode ser nulo."); + if (width <= 0) width = 320; + if (height <= 0) height = 82; + if (primaryColor == null) primaryColor = "#0d47a1"; + if (secondaryColor == null) secondaryColor = "#0284c7"; + if (backgroundColor == null) backgroundColor = "#ffffff"; + if (borderColor == null) borderColor = "#cbd5e1"; + if (textColor == null) textColor = "#0f172a"; + if (iconStyle == null || iconStyle.isBlank()) iconStyle = "shield"; + if (defaultCpfMode == null) defaultCpfMode = CpfDisplayMode.FULL; + } + + /** + * Construtor para temas customizados com logotipo Base64, sem campos extras. + */ + public PdfSealTheme(String id, String name, String description, int width, int height, + String primaryColor, String secondaryColor, String backgroundColor, + String borderColor, String textColor, String svgTemplate, + String customHeader, String customFooter, String iconStyle, + CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom, + String customLogoBase64) { + this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor, + borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle, + defaultCpfMode, defaultIncludeDateTime, isCustom, customLogoBase64, null, null); + } + + /** + * Construtor para temas customizados sem logo personalizada em imagem. + */ + public PdfSealTheme(String id, String name, String description, int width, int height, + String primaryColor, String secondaryColor, String backgroundColor, + String borderColor, String textColor, String svgTemplate, + String customHeader, String customFooter, String iconStyle, + CpfDisplayMode defaultCpfMode, boolean defaultIncludeDateTime, boolean isCustom) { + this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor, + borderColor, textColor, svgTemplate, customHeader, customFooter, iconStyle, + defaultCpfMode, defaultIncludeDateTime, isCustom, null, null, null); + } + + /** + * Construtor legado para os temas embutidos do sistema. + */ + public PdfSealTheme(String id, String name, String description, int width, int height, + String primaryColor, String secondaryColor, String backgroundColor, + String borderColor, String textColor, String svgTemplate) { + this(id, name, description, width, height, primaryColor, secondaryColor, backgroundColor, + borderColor, textColor, svgTemplate, null, null, "shield", CpfDisplayMode.FULL, true, false, null, null, null); + } + + /** + * Formata o campo customizado (ex: "Matrícula: 104829" ou "Cargo: Auditor Fiscal"). + */ + public String formatCustomField() { + boolean hasLabel = customFieldLabel != null && !customFieldLabel.isBlank(); + boolean hasValue = customFieldValue != null && !customFieldValue.isBlank(); + if (hasLabel && hasValue) { + String label = customFieldLabel.trim(); + if (!label.endsWith(":")) label += ":"; + return label + " " + customFieldValue.trim(); + } else if (hasValue) { + return customFieldValue.trim(); + } else if (hasLabel) { + return customFieldLabel.trim(); + } + return ""; + } + + /** + * Gera o código SVG final substituindo os marcadores pelos dados reais da assinatura. + */ + public String renderSvg(PdfSealData data) { + if (svgTemplate == null || svgTemplate.isBlank()) { + return ""; + } + return svgTemplate + .replace("${THEME_PRIMARY}", primaryColor) + .replace("${THEME_SECONDARY}", secondaryColor) + .replace("${THEME_BG}", backgroundColor) + .replace("${THEME_BORDER}", borderColor) + .replace("${THEME_TEXT}", textColor) + .replace("${SIGNER_NAME}", escapeXml(data.signerName())) + .replace("${SIGNER_DOC}", escapeXml(data.identifier())) + .replace("${SIGNING_DATE}", escapeXml(data.signingDate())) + .replace("${CERT_ISSUER}", escapeXml(data.issuer())) + .replace("${HASH_SUMMARY}", escapeXml(data.hashSummary())) + .replace("${REASON}", escapeXml(data.reason())) + .replace("${LOCATION}", escapeXml(data.location())); + } + + private static String escapeXml(String text) { + if (text == null) return ""; + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java new file mode 100644 index 0000000..1e67b81 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/seal/PdfSealThemeRegistry.java @@ -0,0 +1,240 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registro central de temas de selos visuais para assinatura PDF. + * Inclui os temas padrão do sistema e permite instalar, gerenciar e persistir dinamicamente + * temas personalizados de selo criados ou importados pelo usuário. + */ +public final class PdfSealThemeRegistry { + + private static final Logger logger = LoggerFactory.getLogger(PdfSealThemeRegistry.class); + + private static final Map THEMES = new ConcurrentHashMap<>(); + private static final CustomSealStorage STORAGE = new CustomSealStorage(); + + public static final String DEFAULT_THEME_ID = "icp-brasil"; + + static { + registerBuiltInThemes(); + loadCustomThemes(); + } + + private PdfSealThemeRegistry() { + } + + private static void registerBuiltInThemes() { + // 1. Tema ICP-Brasil Institucional + registerTheme(new PdfSealTheme( + "icp-brasil", + "ICP-Brasil Institucional", + "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular", + 320, + 82, + "#0d47a1", + "#0284c7", + "#ffffff", + "#93c5fd", + "#0f172a", + """ + + + + + + + + + + + + + + ASSINADO DIGITALMENTE • ICP-BRASIL + ${SIGNER_NAME} + Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER} + Data/Hora: ${SIGNING_DATE} + Verificação criptográfica válida e com fé pública + + """ + )); + + // 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", + """ + + + + + + + + + + + + CYBERCORE SIGNER • ASSINATURA ELETRÔNICA + ${SIGNER_NAME} + ${SIGNER_DOC} • ${CERT_ISSUER} + Autenticado em: ${SIGNING_DATE} + Hash: ${HASH_SUMMARY} + + """ + )); + + // 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", + """ + + + + + + + + + + Assinado Digitalmente + ${SIGNER_NAME} + ${SIGNER_DOC} • ${CERT_ISSUER} + ${SIGNING_DATE} + Conforme MP 2.200-2/2001 e ICP-Brasil + + """ + )); + + // 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", + """ + + + + + + + + + + + + GOV.BR • DOCUMENTO ASSINADO DIGITALMENTE + ${SIGNER_NAME} + Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER} + Data/Hora: ${SIGNING_DATE} + Validade jurídica assegurada pela MP nº 2.200-2/2001 + + """ + )); + } + + /** + * Carrega os temas personalizados armazenados no disco. + */ + public static void loadCustomThemes() { + try { + List customs = STORAGE.loadAllCustomSeals(); + for (PdfSealTheme theme : customs) { + THEMES.put(theme.id().toLowerCase(), theme); + } + logger.info("Carregados {} temas de selos personalizados do disco.", customs.size()); + } catch (Exception e) { + logger.warn("Não foi possível carregar temas personalizados: {}", e.getMessage()); + } + } + + public static void registerTheme(PdfSealTheme theme) { + Objects.requireNonNull(theme, "Tema não pode ser nulo."); + THEMES.put(theme.id().toLowerCase(), theme); + } + + /** + * Instala um tema personalizado, salvando-o no disco e registrando-o para uso imediato. + */ + public static void installCustomTheme(PdfSealTheme theme) throws IOException { + Objects.requireNonNull(theme, "Tema não pode ser nulo."); + STORAGE.saveSeal(theme); + registerTheme(theme); + } + + /** + * Remove um tema personalizado do disco e do registro. + * Não permite remover temas padrão do sistema. + */ + public static boolean uninstallCustomTheme(String themeId) { + if (themeId == null) return false; + String idLower = themeId.toLowerCase(); + + // Protege temas padrão + if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") || + idLower.equals("minimal-clean") || idLower.equals("gov-br")) { + throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema."); + } + + boolean deleted = STORAGE.deleteSeal(idLower); + THEMES.remove(idLower); + return deleted; + } + + public static Optional findById(String id) { + if (id == null) return Optional.empty(); + return Optional.ofNullable(THEMES.get(id.toLowerCase())); + } + + public static Optional getTheme(String id) { + return findById(id); + } + + public static PdfSealTheme getDefaultTheme() { + return THEMES.getOrDefault(DEFAULT_THEME_ID, THEMES.values().iterator().next()); + } + + public static List listThemes() { + List list = new ArrayList<>(THEMES.values()); + return Collections.unmodifiableList(list); + } + + public static CustomSealStorage getStorage() { + return STORAGE; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspErrorCode.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspErrorCode.java new file mode 100644 index 0000000..2313a58 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspErrorCode.java @@ -0,0 +1,35 @@ +package br.com.cybercore.signer.core.protocol; + +/** + * Códigos de erro padronizados do protocolo CyberCore Signing Protocol (CCSP/1). + */ +public enum CcspErrorCode { + CERTIFICATE_NOT_FOUND("Certificado especificado não foi encontrado."), + CERTIFICATE_EXPIRED("O certificado selecionado está expirado."), + CERTIFICATE_INVALID("O certificado é inválido ou está corrompido."), + PRIVATE_KEY_NOT_AVAILABLE("Chave privada inacessível ou não disponível para a operação."), + TOKEN_NOT_FOUND("Nenhum token ou leitor de smart card compatível foi encontrado."), + TOKEN_REMOVED("O token ou smart card foi desconectado durante a operação."), + PIN_REQUIRED("PIN obrigatório para desbloqueio do dispositivo."), + PIN_INCORRECT("PIN informado é incorreto."), + PKCS11_ERROR("Falha na comunicação com o módulo PKCS#11 do dispositivo."), + SIGNATURE_FAILED("Falha ao gerar a assinatura digital."), + APPLICATION_NOT_AUTHORIZED("Aplicação web não possui autorização para solicitar assinaturas."), + REQUEST_EXPIRED("A requisição de assinatura expirou."), + REPLAY_DETECTED("Requisição duplicada detectada (replay attack prevenido)."), + INVALID_ORIGIN("Origem da requisição não é permitida para esta aplicação."), + UNSUPPORTED_ALGORITHM("Algoritmo de assinatura não suportado."), + USER_CANCELLED("Operação cancelada pelo usuário."), + PROVIDER_NOT_AVAILABLE("O provedor criptográfico necessário não está disponível."), + COMMUNICATION_ERROR("Erro interno de comunicação no agente local."); + + private final String defaultMessage; + + CcspErrorCode(String defaultMessage) { + this.defaultMessage = defaultMessage; + } + + public String getDefaultMessage() { + return defaultMessage; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspOperation.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspOperation.java new file mode 100644 index 0000000..29da06f --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspOperation.java @@ -0,0 +1,39 @@ +package br.com.cybercore.signer.core.protocol; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Operações suportadas pelo protocolo CCSP. + */ +public enum CcspOperation { + SIGN("sign"), + AUTHENTICATE("authenticate"), + LIST_CERTIFICATES("list_certificates"), + STATUS("status"), + PING("ping"); + + private final String value; + + CcspOperation(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static CcspOperation fromValue(String value) { + if (value == null) { + return null; + } + for (CcspOperation op : values()) { + if (op.value.equalsIgnoreCase(value.trim())) { + return op; + } + } + throw new IllegalArgumentException("Operação CCSP desconhecida: " + value); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java new file mode 100644 index 0000000..f744e37 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java @@ -0,0 +1,96 @@ +package br.com.cybercore.signer.core.protocol; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.time.Instant; + +/** + * Mensagem de requisição do CyberCore Signing Protocol (CCSP/1). + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CcspRequest( + @JsonProperty("version") + String version, + + @JsonProperty("request_id") + String requestId, + + @JsonProperty("application_id") + String applicationId, + + @JsonProperty("operation") + String operation, + + @JsonProperty("algorithm") + String algorithm, + + @JsonProperty("digest") + String digest, + + @JsonProperty("certificate_hint") + String certificateHint, + + @JsonProperty("expires_at") + String expiresAt, + + @JsonProperty("timestamp") + Long timestamp, + + @JsonProperty("nonce") + String nonce, + + @JsonProperty("signature_type") + String signatureType, + + @JsonProperty("document_name") + String documentName, + + @JsonProperty("document_type") + String documentType, + + @JsonProperty("purpose") + String purpose, + + @JsonProperty("challenge") + String challenge +) implements Serializable { + + public static final String PROTOCOL_VERSION = "1"; + + public CcspRequest( + String version, String requestId, String applicationId, String operation, + String algorithm, String digest, String certificateHint, String expiresAt, + Long timestamp, String nonce, String signatureType + ) { + this(version, requestId, applicationId, operation, algorithm, digest, + certificateHint, expiresAt, timestamp, nonce, signatureType, + null, null, null, null); + } + + public boolean isVersionSupported() { + return PROTOCOL_VERSION.equals(version); + } + + public boolean isAuthentication() { + return "authenticate".equalsIgnoreCase(operation) + || "AUTHENTICATION".equalsIgnoreCase(purpose) + || "AUTH_CHALLENGE".equalsIgnoreCase(documentType) + || (challenge != null && !challenge.isBlank()); + } + + public boolean isExpired() { + if (expiresAt == null || expiresAt.isBlank()) { + return false; + } + try { + Instant expiry = Instant.parse(expiresAt); + return Instant.now().isAfter(expiry); + } catch (Exception e) { + return false; + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java new file mode 100644 index 0000000..26aeb88 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java @@ -0,0 +1,136 @@ +package br.com.cybercore.signer.core.protocol; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +/** + * Mensagem de resposta do CyberCore Signing Protocol (CCSP/1). + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CcspResponse( + @JsonProperty("version") + String version, + + @JsonProperty("request_id") + String requestId, + + @JsonProperty("status") + String status, + + @JsonProperty("signature") + String signature, + + @JsonProperty("certificate") + String certificate, + + @JsonProperty("certificate_chain") + List certificateChain, + + @JsonProperty("algorithm") + String algorithm, + + @JsonProperty("error_code") + String errorCode, + + @JsonProperty("message") + String message, + + @JsonProperty("subject_name") + String subjectName, + + @JsonProperty("subject_cpf_cnpj") + String subjectCpfCnpj, + + @JsonProperty("document_name") + String documentName, + + @JsonProperty("document_type") + String documentType +) implements Serializable { + + public CcspResponse { + certificateChain = certificateChain != null ? List.copyOf(certificateChain) : null; + } + + @Override + public List certificateChain() { + return certificateChain != null ? Collections.unmodifiableList(certificateChain) : null; + } + + public static final String STATUS_SIGNED = "signed"; + public static final String STATUS_AUTHENTICATED = "authenticated"; + public static final String STATUS_SUCCESS = "success"; + public static final String STATUS_ERROR = "error"; + + public static CcspResponse success(String requestId, String signatureBase64, String certBase64, + List chain, String algorithm) { + return success(requestId, signatureBase64, certBase64, chain, algorithm, null, null, null, null); + } + + public static CcspResponse success(String requestId, String signatureBase64, String certBase64, + List chain, String algorithm, + String subjectName, String subjectCpfCnpj, + String documentName, String documentType) { + return new CcspResponse( + CcspRequest.PROTOCOL_VERSION, + requestId, + STATUS_SIGNED, + signatureBase64, + certBase64, + chain, + algorithm, + null, + null, + subjectName, + subjectCpfCnpj, + documentName, + documentType + ); + } + + public static CcspResponse authenticated(String requestId, String signatureBase64, String certBase64, + List chain, String algorithm, + String subjectName, String subjectCpfCnpj) { + return new CcspResponse( + CcspRequest.PROTOCOL_VERSION, + requestId, + STATUS_AUTHENTICATED, + signatureBase64, + certBase64, + chain, + algorithm, + null, + null, + subjectName, + subjectCpfCnpj, + null, + "AUTH_CHALLENGE" + ); + } + + public static CcspResponse error(String requestId, CcspErrorCode errorCode, String message) { + return new CcspResponse( + CcspRequest.PROTOCOL_VERSION, + requestId, + STATUS_ERROR, + null, + null, + null, + null, + errorCode.name(), + message != null ? message : errorCode.getDefaultMessage(), + null, + null, + null, + null + ); + } + + public static CcspResponse error(String requestId, CcspErrorCode errorCode) { + return error(requestId, errorCode, errorCode.getDefaultMessage()); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java new file mode 100644 index 0000000..6a760d2 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java @@ -0,0 +1,550 @@ +package br.com.cybercore.signer.core.protocol; + +import br.com.cybercore.signer.core.crypto.DigestAlgorithm; +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import java.security.MessageDigest; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import br.com.cybercore.signer.core.security.ApplicationRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import br.com.cybercore.signer.core.security.NonceManager; +import br.com.cybercore.signer.core.security.OriginValidator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Servidor HTTP local do CyberCore Signing Protocol (CCSP/1). + * Vinculado exclusivamente ao loopback 127.0.0.1 para comunicação segura com navegadores web. + */ +public class CcspServer { + + private static final Logger logger = LoggerFactory.getLogger(CcspServer.class); + public static final int DEFAULT_PORT = 21325; + public static final String BIND_HOST = "127.0.0.1"; + + private final int port; + private final CertificateManager certificateManager; + private final SigningService signingService; + private final ApplicationRegistry applicationRegistry; + private final NonceManager nonceManager; + private final OriginValidator originValidator; + private final CcspUserPromptListener promptListener; + private final ObjectMapper objectMapper; + private final ReentrantLock signLock = new ReentrantLock(true); // Concorrência FIFO + + private HttpServer httpServer; + private ExecutorService executor; + private volatile boolean running = false; + + public CcspServer(int port, + CertificateManager certificateManager, + SigningService signingService, + ApplicationRegistry applicationRegistry, + CcspUserPromptListener promptListener) { + this.port = port; + this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null"); + this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null"); + this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance(); + this.promptListener = promptListener; + this.nonceManager = new NonceManager(); + this.originValidator = new OriginValidator(); + this.objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public CcspServer(CertificateManager certificateManager, + SigningService signingService, + CcspUserPromptListener promptListener) { + this(DEFAULT_PORT, certificateManager, signingService, ApplicationRegistry.getInstance(), promptListener); + } + + /** + * Inicia o servidor local na porta configurada. + */ + public synchronized void start() throws IOException { + if (running) { + return; + } + + InetSocketAddress address = new InetSocketAddress(BIND_HOST, port); + httpServer = HttpServer.create(address, 50); + + executor = Executors.newFixedThreadPool(8, r -> { + Thread t = new Thread(r, "ccsp-http-worker"); + t.setDaemon(true); + return t; + }); + httpServer.setExecutor(executor); + + // Registra endpoints do protocolo CCSP/1 + httpServer.createContext("/ccsp/v1/status", new StatusHandler()); + httpServer.createContext("/ccsp/v1/certificates", new CertificatesHandler()); + httpServer.createContext("/ccsp/v1/sign", new SignHandler()); + httpServer.createContext("/ccsp/v1/authenticate", new SignHandler()); + httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler()); + + httpServer.start(); + running = true; + logger.info("Servidor local CCSP/1 iniciado com sucesso em http://{}:{}", BIND_HOST, getPort()); + } + + /** + * Encerra a execução do servidor local. + */ + public synchronized void stop() { + if (!running) { + return; + } + + if (httpServer != null) { + httpServer.stop(1); + httpServer = null; + } + + if (executor != null) { + executor.shutdownNow(); + executor = null; + } + + running = false; + logger.info("Servidor local CCSP/1 finalizado."); + } + + public boolean isRunning() { + return running; + } + + public int getPort() { + if (httpServer != null) { + return httpServer.getAddress().getPort(); + } + return port; + } + + // ========================================================================= + // Handlers HTTP + // ========================================================================= + + private class StatusHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (handleCors(exchange)) return; + + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas GET é suportado."); + return; + } + + Map resp = new HashMap<>(); + resp.put("version", CcspRequest.PROTOCOL_VERSION); + resp.put("status", "ready"); + resp.put("agent", "CyberCore Signer"); + resp.put("agent_version", "0.1.0"); + resp.put("os", System.getProperty("os.name")); + resp.put("timestamp", Instant.now().toString()); + + sendJsonResponse(exchange, 200, resp); + } + } + + private class CertificatesHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (handleCors(exchange)) return; + + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas GET é suportado."); + return; + } + + String origin = getOrigin(exchange); + String appId = exchange.getRequestHeaders().getFirst("X-CyberCore-AppId"); + if (appId == null || appId.isBlank()) { + URI uri = exchange.getRequestURI(); + appId = getQueryParam(uri, "application_id"); + } + + // Verifica autorização se app_id e origin forem informados + if (appId != null && !appId.isBlank()) { + boolean authorized = applicationRegistry.isAuthorized(appId, origin); + if (!authorized) { + boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId); + if (!approved) { + sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(), + "Aplicação não autorizada pelo usuário."); + return; + } + } + } + + List certs = certificateManager.getAvailableCertificates(); + if (certs.isEmpty()) { + certs = certificateManager.discoverCertificates(); + } + + List> certDtoList = certs.stream().map(c -> { + Map dto = new HashMap<>(); + dto.put("id", c.id()); + dto.put("alias", c.alias()); + dto.put("common_name", c.commonName()); + dto.put("subject_dn", c.subjectDn()); + dto.put("issuer_dn", c.issuerDn()); + dto.put("type", c.type().name()); + dto.put("provider_id", c.providerId()); + dto.put("device_name", c.hardwareDeviceName()); + dto.put("valid_from", c.notBefore() != null ? c.notBefore().toString() : null); + dto.put("valid_to", c.notAfter() != null ? c.notAfter().toString() : null); + dto.put("serial_number", c.serialNumber()); + dto.put("can_sign", c.canSign()); + dto.put("status", c.status().name()); + return dto; + }).toList(); + + Map resp = new HashMap<>(); + resp.put("version", CcspRequest.PROTOCOL_VERSION); + resp.put("status", "success"); + resp.put("certificates", certDtoList); + + sendJsonResponse(exchange, 200, resp); + } + } + + private class AuthorizeHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (handleCors(exchange)) return; + + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado."); + return; + } + + String origin = getOrigin(exchange); + try (InputStream is = exchange.getRequestBody()) { + @SuppressWarnings("unchecked") + Map body = objectMapper.readValue(is, Map.class); + String appId = body.get("application_id"); + String appName = body.getOrDefault("name", "Aplicação Web"); + + if (appId == null || appId.isBlank()) { + sendErrorResponse(exchange, 400, "BAD_REQUEST", "application_id é obrigatório."); + return; + } + + if (applicationRegistry.isAuthorized(appId, origin)) { + Map resp = Map.of( + "version", CcspRequest.PROTOCOL_VERSION, + "status", "authorized", + "application_id", appId + ); + sendJsonResponse(exchange, 200, resp); + return; + } + + boolean approved = promptApplicationAuthorization(origin, appName, appId); + if (approved) { + Map resp = Map.of( + "version", CcspRequest.PROTOCOL_VERSION, + "status", "authorized", + "application_id", appId + ); + sendJsonResponse(exchange, 200, resp); + } else { + sendErrorResponse(exchange, 403, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(), + "Autorização negada pelo usuário."); + } + } catch (Exception e) { + sendErrorResponse(exchange, 400, "BAD_REQUEST", "Payload JSON inválido: " + e.getMessage()); + } + } + } + + private class SignHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (handleCors(exchange)) return; + + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado."); + return; + } + + String origin = getOrigin(exchange); + CcspRequest request; + try (InputStream is = exchange.getRequestBody()) { + request = objectMapper.readValue(is, CcspRequest.class); + } catch (Exception e) { + sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido."); + return; + } + + // 1. Validação de versão + if (!request.isVersionSupported()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR, + "Versão do protocolo não suportada: " + request.version()); + return; + } + + // 2. Validação de expiração + if (request.isExpired()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED, + "A requisição de assinatura expirou."); + return; + } + + // 3. Validação anti-replay (Nonce) + if (request.nonce() != null && !request.nonce().isBlank()) { + boolean accepted = nonceManager.registerNonce(request.nonce()); + if (!accepted) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "Requisição duplicada detectada (replay prevenido)."); + return; + } + } + + // 4. Validação de autorização da aplicação + String appId = request.applicationId(); + if (appId == null || appId.isBlank()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED, + "application_id é obrigatório."); + return; + } + + if (!applicationRegistry.isAuthorized(appId, origin)) { + boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId); + if (!approved) { + sendCcspError(exchange, 403, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED, + "Aplicação não autorizada pelo usuário."); + return; + } + } + + // 5. Validação de digest / challenge + byte[] digestBytes; + if (request.digest() != null && !request.digest().isBlank()) { + try { + digestBytes = Base64.getDecoder().decode(request.digest()); + } catch (Exception e) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED, + "Digest em Base64 inválido."); + return; + } + } else if (request.challenge() != null && !request.challenge().isBlank()) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + digestBytes = md.digest(request.challenge().getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED, + "Falha ao gerar digest do desafio: " + e.getMessage()); + return; + } + } else { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED, + "É obrigatório informar 'digest' ou 'challenge' para a operação."); + return; + } + + // 6. Resolução do certificado + Optional certOpt = certificateManager.findByHint(request.certificateHint()); + if (certOpt.isEmpty()) { + sendCcspError(exchange, 404, request.requestId(), CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Nenhum certificado correspondente encontrado."); + return; + } + ManagedCertificate cert = certOpt.get(); + + // 7. Confirmação do usuário na UI (se listener configurado) + if (promptListener != null) { + try { + CompletableFuture approvalFuture = promptListener.requestSignatureConfirmation(request, cert); + Boolean confirmed = approvalFuture.get(60, TimeUnit.SECONDS); + if (!Boolean.TRUE.equals(confirmed)) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED, + "Operação de assinatura cancelada pelo usuário."); + return; + } + } catch (Exception e) { + logger.warn("Falha ou timeout na confirmação visual de assinatura: {}", e.getMessage()); + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED, + "Confirmação da assinatura expirou ou foi cancelada."); + return; + } + } + + // 8. Assinatura com controle sequencial FIFO + signLock.lock(); + try { + SigningContext ctx = new SigningContext( + request.requestId(), + request.applicationId(), + digestBytes, + request.algorithm() != null ? request.algorithm() : "SHA256withRSA", + cert.id(), + Instant.now() + ); + + SigningResult result = signingService.sign(ctx); + String sigBase64 = Base64.getEncoder().encodeToString(result.signatureBytes()); + + String subjectName = X509CertificateUtils.extractHolderName(cert.commonName()); + String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj(); + boolean isAuth = request.isAuthentication() || exchange.getRequestURI().getPath().endsWith("/authenticate"); + + CcspResponse response = isAuth + ? CcspResponse.authenticated( + request.requestId(), + sigBase64, + result.certificateBase64(), + result.certificateChainBase64(), + result.algorithm(), + subjectName, + cpfCnpj + ) + : CcspResponse.success( + request.requestId(), + sigBase64, + result.certificateBase64(), + result.certificateChainBase64(), + result.algorithm(), + subjectName, + cpfCnpj, + request.documentName(), + request.documentType() + ); + + sendJsonResponse(exchange, 200, response); + + } catch (CertificateProviderException cpe) { + sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage()); + } catch (Exception e) { + logger.error("Erro inesperado na assinatura: {}", e.getMessage(), e); + sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED, + "Erro interno: " + e.getMessage()); + } finally { + signLock.unlock(); + } + } + } + + // ========================================================================= + // Utilitários de Segurança e HTTP + // ========================================================================= + + private boolean promptApplicationAuthorization(String origin, String appName, String appId) { + if (promptListener == null) { + // Em testes unitários sem UI, aprova e registra + applicationRegistry.register(new AuthorizedApplication( + appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now() + )); + return true; + } + + try { + CompletableFuture future = promptListener.requestApplicationAuthorization(origin, appName, appId); + Boolean approved = future.get(30, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(approved)) { + applicationRegistry.register(new AuthorizedApplication( + appId, appName, origin != null ? Set.of(origin) : Set.of("*"), true, Instant.now() + )); + return true; + } + } catch (Exception e) { + logger.warn("Timeout ou erro na autorização da aplicação: {}", e.getMessage()); + } + return false; + } + + private boolean handleCors(HttpExchange exchange) throws IOException { + String origin = getOrigin(exchange); + if (origin == null || origin.isBlank()) { + origin = "*"; + } + + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin); + exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce"); + exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); + + if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + exchange.close(); + return true; + } + return false; + } + + private String getOrigin(HttpExchange exchange) { + String origin = exchange.getRequestHeaders().getFirst("Origin"); + if (origin == null || origin.isBlank()) { + origin = exchange.getRequestHeaders().getFirst("Referer"); + } + return origin; + } + + private String getQueryParam(URI uri, String key) { + if (uri.getQuery() == null) return null; + for (String param : uri.getQuery().split("&")) { + String[] pair = param.split("="); + if (pair.length == 2 && pair[0].equalsIgnoreCase(key)) { + return pair[1]; + } + } + return null; + } + + private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException { + byte[] bytes = objectMapper.writeValueAsBytes(data); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException { + Map err = Map.of( + "version", CcspRequest.PROTOCOL_VERSION, + "status", "error", + "error_code", code, + "message", message + ); + sendJsonResponse(exchange, httpStatus, err); + } + + private void sendCcspError(HttpExchange exchange, int httpStatus, String reqId, CcspErrorCode code, String msg) throws IOException { + CcspResponse err = CcspResponse.error(reqId, code, msg != null ? msg : code.getDefaultMessage()); + sendJsonResponse(exchange, httpStatus, err); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspUserPromptListener.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspUserPromptListener.java new file mode 100644 index 0000000..df692ee --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspUserPromptListener.java @@ -0,0 +1,31 @@ +package br.com.cybercore.signer.core.protocol; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; + +import java.util.concurrent.CompletableFuture; + +/** + * Interface de interação com o usuário para autorização de aplicações e confirmação de assinaturas. + * Permite desacoplar o servidor HTTP de comunicação da interface gráfica desktop (JavaFX). + */ +public interface CcspUserPromptListener { + + /** + * Solicita ao usuário autorização para que uma nova aplicação web se comunique com o agente. + * + * @param origin Origem HTTP/HTTPS da aplicação (ex: https://sistema.exemplo.com.br). + * @param appName Nome amigável da aplicação solicitante. + * @param applicationId ID único da aplicação. + * @return Future completado com {@code true} se o usuário autorizar; {@code false} caso recuse. + */ + CompletableFuture requestApplicationAuthorization(String origin, String appName, String applicationId); + + /** + * Solicita ao usuário confirmação visual explícita antes de realizar uma assinatura digital. + * + * @param request Requisição CCSP de assinatura recebida. + * @param certificate Certificado digital selecionado para a operação. + * @return Future completado com {@code true} se o usuário confirmar; {@code false} caso cancele. + */ + CompletableFuture requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate); +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateManager.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateManager.java new file mode 100644 index 0000000..707fad0 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateManager.java @@ -0,0 +1,194 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Gerenciador central de certificados digitais do CyberCore Signer. + * Responsável pela descoberta em múltiplos provedores (Windows Certificate Store, A1, PKCS#11), + * validação de integridade, mapeamento de provedores e seleção segura para operações de assinatura. + */ +public class CertificateManager { + + private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class); + + private final List providers = new CopyOnWriteArrayList<>(); + private final Map certificateToProviderMap = new ConcurrentHashMap<>(); + private final Map cachedCertificates = new ConcurrentHashMap<>(); + + public CertificateManager() { + } + + /** + * Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma. + */ + public static CertificateManager createDefault() { + CertificateManager manager = new CertificateManager(); + manager.registerProvider(new WindowsCertificateStoreProvider()); + manager.registerProvider(new Pkcs12CertificateProvider()); + manager.registerProvider(new Pkcs11CertificateProvider()); + return manager; + } + + /** + * Registra um provedor de certificados no ecossistema do agente. + */ + public void registerProvider(CertificateProvider provider) { + Objects.requireNonNull(provider, "Provedor não pode ser nulo."); + // Evita duplicidade por providerId + providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId())); + providers.add(provider); + logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})", + provider.getDisplayName(), provider.getProviderId(), provider.isAvailable()); + } + + /** + * Remove um provedor previamente registrado. + */ + public void unregisterProvider(String providerId) { + if (providerId != null) { + providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId)); + certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId)); + } + } + + public List getProviders() { + return Collections.unmodifiableList(providers); + } + + /** + * Executa a descoberta de certificados em todos os provedores operacionais. + * Consolida os resultados, atualiza o mapeamento de provedores e ordena por relevância/disponibilidade. + */ + public synchronized List discoverCertificates() { + logger.info("Iniciando descoberta de certificados digitais nos provedores ativos..."); + + certificateToProviderMap.clear(); + cachedCertificates.clear(); + List discovered = new ArrayList<>(); + + for (CertificateProvider provider : providers) { + if (!provider.isAvailable()) { + logger.debug("Provedor '{}' não disponível no momento. Ignorando na descoberta.", provider.getProviderId()); + continue; + } + + try { + List certs = provider.listCertificates(); + logger.info("Provedor '{}' retornou {} certificado(s).", provider.getProviderId(), certs.size()); + + for (ManagedCertificate cert : certs) { + certificateToProviderMap.put(cert.id(), provider); + cachedCertificates.put(cert.id(), cert); + discovered.add(cert); + } + } catch (Exception e) { + logger.error("Erro ao listar certificados do provedor '{}': {}", provider.getProviderId(), e.getMessage()); + } + } + + // Ordenação: certificados disponíveis primeiro, depois por nome do titular + discovered.sort(Comparator + .comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1) + .thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER)); + + logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size()); + return Collections.unmodifiableList(discovered); + } + + /** + * Retorna o provedor responsável pelo certificado com o ID informado. + */ + public CertificateProvider getProviderForCertificate(String certId) { + if (certId == null) { + return null; + } + return certificateToProviderMap.get(certId); + } + + /** + * Obtém o material criptográfico (chave privada e cadeia X.509) do certificado especificado. + */ + public Optional getKeyMaterial(String certId, char[] pinOrPassword) throws CertificateProviderException { + CertificateProvider provider = getProviderForCertificate(certId); + if (provider == null) { + return Optional.empty(); + } + return provider.getKeyMaterial(certId, pinOrPassword); + } + + /** + * Busca um certificado previamente descoberto pelo seu identificador único. + */ + public Optional findById(String certId) { + if (certId == null) { + return Optional.empty(); + } + return Optional.ofNullable(cachedCertificates.get(certId)); + } + + /** + * Localiza um certificado usando hints flexíveis: id, alias, CPF, CNPJ ou impressões digitais SHA-1/SHA-256. + */ + public Optional findByHint(String hint) { + if (hint == null || hint.isBlank()) { + return Optional.empty(); + } + String cleanHint = hint.trim(); + + // 1. Busca por ID direto + ManagedCertificate direct = cachedCertificates.get(cleanHint); + if (direct != null) { + return Optional.of(direct); + } + + // 2. Busca por alias, CPF, CNPJ ou fingerprints + for (ManagedCertificate cert : cachedCertificates.values()) { + if (cleanHint.equalsIgnoreCase(cert.alias()) + || cleanHint.equalsIgnoreCase(cert.cpf()) + || cleanHint.equalsIgnoreCase(cert.cnpj()) + || cleanHint.equalsIgnoreCase(cert.sha1Fingerprint()) + || cleanHint.equalsIgnoreCase(cert.sha256Fingerprint()) + || cleanHint.equalsIgnoreCase(cert.serialNumber())) { + return Optional.of(cert); + } + } + + return Optional.empty(); + } + + /** + * Retorna a lista dos certificados em cache que possuem status AVAILABLE (com chave privada e vigentes). + */ + public List getAvailableCertificates() { + return cachedCertificates.values().stream() + .filter(c -> c.status() == CertificateStatus.AVAILABLE) + .toList(); + } + + /** + * Solicita recarregamento de todos os provedores cadastrados. + */ + public void refreshAll() { + for (CertificateProvider provider : providers) { + try { + provider.refresh(); + } catch (Exception e) { + logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage()); + } + } + discoverCertificates(); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProvider.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProvider.java new file mode 100644 index 0000000..d790db4 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProvider.java @@ -0,0 +1,61 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; + +import java.util.List; + +/** + * Contrato base para provedores de descoberta e assinatura com certificados digitais. + * Permite plugar diferentes backends (A1 em arquivo, Windows-MY, PKCS#11, Mock) + * sem acoplar o núcleo do aplicativo a fabricantes ou dispositivos específicos. + */ +public interface CertificateProvider { + + /** + * Identificador único do provedor (ex: "provider-a1", "provider-win-store", "provider-pkcs11"). + */ + String getProviderId(); + + /** + * Nome legível para apresentação na interface e diagnósticos. + */ + String getDisplayName(); + + /** + * Tipo do provedor. + */ + ProviderType getType(); + + /** + * Indica se o provedor está disponível e operacional no sistema atual. + */ + boolean isAvailable(); + + /** + * Lista os certificados disponíveis atualmente através deste provedor. + */ + List listCertificates() throws CertificateProviderException; + + /** + * Executa a assinatura do digest contido no contexto. + * A chave privada NUNCA deve sair do provedor/dispositivo. + */ + SigningResult sign(SigningContext context) throws CertificateProviderException; + + /** + * Retorna o material criptográfico (chave privada e cadeia) do certificado indicado, + * permitindo que operações que montam envelopes como PAdES ou XMLDSig operem localmente. + */ + default java.util.Optional getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException { + return java.util.Optional.empty(); + } + + /** + * Solicita recarga ou reescaneamento de certificados e dispositivos. + */ + default void refresh() { + // Implementação padrão opcional + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProviderException.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProviderException.java new file mode 100644 index 0000000..b544251 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/CertificateProviderException.java @@ -0,0 +1,32 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.protocol.CcspErrorCode; + +/** + * Exceção disparada por falhas nos provedores de certificado. + * Carrega obrigatoriamente um código de erro padronizado do CCSP. + */ +public class CertificateProviderException extends Exception { + + private static final long serialVersionUID = 1L; + + private final CcspErrorCode errorCode; + + public CertificateProviderException(CcspErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED; + } + + public CertificateProviderException(CcspErrorCode errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode != null ? errorCode : CcspErrorCode.SIGNATURE_FAILED; + } + + public CertificateProviderException(CcspErrorCode errorCode) { + this(errorCode, errorCode != null ? errorCode.getDefaultMessage() : CcspErrorCode.SIGNATURE_FAILED.getDefaultMessage()); + } + + public CcspErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java new file mode 100644 index 0000000..22ca67b --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs11CertificateProvider.java @@ -0,0 +1,247 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.crypto.DigestAlgorithm; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.Security; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; + +/** + * Provedor de certificados A3 via interface padrão PKCS#11 (tokens USB, smart cards e leitores). + * Utiliza o provider SunPKCS11 integrado à JVM para comunicação com middlewares dos fabricantes. + * Nunca persiste PIN em disco e nunca o transmite para servidores web. + */ +public class Pkcs11CertificateProvider implements CertificateProvider { + + private static final Logger logger = LoggerFactory.getLogger(Pkcs11CertificateProvider.class); + private static final String PROVIDER_ID = "provider-pkcs11"; + + /** + * Bibliotecas PKCS#11 comuns de middleware para tokens e smartcards no Windows e Linux. + */ + private static final String[] WELL_KNOWN_LIBRARIES = { + // Windows + "C:/Windows/System32/eps2003csp11.dll", // SafeNet / eToken + "C:/Windows/System32/aetpkss1.dll", // SafeSign AET + "C:/Windows/System32/acospkcs11.dll", // ACO / Giesecke & Devrient + "C:/Windows/System32/opensc-pkcs11.dll",// OpenSC Windows + // Linux + "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so", + "/usr/lib/opensc-pkcs11.so", + "/usr/lib/libaetpkss.so" + }; + + private Provider sunPkcs11Provider; + private KeyStore keyStore; + private String detectedLibraryPath; + + public Pkcs11CertificateProvider() { + detectAndInitialize(); + } + + private synchronized void detectAndInitialize() { + for (String libPath : WELL_KNOWN_LIBRARIES) { + File f = new File(libPath); + if (f.exists() && f.canRead()) { + detectedLibraryPath = libPath; + logger.info("Middleware PKCS#11 detectado no sistema: {}", libPath); + break; + } + } + } + + /** + * Permite registrar explicitamente um provider PKCS#11 já inicializado (ex: via SunPKCS11 com PIN do usuário). + */ + public synchronized void setProvider(Provider provider, KeyStore keyStore) { + this.sunPkcs11Provider = provider; + this.keyStore = keyStore; + } + + @Override + public String getProviderId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayName() { + return "Hardware PKCS#11 (Smart Card / Token USB)"; + } + + @Override + public ProviderType getType() { + return ProviderType.A3_PKCS11; + } + + @Override + public synchronized boolean isAvailable() { + return (detectedLibraryPath != null) || (keyStore != null); + } + + public synchronized String getDetectedLibraryPath() { + return detectedLibraryPath; + } + + @Override + public synchronized List listCertificates() throws CertificateProviderException { + if (keyStore == null) { + // Se o middleware estiver detectado mas o token ainda não estiver autenticado via PIN + return Collections.emptyList(); + } + + try { + List certificates = new ArrayList<>(); + Enumeration aliases = keyStore.aliases(); + + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + boolean isKeyEntry = keyStore.isKeyEntry(alias); + Certificate cert = keyStore.getCertificate(alias); + + if (!(cert instanceof X509Certificate x509)) { + continue; + } + + String certId = "p11-" + Integer.toHexString(alias.hashCode()); + ManagedCertificate mc = X509CertificateUtils.toManagedCertificate( + certId, + alias, + x509, + CertificateType.A3, + isKeyEntry, + PROVIDER_ID, + "Token USB / Smart Card (PKCS#11)" + ); + certificates.add(mc); + } + + return Collections.unmodifiableList(certificates); + } catch (KeyStoreException e) { + throw new CertificateProviderException( + CcspErrorCode.PKCS11_ERROR, + "Falha ao ler certificados do dispositivo PKCS#11: " + e.getMessage(), + e + ); + } + } + + @Override + public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException { + Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo."); + + if (keyStore == null) { + throw new CertificateProviderException( + CcspErrorCode.TOKEN_NOT_FOUND, + "Dispositivo PKCS#11 não conectado ou não autenticado com PIN." + ); + } + + DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm()); + alg.validateDigestLength(context.digest()); + + String targetAlias = resolveAlias(context.certificateHint()); + if (targetAlias == null) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Certificado não encontrado no dispositivo PKCS#11." + ); + } + + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null); + if (privateKey == null) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "Chave privada inacessível no dispositivo PKCS#11." + ); + } + + Certificate[] chain = keyStore.getCertificateChain(targetAlias); + if (chain == null || chain.length == 0) { + Certificate singleCert = keyStore.getCertificate(targetAlias); + if (singleCert != null) { + chain = new Certificate[]{singleCert}; + } else { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Cadeia do certificado não encontrada no dispositivo PKCS#11." + ); + } + } + + logger.info("Executando assinatura digital no hardware PKCS#11 [alg={}]", alg.getJcaName()); + + Signature signature = (sunPkcs11Provider != null) + ? Signature.getInstance(alg.getJcaName(), sunPkcs11Provider) + : Signature.getInstance(alg.getJcaName()); + + signature.initSign(privateKey); + signature.update(context.digest()); + byte[] signatureBytes = signature.sign(); + + String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded()); + List chainBase64 = new ArrayList<>(); + for (Certificate c : chain) { + chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded())); + } + + return new SigningResult( + context.requestId(), + signatureBytes, + certBase64, + chainBase64, + alg.getJcaName(), + Instant.now() + ); + + } catch (CertificateProviderException cpe) { + throw cpe; + } catch (Exception e) { + logger.error("Erro na assinatura via dispositivo PKCS#11: {}", e.getMessage(), e); + throw new CertificateProviderException( + CcspErrorCode.PKCS11_ERROR, + "Erro ao assinar com o dispositivo PKCS#11: " + e.getMessage(), + e + ); + } + } + + private String resolveAlias(String hint) { + try { + Enumeration aliases = keyStore.aliases(); + List list = Collections.list(aliases); + if (hint == null || hint.isBlank()) { + return list.isEmpty() ? null : list.get(0); + } + for (String alias : list) { + String certId = "p11-" + Integer.toHexString(alias.hashCode()); + if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) { + return alias; + } + } + } catch (Exception ignored) { + } + return null; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProvider.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProvider.java new file mode 100644 index 0000000..ee6ef23 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProvider.java @@ -0,0 +1,318 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.crypto.DigestAlgorithm; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; + +/** + * Provedor de certificados A1 em formato PKCS#12 (.pfx / .p12). + * Mantém chaves e senhas isoladas na memória local do agente sem persistência insegura ou vazamento em logs. + */ +public class Pkcs12CertificateProvider implements CertificateProvider { + + private static final Logger logger = LoggerFactory.getLogger(Pkcs12CertificateProvider.class); + private static final String PROVIDER_ID = "provider-a1"; + + private final String providerId; + private final String displayName; + private KeyStore keyStore; + private char[] currentPassword; + + public Pkcs12CertificateProvider() { + this(PROVIDER_ID, "Certificados A1 (PKCS#12)"); + } + + public Pkcs12CertificateProvider(String providerId, String displayName) { + this.providerId = Objects.requireNonNull(providerId); + this.displayName = Objects.requireNonNull(displayName); + } + + /** + * Carrega um KeyStore PKCS#12 diretamente a partir de bytes. + * A senha é utilizada exclusivamente para decifrar a chave em memória e nunca é gravada em logs. + */ + public synchronized void loadFromBytes(byte[] pfxData, char[] password) throws CertificateProviderException { + Objects.requireNonNull(pfxData, "Dados do certificado não podem ser nulos."); + try (ByteArrayInputStream bais = new ByteArrayInputStream(pfxData)) { + loadFromStream(bais, password); + } catch (CertificateProviderException cpe) { + throw cpe; + } catch (Exception e) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_INVALID, + "Falha ao processar arquivo PKCS#12: " + e.getMessage(), + e + ); + } + } + + /** + * Carrega um KeyStore PKCS#12 a partir de um InputStream. + */ + public synchronized void loadFromStream(InputStream stream, char[] password) throws CertificateProviderException { + try { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(stream, password); + this.keyStore = ks; + this.currentPassword = password != null ? password.clone() : new char[0]; + logger.info("KeyStore PKCS#12 carregado com sucesso no provedor '{}'", providerId); + } catch (Exception e) { + String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : ""; + if (msg.contains("password") || msg.contains("keystore password was incorrect") || msg.contains("mac")) { + throw new CertificateProviderException( + CcspErrorCode.PIN_INCORRECT, + "Senha do certificado A1 incorreta.", + e + ); + } + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_INVALID, + "Não foi possível carregar o arquivo PKCS#12: " + e.getMessage(), + e + ); + } + } + + /** + * Define diretamente uma instância de KeyStore PKCS#12 pré-carregada (útil em testes ou integrações). + */ + public synchronized void setKeyStore(KeyStore keyStore, char[] password) { + this.keyStore = keyStore; + this.currentPassword = password != null ? password.clone() : new char[0]; + } + + public synchronized void clear() { + this.keyStore = null; + if (this.currentPassword != null) { + java.util.Arrays.fill(this.currentPassword, ' '); + this.currentPassword = null; + } + } + + @Override + public String getProviderId() { + return providerId; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public ProviderType getType() { + return ProviderType.A1_FILE; + } + + @Override + public synchronized boolean isAvailable() { + return keyStore != null; + } + + @Override + public synchronized List listCertificates() throws CertificateProviderException { + if (!isAvailable()) { + return Collections.emptyList(); + } + + try { + List certificates = new ArrayList<>(); + Enumeration aliases = keyStore.aliases(); + + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + boolean isKeyEntry = keyStore.isKeyEntry(alias); + Certificate cert = keyStore.getCertificate(alias); + + if (!(cert instanceof X509Certificate x509)) { + continue; + } + + String certId = "a1-" + Integer.toHexString(alias.hashCode()); + ManagedCertificate mc = X509CertificateUtils.toManagedCertificate( + certId, + alias, + x509, + CertificateType.A1, + isKeyEntry, + providerId, + "Arquivo PKCS#12 (.pfx/.p12)" + ); + certificates.add(mc); + } + + return Collections.unmodifiableList(certificates); + } catch (KeyStoreException e) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "Erro ao consultar certificados do KeyStore PKCS#12: " + e.getMessage(), + e + ); + } + } + + @Override + public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException { + Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo."); + + if (!isAvailable()) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "Nenhum certificado A1 carregado neste provedor." + ); + } + + DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm()); + alg.validateDigestLength(context.digest()); + + String targetAlias = resolveAlias(context.certificateHint()); + if (targetAlias == null) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Certificado A1 não encontrado para a requisição: " + context.certificateHint() + ); + } + + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, currentPassword); + if (privateKey == null) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "Chave privada não encontrada para o certificado: " + targetAlias + ); + } + + Certificate[] chain = keyStore.getCertificateChain(targetAlias); + if (chain == null || chain.length == 0) { + Certificate singleCert = keyStore.getCertificate(targetAlias); + if (singleCert != null) { + chain = new Certificate[]{singleCert}; + } else { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Cadeia de certificados ausente para o alias: " + targetAlias + ); + } + } + + logger.info("Executando assinatura com certificado A1 [alg={}]", alg.getJcaName()); + + Signature signature = Signature.getInstance(alg.getJcaName()); + signature.initSign(privateKey); + signature.update(context.digest()); + byte[] signatureBytes = signature.sign(); + + String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded()); + List chainBase64 = new ArrayList<>(); + for (Certificate c : chain) { + chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded())); + } + + return new SigningResult( + context.requestId(), + signatureBytes, + certBase64, + chainBase64, + alg.getJcaName(), + Instant.now() + ); + + } catch (CertificateProviderException cpe) { + throw cpe; + } catch (Exception e) { + logger.error("Erro ao realizar assinatura com certificado A1: {}", e.getMessage(), e); + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "Erro ao assinar com certificado A1: " + e.getMessage(), + e + ); + } + } + + @Override + public synchronized java.util.Optional getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException { + if (!isAvailable()) { + return java.util.Optional.empty(); + } + String targetAlias = resolveAlias(certificateId); + if (targetAlias == null) { + return java.util.Optional.empty(); + } + char[] passToUse = (currentPassword != null && currentPassword.length > 0) ? currentPassword : pinOrPassword; + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, passToUse); + if (privateKey == null) { + return java.util.Optional.empty(); + } + Certificate[] chain = keyStore.getCertificateChain(targetAlias); + if (chain == null || chain.length == 0) { + Certificate single = keyStore.getCertificate(targetAlias); + if (single != null) { + chain = new Certificate[]{single}; + } + } + if (chain == null || chain.length == 0) { + return java.util.Optional.empty(); + } + X509Certificate[] x509Chain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + x509Chain[i] = (X509Certificate) chain[i]; + } + return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, null)); + } catch (Exception e) { + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "Falha ao obter chave privada e certificados A1: " + e.getMessage(), + e + ); + } + } + + private String resolveAlias(String hint) { + try { + Enumeration aliases = keyStore.aliases(); + List list = Collections.list(aliases); + if (hint == null || hint.isBlank()) { + return list.isEmpty() ? null : list.get(0); + } + for (String alias : list) { + String certId = "a1-" + Integer.toHexString(alias.hashCode()); + if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) { + return alias; + } + Certificate c = keyStore.getCertificate(alias); + if (c instanceof X509Certificate x509) { + String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1"); + String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256"); + if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) { + return alias; + } + } + } + } catch (Exception ignored) { + } + return null; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/ProviderType.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/ProviderType.java new file mode 100644 index 0000000..b8976c4 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/ProviderType.java @@ -0,0 +1,21 @@ +package br.com.cybercore.signer.core.provider; + +/** + * Classificação da tecnologia ou repositório do provedor de certificados. + */ +public enum ProviderType { + A1_FILE("Arquivo PKCS#12 (.pfx/.p12)"), + A3_WINDOWS_STORE("Windows Certificate Store (CAPI/CNG)"), + A3_PKCS11("Hardware PKCS#11 (Smart Card / Token USB)"), + MOCK("Simulador de Desenvolvimento"); + + private final String description; + + ProviderType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProvider.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProvider.java new file mode 100644 index 0000000..9fd01cf --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProvider.java @@ -0,0 +1,307 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.crypto.DigestAlgorithm; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; + +/** + * Provedor de certificados integrado nativamente ao repositório de certificados do Windows (Windows-MY). + * Utiliza o subsistema SunMSCAPI/CAPI/CNG para gerenciar chaves privadas e certificados A1/A3 sem expor credenciais. + */ +public class WindowsCertificateStoreProvider implements CertificateProvider { + + private static final Logger logger = LoggerFactory.getLogger(WindowsCertificateStoreProvider.class); + private static final String PROVIDER_ID = "provider-win-store"; + private static final String KEYSTORE_TYPE = "Windows-MY"; + + private KeyStore keyStore; + private final boolean isWindows; + + public WindowsCertificateStoreProvider() { + String osName = System.getProperty("os.name", "").toLowerCase(); + this.isWindows = osName.contains("win"); + if (this.isWindows) { + initKeyStore(); + } + } + + private synchronized void initKeyStore() { + try { + this.keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + this.keyStore.load(null, null); + logger.info("Windows Certificate Store (Windows-MY) inicializado com sucesso."); + } catch (Throwable t) { + logger.warn("Não foi possível carregar o repositório Windows-MY: {}", t.getMessage()); + this.keyStore = null; + } + } + + @Override + public String getProviderId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayName() { + return "Windows Certificate Store (Windows-MY)"; + } + + @Override + public ProviderType getType() { + return ProviderType.A3_WINDOWS_STORE; + } + + @Override + public boolean isAvailable() { + return isWindows && keyStore != null; + } + + @Override + public synchronized List listCertificates() throws CertificateProviderException { + if (!isAvailable()) { + return Collections.emptyList(); + } + + try { + // Recarrega o repositório para detectar tokens recém-inseridos + this.keyStore.load(null, null); + + List certificates = new ArrayList<>(); + Enumeration aliases = keyStore.aliases(); + + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + try { + if (!keyStore.isKeyEntry(alias)) { + continue; + } + + Certificate cert = keyStore.getCertificate(alias); + if (!(cert instanceof X509Certificate x509)) { + continue; + } + + // Heurística de tipo A1 vs A3: + // ICP-Brasil A3 tem tipicamente 3 anos (validade > ~400 dias). + // A1 tem validade de até 1 ano (<= 370 dias). + long validityDays = ChronoUnit.DAYS.between(x509.getNotBefore().toInstant(), x509.getNotAfter().toInstant()); + CertificateType certType = validityDays > 400 ? CertificateType.A3 : CertificateType.A1; + String deviceName = certType == CertificateType.A3 + ? "Token / Smart Card (Windows CAPI/CNG)" + : "Repositório do Windows (Software A1)"; + + String certId = "win-" + Integer.toHexString(alias.hashCode()); + + ManagedCertificate managedCert = X509CertificateUtils.toManagedCertificate( + certId, + alias, + x509, + certType, + true, + PROVIDER_ID, + deviceName + ); + + certificates.add(managedCert); + } catch (Exception e) { + logger.warn("Ignorando alias '{}' devido a erro na leitura: {}", alias, e.getMessage()); + } + } + + return Collections.unmodifiableList(certificates); + } catch (Exception e) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "Falha ao enumerar certificados do Windows Certificate Store: " + e.getMessage(), + e + ); + } + } + + @Override + public synchronized SigningResult sign(SigningContext context) throws CertificateProviderException { + Objects.requireNonNull(context, "Contexto de assinatura não pode ser nulo."); + + if (!isAvailable()) { + throw new CertificateProviderException( + CcspErrorCode.PROVIDER_NOT_AVAILABLE, + "Repositório Windows Certificate Store não está disponível neste sistema operacional." + ); + } + + // Valida algoritmo e tamanho de digest + DigestAlgorithm alg = DigestAlgorithm.fromName(context.algorithm()); + alg.validateDigestLength(context.digest()); + + String targetAlias = resolveAlias(context.certificateHint()); + if (targetAlias == null) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Nenhum certificado correspondente encontrado no Windows Certificate Store para o hint: " + context.certificateHint() + ); + } + + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null); + if (privateKey == null) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "Chave privada não acessível para o certificado: " + targetAlias + ); + } + + Certificate[] chain = keyStore.getCertificateChain(targetAlias); + if (chain == null || chain.length == 0) { + Certificate singleCert = keyStore.getCertificate(targetAlias); + if (singleCert != null) { + chain = new Certificate[]{singleCert}; + } else { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Cadeia de certificação não encontrada para o alias: " + targetAlias + ); + } + } + + logger.info("Executando assinatura digital com algoritmo {} via Windows-MY para alias '{}'", + alg.getJcaName(), targetAlias); + + Signature signature = Signature.getInstance(alg.getJcaName()); + signature.initSign(privateKey); + signature.update(context.digest()); + byte[] signatureBytes = signature.sign(); + + String certBase64 = Base64.getEncoder().encodeToString(chain[0].getEncoded()); + List chainBase64 = new ArrayList<>(); + for (Certificate c : chain) { + chainBase64.add(Base64.getEncoder().encodeToString(c.getEncoded())); + } + + return new SigningResult( + context.requestId(), + signatureBytes, + certBase64, + chainBase64, + alg.getJcaName(), + Instant.now() + ); + + } catch (CertificateProviderException cpe) { + throw cpe; + } catch (Exception e) { + logger.error("Erro durante a assinatura via Windows-MY: {}", e.getMessage(), e); + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "Erro ao assinar com o Windows Certificate Store: " + e.getMessage(), + e + ); + } finally { + try { + // Força liberação de handles no SunMSCAPI para que o driver de SmartCard/Token + // possa reavaliar o estado de autenticação de PIN + if (keyStore != null) { + keyStore.load(null, null); + } + } catch (Exception ignored) { + } + + // Se configurado para sempre exigir PIN (sem cache), forçamos o reset da sessão no Smart Card + // para que o Windows BaseCSP e o SafeSign invalidem o cache de processo e peçam o PIN na próxima assinatura + if (br.com.cybercore.signer.core.security.PinSessionManager.getInstance().isAlwaysPromptPin()) { + br.com.cybercore.signer.core.security.SmartCardSessionManager.resetCardSessions(); + } + } + } + + private String resolveAlias(String hint) { + if (hint == null || hint.isBlank()) { + return null; + } + try { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + String certId = "win-" + Integer.toHexString(alias.hashCode()); + if (alias.equalsIgnoreCase(hint) || certId.equalsIgnoreCase(hint)) { + return alias; + } + Certificate c = keyStore.getCertificate(alias); + if (c instanceof X509Certificate x509) { + String sha1 = X509CertificateUtils.calculateFingerprint(x509, "SHA-1"); + String sha256 = X509CertificateUtils.calculateFingerprint(x509, "SHA-256"); + if (sha1.equalsIgnoreCase(hint) || sha256.equalsIgnoreCase(hint)) { + return alias; + } + } + } + } catch (Exception ignored) { + } + return null; + } + + @Override + public synchronized java.util.Optional getKeyMaterial(String certificateId, char[] pinOrPassword) throws CertificateProviderException { + if (!isAvailable()) { + return java.util.Optional.empty(); + } + String targetAlias = resolveAlias(certificateId); + if (targetAlias == null) { + return java.util.Optional.empty(); + } + try { + PrivateKey privateKey = (PrivateKey) keyStore.getKey(targetAlias, null); + if (privateKey == null) { + return java.util.Optional.empty(); + } + Certificate[] chain = keyStore.getCertificateChain(targetAlias); + if (chain == null || chain.length == 0) { + Certificate single = keyStore.getCertificate(targetAlias); + if (single != null) { + chain = new Certificate[]{single}; + } + } + if (chain == null || chain.length == 0) { + return java.util.Optional.empty(); + } + X509Certificate[] x509Chain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + x509Chain[i] = (X509Certificate) chain[i]; + } + return java.util.Optional.of(new br.com.cybercore.signer.core.domain.SigningKeyMaterial(privateKey, x509Chain, keyStore.getProvider())); + } catch (Exception e) { + throw new CertificateProviderException( + CcspErrorCode.SIGNATURE_FAILED, + "Falha ao obter chave privada e certificados no Windows Store: " + e.getMessage(), + e + ); + } + } + + @Override + public synchronized void refresh() { + if (isWindows) { + initKeyStore(); + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/ApplicationRegistry.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/ApplicationRegistry.java new file mode 100644 index 0000000..234239d --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/ApplicationRegistry.java @@ -0,0 +1,162 @@ +package br.com.cybercore.signer.core.security; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registro de aplicações web autorizadas a se comunicar com o agente CyberCore Signer. + * Suporta persistência segura no diretório do usuário (~/.cybercore/authorized-apps.json). + */ +public final class ApplicationRegistry { + + private static final Logger logger = LoggerFactory.getLogger(ApplicationRegistry.class); + private static final String DEFAULT_FILE_NAME = "authorized-apps.json"; + + private static final ApplicationRegistry INSTANCE = new ApplicationRegistry(); + + private final Path storagePath; + private final Map applications = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper; + + public static ApplicationRegistry getInstance() { + return INSTANCE; + } + + public ApplicationRegistry() { + this(resolveDefaultStoragePath()); + } + + public ApplicationRegistry(Path storagePath) { + this.storagePath = storagePath; + this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + load(); + } + + private static Path resolveDefaultStoragePath() { + String userHome = System.getProperty("user.home", "."); + return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME); + } + + /** + * Busca uma aplicação pelo seu ID único. + */ + public Optional findById(String applicationId) { + if (applicationId == null || applicationId.isBlank()) { + return Optional.empty(); + } + return Optional.ofNullable(applications.get(applicationId)); + } + + /** + * Busca uma aplicação que contenha a origem especificada autorizada. + */ + public Optional findByOrigin(String origin) { + if (origin == null || origin.isBlank()) { + return Optional.empty(); + } + return applications.values().stream() + .filter(app -> app.isOriginAllowed(origin)) + .findFirst(); + } + + /** + * Verifica se uma aplicação e sua respectiva origem estão autorizadas e ativas. + */ + public boolean isAuthorized(String applicationId, String origin) { + Optional appOpt = findById(applicationId); + return appOpt.map(app -> app.isOriginAllowed(origin)).orElse(false); + } + + /** + * Registra ou atualiza uma aplicação autorizada e persiste a alteração. + */ + public synchronized void register(AuthorizedApplication app) { + Objects.requireNonNull(app, "Aplicação não pode ser nula"); + applications.put(app.applicationId(), app); + logger.info("Aplicação registrada: '{}' [id={}] com origens: {}", app.name(), app.applicationId(), app.allowedOrigins()); + save(); + } + + /** + * Revoga a autorização de uma aplicação. + */ + public synchronized void revoke(String applicationId) { + if (applicationId != null && applications.remove(applicationId) != null) { + logger.info("Autorização revogada para a aplicação [id={}]", applicationId); + save(); + } + } + + /** + * Retorna a lista imutável de todas as aplicações cadastradas. + */ + public List listAll() { + return Collections.unmodifiableList(new ArrayList<>(applications.values())); + } + + /** + * Carrega as aplicações persistidas em disco. + */ + public synchronized void load() { + if (storagePath == null || !Files.exists(storagePath)) { + logger.debug("Nenhum arquivo de aplicações persistidas encontrado em {}", storagePath); + return; + } + + try { + List list = objectMapper.readValue( + storagePath.toFile(), + new TypeReference>() {} + ); + applications.clear(); + for (AuthorizedApplication app : list) { + applications.put(app.applicationId(), app); + } + logger.info("Carregadas {} aplicação(ões) autorizada(s) de {}", applications.size(), storagePath); + } catch (Exception e) { + logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage()); + } + } + + /** + * Salva as aplicações atuais em disco. + */ + public synchronized void save() { + if (storagePath == null) { + return; + } + + try { + Path parent = storagePath.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + objectMapper.writerWithDefaultPrettyPrinter().writeValue( + storagePath.toFile(), + new ArrayList<>(applications.values()) + ); + logger.debug("Aplicações autorizadas salvas com sucesso em {}", storagePath); + } catch (Exception e) { + logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage()); + } + } + + public synchronized void clear() { + applications.clear(); + save(); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/AuthorizedApplication.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/AuthorizedApplication.java new file mode 100644 index 0000000..ab6dc29 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/AuthorizedApplication.java @@ -0,0 +1,44 @@ +package br.com.cybercore.signer.core.security; + +import java.io.Serializable; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Representa uma aplicação web autorizada a solicitar assinaturas ao CyberCore Signer. + */ +public record AuthorizedApplication( + String applicationId, + String name, + Set allowedOrigins, + boolean enabled, + Instant registeredAt +) implements Serializable { + + public AuthorizedApplication { + Objects.requireNonNull(applicationId, "applicationId must not be null"); + Objects.requireNonNull(name, "name must not be null"); + allowedOrigins = allowedOrigins != null ? Set.copyOf(allowedOrigins) : Set.of(); + if (registeredAt == null) { + registeredAt = Instant.now(); + } + } + + /** + * Verifica se uma determinada Origin web está autorizada para esta aplicação. + */ + public boolean isOriginAllowed(String origin) { + if (!enabled || origin == null || origin.isBlank()) { + return false; + } + String normalized = origin.trim().toLowerCase(); + for (String allowed : allowedOrigins) { + if (allowed.trim().toLowerCase().equals(normalized)) { + return true; + } + } + return false; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/NonceManager.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/NonceManager.java new file mode 100644 index 0000000..bb3d85b --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/NonceManager.java @@ -0,0 +1,53 @@ +package br.com.cybercore.signer.core.security; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Gerenciador de nonces em memória para proteção contra ataques de replay (CCSP/1). + * Garante que uma requisição com o mesmo nonce e correlation_id não seja reprocessada. + */ +public class NonceManager { + + private final Duration windowDuration; + private final ConcurrentHashMap usedNonces = new ConcurrentHashMap<>(); + + public NonceManager(Duration windowDuration) { + this.windowDuration = windowDuration != null ? windowDuration : Duration.ofSeconds(120); + } + + public NonceManager() { + this(Duration.ofSeconds(120)); + } + + /** + * Tenta registrar um nonce. + * + * @param nonce Nonce gerado pelo cliente/requisição. + * @return {@code true} se o nonce for novo e válido; {@code false} se já tiver sido utilizado (replay). + */ + public boolean registerNonce(String nonce) { + if (nonce == null || nonce.isBlank()) { + return false; + } + + cleanupExpired(); + + Instant now = Instant.now(); + Instant existing = usedNonces.putIfAbsent(nonce, now); + return existing == null; + } + + /** + * Remove nonces expirados além da janela máxima de aceitação. + */ + public void cleanupExpired() { + Instant cutoff = Instant.now().minus(windowDuration); + usedNonces.entrySet().removeIf(entry -> entry.getValue().isBefore(cutoff)); + } + + public int activeNonceCount() { + return usedNonces.size(); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/OriginValidator.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/OriginValidator.java new file mode 100644 index 0000000..87f6fd5 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/OriginValidator.java @@ -0,0 +1,44 @@ +package br.com.cybercore.signer.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.Objects; +import java.util.Set; + +/** + * Validador estrito de origens web conforme as especificações de segurança do protocolo CCSP. + */ +public class OriginValidator { + + private static final Logger logger = LoggerFactory.getLogger(OriginValidator.class); + + /** + * Valida se a origem recebida na requisição corresponde às origens autorizadas da aplicação. + */ + public boolean isAllowed(String requestOrigin, AuthorizedApplication application) { + if (application == null || !application.enabled()) { + logger.warn("Aplicação nula ou desabilitada durante validação de origem."); + return false; + } + + if (requestOrigin == null || requestOrigin.isBlank()) { + logger.warn("Requisição sem cabeçalho Origin rejeitada para a aplicação {}", application.applicationId()); + return false; + } + + try { + URI uri = URI.create(requestOrigin.trim()); + String normalizedOrigin = uri.getScheme() + "://" + uri.getAuthority(); + boolean allowed = application.isOriginAllowed(normalizedOrigin); + if (!allowed) { + logger.warn("Origem '{}' rejeitada para a aplicação '{}'", normalizedOrigin, application.applicationId()); + } + return allowed; + } catch (Exception e) { + logger.warn("Origem malformatada '{}': {}", requestOrigin, e.getMessage()); + return false; + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/PinSessionManager.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/PinSessionManager.java new file mode 100644 index 0000000..9d83944 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/PinSessionManager.java @@ -0,0 +1,106 @@ +package br.com.cybercore.signer.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Gerenciador de PIN e credenciais voláteis de sessão do CyberCore Signer. + * Armazena temporariamente em memória RAM (nunca em disco) o PIN de tokens A3 + * exclusivamente quando o usuário autorizar explicitamente a opção "Salvar PIN na sessão". + * Todas as senhas e PINs são sobrescritos com espaços em branco (' ') no descarte. + */ +public class PinSessionManager { + + private static final Logger logger = LoggerFactory.getLogger(PinSessionManager.class); + + private static final PinSessionManager INSTANCE = new PinSessionManager(); + + private final Map sessionPins = new ConcurrentHashMap<>(); + private volatile boolean alwaysPromptPin = false; + + public PinSessionManager() { + } + + public static PinSessionManager getInstance() { + return INSTANCE; + } + + /** + * Salva o PIN de um certificado/token para a sessão atual em memória volátil. + */ + public synchronized void savePin(String certId, char[] pin) { + Objects.requireNonNull(certId, "certId must not be null"); + if (pin == null || pin.length == 0) { + clearPin(certId); + return; + } + + // Limpa PIN anterior caso exista + clearPin(certId); + + sessionPins.put(certId, pin.clone()); + logger.debug("PIN salvo em memória de sessão para o certificado '{}'. Tamanho: {} dígitos", certId, pin.length); + } + + /** + * Recupera o PIN salvo em sessão para o certificado informado. + */ + public synchronized char[] getPin(String certId) { + if (certId == null || alwaysPromptPin) { + return null; + } + char[] stored = sessionPins.get(certId); + return stored != null ? stored.clone() : null; + } + + /** + * Verifica se existe um PIN válido salvo nesta sessão para o certificado. + */ + public synchronized boolean hasPin(String certId) { + if (certId == null || alwaysPromptPin) { + return false; + } + return sessionPins.containsKey(certId); + } + + /** + * Remove o PIN salvo para um certificado específico, zerando a memória. + */ + public synchronized void clearPin(String certId) { + if (certId != null) { + char[] old = sessionPins.remove(certId); + if (old != null) { + Arrays.fill(old, ' '); + } + } + } + + /** + * Limpa imediatamente todos os PINs em memória e zera seus caracteres. + */ + public synchronized void clearAll() { + for (char[] pin : sessionPins.values()) { + if (pin != null) { + Arrays.fill(pin, ' '); + } + } + sessionPins.clear(); + logger.info("Todos os PINs em memória de sessão foram limpos e zerados com sucesso."); + } + + public boolean isAlwaysPromptPin() { + return alwaysPromptPin; + } + + public void setAlwaysPromptPin(boolean alwaysPromptPin) { + this.alwaysPromptPin = alwaysPromptPin; + if (alwaysPromptPin) { + clearAll(); + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardSessionManager.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardSessionManager.java new file mode 100644 index 0000000..8bafeda --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardSessionManager.java @@ -0,0 +1,69 @@ +package br.com.cybercore.signer.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * Gerencia sessões de hardware Smart Card (Tokens USB / Cartões ICP-Brasil) no nível do subsistema PC/SC (winscard). + * Permite forçar o reset do canal do cartão para invalidar caches de PIN de processo (BaseCSP / Minidriver), + * garantindo que novas operações de assinatura exijam autenticação explícita quando configurado. + */ +public final class SmartCardSessionManager { + + private static final Logger logger = LoggerFactory.getLogger(SmartCardSessionManager.class); + + private SmartCardSessionManager() { + } + + /** + * Reseta as conexões ativas com todos os terminais Smart Card conectados que possuam cartão presente. + * Envia o comando SCARD_RESET_CARD (warm reset via javax.smartcardio Card.disconnect(true)), + * invalidando o estado de autenticação de PIN nos drivers e no CryptoAPI/CNG do Windows. + */ + @SuppressWarnings("unchecked") + public static void resetCardSessions() { + try { + Class tfClass = Class.forName("javax.smartcardio.TerminalFactory"); + Method getDefaultMethod = tfClass.getMethod("getDefault"); + Object factory = getDefaultMethod.invoke(null); + + Method terminalsMethod = factory.getClass().getMethod("terminals"); + Object cardTerminals = terminalsMethod.invoke(factory); + + Class stateEnumClass = Class.forName("javax.smartcardio.CardTerminals$State"); + Object cardPresent = Enum.valueOf((Class) stateEnumClass, "CARD_PRESENT"); + + Method listMethod = cardTerminals.getClass().getMethod("list", stateEnumClass); + List terminals = (List) listMethod.invoke(cardTerminals, cardPresent); + + if (terminals == null || terminals.isEmpty()) { + logger.debug("Nenhum terminal Smart Card com cartão presente encontrado para reset de sessão."); + return; + } + + for (Object terminal : terminals) { + try { + Method getNameMethod = terminal.getClass().getMethod("getName"); + String termName = (String) getNameMethod.invoke(terminal); + + Method connectMethod = terminal.getClass().getMethod("connect", String.class); + Object card = connectMethod.invoke(terminal, "*"); + + if (card != null) { + Method disconnectMethod = card.getClass().getMethod("disconnect", boolean.class); + // disconnect(true) envia SCARD_RESET_CARD ao driver do Windows / SafeSign + disconnectMethod.invoke(card, true); + logger.info("Sessão de Smart Card resetada com sucesso no terminal '{}' (cache de PIN invalidado)", termName); + } + } catch (Throwable cardError) { + logger.debug("Falha ao resetar terminal Smart Card individual: {}", cardError.getMessage()); + } + } + } catch (Throwable t) { + logger.debug("SmartCardIO não disponível ou sem suporte no ambiente: {}", t.getMessage()); + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardWatcher.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardWatcher.java new file mode 100644 index 0000000..71e70aa --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SmartCardWatcher.java @@ -0,0 +1,197 @@ +package br.com.cybercore.signer.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Monitor em segundo plano para detecção em tempo real de inserção e remoção física + * de Smart Cards e Tokens USB ICP-Brasil via subsistema PC/SC (javax.smartcardio). + * + * Implementa os requisitos 5 e 13 do CyberCore Signer: + * - Detecta conexão e remoção de hardware; + * - Registra logs explícitos de auditoria para eventos de TOKEN_REMOVED; + * - Invalida automaticamente sessões e caches de PIN quando um dispositivo físico é removido; + * - Notifica observadores (ex: UI e CertificateManager) para atualização em tempo real. + */ +public class SmartCardWatcher { + + private static final Logger logger = LoggerFactory.getLogger(SmartCardWatcher.class); + + public interface CardEventListener { + void onCardInserted(String terminalName); + void onCardRemoved(String terminalName); + } + + private final List listeners = new CopyOnWriteArrayList<>(); + private final Set knownCards = Collections.synchronizedSet(new HashSet<>()); + + private volatile boolean running = false; + private Thread watcherThread; + + public SmartCardWatcher() { + } + + public void addListener(CardEventListener listener) { + if (listener != null) { + listeners.add(listener); + } + } + + public void removeListener(CardEventListener listener) { + listeners.remove(listener); + } + + /** + * Inicia o thread de monitoramento contínuo em segundo plano. + */ + public synchronized void start() { + if (running) { + return; + } + + running = true; + watcherThread = new Thread(this::pollLoop, "smartcard-hardware-watcher"); + watcherThread.setDaemon(true); + watcherThread.start(); + logger.info("Monitoramento dinâmico de hardware Smart Card / Token USB iniciado."); + } + + /** + * Finaliza o monitoramento. + */ + public synchronized void stop() { + running = false; + if (watcherThread != null) { + watcherThread.interrupt(); + watcherThread = null; + } + logger.info("Monitoramento dinâmico de hardware Smart Card / Token USB finalizado."); + } + + public boolean isRunning() { + return running; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void pollLoop() { + Object cardTerminals = null; + Method waitForChangeMethod = null; + Method listMethod = null; + Object cardPresentState = null; + + try { + Class tfClass = Class.forName("javax.smartcardio.TerminalFactory"); + Method getDefaultMethod = tfClass.getMethod("getDefault"); + Object factory = getDefaultMethod.invoke(null); + + Method terminalsMethod = factory.getClass().getMethod("terminals"); + cardTerminals = terminalsMethod.invoke(factory); + + Class stateEnumClass = Class.forName("javax.smartcardio.CardTerminals$State"); + cardPresentState = Enum.valueOf((Class) stateEnumClass, "CARD_PRESENT"); + + listMethod = cardTerminals.getClass().getMethod("list", stateEnumClass); + waitForChangeMethod = cardTerminals.getClass().getMethod("waitForChange", long.class); + + // Popula estado inicial + Set initial = getCurrentTerminalsWithCards(cardTerminals, listMethod, cardPresentState); + knownCards.addAll(initial); + + } catch (Throwable t) { + logger.debug("Subsistema PC/SC (javax.smartcardio) indisponível ou sem suporte neste ambiente: {}", t.getMessage()); + return; + } + + while (running) { + try { + // Aguarda alteração de estado no hardware (timeout de 1.5s para checar flag running) + Boolean changed = (Boolean) waitForChangeMethod.invoke(cardTerminals, 1500L); + + if (Boolean.TRUE.equals(changed) || !running) { + if (!running) break; + + Set current = getCurrentTerminalsWithCards(cardTerminals, listMethod, cardPresentState); + + // Dispositivos removidos + Set removed = new HashSet<>(knownCards); + removed.removeAll(current); + for (String term : removed) { + logger.warn("AUDITORIA [TOKEN_REMOVED]: Smart Card / Token USB desconectado do terminal '{}'", term); + // Ao remover o token, zera imediatamente PINs salvos em memória por segurança + PinSessionManager.getInstance().clearAll(); + SmartCardSessionManager.resetCardSessions(); + notifyRemoved(term); + } + + // Dispositivos inseridos + Set inserted = new HashSet<>(current); + inserted.removeAll(knownCards); + for (String term : inserted) { + logger.info("AUDITORIA: Smart Card / Token USB detectado no terminal '{}'", term); + notifyInserted(term); + } + + knownCards.clear(); + knownCards.addAll(current); + } + } catch (Throwable e) { + if (e instanceof InterruptedException || (e.getCause() instanceof InterruptedException)) { + Thread.currentThread().interrupt(); + break; + } + if (!running) break; + // Espera curta para evitar consumo excessivo de CPU em caso de falha transitória + try { + Thread.sleep(2000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + + @SuppressWarnings("unchecked") + private Set getCurrentTerminalsWithCards(Object cardTerminals, Method listMethod, Object state) { + Set result = new HashSet<>(); + try { + List terminals = (List) listMethod.invoke(cardTerminals, state); + if (terminals != null) { + for (Object t : terminals) { + Method getName = t.getClass().getMethod("getName"); + String name = (String) getName.invoke(t); + result.add(name); + } + } + } catch (Throwable ignored) { + } + return result; + } + + private void notifyInserted(String terminal) { + for (CardEventListener listener : listeners) { + try { + listener.onCardInserted(terminal); + } catch (Throwable t) { + logger.error("Erro no listener onCardInserted: {}", t.getMessage()); + } + } + } + + private void notifyRemoved(String terminal) { + for (CardEventListener listener : listeners) { + try { + listener.onCardRemoved(terminal); + } catch (Throwable t) { + logger.error("Erro no listener onCardRemoved: {}", t.getMessage()); + } + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/SignatureValidationService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/SignatureValidationService.java new file mode 100644 index 0000000..f4fe8f6 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/SignatureValidationService.java @@ -0,0 +1,417 @@ +package br.com.cybercore.signer.core.validation; + +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.util.Store; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * Serviço especializado para Validação Criptográfica de Assinaturas Digitais. + * Suporta Assinaturas Destacadas (Detached CMS/PKCS#7) e Assinaturas Incorporadas + * (PAdES em PDF, CMS/P7M com conteúdo encapsulado e XMLDSig). + */ +public final class SignatureValidationService { + + private static final Logger logger = LoggerFactory.getLogger(SignatureValidationService.class); + private static final JcaX509CertificateConverter CERT_CONVERTER = new JcaX509CertificateConverter(); + + /** + * Valida uma Assinatura Destacada (Detached). + * Requer os bytes do documento original e os bytes do arquivo de assinatura (.p7s / .sig). + */ + public ValidationResult validateDetached(byte[] originalContent, byte[] signatureBytes, String docName) { + if (originalContent == null || originalContent.length == 0) { + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.DETACHED_CMS, + "Documento original não informado ou vazio.", + List.of(), + null, + docName + ); + } + + if (signatureBytes == null || signatureBytes.length == 0) { + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.DETACHED_CMS, + "Arquivo de assinatura (.p7s) não informado ou vazio.", + List.of(), + null, + docName + ); + } + + try { + CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(originalContent), signatureBytes); + return processCmsSignatures(signedData, ValidationType.DETACHED_CMS, null, docName); + } catch (Exception e) { + logger.warn("Erro ao processar assinatura destacada: {}", e.getMessage()); + return new ValidationResult( + ValidationStatus.INVALID, + ValidationType.DETACHED_CMS, + "Falha ao decodificar pacote CMS/PKCS#7: " + e.getMessage(), + List.of(), + null, + docName + ); + } + } + + /** + * Valida uma Assinatura Incorporada / Anexada (Attached / Enveloped / Embedded). + * O arquivo assinado contém os dados e a assinatura juntos (PDF, P7M/CMS com dados, ou XML). + */ + public ValidationResult validateAttached(byte[] signedFileBytes, String fileName) { + if (signedFileBytes == null || signedFileBytes.length == 0) { + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.UNKNOWN, + "Arquivo assinado não informado ou vazio.", + List.of(), + null, + fileName + ); + } + + String nameLower = (fileName != null) ? fileName.toLowerCase() : ""; + + // 1. Verificação se é documento PDF + if (nameLower.endsWith(".pdf") || isPdfBytes(signedFileBytes)) { + return validatePdf(signedFileBytes, fileName); + } + + // 2. Verificação se é documento XML + if (nameLower.endsWith(".xml") || isXmlBytes(signedFileBytes)) { + return validateXml(signedFileBytes, fileName); + } + + // 3. Verificação se é CMS incorporado (P7M / P7S com dados embutidos) + try { + CMSSignedData signedData = new CMSSignedData(signedFileBytes); + if (signedData.getSignedContent() != null) { + Object contentObj = signedData.getSignedContent().getContent(); + byte[] extracted = null; + if (contentObj instanceof byte[] b) { + extracted = b; + } + return processCmsSignatures(signedData, ValidationType.ATTACHED_CMS, extracted, fileName); + } else { + return new ValidationResult( + ValidationStatus.WARNING, + ValidationType.DETACHED_CMS, + "O arquivo de assinatura selecionado é do tipo Destacado (Detached) e não contém o documento embutido. Selecione o Documento Original para validar.", + List.of(), + null, + fileName + ); + } + } catch (Exception e) { + logger.warn("Não foi possível decodificar como CMS: {}", e.getMessage()); + } + + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.UNKNOWN, + "Formato de arquivo assinado não reconhecido (esperado .pdf, .p7m, .p7s ou .xml).", + List.of(), + null, + fileName + ); + } + + /** + * Validação de assinaturas embutidas em documentos PDF (PAdES). + */ + private ValidationResult validatePdf(byte[] pdfBytes, String fileName) { + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + List signatures = document.getSignatureDictionaries(); + + if (signatures == null || signatures.isEmpty()) { + return new ValidationResult( + ValidationStatus.INVALID, + ValidationType.PADES_PDF, + "Nenhuma assinatura digital encontrada no documento PDF.", + List.of(), + null, + fileName + ); + } + + List signerInfos = new ArrayList<>(); + boolean allValid = true; + + for (PDSignature sig : signatures) { + byte[] contents = sig.getContents(pdfBytes); + byte[] signedByteRange = sig.getSignedContent(pdfBytes); + + if (contents == null || signedByteRange == null) { + continue; + } + + CMSSignedData cms = new CMSSignedData(new CMSProcessableByteArray(signedByteRange), contents); + ValidationResult cmsRes = processCmsSignatures(cms, ValidationType.PADES_PDF, null, fileName); + + if (!cmsRes.isValid()) { + allValid = false; + } + + for (ValidationSignerInfo info : cmsRes.signers()) { + String dateStr = (sig.getSignDate() != null) + ? new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(sig.getSignDate().getTime()) + : info.signingTime(); + + signerInfos.add(new ValidationSignerInfo( + (sig.getName() != null && !sig.getName().isBlank()) ? sig.getName() : info.signerName(), + info.identifier(), + info.issuer(), + dateStr, + info.digestValid(), + info.signatureValid(), + info.algorithm(), + info.certificateSubject(), + (sig.getReason() != null ? "Motivo: " + sig.getReason() : "Assinatura PAdES embutida no PDF") + )); + } + } + + ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID; + String msg = allValid + ? String.format("PDF assinado e íntegro com %d assinatura(s) digital(is) válida(s).", signerInfos.size()) + : "Assinatura(s) do PDF contêm falha de integridade ou autenticidade."; + + return new ValidationResult(status, ValidationType.PADES_PDF, msg, signerInfos, null, fileName); + + } catch (Exception e) { + logger.error("Falha ao validar documento PDF: {}", e.getMessage(), e); + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.PADES_PDF, + "Erro ao analisar documento PDF: " + e.getMessage(), + List.of(), + null, + fileName + ); + } + } + + /** + * Validação de assinaturas embutidas em documentos XML (XMLDSig). + */ + private ValidationResult validateXml(byte[] xmlBytes, String fileName) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes)); + + NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (nl.getLength() == 0) { + return new ValidationResult( + ValidationStatus.INVALID, + ValidationType.XML_DSIG, + "Nenhum elemento XMLDSig encontrado no documento.", + List.of(), + null, + fileName + ); + } + + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + List signers = new ArrayList<>(); + boolean allValid = true; + + for (int i = 0; i < nl.getLength(); i++) { + DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(i)); + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + boolean coreValidity = signature.validate(valContext); + + if (!coreValidity) { + allValid = false; + } + + signers.add(new ValidationSignerInfo( + "Signatário XML #" + (i + 1), + "", + "Assinatura Enveloped XML", + new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()), + coreValidity, + coreValidity, + signature.getSignedInfo().getSignatureMethod().getAlgorithm(), + "XMLDSig Element", + "Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA") + )); + } + + 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."; + return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName); + + } catch (Exception e) { + logger.error("Erro ao validar XML: {}", e.getMessage(), e); + return new ValidationResult( + ValidationStatus.ERROR, + ValidationType.XML_DSIG, + "Erro ao processar assinatura XML: " + e.getMessage(), + List.of(), + null, + fileName + ); + } + } + + /** + * Processa e valida criptograficamente os signatários de um pacote CMSSignedData. + */ + @SuppressWarnings("unchecked") + private ValidationResult processCmsSignatures(CMSSignedData signedData, ValidationType type, + byte[] extractedContent, String docName) { + List signers = new ArrayList<>(); + boolean allValid = true; + + Store certStore = signedData.getCertificates(); + SignerInformationStore signerStore = signedData.getSignerInfos(); + + for (SignerInformation signer : signerStore.getSigners()) { + boolean sigValid = false; + String signerName = "Signatário Criptográfico"; + String identifier = ""; + String issuer = "Desconhecido"; + String subject = ""; + String signingTime = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()); + + try { + Collection matches = certStore.getMatches(signer.getSID()); + if (!matches.isEmpty()) { + X509CertificateHolder certHolder = matches.iterator().next(); + X509Certificate cert = CERT_CONVERTER.getCertificate(certHolder); + + subject = cert.getSubjectX500Principal().getName(); + issuer = cert.getIssuerX500Principal().getName(); + signerName = X509CertificateUtils.extractCommonName(subject); + + 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; + } + } + + // Validação Criptográfica real com a chave pública do certificado + sigValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert)); + } + } catch (Exception e) { + logger.warn("Falha ao verificar assinatura de {}: {}", signerName, e.getMessage()); + sigValid = false; + } + + if (!sigValid) { + allValid = false; + } + + signers.add(new ValidationSignerInfo( + signerName, + identifier, + issuer, + signingTime, + sigValid, + sigValid, + signer.getDigestAlgOID(), + subject, + sigValid ? "Integridade e assinatura conferidas com sucesso." : "Falha na validação criptográfica do resumo ou chave." + )); + } + + if (signers.isEmpty()) { + return new ValidationResult( + ValidationStatus.INVALID, + type, + "Nenhum signatário encontrado no arquivo de assinatura.", + List.of(), + extractedContent, + docName + ); + } + + ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID; + String summary = allValid + ? "Assinatura criptográfica válida e integridade do documento confirmada." + : "Uma ou mais assinaturas criptográficas falharam na validação."; + + return new ValidationResult(status, type, summary, signers, extractedContent, docName); + } + + private static boolean isPdfBytes(byte[] bytes) { + if (bytes.length < 5) return false; + return bytes[0] == '%' && bytes[1] == 'P' && bytes[2] == 'D' && bytes[3] == 'F'; + } + + private static boolean isXmlBytes(byte[] bytes) { + if (bytes.length < 5) return false; + // Pula possíveis BOM + int offset = 0; + if (bytes.length > 3 && (bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF)) { + offset = 3; + } + while (offset < bytes.length && Character.isWhitespace(bytes[offset])) { + offset++; + } + return offset < bytes.length && bytes[offset] == '<'; + } + + /** + * KeySelector simples para resolução de chave pública em assinaturas XML via KeyInfo. + */ + private static class KeyValueKeySelector extends javax.xml.crypto.KeySelector { + @Override + public javax.xml.crypto.KeySelectorResult select(javax.xml.crypto.dsig.keyinfo.KeyInfo keyInfo, + Purpose purpose, + javax.xml.crypto.AlgorithmMethod method, + javax.xml.crypto.XMLCryptoContext context) { + if (keyInfo == null) return () -> null; + for (Object info : keyInfo.getContent()) { + if (info instanceof javax.xml.crypto.dsig.keyinfo.KeyValue kv) { + try { + java.security.PublicKey pk = kv.getPublicKey(); + return () -> pk; + } catch (Exception ignored) { + } + } else if (info instanceof javax.xml.crypto.dsig.keyinfo.X509Data x509Data) { + for (Object o : x509Data.getContent()) { + if (o instanceof X509Certificate cert) { + return cert::getPublicKey; + } + } + } + } + return () -> null; + } + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationResult.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationResult.java new file mode 100644 index 0000000..003cc16 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationResult.java @@ -0,0 +1,19 @@ +package br.com.cybercore.signer.core.validation; + +import java.util.List; + +/** + * Resultado completo da validação de uma assinatura digital (incorporada ou destacada). + */ +public record ValidationResult( + ValidationStatus status, + ValidationType type, + String summaryMessage, + List signers, + byte[] extractedContent, + String documentName +) { + public boolean isValid() { + return status == ValidationStatus.VALID; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationSignerInfo.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationSignerInfo.java new file mode 100644 index 0000000..4836c49 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationSignerInfo.java @@ -0,0 +1,20 @@ +package br.com.cybercore.signer.core.validation; + +/** + * Informações e resultado da validação criptográfica de um signatário individual. + */ +public record ValidationSignerInfo( + String signerName, + String identifier, + String issuer, + String signingTime, + boolean digestValid, + boolean signatureValid, + String algorithm, + String certificateSubject, + String details +) { + public boolean isValid() { + return digestValid && signatureValid; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationStatus.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationStatus.java new file mode 100644 index 0000000..88bb35c --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationStatus.java @@ -0,0 +1,21 @@ +package br.com.cybercore.signer.core.validation; + +/** + * Status do resultado da validação criptográfica. + */ +public enum ValidationStatus { + VALID("VÁLIDA"), + INVALID("INVÁLIDA"), + WARNING("VÁLIDA COM RESSALVAS"), + ERROR("ERRO DE PROCESSAMENTO"); + + private final String description; + + ValidationStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationType.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationType.java new file mode 100644 index 0000000..8be237c --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/validation/ValidationType.java @@ -0,0 +1,28 @@ +package br.com.cybercore.signer.core.validation; + +/** + * Tipos de assinatura digital suportados para validação. + */ +public enum ValidationType { + DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false), + ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true), + PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true), + XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true), + UNKNOWN("Formato Desconhecido", false); + + private final String description; + private final boolean isEmbedded; + + ValidationType(String description, boolean isEmbedded) { + this.description = description; + this.isEmbedded = isEmbedded; + } + + public String getDescription() { + return description; + } + + public boolean isEmbedded() { + return isEmbedded; + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSignatureOptions.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSignatureOptions.java new file mode 100644 index 0000000..75424ef --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSignatureOptions.java @@ -0,0 +1,32 @@ +package br.com.cybercore.signer.core.xml; + +import java.util.Objects; + +/** + * Parâmetros de configuração para a operação de assinatura digital XMLDSig. + */ +public record XmlSignatureOptions( + String certificateId, + char[] pinOrPassword, + String targetElementTag, + String targetElementId, + String signatureAlgorithm +) { + + public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + + public XmlSignatureOptions { + Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo."); + if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) { + signatureAlgorithm = DEFAULT_ALGORITHM; + } + } + + public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) { + return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM); + } + + public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) { + return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM); + } +} diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSigningService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSigningService.java new file mode 100644 index 0000000..9699515 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/xml/XmlSigningService.java @@ -0,0 +1,290 @@ +package br.com.cybercore.signer.core.xml; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningKeyMaterial; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.crypto.AlgorithmMethod; +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.KeySelectorException; +import javax.xml.crypto.KeySelectorResult; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.X509Data; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.security.Key; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Serviço de alto nível para Assinatura Digital de Documentos XML segundo o padrão W3C XMLDSig e ICP-Brasil. + * Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.). + * Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig). + */ +public class XmlSigningService { + + private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class); + + private final CertificateManager certificateManager; + + public XmlSigningService(CertificateManager certificateManager) { + this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo."); + } + + /** + * Assina digitalmente um arquivo XML e grava no arquivo de destino. + */ + public void signXmlFile(File sourceXml, File targetXml, XmlSignatureOptions options) throws Exception { + Objects.requireNonNull(sourceXml, "Arquivo XML de origem não pode ser nulo."); + Objects.requireNonNull(targetXml, "Arquivo XML de destino não pode ser nulo."); + Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas."); + + byte[] inputBytes = Files.readAllBytes(sourceXml.toPath()); + byte[] signedBytes = signXml(inputBytes, options); + + if (targetXml.getParentFile() != null) { + targetXml.getParentFile().mkdirs(); + } + try (FileOutputStream fos = new FileOutputStream(targetXml)) { + fos.write(signedBytes); + } + logger.info("XML assinado com sucesso e salvo em: {}", targetXml.getAbsolutePath()); + } + + /** + * Assina digitalmente o documento XML contido em bytes segundo as opções configuradas. + */ + public byte[] signXml(byte[] xmlBytes, XmlSignatureOptions options) throws Exception { + Objects.requireNonNull(xmlBytes, "Bytes do XML não podem ser nulos."); + Objects.requireNonNull(options, "Opções de assinatura não podem ser nulas."); + + // 1. Resolução do Certificado e Chave Privada + Optional certOpt = certificateManager.findById(options.certificateId()); + if (certOpt.isEmpty()) { + certOpt = certificateManager.findByHint(options.certificateId()); + } + if (certOpt.isEmpty()) { + throw new CertificateProviderException( + CcspErrorCode.CERTIFICATE_NOT_FOUND, + "Certificado com ID '" + options.certificateId() + "' não encontrado." + ); + } + ManagedCertificate cert = certOpt.get(); + + Optional keyMaterialOpt = certificateManager.getKeyMaterial(cert.id(), options.pinOrPassword()); + if (keyMaterialOpt.isEmpty()) { + throw new CertificateProviderException( + CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE, + "Chave privada inacessível para o certificado '" + cert.alias() + "'." + ); + } + SigningKeyMaterial keyMaterial = keyMaterialOpt.get(); + + logger.info("Iniciando assinatura XMLDSig com certificado '{}' [tipo={}]", cert.commonName(), cert.type()); + + // 2. Parse Seguro do XML (Prevenção rigorosa de XXE) + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(new ByteArrayInputStream(xmlBytes)); + + // 3. Configuração do XMLSignatureFactory + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + + // Transformações padrão: Enveloped + C14N + List transforms = new ArrayList<>(); + transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); + transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)); + + // Determinação da Referência e Ponto de Inserção + Reference ref; + Node insertionParent; + + if (options.targetElementId() != null && !options.targetElementId().isBlank()) { + String elemId = options.targetElementId(); + Element targetElement = findElementById(doc, elemId, options.targetElementTag()); + if (targetElement == null) { + throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento."); + } + targetElement.setIdAttribute("Id", true); + ref = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); + insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement(); + } else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) { + NodeList list = doc.getElementsByTagName(options.targetElementTag()); + if (list.getLength() == 0) { + list = doc.getElementsByTagNameNS("*", options.targetElementTag()); + } + if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) { + String idVal = elem.getAttribute("Id"); + elem.setIdAttribute("Id", true); + ref = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); + insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement(); + } else { + ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); + insertionParent = doc.getDocumentElement(); + } + } else { + // Assinatura Enveloped do documento completo + ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); + insertionParent = doc.getDocumentElement(); + } + + // 4. Montagem do SignedInfo + SignedInfo si = fac.newSignedInfo( + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), + fac.newSignatureMethod(options.signatureAlgorithm(), null), + Collections.singletonList(ref) + ); + + // 5. Inclusão da Cadeia de Certificados no KeyInfo + KeyInfoFactory kif = fac.getKeyInfoFactory(); + List x509Content = new ArrayList<>(); + x509Content.add(keyMaterial.signingCertificate()); + X509Data x509Data = kif.newX509Data(x509Content); + KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data)); + + // 6. Execução da Assinatura DOM + DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent); + if (keyMaterial.jcaProvider() != null) { + dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider()); + } + + XMLSignature signature = fac.newXMLSignature(si, ki); + signature.sign(dsc); + + // 7. Serialização do XML Assinado + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer trans = tf.newTransformer(); + trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + trans.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + trans.setOutputProperty(OutputKeys.INDENT, "no"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + trans.transform(new DOMSource(doc), new StreamResult(baos)); + + logger.info("Assinatura XMLDSig gerada com sucesso. Tamanho: {} bytes", baos.size()); + return baos.toByteArray(); + } + + /** + * Valida a integridade criptográfica de um documento XML assinado com XMLDSig. + */ + public boolean validateXmlSignature(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)); + + // Registra nós com atributo Id/id/ID para que o ResourceResolver do XMLDSig localize a referência + NodeList allElements = doc.getElementsByTagName("*"); + 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"); + if (nl.getLength() == 0) { + nl = doc.getElementsByTagName("Signature"); + } + if (nl.getLength() == 0) { + throw new IllegalArgumentException("Nenhum elemento encontrado no XML para validação."); + } + + Element sigElement = (Element) nl.item(0); + + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement); + + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + return signature.validate(valContext); + } + + private static Element findElementById(Document doc, String id, String hintTag) { + if (hintTag != null && !hintTag.isBlank()) { + NodeList nl = doc.getElementsByTagName(hintTag); + for (int i = 0; i < nl.getLength(); i++) { + if (nl.item(i) instanceof Element e && id.equals(e.getAttribute("Id"))) { + return e; + } + } + } + return doc.getElementById(id); + } + + /** + * Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo. + */ + private static class X509KeySelector extends KeySelector { + @Override + public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, + AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { + if (keyInfo == null) { + throw new KeySelectorException("Elemento KeyInfo ausente na assinatura XML."); + } + for (Object info : keyInfo.getContent()) { + if (info instanceof X509Data x509Data) { + for (Object o : x509Data.getContent()) { + if (o instanceof X509Certificate cert) { + PublicKey pk = cert.getPublicKey(); + return () -> pk; + } + } + } + } + throw new KeySelectorException("Nenhuma chave pública X.509 encontrada no KeyInfo da assinatura."); + } + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/config/AgentConfigurationServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/config/AgentConfigurationServiceTest.java new file mode 100644 index 0000000..3a8c986 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/config/AgentConfigurationServiceTest.java @@ -0,0 +1,47 @@ +package br.com.cybercore.signer.core.config; + +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes de Configurações do Agente (AgentConfigurationService)") +class AgentConfigurationServiceTest { + + @TempDir + Path tempDir; + + @Test + @DisplayName("Deve salvar e recuperar configurações personalizadas do agente") + void shouldSaveAndLoadConfigurations() { + Path configFile = tempDir.resolve("config.json"); + AgentConfigurationService service = new AgentConfigurationService(configFile); + + AgentConfiguration customConfig = new AgentConfiguration( + false, + true, + true, + "cybercore-modern", + CpfDisplayMode.MASKED, + false, + 18420 + ); + + service.updateConfig(customConfig); + + // Novo serviço lendo o mesmo arquivo + AgentConfigurationService reloadedService = new AgentConfigurationService(configFile); + AgentConfiguration loaded = reloadedService.getConfig(); + + assertThat(loaded.requireConfirmation()).isFalse(); + assertThat(loaded.alwaysPromptPin()).isTrue(); + assertThat(loaded.autoStart()).isTrue(); + assertThat(loaded.defaultSealThemeId()).isEqualTo("cybercore-modern"); + assertThat(loaded.defaultCpfDisplayMode()).isEqualTo(CpfDisplayMode.MASKED); + assertThat(loaded.defaultIncludeDateTime()).isFalse(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/DigestAlgorithmTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/DigestAlgorithmTest.java new file mode 100644 index 0000000..9b49317 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/DigestAlgorithmTest.java @@ -0,0 +1,45 @@ +package br.com.cybercore.signer.core.crypto; + +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DigestAlgorithmTest { + + @Test + @DisplayName("Deve identificar algoritmos suportados pelo nome JCA") + void shouldFindSupportedAlgorithm() throws Exception { + DigestAlgorithm alg = DigestAlgorithm.fromName("SHA256withRSA"); + assertThat(alg).isEqualTo(DigestAlgorithm.SHA256_WITH_RSA); + assertThat(alg.getExpectedDigestLengthBytes()).isEqualTo(32); + + DigestAlgorithm algEc = DigestAlgorithm.fromName("SHA384withECDSA"); + assertThat(algEc).isEqualTo(DigestAlgorithm.SHA384_WITH_ECDSA); + assertThat(algEc.getExpectedDigestLengthBytes()).isEqualTo(48); + } + + @Test + @DisplayName("Deve lançar CertificateProviderException para algoritmo desconhecido") + void shouldFailOnUnknownAlgorithm() { + assertThatThrownBy(() -> DigestAlgorithm.fromName("MD5withRSA")) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.UNSUPPORTED_ALGORITHM); + } + + @Test + @DisplayName("Deve validar comprimento correto do hash digest") + void shouldValidateDigestLength() throws Exception { + DigestAlgorithm alg = DigestAlgorithm.SHA256_WITH_RSA; + byte[] validSha256 = new byte[32]; + alg.validateDigestLength(validSha256); + + byte[] invalidLength = new byte[16]; + assertThatThrownBy(() -> alg.validateDigestLength(invalidLength)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.SIGNATURE_FAILED); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/SigningServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/SigningServiceTest.java new file mode 100644 index 0000000..4f46c5a --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/crypto/SigningServiceTest.java @@ -0,0 +1,147 @@ +package br.com.cybercore.signer.core.crypto; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProvider; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class SigningServiceTest { + + private CertificateManager certificateManager; + private CertificateProvider mockProvider; + private SigningService signingService; + private ManagedCertificate validCert; + + @BeforeEach + void setUp() throws Exception { + certificateManager = new CertificateManager(); + mockProvider = mock(CertificateProvider.class); + when(mockProvider.getProviderId()).thenReturn("provider-mock"); + when(mockProvider.isAvailable()).thenReturn(true); + + validCert = new ManagedCertificate( + "cert-valid", "João da Silva", CertificateType.A3, CertificateStatus.AVAILABLE, + "CN=João da Silva:12345678909, O=ICP-Brasil, C=BR", "CN=AC Certisign, C=BR", + "ABC1234", "sha1_val", "sha256_val", + Instant.now().minus(30, ChronoUnit.DAYS), Instant.now().plus(300, ChronoUnit.DAYS), + "RSA", 2048, "João da Silva", "12345678909", null, "provider-mock", "Token USB" + ); + + when(mockProvider.listCertificates()).thenReturn(List.of(validCert)); + certificateManager.registerProvider(mockProvider); + certificateManager.discoverCertificates(); + + signingService = new SigningService(certificateManager); + } + + @Test + @DisplayName("Deve executar assinatura com sucesso delegando ao provider") + void shouldSignSuccessfully() throws Exception { + byte[] digest = new byte[32]; // SHA-256 digest + SigningContext context = new SigningContext( + "req-uuid-1", "app-erp", digest, "SHA256withRSA", "cert-valid", Instant.now() + ); + + SigningResult expectedResult = new SigningResult( + "req-uuid-1", new byte[]{1, 2, 3, 4}, "certBase64", + List.of("certBase64"), "SHA256withRSA", Instant.now() + ); + + when(mockProvider.sign(any())).thenReturn(expectedResult); + + SigningResult actual = signingService.sign(context); + + assertThat(actual).isNotNull(); + assertThat(actual.requestId()).isEqualTo("req-uuid-1"); + assertThat(actual.signatureBytes()).isEqualTo(new byte[]{1, 2, 3, 4}); + } + + @Test + @DisplayName("Deve rejeitar requisição sem request_id") + void shouldRejectMissingRequestId() { + byte[] digest = new byte[32]; + + assertThatThrownBy(() -> new SigningContext( + null, "app-erp", digest, "SHA256withRSA", "cert-valid", Instant.now() + )).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("Deve falhar ao tentar assinar com algoritmo não suportado") + void shouldFailOnUnsupportedAlgorithm() { + byte[] digest = new byte[32]; + SigningContext context = new SigningContext( + "req-2", "app-erp", digest, "MD5withRSA", "cert-valid", Instant.now() + ); + + assertThatThrownBy(() -> signingService.sign(context)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.UNSUPPORTED_ALGORITHM); + } + + @Test + @DisplayName("Deve falhar se o certificado estiver expirado") + void shouldFailOnExpiredCertificate() throws Exception { + ManagedCertificate expiredCert = new ManagedCertificate( + "cert-exp", "Maria Expirada", CertificateType.A1, CertificateStatus.EXPIRED, + "CN=Maria Expirada, C=BR", "CN=AC Raiz, C=BR", + "EXP123", "sha1_exp", "sha256_exp", + Instant.now().minus(300, ChronoUnit.DAYS), Instant.now().minus(10, ChronoUnit.DAYS), + "RSA", 2048, "Maria Expirada", null, null, "provider-mock", null + ); + + when(mockProvider.listCertificates()).thenReturn(List.of(expiredCert)); + certificateManager.discoverCertificates(); + + byte[] digest = new byte[32]; + SigningContext context = new SigningContext( + "req-3", "app-erp", digest, "SHA256withRSA", "cert-exp", Instant.now() + ); + + assertThatThrownBy(() -> signingService.sign(context)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.CERTIFICATE_EXPIRED); + } + + @Test + @DisplayName("Deve falhar se o certificado não possuir chave privada") + void shouldFailWhenPrivateKeyNotAvailable() throws Exception { + ManagedCertificate noKeyCert = new ManagedCertificate( + "cert-nokey", "Certificado Público", CertificateType.A1, CertificateStatus.NO_PRIVATE_KEY, + "CN=Certificado Público, C=BR", "CN=AC Raiz, C=BR", + "NOKEY123", "sha1_nokey", "sha256_nokey", + Instant.now().minus(10, ChronoUnit.DAYS), Instant.now().plus(300, ChronoUnit.DAYS), + "RSA", 2048, "Certificado Público", null, null, "provider-mock", null + ); + + when(mockProvider.listCertificates()).thenReturn(List.of(noKeyCert)); + certificateManager.discoverCertificates(); + + byte[] digest = new byte[32]; + SigningContext context = new SigningContext( + "req-4", "app-erp", digest, "SHA256withRSA", "cert-nokey", Instant.now() + ); + + assertThatThrownBy(() -> signingService.sign(context)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.PRIVATE_KEY_NOT_AVAILABLE); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/diagnostics/DiagnosticServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/diagnostics/DiagnosticServiceTest.java new file mode 100644 index 0000000..418b012 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/diagnostics/DiagnosticServiceTest.java @@ -0,0 +1,22 @@ +package br.com.cybercore.signer.core.diagnostics; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DiagnosticServiceTest { + + @Test + @DisplayName("Deve gerar relatório de diagnóstico com sucesso e validar crypto engine") + void shouldGenerateDiagnosticReport() { + DiagnosticService service = new DiagnosticService(); + DiagnosticReport report = service.runDiagnostic(); + + assertThat(report).isNotNull(); + assertThat(report.cryptoEngineOk()).isTrue(); + assertThat(report.javaVersion()).isNotBlank(); + assertThat(report.registeredSecurityProviders()).isNotEmpty(); + assertThat(report.additionalDetails()).containsKey("VirtualThreadsSupported"); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/PdfSigningServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/PdfSigningServiceTest.java new file mode 100644 index 0000000..6c061fc --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/PdfSigningServiceTest.java @@ -0,0 +1,218 @@ +package br.com.cybercore.signer.core.pdf; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +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.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +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.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PdfSigningServiceTest { + + private CertificateManager certificateManager; + private PdfSigningService pdfSigningService; + private String testCertId; + + @BeforeEach + void setUp() throws Exception { + certificateManager = new CertificateManager(); + + // Gera par de chaves e certificado X.509 em memória + 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=Joao da Silva: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)); + + // Cria KeyStore PKCS12 em memória + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + char[] pwd = "password123".toCharArray(); + ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert}); + + Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste"); + provider.setKeyStore(ks, pwd); + certificateManager.registerProvider(provider); + + List list = certificateManager.discoverCertificates(); + assertThat(list).isNotEmpty(); + this.testCertId = list.get(0).id(); + + this.pdfSigningService = new PdfSigningService(certificateManager); + } + + 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), 14); + cs.newLineAtOffset(100, 700); + cs.showText("Documento de Contrato de Teste - CyberCore Signer"); + cs.endText(); + } + doc.save(baos); + } + return baos.toByteArray(); + } + + @Test + @DisplayName("Deve assinar documento PDF aplicando selo visual com o tema ICP-Brasil") + void shouldSignPdfWithIcpBrasilVisualSeal() throws Exception { + byte[] originalPdf = createSamplePdf(); + + PdfSealTheme theme = PdfSealThemeRegistry.findById("icp-brasil").orElseThrow(); + PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId) + .sealTheme(theme) + .visibleSeal(true) + .position(PdfSealPosition.BOTTOM_RIGHT) + .pageChoice(PdfSealPageChoice.LAST_PAGE) + .reason("Aprovação Contratual") + .location("Brasília, DF") + .build(); + + byte[] signedPdf = pdfSigningService.signPdf(originalPdf, options); + + assertThat(signedPdf).isNotNull(); + assertThat(signedPdf.length).isGreaterThan(originalPdf.length); + + // Valida se o PDF possui a assinatura PDSignature incorporada + try (PDDocument signedDoc = Loader.loadPDF(signedPdf)) { + List signatures = signedDoc.getSignatureDictionaries(); + assertThat(signatures).hasSize(1); + + PDSignature sig = signatures.get(0); + assertThat(sig.getName()).contains("Joao da Silva"); + assertThat(sig.getReason()).isEqualTo("Aprovação Contratual"); + assertThat(sig.getLocation()).isEqualTo("Brasília, DF"); + } + } + + @Test + @DisplayName("Deve suportar todos os temas de selos visuais em SVG (CyberCore, Minimalista, Gov.br)") + void shouldSupportAllVisualSealThemes() throws Exception { + byte[] originalPdf = createSamplePdf(); + + for (PdfSealTheme theme : PdfSealThemeRegistry.listThemes()) { + PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId) + .sealTheme(theme) + .visibleSeal(true) + .position(PdfSealPosition.BOTTOM_LEFT) + .build(); + + byte[] signed = pdfSigningService.signPdf(originalPdf, options); + assertThat(signed).isNotNull(); + + try (PDDocument doc = Loader.loadPDF(signed)) { + assertThat(doc.getSignatureDictionaries()).hasSize(1); + } + } + } + + @Test + @DisplayName("Deve assinar PDF no modo invisível (sem selo gráfico)") + void shouldSignPdfInvisibleWithoutSeal() throws Exception { + byte[] originalPdf = createSamplePdf(); + + PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId) + .visibleSeal(false) + .build(); + + byte[] signedPdf = pdfSigningService.signPdf(originalPdf, options); + + try (PDDocument doc = Loader.loadPDF(signedPdf)) { + assertThat(doc.getSignatureDictionaries()).hasSize(1); + } + } + + @Test + @DisplayName("Deve assinar PDF com coordenadas customizadas e página específica") + void shouldSignPdfWithCustomCoordinatesAndPage() throws Exception { + byte[] originalPdf = createSamplePdf(); + + PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId) + .visibleSeal(true) + .customPosition(120.5f, 250.0f, 1) + .reason("Assinatura em local personalizado") + .location("São Paulo, SP") + .build(); + + assertThat(options.position()).isEqualTo(PdfSealPosition.CUSTOM); + assertThat(options.customX()).isEqualTo(120.5f); + assertThat(options.customY()).isEqualTo(250.0f); + assertThat(options.targetPageNumber()).isEqualTo(1); + + byte[] signedPdf = pdfSigningService.signPdf(originalPdf, options); + assertThat(signedPdf).isNotNull(); + + try (PDDocument doc = Loader.loadPDF(signedPdf)) { + assertThat(doc.getSignatureDictionaries()).hasSize(1); + PDSignature sig = doc.getSignatureDictionaries().get(0); + assertThat(sig.getReason()).isEqualTo("Assinatura em local personalizado"); + } + } + + @Test + @DisplayName("Deve contar páginas e renderizar imagem da página do PDF") + void shouldCountPagesAndRenderPageImage() throws Exception { + byte[] originalPdf = createSamplePdf(); + + int count = PdfSigningService.getPageCount(originalPdf); + assertThat(count).isEqualTo(1); + + java.awt.image.BufferedImage img = PdfSigningService.renderPageImage(originalPdf, 0, 72f); + assertThat(img).isNotNull(); + assertThat(img.getWidth()).isGreaterThan(100); + assertThat(img.getHeight()).isGreaterThan(100); + + byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f); + assertThat(pngBytes).isNotEmpty(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayModeTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayModeTest.java new file mode 100644 index 0000000..9787008 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CpfDisplayModeTest.java @@ -0,0 +1,31 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes do Modo de Exibição de CPF nos Selos (CpfDisplayMode)") +class CpfDisplayModeTest { + + @Test + @DisplayName("Deve formatar CPF completo com pontuação padrão") + void shouldFormatFullCpf() { + String formatted = CpfDisplayMode.FULL.formatCpf("12345678900"); + assertThat(formatted).isEqualTo("123.456.789-00"); + } + + @Test + @DisplayName("Deve formatar CPF mascarado protegendo dados sensíveis") + void shouldFormatMaskedCpf() { + String formatted = CpfDisplayMode.MASKED.formatCpf("12345678900"); + assertThat(formatted).isEqualTo("***.456.789-**"); + } + + @Test + @DisplayName("Deve omitir CPF quando o modo for NONE") + void shouldOmitCpfWhenNone() { + String formatted = CpfDisplayMode.NONE.formatCpf("12345678900"); + assertThat(formatted).isEmpty(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorageTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorageTest.java new file mode 100644 index 0000000..09017ee --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/CustomSealStorageTest.java @@ -0,0 +1,137 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes de Armazenamento e Instalação de Selos Personalizados (CustomSealStorage)") +class CustomSealStorageTest { + + @TempDir + Path tempDir; + + @Test + @DisplayName("Deve salvar, listar e excluir selos personalizados com sucesso") + void shouldSaveListAndDeleteCustomSeal() throws IOException { + CustomSealStorage storage = new CustomSealStorage(tempDir); + + PdfSealTheme customTheme = new PdfSealTheme( + "selo-meu-escritorio", + "Meu Escritório de Advocacia", + "Selo personalizado com brasão dourado e fundo escuro", + 320, + 82, + "#b45309", + "#d97706", + "#ffffff", + "#fde68a", + "#1f2937", + null, + "ADVOCACIA SILVA • ASSINADO DIGITALMENTE", + "Validade jurídica plena conforme MP nº 2.200-2/2001", + "notary", + CpfDisplayMode.MASKED, + true, + true + ); + + storage.saveSeal(customTheme); + + List seals = storage.loadAllCustomSeals(); + assertThat(seals).hasSize(1); + + PdfSealTheme loaded = seals.get(0); + assertThat(loaded.id()).isEqualTo("selo-meu-escritorio"); + assertThat(loaded.name()).isEqualTo("Meu Escritório de Advocacia"); + assertThat(loaded.customHeader()).isEqualTo("ADVOCACIA SILVA • ASSINADO DIGITALMENTE"); + assertThat(loaded.defaultCpfMode()).isEqualTo(CpfDisplayMode.MASKED); + assertThat(loaded.isCustom()).isTrue(); + + boolean deleted = storage.deleteSeal("selo-meu-escritorio"); + assertThat(deleted).isTrue(); + assertThat(storage.loadAllCustomSeals()).isEmpty(); + } + + @Test + @DisplayName("Deve salvar e carregar selo customizado com logotipo em imagem Base64") + void shouldSaveAndLoadCustomSealWithCustomLogo() throws IOException { + CustomSealStorage storage = new CustomSealStorage(tempDir); + + String sampleBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + + PdfSealTheme customWithLogo = new PdfSealTheme( + "selo-logo-propria", + "Selo com Logo Própria", + "Selo para empresa com logotipo PNG em Base64", + 320, + 82, + "#1e3a8a", + "#3b82f6", + "#ffffff", + "#93c5fd", + "#172554", + null, + "MEU ESCRITÓRIO • ASSINADO DIGITALMENTE", + "Validade jurídica plena conforme MP nº 2.200-2/2001", + "shield", + CpfDisplayMode.FULL, + true, + true, + sampleBase64 + ); + + storage.saveSeal(customWithLogo); + + List seals = storage.loadAllCustomSeals(); + assertThat(seals).hasSize(1); + PdfSealTheme loaded = seals.get(0); + assertThat(loaded.id()).isEqualTo("selo-logo-propria"); + assertThat(loaded.customLogoBase64()).isEqualTo(sampleBase64); + assertThat(loaded.isCustom()).isTrue(); + } + + @Test + @DisplayName("Deve salvar e carregar selo customizado com campos customizados (ex: Matrícula / Cargo / OAB)") + void shouldSaveAndLoadCustomSealWithCustomFields() throws IOException { + CustomSealStorage storage = new CustomSealStorage(tempDir); + + PdfSealTheme customWithFields = new PdfSealTheme( + "selo-servidor-publico", + "Selo Servidor Público", + "Selo para auditores e servidores com matrícula funcional", + 320, + 82, + "#065f46", + "#059669", + "#ffffff", + "#a7f3d0", + "#064e3b", + null, + "TRIBUNAL DE CONTAS • AUDITORIA FISCAL", + "Autenticidade ICP-Brasil conferida", + "shield", + CpfDisplayMode.MASKED, + true, + true, + null, + "Matrícula", + "984210-AUD" + ); + + storage.saveSeal(customWithFields); + + List seals = storage.loadAllCustomSeals(); + assertThat(seals).hasSize(1); + PdfSealTheme loaded = seals.get(0); + assertThat(loaded.id()).isEqualTo("selo-servidor-publico"); + assertThat(loaded.customFieldLabel()).isEqualTo("Matrícula"); + assertThat(loaded.customFieldValue()).isEqualTo("984210-AUD"); + assertThat(loaded.formatCustomField()).isEqualTo("Matrícula: 984210-AUD"); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRendererTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRendererTest.java new file mode 100644 index 0000000..f9233f1 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/pdf/seal/PdfSealRendererTest.java @@ -0,0 +1,140 @@ +package br.com.cybercore.signer.core.pdf.seal; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes de Renderização de Selos Visuais (PdfSealRenderer)") +class PdfSealRendererTest { + + @Test + @DisplayName("Deve renderizar selo visual em alta resolução com sucesso") + void shouldRenderSealImageInHighResolution() throws IOException { + PdfSealTheme theme = PdfSealThemeRegistry.getDefaultTheme(); + PdfSealData data = new PdfSealData( + "Dr. Carlos Alberto de Oliveira Santos Júnior", + "CPF: 12345678900", + null, + "AC CyberCore JUS v5", + "8A7F4B2C91D0E3F5", + "Assinatura Digital", + "São Paulo, SP", + CpfDisplayMode.MASKED, + true + ); + + BufferedImage img = PdfSealRenderer.renderToImage(theme, data); + assertThat(img).isNotNull(); + // Escala 2x: 320 * 2 = 640, 82 * 2 = 164 + assertThat(img.getWidth()).isEqualTo(640); + assertThat(img.getHeight()).isEqualTo(164); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, data); + assertThat(pngBytes).isNotEmpty(); + // Header PNG: 0x89 0x50 0x4E 0x47 + assertThat(pngBytes[0]).isEqualTo((byte) 0x89); + assertThat(pngBytes[1]).isEqualTo((byte) 0x50); + assertThat(pngBytes[2]).isEqualTo((byte) 0x4E); + assertThat(pngBytes[3]).isEqualTo((byte) 0x47); + } + + @Test + @DisplayName("Deve renderizar selo com logotipo personalizado em Base64") + void shouldRenderSealWithCustomLogoBase64() throws IOException { + // Pixel PNG 1x1 em Base64 + String sampleBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + + PdfSealTheme themeWithLogo = new PdfSealTheme( + "custom-seal", + "Selo com Logo Própria", + "Descrição do selo", + 320, + 82, + "#0d47a1", + "#0284c7", + "#ffffff", + "#cbd5e1", + "#0f172a", + null, + "MEU ESCRITÓRIO • ASSINADO DIGITALMENTE", + "Validade jurídica plena conforme MP nº 2.200-2/2001", + "shield", + CpfDisplayMode.FULL, + false, + true, + sampleBase64 + ); + + PdfSealData data = new PdfSealData( + "Dra. Maria de Souza Ferreira", + "CPF: 98765432100", + null, + "AC Raiz ICP-Brasil", + "AABBCCDDEEFF0011", + "Assinatura de Contrato", + "Curitiba, PR", + CpfDisplayMode.FULL, + false + ); + + BufferedImage img = PdfSealRenderer.renderToImage(themeWithLogo, data); + assertThat(img).isNotNull(); + assertThat(img.getWidth()).isEqualTo(640); + assertThat(img.getHeight()).isEqualTo(164); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(themeWithLogo, data); + assertThat(pngBytes).isNotEmpty(); + } + + @Test + @DisplayName("Deve renderizar selo quando CPF é omitido e sem data/hora (apenas nome em evidência)") + void shouldRenderSealWithOnlySignerName() throws IOException { + PdfSealTheme theme = PdfSealThemeRegistry.getDefaultTheme(); + PdfSealData data = new PdfSealData( + "Gabriel Soares de Alcantara", + "CPF: 11122233344", + null, + "AC OAB v3", + "12345678", + "Assinatura", + "Brasília, DF", + CpfDisplayMode.NONE, + false + ); + + BufferedImage img = PdfSealRenderer.renderToImage(theme, data); + assertThat(img).isNotNull(); + assertThat(img.getWidth()).isEqualTo(640); + assertThat(img.getHeight()).isEqualTo(164); + } + + @Test + @DisplayName("Deve renderizar selo com informação customizada (ex: Matrícula / Cargo) na linha de identificação") + void shouldRenderSealWithCustomExtraInfo() throws IOException { + PdfSealTheme theme = PdfSealThemeRegistry.getDefaultTheme(); + PdfSealData data = new PdfSealData( + "Dra. Juliana Mendes Silveira", + "CPF: 12345678900", + null, + "AC OAB Nacional", + "FEEDCBA012345678", + "Assinatura de Petição", + "São Paulo, SP", + CpfDisplayMode.MASKED, + true, + "OAB/SP 431.902" + ); + + BufferedImage img = PdfSealRenderer.renderToImage(theme, data); + assertThat(img).isNotNull(); + assertThat(img.getWidth()).isEqualTo(640); + assertThat(img.getHeight()).isEqualTo(164); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, data); + assertThat(pngBytes).isNotEmpty(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspConcurrencyTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspConcurrencyTest.java new file mode 100644 index 0000000..66a570d --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspConcurrencyTest.java @@ -0,0 +1,198 @@ +package br.com.cybercore.signer.core.protocol; + +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +import br.com.cybercore.signer.core.security.ApplicationRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.security.auth.x500.X500Principal; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Testes de Concorrência e Multiaplicação do Protocolo CCSP/1 (CcspConcurrencyTest)") +class CcspConcurrencyTest { + + @TempDir + Path tempDir; + + private CcspServer server; + private int port; + private ApplicationRegistry registry; + private String certId; + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + void setUp() throws Exception { + // Encontra porta livre + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + + registry = new ApplicationRegistry(tempDir.resolve("apps.json")); + + // Cadastra 3 aplicações autorizadas distintas + registry.register(new AuthorizedApplication("app-erp", "ERP Corporativo", Set.of("http://localhost:3000"), true, Instant.now())); + registry.register(new AuthorizedApplication("app-juridico", "Portal Jurídico", Set.of("http://localhost:3001"), true, Instant.now())); + registry.register(new AuthorizedApplication("app-rh", "Sistema de RH", Set.of("http://localhost:3002"), true, Instant.now())); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + X509Certificate cert = mock(X509Certificate.class); + when(cert.getSubjectX500Principal()).thenReturn(new X500Principal("CN=Empresa Concorrente:12345678000199, O=ICP-Brasil, C=BR")); + when(cert.getIssuerX500Principal()).thenReturn(new X500Principal("CN=AC Teste ICP-Brasil, C=BR")); + when(cert.getSerialNumber()).thenReturn(BigInteger.valueOf(987654321)); + when(cert.getNotBefore()).thenReturn(Date.from(Instant.now().minus(10, ChronoUnit.DAYS))); + when(cert.getNotAfter()).thenReturn(Date.from(Instant.now().plus(300, ChronoUnit.DAYS))); + when(cert.getPublicKey()).thenReturn(keyPair.getPublic()); + when(cert.getEncoded()).thenReturn(new byte[]{0x30, 0x10, 0x01, 0x02}); + + char[] password = "concurrency-password".toCharArray(); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry("cert-real-a1", keyPair.getPrivate(), password, new Certificate[]{cert}); + + Pkcs12CertificateProvider a1Provider = new Pkcs12CertificateProvider(); + a1Provider.setKeyStore(keyStore, password); + + CertificateManager certManager = new CertificateManager(); + certManager.registerProvider(a1Provider); + certManager.discoverCertificates(); + + ManagedCertificate managedCert = certManager.getAvailableCertificates().get(0); + this.certId = managedCert.id(); + + SigningService signingService = new SigningService(certManager); + + server = new CcspServer(port, certManager, signingService, registry, null); + server.start(); + } + + @AfterEach + void tearDown() { + if (server != null) { + server.stop(); + } + } + + @Test + @DisplayName("Deve processar 16 requisições de assinatura concorrentes de aplicações distintas com provedor real sem corrupção") + void shouldHandleConcurrentSigningRequestsSafely() throws Exception { + int threadCount = 8; + int requestCount = 16; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + CountDownLatch startLatch = new CountDownLatch(1); + List> futures = new ArrayList<>(); + + String[] appIds = {"app-erp", "app-juridico", "app-rh"}; + String[] origins = {"http://localhost:3000", "http://localhost:3001", "http://localhost:3002"}; + + for (int i = 0; i < requestCount; i++) { + final int index = i; + final String appId = appIds[i % appIds.length]; + final String origin = origins[i % origins.length]; + final String reqId = UUID.randomUUID().toString(); + final String nonce = UUID.randomUUID().toString(); + + Callable task = () -> { + // Aguarda largada simultânea + startLatch.await(5, TimeUnit.SECONDS); + + byte[] digest = MessageDigest.getInstance("SHA-256") + .digest(("Documento_Concorrente_" + index).getBytes(StandardCharsets.UTF_8)); + String digestBase64 = Base64.getEncoder().encodeToString(digest); + + CcspRequest request = new CcspRequest( + "1", + reqId, + appId, + "sign", + "SHA256withRSA", + digestBase64, + certId, + Instant.now().plus(60, ChronoUnit.SECONDS).toString(), + System.currentTimeMillis(), + nonce, + null + ); + + return sendSignRequest(request, origin, appId); + }; + + futures.add(executor.submit(task)); + } + + // Dispara todas simultaneamente + startLatch.countDown(); + + for (Future f : futures) { + CcspResponse resp = f.get(10, TimeUnit.SECONDS); + assertThat(resp).isNotNull(); + assertThat(resp.status()).isEqualTo("signed"); + assertThat(resp.signature()).isNotBlank(); + assertThat(resp.algorithm()).isEqualTo("SHA256withRSA"); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue(); + } + + private CcspResponse sendSignRequest(CcspRequest request, String origin, String appId) throws Exception { + URI uri = URI.create("http://127.0.0.1:" + port + "/ccsp/v1/sign"); + HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Origin", origin); + conn.setRequestProperty("X-CyberCore-AppId", appId); + conn.setDoOutput(true); + + byte[] payload = mapper.writeValueAsBytes(request); + try (OutputStream os = conn.getOutputStream()) { + os.write(payload); + } + + try (InputStream is = conn.getInputStream()) { + return mapper.readValue(is, CcspResponse.class); + } + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspProtocolTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspProtocolTest.java new file mode 100644 index 0000000..d1d23dc --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspProtocolTest.java @@ -0,0 +1,87 @@ +package br.com.cybercore.signer.core.protocol; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class CcspProtocolTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + @DisplayName("Deve serializar e deserializar CcspRequest com sucesso") + void shouldSerializeAndDeserializeRequest() throws Exception { + String json = """ + { + "version": "1", + "request_id": "c1f7b640-5e6e-48a5-b1a7-0242ac120002", + "application_id": "app_erp_123", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "certificate_hint": "serial_12345", + "expires_at": "2030-01-01T00:00:00Z", + "timestamp": 1728418000, + "nonce": "test_nonce_987", + "signature_type": "RAW_DIGEST" + } + """; + + CcspRequest request = objectMapper.readValue(json, CcspRequest.class); + + assertThat(request.version()).isEqualTo("1"); + assertThat(request.requestId()).isEqualTo("c1f7b640-5e6e-48a5-b1a7-0242ac120002"); + assertThat(request.applicationId()).isEqualTo("app_erp_123"); + assertThat(request.operation()).isEqualTo("sign"); + assertThat(request.algorithm()).isEqualTo("SHA256withRSA"); + assertThat(request.digest()).isEqualTo("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="); + assertThat(request.isVersionSupported()).isTrue(); + assertThat(request.isExpired()).isFalse(); + } + + @Test + @DisplayName("Deve detectar requisição com expiração ultrapassada") + void shouldDetectExpiredRequest() { + String pastIso = Instant.now().minus(10, ChronoUnit.MINUTES).toString(); + CcspRequest request = new CcspRequest( + "1", "uuid", "app", "sign", "SHA256withRSA", "digest", null, pastIso, null, null, null + ); + + assertThat(request.isExpired()).isTrue(); + } + + @Test + @DisplayName("Deve serializar CcspResponse de sucesso") + void shouldSerializeSuccessResponse() throws Exception { + CcspResponse response = CcspResponse.success( + "req_001", + "sig_bytes_b64", + "cert_b64", + List.of("cert_b64", "ca_b64"), + "SHA256withRSA" + ); + + String json = objectMapper.writeValueAsString(response); + assertThat(json).contains("\"status\":\"signed\""); + assertThat(json).contains("\"signature\":\"sig_bytes_b64\""); + assertThat(json).contains("\"certificate\":\"cert_b64\""); + assertThat(json).doesNotContain("\"mock\""); + } + + @Test + @DisplayName("Deve serializar CcspResponse de erro estruturado") + void shouldSerializeErrorResponse() throws Exception { + CcspResponse response = CcspResponse.error("req_002", CcspErrorCode.USER_CANCELLED); + + String json = objectMapper.writeValueAsString(response); + assertThat(json).contains("\"status\":\"error\""); + assertThat(json).contains("\"error_code\":\"USER_CANCELLED\""); + assertThat(json).contains("Operação cancelada pelo usuário"); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java new file mode 100644 index 0000000..65bc161 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/protocol/CcspServerTest.java @@ -0,0 +1,323 @@ +package br.com.cybercore.signer.core.protocol; + +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProvider; +import br.com.cybercore.signer.core.provider.ProviderType; +import br.com.cybercore.signer.core.security.ApplicationRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("Testes de Integração do Servidor Local CCSP/1 (CcspServer)") +class CcspServerTest { + + private CcspServer server; + private HttpClient httpClient; + private String baseUrl; + + @BeforeEach + void setUp(@TempDir Path tempDir) throws Exception { + CertificateManager certManager = new CertificateManager(); + SigningService signingService = mock(SigningService.class); + ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json")); + + // Registra uma aplicação de teste + appRegistry.register(new AuthorizedApplication( + "app-test", + "App Web Teste", + Set.of("http://localhost:8080", "https://meusistema.com.br"), + true, + Instant.now() + )); + + // Mock de certificado + ManagedCertificate testCert = new ManagedCertificate( + "cert-1", + "AliasTeste", + CertificateType.A1, + CertificateStatus.AVAILABLE, + "CN=Teste ICP-Brasil, C=BR", + "CN=AC Raiz, C=BR", + "12345678", + "a1b2c3", + "d4e5f6", + Instant.now().minus(10, ChronoUnit.DAYS), + Instant.now().plus(300, ChronoUnit.DAYS), + "SHA256withRSA", + 2048, + "Teste ICP-Brasil", + null, + null, + "provider-test", + "Certificado de Teste" + ); + + CertificateProvider dummyProvider = new CertificateProvider() { + @Override public String getProviderId() { return "provider-test"; } + @Override public String getDisplayName() { return "Provedor Teste"; } + @Override public ProviderType getType() { return ProviderType.A1_FILE; } + @Override public boolean isAvailable() { return true; } + @Override public List listCertificates() { return List.of(testCert); } + @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()); + } + }; + + certManager.registerProvider(dummyProvider); + certManager.discoverCertificates(); + + when(signingService.sign(any())).thenReturn( + new SigningResult("req-1", new byte[]{10, 20, 30}, "base64-cert", List.of("base64-cert"), "SHA256withRSA", Instant.now()) + ); + + // Instancia o servidor em porta aleatória (0) + server = new CcspServer(0, certManager, signingService, appRegistry, new CcspUserPromptListener() { + @Override + public CompletableFuture requestApplicationAuthorization(String origin, String appName, String applicationId) { + return CompletableFuture.completedFuture(true); + } + + @Override + public CompletableFuture requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) { + return CompletableFuture.completedFuture(true); + } + }); + + server.start(); + httpClient = HttpClient.newHttpClient(); + baseUrl = "http://127.0.0.1:" + server.getPort(); + } + + @AfterEach + void tearDown() { + if (server != null) { + server.stop(); + } + } + + @Test + @DisplayName("Endpoint /status deve retornar status ready e versão do agente") + void testGetStatus() throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/status")) + .GET() + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + assertThat(resp.statusCode()).isEqualTo(200); + assertThat(resp.body()).contains("\"status\":\"ready\""); + assertThat(resp.body()).contains("\"agent\":\"CyberCore Signer\""); + } + + @Test + @DisplayName("Endpoint /certificates deve retornar a lista de certificados disponíveis") + void testGetCertificates() throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/certificates?application_id=app-test")) + .header("Origin", "https://meusistema.com.br") + .GET() + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + assertThat(resp.statusCode()).isEqualTo(200); + assertThat(resp.body()).contains("\"status\":\"success\""); + assertThat(resp.body()).contains("CN=Teste ICP-Brasil, C=BR"); + } + + @Test + @DisplayName("Requisição OPTIONS deve retornar headers CORS válidos") + void testCorsPreflight() throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .method("OPTIONS", HttpRequest.BodyPublishers.noBody()) + .header("Origin", "https://meusistema.com.br") + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.discarding()); + assertThat(resp.statusCode()).isEqualTo(204); + assertThat(resp.headers().firstValue("Access-Control-Allow-Origin")).hasValue("https://meusistema.com.br"); + assertThat(resp.headers().firstValue("Access-Control-Allow-Methods")).hasValue("GET, POST, OPTIONS"); + } + + @Test + @DisplayName("Endpoint /sign deve assinar digest com sucesso") + void testSignSuccess() throws Exception { + byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Contrato Digital".getBytes()); + String hashBase64 = Base64.getEncoder().encodeToString(docHash); + + String jsonPayload = """ + { + "version": "1", + "request_id": "%s", + "application_id": "app-test", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "%s", + "certificate_hint": "cert-1", + "nonce": "%s" + } + """.formatted(UUID.randomUUID(), hashBase64, UUID.randomUUID()); + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .header("Content-Type", "application/json") + .header("Origin", "https://meusistema.com.br") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + assertThat(resp.statusCode()).isEqualTo(200); + assertThat(resp.body()).contains("\"status\":\"signed\""); + assertThat(resp.body()).contains("\"signature\":"); + } + + @Test + @DisplayName("Endpoint /sign deve prevenir replay attack se mesmo nonce for submetido") + void testReplayPrevention() throws Exception { + byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Documento".getBytes()); + String hashBase64 = Base64.getEncoder().encodeToString(docHash); + String fixedNonce = "nonce-repetido-123"; + + String jsonPayload = """ + { + "version": "1", + "request_id": "%s", + "application_id": "app-test", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "%s", + "certificate_hint": "cert-1", + "nonce": "%s" + } + """.formatted(UUID.randomUUID(), hashBase64, fixedNonce); + + HttpRequest req1 = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .header("Content-Type", "application/json") + .header("Origin", "https://meusistema.com.br") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse resp1 = httpClient.send(req1, HttpResponse.BodyHandlers.ofString()); + assertThat(resp1.statusCode()).isEqualTo(200); + + // Segunda tentativa com mesmo nonce deve falhar por REPLAY_DETECTED + String jsonPayload2 = """ + { + "version": "1", + "request_id": "%s", + "application_id": "app-test", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "%s", + "certificate_hint": "cert-1", + "nonce": "%s" + } + """.formatted(UUID.randomUUID(), hashBase64, fixedNonce); + + HttpRequest req2 = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .header("Content-Type", "application/json") + .header("Origin", "https://meusistema.com.br") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload2)) + .build(); + + HttpResponse resp2 = httpClient.send(req2, HttpResponse.BodyHandlers.ofString()); + assertThat(resp2.statusCode()).isEqualTo(400); + assertThat(resp2.body()).contains("REPLAY_DETECTED"); + } + + @Test + @DisplayName("Deve autenticar usuário via /ccsp/v1/authenticate assinando o desafio do sistema") + void shouldAuthenticateUserWithChallengeSuccessfully() throws Exception { + String jsonPayload = """ + { + "version": "1", + "request_id": "%s", + "application_id": "app-test", + "operation": "authenticate", + "challenge": "portal-auth-challenge-%s", + "certificate_hint": "cert-1", + "nonce": "%s" + } + """.formatted(UUID.randomUUID(), System.currentTimeMillis(), UUID.randomUUID()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/authenticate")) + .header("Content-Type", "application/json") + .header("Origin", "https://meusistema.com.br") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).contains("\"status\":\"authenticated\""); + assertThat(response.body()).contains("\"signature\":\"ChQe\""); + assertThat(response.body()).contains("\"subject_name\":\"Teste ICP-Brasil\""); + } + + @Test + @DisplayName("Deve assinar documento PDF preservando metadados de arquivo na resposta") + void shouldSignDocumentWithMetadataPdfSuccessfully() throws Exception { + byte[] hash = MessageDigest.getInstance("SHA-256").digest("PDF_CONTENT_TEST".getBytes()); + String hashBase64 = Base64.getEncoder().encodeToString(hash); + + String jsonPayload = """ + { + "version": "1", + "request_id": "%s", + "application_id": "app-test", + "operation": "sign", + "algorithm": "SHA256withRSA", + "digest": "%s", + "certificate_hint": "cert-1", + "document_name": "proposta_comercial.pdf", + "document_type": "PDF", + "nonce": "%s" + } + """.formatted(UUID.randomUUID(), hashBase64, UUID.randomUUID()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .header("Content-Type", "application/json") + .header("Origin", "https://meusistema.com.br") + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).contains("\"status\":\"signed\""); + assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.pdf\""); + assertThat(response.body()).contains("\"document_type\":\"PDF\""); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/CertificateManagerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/CertificateManagerTest.java new file mode 100644 index 0000000..f537f1d --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/CertificateManagerTest.java @@ -0,0 +1,66 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CertificateManagerTest { + + @Test + @DisplayName("Deve inicializar com provedores padrão") + void shouldCreateDefaultManagerWithProviders() { + CertificateManager manager = CertificateManager.createDefault(); + + assertThat(manager.getProviders()).isNotEmpty(); + assertThat(manager.getProviders()) + .extracting(CertificateProvider::getProviderId) + .contains("provider-win-store", "provider-a1", "provider-pkcs11"); + } + + @Test + @DisplayName("Deve descobrir certificados de múltiplos provedores e indexá-los") + void shouldDiscoverAndIndexCertificates() throws Exception { + CertificateManager manager = new CertificateManager(); + + CertificateProvider providerA1 = mock(CertificateProvider.class); + when(providerA1.getProviderId()).thenReturn("provider-a1"); + when(providerA1.isAvailable()).thenReturn(true); + + ManagedCertificate cert1 = new ManagedCertificate( + "cert-1", "Empresa ABC", CertificateType.A1, CertificateStatus.AVAILABLE, + "CN=Empresa ABC", "CN=AC Raiz", "123", "sha1_1", "sha256_1", + Instant.now().minus(1, ChronoUnit.DAYS), Instant.now().plus(300, ChronoUnit.DAYS), + "RSA", 2048, "Empresa ABC", null, "11222333000199", "provider-a1", null + ); + when(providerA1.listCertificates()).thenReturn(List.of(cert1)); + + manager.registerProvider(providerA1); + + List discovered = manager.discoverCertificates(); + + assertThat(discovered).hasSize(1); + assertThat(discovered.get(0).id()).isEqualTo("cert-1"); + assertThat(manager.getProviderForCertificate("cert-1")).isSameAs(providerA1); + + // Busca por ID + Optional byId = manager.findById("cert-1"); + assertThat(byId).isPresent(); + assertThat(byId.get().commonName()).isEqualTo("Empresa ABC"); + + // Busca por CNPJ + Optional byCnpj = manager.findByHint("11222333000199"); + assertThat(byCnpj).isPresent(); + assertThat(byCnpj.get().id()).isEqualTo("cert-1"); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProviderTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProviderTest.java new file mode 100644 index 0000000..2c57824 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/Pkcs12CertificateProviderTest.java @@ -0,0 +1,130 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class Pkcs12CertificateProviderTest { + + private Pkcs12CertificateProvider provider; + private KeyPair keyPair; + private X509Certificate mockCert; + private KeyStore keyStore; + private static final char[] PASSWORD = "senha-teste".toCharArray(); + + @BeforeEach + void setUp() throws Exception { + provider = new Pkcs12CertificateProvider(); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + keyPair = kpg.generateKeyPair(); + + mockCert = mock(X509Certificate.class); + when(mockCert.getSubjectX500Principal()).thenReturn(new X500Principal("CN=Empresa Teste:12345678000199, O=ICP-Brasil, C=BR")); + when(mockCert.getIssuerX500Principal()).thenReturn(new X500Principal("CN=AC Teste ICP-Brasil, C=BR")); + when(mockCert.getSerialNumber()).thenReturn(BigInteger.valueOf(987654321)); + when(mockCert.getNotBefore()).thenReturn(Date.from(Instant.now().minus(10, ChronoUnit.DAYS))); + when(mockCert.getNotAfter()).thenReturn(Date.from(Instant.now().plus(300, ChronoUnit.DAYS))); + when(mockCert.getPublicKey()).thenReturn(keyPair.getPublic()); + when(mockCert.getEncoded()).thenReturn(new byte[]{0x30, 0x10, 0x01, 0x02}); + + keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry("alias-a1", keyPair.getPrivate(), PASSWORD, new Certificate[]{mockCert}); + } + + @Test + @DisplayName("Deve listar certificados carregados no KeyStore PKCS#12") + void shouldListCertificatesFromKeyStore() throws Exception { + provider.setKeyStore(keyStore, PASSWORD); + + assertThat(provider.isAvailable()).isTrue(); + List certs = provider.listCertificates(); + + assertThat(certs).hasSize(1); + ManagedCertificate cert = certs.get(0); + assertThat(cert.alias()).isEqualTo("alias-a1"); + assertThat(cert.type()).isEqualTo(CertificateType.A1); + assertThat(cert.status()).isEqualTo(CertificateStatus.AVAILABLE); + assertThat(cert.cnpj()).isEqualTo("12345678000199"); + } + + @Test + @DisplayName("Deve assinar digest com sucesso utilizando chave RSA do PKCS#12") + void shouldSignDigestSuccessfully() throws Exception { + provider.setKeyStore(keyStore, PASSWORD); + + byte[] originalData = "Documento Contratual CyberCore 2026".getBytes(StandardCharsets.UTF_8); + byte[] sha256Digest = MessageDigest.getInstance("SHA-256").digest(originalData); + + SigningContext context = new SigningContext( + "req-101", + "app-web-01", + sha256Digest, + "SHA256withRSA", + "alias-a1", + Instant.now() + ); + + SigningResult result = provider.sign(context); + + assertThat(result).isNotNull(); + assertThat(result.requestId()).isEqualTo("req-101"); + assertThat(result.algorithm()).isEqualTo("SHA256withRSA"); + assertThat(result.signatureBytes()).isNotEmpty(); + + // Validação criptográfica da assinatura gerada + Signature verifier = Signature.getInstance("SHA256withRSA"); + verifier.initVerify(keyPair.getPublic()); + verifier.update(sha256Digest); + assertThat(verifier.verify(result.signatureBytes())).isTrue(); + } + + @Test + @DisplayName("Deve falhar ao tentar assinar com algoritmo incompatível ou digest inválido") + void shouldFailOnInvalidDigest() { + provider.setKeyStore(keyStore, PASSWORD); + + byte[] invalidDigest = new byte[10]; // Digest menor que os 32 bytes de SHA-256 + + SigningContext context = new SigningContext( + "req-102", + "app-web-01", + invalidDigest, + "SHA256withRSA", + "alias-a1", + Instant.now() + ); + + assertThatThrownBy(() -> provider.sign(context)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.SIGNATURE_FAILED); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProviderTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProviderTest.java new file mode 100644 index 0000000..cc88720 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/provider/WindowsCertificateStoreProviderTest.java @@ -0,0 +1,47 @@ +package br.com.cybercore.signer.core.provider; + +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WindowsCertificateStoreProviderTest { + + @Test + @DisplayName("Deve inicializar o provedor e reportar identificador correto") + void shouldReportCorrectMetadata() { + WindowsCertificateStoreProvider provider = new WindowsCertificateStoreProvider(); + + assertThat(provider.getProviderId()).isEqualTo("provider-win-store"); + assertThat(provider.getDisplayName()).contains("Windows"); + assertThat(provider.getType()).isEqualTo(ProviderType.A3_WINDOWS_STORE); + } + + @Test + @DisplayName("Em ambiente não-Windows deve tratar graciosamente a indisponibilidade") + void shouldHandleNonWindowsGracefully() throws Exception { + WindowsCertificateStoreProvider provider = new WindowsCertificateStoreProvider(); + + boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win"); + if (!isWindows) { + assertThat(provider.isAvailable()).isFalse(); + assertThat(provider.listCertificates()).isEmpty(); + + byte[] digest = new byte[32]; + SigningContext context = new SigningContext( + "req_test", "app_1", digest, "SHA256withRSA", "cert_hint", Instant.now() + ); + + assertThatThrownBy(() -> provider.sign(context)) + .isInstanceOf(CertificateProviderException.class) + .hasFieldOrPropertyWithValue("errorCode", CcspErrorCode.PROVIDER_NOT_AVAILABLE); + } + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/ApplicationRegistryTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/ApplicationRegistryTest.java new file mode 100644 index 0000000..0c2cc9f --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/ApplicationRegistryTest.java @@ -0,0 +1,47 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes do Registro de Aplicações Autorizadas (ApplicationRegistry)") +class ApplicationRegistryTest { + + @Test + @DisplayName("Deve registrar, consultar e persistir aplicação com origens permitidas") + void testRegisterAndPersist(@TempDir Path tempDir) { + Path jsonPath = tempDir.resolve("test-apps.json"); + ApplicationRegistry registry = new ApplicationRegistry(jsonPath); + + AuthorizedApplication app = new AuthorizedApplication( + "app-123", + "Portal ERP Corporativo", + Set.of("https://erp.empresa.com.br", "http://localhost:3000"), + true, + Instant.now() + ); + + registry.register(app); + + assertThat(registry.findById("app-123")).isPresent(); + assertThat(registry.isAuthorized("app-123", "https://erp.empresa.com.br")).isTrue(); + assertThat(registry.isAuthorized("app-123", "https://malicious-site.com")).isFalse(); + + // Novo registry no mesmo arquivo para validar persistência + ApplicationRegistry reloaded = new ApplicationRegistry(jsonPath); + Optional loaded = reloaded.findById("app-123"); + assertThat(loaded).isPresent(); + assertThat(loaded.get().name()).isEqualTo("Portal ERP Corporativo"); + + // Revogação + reloaded.revoke("app-123"); + assertThat(reloaded.findById("app-123")).isEmpty(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/NonceManagerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/NonceManagerTest.java new file mode 100644 index 0000000..4a59247 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/NonceManagerTest.java @@ -0,0 +1,31 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +class NonceManagerTest { + + @Test + @DisplayName("Deve aceitar primeiro registro e rejeitar repetição (replay)") + void shouldAcceptOnceAndRejectDuplicate() { + NonceManager manager = new NonceManager(Duration.ofMinutes(1)); + + boolean first = manager.registerNonce("nonce_abc_123"); + boolean second = manager.registerNonce("nonce_abc_123"); + + assertThat(first).isTrue(); + assertThat(second).isFalse(); + } + + @Test + @DisplayName("Deve rejeitar nonce nulo ou vazio") + void shouldRejectInvalidNonce() { + NonceManager manager = new NonceManager(); + assertThat(manager.registerNonce(null)).isFalse(); + assertThat(manager.registerNonce(" ")).isFalse(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/OriginValidatorTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/OriginValidatorTest.java new file mode 100644 index 0000000..a417299 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/OriginValidatorTest.java @@ -0,0 +1,46 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class OriginValidatorTest { + + private final OriginValidator validator = new OriginValidator(); + + @Test + @DisplayName("Deve permitir origem exatamente autorizada") + void shouldAllowAuthorizedOrigin() { + AuthorizedApplication app = new AuthorizedApplication( + "app_1", "Portal ERP", Set.of("https://erp.empresa.com.br", "http://localhost:3000"), true, null + ); + + assertThat(validator.isAllowed("https://erp.empresa.com.br", app)).isTrue(); + assertThat(validator.isAllowed("http://localhost:3000", app)).isTrue(); + } + + @Test + @DisplayName("Deve rejeitar origens não cadastradas ou fraudulentas") + void shouldRejectUnauthorizedOrigin() { + AuthorizedApplication app = new AuthorizedApplication( + "app_1", "Portal ERP", Set.of("https://erp.empresa.com.br"), true, null + ); + + assertThat(validator.isAllowed("https://malicious-site.com", app)).isFalse(); + assertThat(validator.isAllowed("https://erp.empresa.com.br.attacker.com", app)).isFalse(); + assertThat(validator.isAllowed(null, app)).isFalse(); + } + + @Test + @DisplayName("Deve rejeitar qualquer origem se a aplicação estiver desabilitada") + void shouldRejectWhenApplicationIsDisabled() { + AuthorizedApplication app = new AuthorizedApplication( + "app_1", "Portal ERP", Set.of("https://erp.empresa.com.br"), false, null + ); + + assertThat(validator.isAllowed("https://erp.empresa.com.br", app)).isFalse(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/PinSessionManagerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/PinSessionManagerTest.java new file mode 100644 index 0000000..759750e --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/PinSessionManagerTest.java @@ -0,0 +1,61 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PinSessionManagerTest { + + private PinSessionManager manager; + + @BeforeEach + void setUp() { + manager = new PinSessionManager(); + manager.clearAll(); + } + + @Test + @DisplayName("Deve salvar e recuperar PIN de sessão para um certificado") + void shouldSaveAndRetrievePin() { + char[] pin = "1234".toCharArray(); + manager.savePin("cert-1", pin); + + assertThat(manager.hasPin("cert-1")).isTrue(); + assertThat(manager.getPin("cert-1")).isEqualTo("1234".toCharArray()); + } + + @Test + @DisplayName("Deve limpar PIN e sobrescrever array com espaços") + void shouldClearPinAndWipeMemory() { + char[] pin = "5678".toCharArray(); + manager.savePin("cert-2", pin); + + manager.clearPin("cert-2"); + assertThat(manager.hasPin("cert-2")).isFalse(); + assertThat(manager.getPin("cert-2")).isNull(); + } + + @Test + @DisplayName("Quando alwaysPromptPin estiver ativo, não deve retornar PIN salvo") + void shouldIgnoreStoredPinWhenAlwaysPromptIsActive() { + manager.savePin("cert-3", "9999".toCharArray()); + manager.setAlwaysPromptPin(true); + + assertThat(manager.hasPin("cert-3")).isFalse(); + assertThat(manager.getPin("cert-3")).isNull(); + } + + @Test + @DisplayName("Deve limpar todos os PINs em memória") + void shouldClearAllPins() { + manager.savePin("cert-a", "1111".toCharArray()); + manager.savePin("cert-b", "2222".toCharArray()); + + manager.clearAll(); + + assertThat(manager.hasPin("cert-a")).isFalse(); + assertThat(manager.hasPin("cert-b")).isFalse(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardSessionManagerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardSessionManagerTest.java new file mode 100644 index 0000000..4627671 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardSessionManagerTest.java @@ -0,0 +1,18 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@DisplayName("Testes de Gerenciamento de Sessão de Smart Card (SmartCardSessionManager)") +class SmartCardSessionManagerTest { + + @Test + @DisplayName("Deve executar resetCardSessions sem lançar exceções mesmo se o hardware não estiver presente") + void testResetCardSessionsSafeExecution() { + assertDoesNotThrow(() -> { + SmartCardSessionManager.resetCardSessions(); + }); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardWatcherTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardWatcherTest.java new file mode 100644 index 0000000..e49d519 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/SmartCardWatcherTest.java @@ -0,0 +1,51 @@ +package br.com.cybercore.signer.core.security; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes de Monitoramento Dinâmico de Smart Cards (SmartCardWatcher)") +class SmartCardWatcherTest { + + @Test + @DisplayName("Deve gerenciar ciclo de vida (start/stop) sem exceção") + void shouldManageLifecycleGracefully() { + SmartCardWatcher watcher = new SmartCardWatcher(); + assertThat(watcher.isRunning()).isFalse(); + + watcher.start(); + assertThat(watcher.isRunning()).isTrue(); + + watcher.stop(); + assertThat(watcher.isRunning()).isFalse(); + } + + @Test + @DisplayName("Deve adicionar e remover listeners corretamente") + void shouldAddAndRemoveListeners() { + SmartCardWatcher watcher = new SmartCardWatcher(); + AtomicBoolean inserted = new AtomicBoolean(false); + AtomicBoolean removed = new AtomicBoolean(false); + + SmartCardWatcher.CardEventListener listener = new SmartCardWatcher.CardEventListener() { + @Override + public void onCardInserted(String terminalName) { + inserted.set(true); + } + + @Override + public void onCardRemoved(String terminalName) { + removed.set(true); + } + }; + + watcher.addListener(listener); + watcher.removeListener(listener); + // Não deve falhar nem disparar listeners removidos + assertThat(inserted.get()).isFalse(); + assertThat(removed.get()).isFalse(); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/validation/SignatureValidationServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/validation/SignatureValidationServiceTest.java new file mode 100644 index 0000000..5dfdfa5 --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/validation/SignatureValidationServiceTest.java @@ -0,0 +1,183 @@ +package br.com.cybercore.signer.core.validation; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.pdf.PdfSignatureOptions; +import br.com.cybercore.signer.core.pdf.PdfSigningService; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes do Serviço de Validação de Assinaturas (SignatureValidationService)") +class SignatureValidationServiceTest { + + private SignatureValidationService validationService; + private KeyPair keyPair; + private X509Certificate certificate; + private CertificateManager certificateManager; + private String testCertId; + + @BeforeEach + void setUp() throws Exception { + validationService = new SignatureValidationService(); + + // 1. Gera par de chaves e certificado X.509 em memória + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + keyPair = kpg.generateKeyPair(); + + long now = System.currentTimeMillis(); + Date notBefore = new Date(now - 10000); + Date notAfter = new Date(now + 365L * 24 * 3600 * 1000); + + X500Name subject = new X500Name("CN=Advogado Teste da Silva:98765432100, O=CyberCore, C=BR"); + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, + BigInteger.valueOf(now), + notBefore, + notAfter, + subject, + keyPair.getPublic() + ); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + + // 2. Configura KeyStore em memória e CertificateManager + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + ks.setKeyEntry("test-alias", keyPair.getPrivate(), "secret".toCharArray(), new Certificate[]{certificate}); + + Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste"); + provider.setKeyStore(ks, "secret".toCharArray()); + certificateManager = new CertificateManager(); + certificateManager.registerProvider(provider); + + List certs = certificateManager.discoverCertificates(); + testCertId = certs.get(0).id(); + } + + @Test + @DisplayName("Deve validar com sucesso uma assinatura CMS destacada (Detached)") + void shouldValidateDetachedSignatureSuccessfully() throws Exception { + byte[] docBytes = "Conteúdo importante do contrato digital CyberCore.".getBytes(StandardCharsets.UTF_8); + + // Gera assinatura CMS destacada + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build() + ).build(sha256Signer, certificate)); + gen.addCertificate(new X509CertificateHolder(certificate.getEncoded())); + + CMSSignedData signedData = gen.generate(new CMSProcessableByteArray(docBytes), false); + byte[] signatureBytes = signedData.getEncoded(); + + ValidationResult result = validationService.validateDetached(docBytes, signatureBytes, "contrato.txt"); + + assertThat(result.isValid()).isTrue(); + assertThat(result.status()).isEqualTo(ValidationStatus.VALID); + assertThat(result.type()).isEqualTo(ValidationType.DETACHED_CMS); + assertThat(result.signers()).hasSize(1); + assertThat(result.signers().get(0).signerName()).contains("Advogado Teste da Silva"); + assertThat(result.signers().get(0).digestValid()).isTrue(); + assertThat(result.signers().get(0).signatureValid()).isTrue(); + } + + @Test + @DisplayName("Deve acusar falha de validação quando o documento original foi alterado (adulteração)") + void shouldFailWhenDocumentHasBeenTampered() throws Exception { + byte[] originalBytes = "Documento original intocado.".getBytes(StandardCharsets.UTF_8); + byte[] tamperedBytes = "Documento adulterado maliciosamente.".getBytes(StandardCharsets.UTF_8); + + // Gera assinatura do original + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().build() + ).build(sha256Signer, certificate)); + gen.addCertificate(new X509CertificateHolder(certificate.getEncoded())); + + CMSSignedData signedData = gen.generate(new CMSProcessableByteArray(originalBytes), false); + byte[] signatureBytes = signedData.getEncoded(); + + // Valida passando o documento adulterado + ValidationResult result = validationService.validateDetached(tamperedBytes, signatureBytes, "doc.txt"); + + assertThat(result.isValid()).isFalse(); + assertThat(result.status()).isEqualTo(ValidationStatus.INVALID); + } + + @Test + @DisplayName("Deve validar com sucesso um documento PDF assinado (PAdES Incorporado)") + void shouldValidateSignedPdfSuccessfully() throws Exception { + // Cria PDF simples em memória + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PDDocument doc = new PDDocument()) { + doc.addPage(new PDPage()); + doc.save(baos); + } + byte[] rawPdf = baos.toByteArray(); + + // Assina o PDF via PdfSigningService + PdfSigningService signingService = new PdfSigningService(certificateManager); + PdfSignatureOptions options = PdfSignatureOptions.builder(testCertId) + .visibleSeal(true) + .reason("Assinatura de teste unitário") + .build(); + + byte[] signedPdf = signingService.signPdf(rawPdf, options); + + // Valida como assinatura incorporada + ValidationResult result = validationService.validateAttached(signedPdf, "documento_assinado.pdf"); + + assertThat(result.isValid()).isTrue(); + assertThat(result.type()).isEqualTo(ValidationType.PADES_PDF); + assertThat(result.signers()).isNotEmpty(); + assertThat(result.signers().get(0).signatureValid()).isTrue(); + } + + @Test + @DisplayName("Deve acusar inválido para PDF que não possui assinaturas") + void shouldFailForUnsignedPdf() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PDDocument doc = new PDDocument()) { + doc.addPage(new PDPage()); + doc.save(baos); + } + + ValidationResult result = validationService.validateAttached(baos.toByteArray(), "em_branco.pdf"); + + assertThat(result.isValid()).isFalse(); + assertThat(result.type()).isEqualTo(ValidationType.PADES_PDF); + assertThat(result.summaryMessage()).contains("Nenhuma assinatura digital encontrada"); + } +} diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/xml/XmlSigningServiceTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/xml/XmlSigningServiceTest.java new file mode 100644 index 0000000..254028b --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/xml/XmlSigningServiceTest.java @@ -0,0 +1,152 @@ +package br.com.cybercore.signer.core.xml; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +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.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class XmlSigningServiceTest { + + private CertificateManager certificateManager; + private XmlSigningService xmlSigningService; + private String testCertId; + + @BeforeEach + void setUp() throws Exception { + certificateManager = new CertificateManager(); + + 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=Empresa Teste LTDA:98765432000199, O=CyberCore XML, 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)); + + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null, null); + char[] pwd = "xmlpassword".toCharArray(); + ks.setKeyEntry("xml-cert", kp.getPrivate(), pwd, new Certificate[]{cert}); + + Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("xml-provider", "Provedor XML Teste"); + provider.setKeyStore(ks, pwd); + certificateManager.registerProvider(provider); + + List list = certificateManager.discoverCertificates(); + this.testCertId = list.get(0).id(); + + this.xmlSigningService = new XmlSigningService(certificateManager); + } + + @Test + @DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso") + void shouldSignWholeXmlDocumentAndValidate() throws Exception { + String xml = """ + + + Fulano de Tal + 1500.00 + + """; + + XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null); + byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); + + String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8); + assertThat(signedXmlStr).contains(" + + + + 1 + + + 250.00 + + + + """; + + XmlSignatureOptions options = XmlSignatureOptions.forElement( + testCertId, + null, + "infNFe", + "NFe35261012345678000195550010000000011000000010" + ); + + byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options); + String signedStr = new String(signedNfe, StandardCharsets.UTF_8); + + assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\""); + + boolean isValid = xmlSigningService.validateXmlSignature(signedNfe); + assertThat(isValid).isTrue(); + } + + @Test + @DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação") + void shouldDetectTamperingInSignedXml() throws Exception { + String xml = """ + + + Pagamento de R$ 100,00 + + """; + + XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null); + byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); + + // Adulteração intencional do conteúdo original + String tampered = new String(signedXml, StandardCharsets.UTF_8) + .replace("R$ 100,00", "R$ 999.999,00"); + + boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8)); + assertThat(isValid).isFalse(); + } +} diff --git a/modules/cybercore-ui/pom.xml b/modules/cybercore-ui/pom.xml new file mode 100644 index 0000000..dd7fc5f --- /dev/null +++ b/modules/cybercore-ui/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + br.com.cybercore.signer + cybercore-signer-parent + 0.1.0-SNAPSHOT + ../../pom.xml + + + cybercore-ui + jar + + CyberCore Signer :: UI + Camada de apresentação JavaFX, System Tray nativo, diálogos e gerenciamento de temas. + + + + br.com.cybercore.signer + cybercore-core + + + + org.openjfx + javafx-controls + + + org.openjfx + javafx-graphics + + + org.openjfx + javafx-base + + + + org.slf4j + slf4j-api + + + + + org.mockito + mockito-core + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java new file mode 100644 index 0000000..9314f21 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreDesktopApplication.java @@ -0,0 +1,496 @@ +package br.com.cybercore.signer.ui; + +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.diagnostics.DiagnosticService; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.protocol.CcspErrorCode; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.CertificateProviderException; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +import br.com.cybercore.signer.ui.view.A1PasswordDialog; +import br.com.cybercore.signer.ui.view.CertificateDetailsDialog; +import br.com.cybercore.signer.ui.view.MainAppView; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import br.com.cybercore.signer.core.protocol.CcspRequest; +import br.com.cybercore.signer.core.protocol.CcspServer; +import br.com.cybercore.signer.core.protocol.CcspUserPromptListener; +import br.com.cybercore.signer.core.security.ApplicationRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import br.com.cybercore.signer.core.security.SmartCardWatcher; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.view.PdfSignDialog; +import br.com.cybercore.signer.ui.view.XmlSignDialog; +import br.com.cybercore.signer.ui.tray.SystemTrayManager; +import br.com.cybercore.signer.ui.view.AboutView; +import br.com.cybercore.signer.ui.view.ApplicationApprovalDialog; +import br.com.cybercore.signer.ui.view.BatchSigningDialog; +import br.com.cybercore.signer.ui.view.CertificatesManagerDialog; +import br.com.cybercore.signer.ui.view.FileSignDialog; +import br.com.cybercore.signer.ui.view.LogsViewerDialog; +import br.com.cybercore.signer.ui.view.SealDesignerDialog; +import br.com.cybercore.signer.ui.view.SealManagementDialog; +import br.com.cybercore.signer.ui.view.SettingsView; +import br.com.cybercore.signer.ui.view.SignaturePromptDialog; +import br.com.cybercore.signer.ui.view.SignatureValidationDialog; +import br.com.cybercore.signer.ui.view.TutorialDialog; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Ponto de entrada JavaFX da interface desktop do CyberCore Signer. + * Conectado diretamente ao CertificateManager e SigningService reais do sistema operacional. + */ +public class CyberCoreDesktopApplication extends Application { + + private static final Logger logger = LoggerFactory.getLogger(CyberCoreDesktopApplication.class); + + private static boolean developmentMode = false; + private static MainAppView mainAppView; + + private CertificateManager certificateManager; + private SigningService signingService; + private CcspServer ccspServer; + private SystemTrayManager systemTrayManager; + private SmartCardWatcher smartCardWatcher; + + public static void setDevelopmentMode(boolean devMode) { + developmentMode = devMode; + } + + public static MainAppView getMainAppView() { + return mainAppView; + } + + @Override + public void start(Stage primaryStage) { + logger.info("Iniciando interface gráfica JavaFX..."); + Platform.setImplicitExit(false); + + // Inicializa serviços centrais reais da plataforma + this.certificateManager = CertificateManager.createDefault(); + + if (developmentMode) { + logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina)."); + } + + mainAppView = new MainAppView(primaryStage); + + // Ações da Grade de Assinatura e Validação + mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show()); + mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show()); + mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show()); + mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show()); + mainAppView.setOnAttachedSign(() -> new FileSignDialog(primaryStage, "Assinatura Atachada", "*.*", SvgIcons.attachedIcon(), certificateManager, signingService).show()); + mainAppView.setOnCoSign(() -> new FileSignDialog(primaryStage, "Co-Assinar Documento", "*.*", SvgIcons.coSignIcon(), certificateManager, signingService).show()); + mainAppView.setOnTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show()); + mainAppView.setOnLtvSign(() -> new FileSignDialog(primaryStage, "Assinatura de Longa Duração (LTV)", "*.*", SvgIcons.ltvIcon(), certificateManager, signingService).show()); + mainAppView.setOnValidateSignature(() -> new SignatureValidationDialog(primaryStage, false).show()); + mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show()); + mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show()); + mainAppView.setOnExit(() -> { + logger.info("Encerrando CyberCore Signer pela interface gráfica."); + primaryStage.close(); + Platform.exit(); + System.exit(0); + }); + + // Ações do Painel Lateral + mainAppView.setOnLogs(() -> new LogsViewerDialog(primaryStage).show()); + mainAppView.setOnSettings(() -> openSettingsDialog(primaryStage)); + mainAppView.setOnTutorial(() -> new TutorialDialog(primaryStage).show()); + mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show()); + mainAppView.setOnAbout(() -> openAboutDialog(primaryStage)); + + // Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador + initCcspServer(primaryStage); + + // Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB + initSmartCardWatcher(); + + // Configura bandeja do sistema (System Tray) para operação contínua em segundo plano + this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates); + this.systemTrayManager.setup(); + + // Carrega os certificados instalados na máquina do usuário + refreshCertificates(); + + mainAppView.show(); + logger.info("Janela principal JavaFX exibida."); + } + + private void refreshCertificates() { + try { + List certs = certificateManager.discoverCertificates(); + logger.info("Descoberta concluída: {} certificado(s) real(is) identificado(s).", certs.size()); + } catch (Exception e) { + logger.error("Erro ao atualizar lista de certificados: {}", e.getMessage(), e); + } + } + + private void openSettingsDialog(Stage owner) { + Stage dlg = new Stage(); + dlg.initModality(Modality.APPLICATION_MODAL); + dlg.initOwner(owner); + dlg.setTitle("CyberCore Signer — Configurações"); + dlg.setResizable(true); + dlg.setMinWidth(660); + dlg.setMinHeight(520); + + SettingsView settingsView = new SettingsView(); + settingsView.updateApplications(ApplicationRegistry.getInstance().listAll()); + settingsView.setOnRevokeApplication(appId -> { + logger.info("Revogando autorização da aplicação web [id={}]", appId); + ApplicationRegistry.getInstance().revoke(appId); + settingsView.updateApplications(ApplicationRegistry.getInstance().listAll()); + }); + + ScrollPane scrollPane = new ScrollPane(settingsView); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + + Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> dlg.close()); + + HBox footer = new HBox(btnClose); + footer.setAlignment(Pos.CENTER_RIGHT); + footer.setPadding(new Insets(10, 24, 16, 24)); + + VBox layout = new VBox(scrollPane, footer); + layout.getStyleClass().add("dialog-container"); + + Scene sc = new Scene(layout, 720, 580); + ThemeManager.applyTheme(sc); + dlg.setScene(sc); + dlg.show(); + } + + private void openAboutDialog(Stage owner) { + Stage dlg = new Stage(); + dlg.initModality(Modality.APPLICATION_MODAL); + dlg.initOwner(owner); + dlg.setTitle("CyberCore Signer — Sobre"); + dlg.setResizable(true); + dlg.setMinWidth(540); + dlg.setMinHeight(420); + + AboutView aboutView = new AboutView(); + Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-primary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> dlg.close()); + + HBox footer = new HBox(btnClose); + footer.setAlignment(Pos.CENTER); + footer.setPadding(new Insets(10, 24, 20, 24)); + + VBox layout = new VBox(aboutView, footer); + layout.getStyleClass().add("dialog-container"); + + Scene sc = new Scene(layout, 560, 440); + ThemeManager.applyTheme(sc); + dlg.setScene(sc); + dlg.show(); + } + + private void showCertificateDetails(Stage owner, ManagedCertificate cert) { + CertificateDetailsDialog dialog = new CertificateDetailsDialog(owner, cert); + dialog.show(); + } + + private void handleAddA1Certificate(Stage owner) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Selecionar Certificado Digital A1 (.pfx, .p12)"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Certificados Digitais PKCS#12 (*.pfx, *.p12)", "*.pfx", "*.p12"), + new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*") + ); + + File selectedFile = fileChooser.showOpenDialog(owner); + if (selectedFile == null) { + logger.info("Seleção de certificado A1 cancelada pelo usuário."); + return; + } + + A1PasswordDialog pwdDialog = new A1PasswordDialog(owner, selectedFile.getName()); + pwdDialog.showAndAwait().thenAccept(password -> { + if (password == null) { + logger.info("Importação de certificado A1 cancelada na etapa de senha."); + return; + } + + try { + byte[] pfxData = Files.readAllBytes(selectedFile.toPath()); + String providerId = "provider-a1-" + Integer.toHexString(selectedFile.getAbsolutePath().hashCode()); + Pkcs12CertificateProvider a1Provider = new Pkcs12CertificateProvider(providerId, selectedFile.getName()); + + a1Provider.loadFromBytes(pfxData, password); + + // Limpa senha da memória + Arrays.fill(password, ' '); + + certificateManager.registerProvider(a1Provider); + refreshCertificates(); + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION, "Certificado A1 importado com sucesso!", ButtonType.OK); + alert.initOwner(owner); + alert.setTitle("CyberCore Signer — Certificado A1"); + alert.setHeaderText("Importação Concluída"); + alert.show(); + }); + + logger.info("Certificado A1 '{}' importado e registrado com sucesso.", selectedFile.getName()); + + } catch (CertificateProviderException cpe) { + Arrays.fill(password, ' '); + Platform.runLater(() -> { + String msg = cpe.getErrorCode() == CcspErrorCode.PIN_INCORRECT + ? "A senha informada para o certificado A1 está incorreta." + : "Erro ao processar certificado: " + cpe.getMessage(); + + Alert alert = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK); + alert.initOwner(owner); + alert.setTitle("CyberCore Signer — Erro"); + alert.setHeaderText("Falha na Importação"); + alert.show(); + }); + } catch (Exception e) { + Arrays.fill(password, ' '); + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR, "Não foi possível ler o arquivo: " + e.getMessage(), ButtonType.OK); + alert.initOwner(owner); + alert.setTitle("CyberCore Signer — Erro"); + alert.setHeaderText("Falha na Leitura"); + alert.show(); + }); + } + }); + } + + private void runSelfTestSignature(Stage stage) { + List available = certificateManager.getAvailableCertificates(); + if (available.isEmpty()) { + logger.warn("Nenhum certificado digital disponível com chave privada para teste de assinatura."); + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.WARNING, + "Nenhum certificado digital disponível com chave privada para teste de assinatura.\n\n" + + "Conecte um token USB/Smart Card ou importe um certificado A1.", + ButtonType.OK); + alert.initOwner(stage); + alert.setTitle("CyberCore Signer — Auto-teste"); + alert.setHeaderText("Certificado Não Encontrado"); + alert.show(); + }); + return; + } + + ManagedCertificate target = available.get(0); + openSignaturePrompt(stage, target); + } + + private void openSignaturePrompt(Stage owner, ManagedCertificate cert) { + try { + byte[] docDigest = MessageDigest.getInstance("SHA-256") + .digest(("Documento_Teste_CyberCore_" + Instant.now()).getBytes(StandardCharsets.UTF_8)); + + SigningContext context = new SigningContext( + "req_test_" + System.currentTimeMillis(), + "app_teste_local", + docDigest, + "SHA256withRSA", + cert.id(), + Instant.now() + ); + + SignaturePromptDialog dialog = new SignaturePromptDialog( + owner, "CyberCore Agente Local", "http://localhost", cert, context + ); + + dialog.showAndAwait().thenAccept(approved -> { + if (Boolean.TRUE.equals(approved)) { + logger.info("Usuário aprovou a assinatura para o certificado: {}", cert.alias()); + try { + SigningResult result = signingService.sign(context); + logger.info("Assinatura real gerada com sucesso! Algoritmo={}, Tamanho={}", + result.algorithm(), result.signatureBytes().length); + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION, + "Documento assinado com sucesso via " + result.algorithm() + "!\n\n" + + "Tamanho da assinatura: " + result.signatureBytes().length + " bytes\n" + + "Certificado: " + cert.commonName(), + ButtonType.OK); + alert.initOwner(owner); + alert.setTitle("CyberCore Signer — Sucesso"); + alert.setHeaderText("Assinatura Realizada"); + alert.show(); + }); + + } catch (Exception e) { + logger.error("Falha ao executar assinatura: {}", e.getMessage(), e); + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR, + "Falha ao realizar assinatura digital:\n\n" + e.getMessage(), + ButtonType.OK); + alert.initOwner(owner); + alert.setTitle("CyberCore Signer — Erro"); + alert.setHeaderText("Falha na Assinatura"); + alert.show(); + }); + } + } else { + logger.info("Usuário cancelou a assinatura para o certificado: {}", cert.alias()); + } + }); + } catch (Exception e) { + logger.error("Erro ao abrir diálogo de solicitação de assinatura: {}", e.getMessage(), e); + } + } + + private void initCcspServer(Stage stage) { + CcspUserPromptListener promptListener = new CcspUserPromptListener() { + @Override + public CompletableFuture requestApplicationAuthorization(String origin, String appName, String applicationId) { + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + ApplicationApprovalDialog dialog = new ApplicationApprovalDialog(stage, origin, appName, applicationId); + dialog.showAndAwait().whenComplete((approved, err) -> { + if (err != null) { + future.complete(false); + } else { + future.complete(Boolean.TRUE.equals(approved)); + } + }); + } catch (Exception ex) { + logger.error("Erro ao abrir diálogo de autorização de aplicação: {}", ex.getMessage()); + future.complete(false); + } + }); + return future; + } + + @Override + public CompletableFuture requestSignatureConfirmation(CcspRequest request, ManagedCertificate certificate) { + CompletableFuture future = new CompletableFuture<>(); + Platform.runLater(() -> { + try { + byte[] digestBytes; + if (request.digest() != null && !request.digest().isBlank()) { + digestBytes = Base64.getDecoder().decode(request.digest()); + } else if (request.challenge() != null && !request.challenge().isBlank()) { + digestBytes = MessageDigest.getInstance("SHA-256") + .digest(request.challenge().getBytes(StandardCharsets.UTF_8)); + } else { + digestBytes = new byte[32]; + } + + SigningContext context = new SigningContext( + request.requestId(), + request.applicationId(), + digestBytes, + request.algorithm() != null ? request.algorithm() : "SHA256withRSA", + certificate.id(), + Instant.now() + ); + + String appName = ApplicationRegistry.getInstance().findById(request.applicationId()) + .map(AuthorizedApplication::name) + .orElse(request.applicationId()); + + SignaturePromptDialog dialog = new SignaturePromptDialog( + stage, appName, "Requisição Web CCSP", certificate, context, + request.documentName(), request.documentType(), request.isAuthentication() + ); + dialog.showAndAwait().whenComplete((approved, err) -> { + if (err != null) { + future.complete(false); + } else { + future.complete(Boolean.TRUE.equals(approved)); + } + }); + } catch (Exception ex) { + logger.error("Erro ao abrir diálogo de confirmação de assinatura: {}", ex.getMessage()); + future.complete(false); + } + }); + return future; + } + }; + + try { + this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener); + this.ccspServer.start(); + logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort()); + } catch (Exception e) { + logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e); + } + } + + private void initSmartCardWatcher() { + try { + this.smartCardWatcher = new SmartCardWatcher(); + this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() { + @Override + public void onCardInserted(String terminalName) { + logger.info("Novo dispositivo detectado no terminal '{}'. Atualizando certificados...", terminalName); + refreshCertificates(); + } + + @Override + public void onCardRemoved(String terminalName) { + logger.warn("Dispositivo desconectado do terminal '{}'. Atualizando certificados...", terminalName); + refreshCertificates(); + } + }); + this.smartCardWatcher.start(); + } catch (Throwable t) { + logger.warn("Não foi possível iniciar monitor dinâmico de smart cards: {}", t.getMessage()); + } + } + + @Override + public void stop() { + logger.info("Encerrando componentes da interface JavaFX..."); + if (smartCardWatcher != null) { + smartCardWatcher.stop(); + } + if (systemTrayManager != null) { + systemTrayManager.remove(); + } + if (ccspServer != null) { + ccspServer.stop(); + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreUiConstants.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreUiConstants.java new file mode 100644 index 0000000..ca2573f --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/CyberCoreUiConstants.java @@ -0,0 +1,17 @@ +package br.com.cybercore.signer.ui; + +/** + * Constantes visuais e de apresentação da interface do CyberCore Signer. + */ +public final class CyberCoreUiConstants { + + public static final String APP_NAME = "CyberCore Signer"; + public static final String APP_VERSION = "0.1.0"; + public static final String APP_STATUS_READY = "Serviço ativo e pronto para assinar"; + + // Temas e estilos + public static final String MAIN_CSS_PATH = "/styles/theme.css"; + + private CyberCoreUiConstants() { + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/assets/AssetManager.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/assets/AssetManager.java new file mode 100644 index 0000000..b4b1cc0 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/assets/AssetManager.java @@ -0,0 +1,449 @@ +package br.com.cybercore.signer.ui.assets; + +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; +import javafx.scene.shape.Polygon; +import javafx.scene.shape.Polyline; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.SVGPath; +import javafx.scene.shape.Shape; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.transform.Scale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * 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) + * do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/' + * na raiz do projeto ou no diretório ~/.cybercore/assets/. + */ +public final class AssetManager { + + private static final Logger logger = LoggerFactory.getLogger(AssetManager.class); + + private static final String[] RASTER_EXTENSIONS = {".png", ".jpg", ".jpeg"}; + + private AssetManager() { + } + + /** + * Retorna o caminho do diretório externo prioritário de assets. + */ + public static Path getExternalAssetsDir() { + Path localAssets = Path.of("assets"); + if (Files.isDirectory(localAssets)) { + return localAssets; + } + + String userHome = System.getProperty("user.home", "."); + return Path.of(userHome, ".cybercore", "assets"); + } + + /** + * Tenta carregar uma imagem bitmap personalizada (.png, .jpg) pelo nome nas pastas de assets + * ou nos recursos empacotados. + */ + public static Optional loadImage(String category, String baseName, double width, double height) { + if (baseName == null || baseName.isBlank()) { + return Optional.empty(); + } + + String cleanName = cleanBaseName(baseName); + + // 1. Procura no diretório local ./assets/{category}/ + Path localDir = Path.of("assets", category); + for (String ext : RASTER_EXTENSIONS) { + Path file = localDir.resolve(cleanName + ext); + if (Files.isRegularFile(file)) { + try (InputStream in = new FileInputStream(file.toFile())) { + logger.debug("Carregando imagem bitmap do diretório local: {}", file); + return Optional.of(new Image(in, width, height, true, true)); + } catch (Exception e) { + logger.warn("Falha ao ler imagem customizada {}: {}", file, e.getMessage()); + } + } + } + + // 2. Procura em ~/.cybercore/assets/{category}/ + String userHome = System.getProperty("user.home", "."); + Path userDir = Path.of(userHome, ".cybercore", "assets", category); + for (String ext : RASTER_EXTENSIONS) { + Path file = userDir.resolve(cleanName + ext); + if (Files.isRegularFile(file)) { + try (InputStream in = new FileInputStream(file.toFile())) { + logger.debug("Carregando imagem bitmap de ~/.cybercore: {}", file); + return Optional.of(new Image(in, width, height, true, true)); + } catch (Exception e) { + logger.warn("Falha ao ler imagem do usuário {}: {}", file, e.getMessage()); + } + } + } + + // 3. Procura no Classpath interno (/assets/{category}/{name}) + for (String ext : RASTER_EXTENSIONS) { + String resPath = "/assets/" + category + "/" + cleanName + ext; + InputStream resStream = AssetManager.class.getResourceAsStream(resPath); + if (resStream != null) { + try (resStream) { + return Optional.of(new Image(resStream, width, height, true, true)); + } catch (Exception e) { + logger.warn("Falha ao ler recurso interno {}: {}", resPath, e.getMessage()); + } + } + } + + return Optional.empty(); + } + + /** + * Retorna um Node de ícone customizado (SVG ou PNG) ou invoca o fallback caso não exista imagem customizada. + */ + public static Node getIconOrDefault(String iconName, double size, Supplier fallbackSupplier) { + if (iconName != null && !iconName.isBlank()) { + String cleanName = cleanBaseName(iconName); + + // 1. Prioridade para SVG em ./assets/icons/ + Optional localSvg = loadSvgIconFromPath(Path.of("assets", "icons", cleanName + ".svg"), size); + if (localSvg.isPresent()) { + return localSvg.get(); + } + + // 2. Procura SVG em ~/.cybercore/assets/icons/ + String userHome = System.getProperty("user.home", "."); + Optional userSvg = loadSvgIconFromPath(Path.of(userHome, ".cybercore", "assets", "icons", cleanName + ".svg"), size); + if (userSvg.isPresent()) { + return userSvg.get(); + } + + // 3. Se não houver SVG, tenta imagem bitmap (.png, .jpg) nas pastas externas + Optional customImg = loadImage("icons", cleanName, size, size); + if (customImg.isPresent() && !customImg.get().isError()) { + ImageView iv = new ImageView(customImg.get()); + iv.setFitWidth(size); + iv.setFitHeight(size); + iv.setPreserveRatio(true); + return iv; + } + } + + return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView(); + } + + /** + * Retorna a imagem do logotipo do sistema ou o fallback vetorial. + */ + public static Node getLogoOrDefault(double width, double height, Supplier fallbackSupplier) { + // Tenta carregar app-logo bitmap (.png) primeiro + Optional customLogo = loadImage("images", "app-logo", width, height); + if (customLogo.isPresent() && !customLogo.get().isError()) { + ImageView iv = new ImageView(customLogo.get()); + iv.setFitWidth(width); + iv.setFitHeight(height); + iv.setPreserveRatio(true); + return iv; + } + + // Tenta carregar SVG de app-logo se existir + Optional svgLogo = loadSvgIconFromPath(Path.of("assets", "images", "app-logo.svg"), height); + if (svgLogo.isPresent()) { + return svgLogo.get(); + } + + return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView(); + } + + private static String cleanBaseName(String name) { + String clean = name; + int dotIdx = clean.lastIndexOf('.'); + if (dotIdx > 0) { + clean = clean.substring(0, dotIdx); + } + return clean; + } + + private static Optional loadSvgIconFromPath(Path path, double size) { + if (Files.isRegularFile(path)) { + try (InputStream in = new FileInputStream(path.toFile())) { + Node node = parseSvgToNode(in, size); + if (node != null) { + logger.debug("Carregado ícone SVG customizado de: {}", path); + return Optional.of(node); + } + } catch (Exception e) { + logger.warn("Falha ao carregar ícone SVG {}: {}", path, e.getMessage()); + } + } + return Optional.empty(); + } + + /** + * Parser analítico leve de SVG padrão para nós gráficos JavaFX com preservação geométrica exata. + */ + public static Node parseSvgToNode(InputStream in, double targetSize) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(false); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(in); + + Element svg = doc.getDocumentElement(); + if (!"svg".equalsIgnoreCase(svg.getNodeName())) { + return null; + } + + double vbMinX = 0; + double vbMinY = 0; + double vbWidth = 48; + double vbHeight = 48; + + String viewBox = svg.getAttribute("viewBox"); + if (viewBox != null && !viewBox.isBlank()) { + String[] parts = viewBox.trim().split("[,\\s]+"); + if (parts.length >= 4) { + try { + vbMinX = Double.parseDouble(parts[0]); + vbMinY = Double.parseDouble(parts[1]); + vbWidth = Double.parseDouble(parts[2]); + vbHeight = Double.parseDouble(parts[3]); + } catch (NumberFormatException ignored) { + } + } + } else { + String wAttr = svg.getAttribute("width"); + String hAttr = svg.getAttribute("height"); + if (!wAttr.isBlank() && !hAttr.isBlank()) { + try { + vbWidth = Double.parseDouble(wAttr.replaceAll("[^0-9.]", "")); + vbHeight = Double.parseDouble(hAttr.replaceAll("[^0-9.]", "")); + } catch (NumberFormatException ignored) { + } + } + } + + if (vbWidth <= 0) vbWidth = 48; + if (vbHeight <= 0) vbHeight = 48; + + Group contentGroup = new Group(); + renderChildren(svg, contentGroup); + + double maxDim = Math.max(vbWidth, vbHeight); + double scale = (targetSize * 0.90) / maxDim; + + double drawnW = vbWidth * scale; + double drawnH = vbHeight * scale; + double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale); + double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale); + + contentGroup.getTransforms().setAll( + new javafx.scene.transform.Translate(tx, ty), + new javafx.scene.transform.Scale(scale, scale, 0, 0) + ); + + StackPane container = new StackPane(contentGroup); + container.setPrefSize(targetSize, targetSize); + container.setMaxSize(targetSize, targetSize); + container.setMinSize(targetSize, targetSize); + container.setAlignment(Pos.CENTER); + return container; + + } catch (Exception e) { + logger.warn("Erro ao parsear arquivo SVG: {}", e.getMessage()); + return null; + } + } + + private static void renderChildren(Element parent, Group targetGroup) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + org.w3c.dom.Node n = children.item(i); + if (n instanceof Element el) { + Node jfxNode = createElementNode(el); + if (jfxNode != null) { + targetGroup.getChildren().add(jfxNode); + } + } + } + } + + private static Node createElementNode(Element el) { + String tagName = el.getTagName().toLowerCase(); + Shape shape = null; + + switch (tagName) { + case "path" -> { + String d = el.getAttribute("d"); + if (!d.isBlank()) { + SVGPath p = new SVGPath(); + p.setContent(d); + shape = p; + } + } + case "rect" -> { + double x = parseDouble(el.getAttribute("x"), 0); + double y = parseDouble(el.getAttribute("y"), 0); + double w = parseDouble(el.getAttribute("width"), 0); + double h = parseDouble(el.getAttribute("height"), 0); + double rx = parseDouble(el.getAttribute("rx"), 0); + Rectangle r = new Rectangle(x, y, w, h); + if (rx > 0) { + r.setArcWidth(rx * 2); + r.setArcHeight(rx * 2); + } + shape = r; + } + case "circle" -> { + double cx = parseDouble(el.getAttribute("cx"), 0); + double cy = parseDouble(el.getAttribute("cy"), 0); + double rad = parseDouble(el.getAttribute("r"), 0); + shape = new Circle(cx, cy, rad); + } + case "line" -> { + double x1 = parseDouble(el.getAttribute("x1"), 0); + double y1 = parseDouble(el.getAttribute("y1"), 0); + double x2 = parseDouble(el.getAttribute("x2"), 0); + double y2 = parseDouble(el.getAttribute("y2"), 0); + shape = new Line(x1, y1, x2, y2); + } + case "polygon" -> { + String pts = el.getAttribute("points"); + if (!pts.isBlank()) { + Polygon poly = new Polygon(); + parsePoints(pts, poly.getPoints()); + shape = poly; + } + } + case "polyline" -> { + String pts = el.getAttribute("points"); + if (!pts.isBlank()) { + Polyline poly = new Polyline(); + parsePoints(pts, poly.getPoints()); + shape = poly; + } + } + case "text" -> { + double x = parseDouble(el.getAttribute("x"), 0); + double y = parseDouble(el.getAttribute("y"), 0); + String content = el.getTextContent(); + Text t = new Text(x, y, content); + double fs = parseDouble(el.getAttribute("font-size"), 10); + String fw = el.getAttribute("font-weight"); + t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs)); + if (el.hasAttribute("fill")) { + t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK)); + } + if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) { + t.setTextOrigin(javafx.geometry.VPos.CENTER); + } + return t; + } + case "g" -> { + Group g = new Group(); + renderChildren(el, g); + if (el.hasAttribute("stroke")) { + Color strokeCol = parseColor(el.getAttribute("stroke"), null); + double sw = parseDouble(el.getAttribute("stroke-width"), 1.0); + for (Node child : g.getChildren()) { + if (child instanceof Shape s) { + if (strokeCol != null) s.setStroke(strokeCol); + s.setStrokeWidth(sw); + } + } + } + return g; + } + } + + if (shape != null) { + applyShapeStyles(el, shape); + return shape; + } + + return null; + } + + private static void applyShapeStyles(Element el, Shape shape) { + if (el.hasAttribute("fill")) { + shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT)); + } else if (shape instanceof Line) { + shape.setFill(Color.TRANSPARENT); + } + + if (el.hasAttribute("stroke")) { + shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT)); + } + + if (el.hasAttribute("stroke-width")) { + shape.setStrokeWidth(parseDouble(el.getAttribute("stroke-width"), 1.0)); + } + + if (el.hasAttribute("fill-rule") && shape instanceof SVGPath sp) { + if ("evenodd".equalsIgnoreCase(el.getAttribute("fill-rule"))) { + sp.setFillRule(javafx.scene.shape.FillRule.EVEN_ODD); + } + } + } + + private static void parsePoints(String ptsStr, java.util.List pointsList) { + String[] tokens = ptsStr.trim().split("[,\\s]+"); + for (String t : tokens) { + if (!t.isBlank()) { + try { + pointsList.add(Double.parseDouble(t)); + } catch (NumberFormatException ignored) { + } + } + } + } + + private static Color parseColor(String str, Color fallback) { + if (str == null || str.isBlank() || "none".equalsIgnoreCase(str)) { + return Color.TRANSPARENT; + } + String trimmed = str.trim(); + if ("currentColor".equalsIgnoreCase(trimmed)) { + return Color.web("#0288d1"); + } + try { + return Color.web(trimmed); + } catch (Exception e) { + return fallback != null ? fallback : Color.BLACK; + } + } + + private static double parseDouble(String str, double defaultValue) { + if (str == null || str.isBlank()) { + return defaultValue; + } + try { + return Double.parseDouble(str.replaceAll("[^0-9.-]", "")); + } catch (Exception e) { + return defaultValue; + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java new file mode 100644 index 0000000..e4273e8 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/CertificateCardNode.java @@ -0,0 +1,129 @@ +package br.com.cybercore.signer.ui.components; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * Componente visual reutilizável para exibição de um certificado digital. + */ +@SuppressWarnings("this-escape") +public final class CertificateCardNode extends VBox { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy") + .withZone(ZoneId.systemDefault()); + + private final ManagedCertificate certificate; + + public CertificateCardNode(ManagedCertificate certificate) { + this.certificate = certificate; + initLayout(); + } + + private void initLayout() { + getStyleClass().add("cert-card"); + setCursor(javafx.scene.Cursor.HAND); + setSpacing(8); + + // Linha 1: Nome do Titular e Badge A1/A3 + HBox topRow = new HBox(10); + topRow.setAlignment(Pos.CENTER_LEFT); + + Label titleLabel = new Label(certificate.commonName() != null ? certificate.commonName() : certificate.alias()); + titleLabel.getStyleClass().add("cert-title"); + HBox.setHgrow(titleLabel, Priority.ALWAYS); + + Label badgeLabel = new Label(); + if (certificate.type() == CertificateType.A1) { + badgeLabel.setText("A1 • Certificado local"); + badgeLabel.getStyleClass().add("cert-badge-a1"); + } else { + String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB"; + badgeLabel.setText("A3 • " + tokenDesc); + badgeLabel.getStyleClass().add("cert-badge-a3"); + } + + topRow.getChildren().addAll(titleLabel, badgeLabel); + + // Linha 2: Documento (CPF / CNPJ) e Emissor + HBox midRow = new HBox(12); + midRow.setAlignment(Pos.CENTER_LEFT); + + if (certificate.cpf() != null) { + Label docLabel = new Label("CPF: " + formatCpf(certificate.cpf())); + docLabel.getStyleClass().add("cert-detail"); + midRow.getChildren().add(docLabel); + } else if (certificate.cnpj() != null) { + Label docLabel = new Label("CNPJ: " + formatCnpj(certificate.cnpj())); + docLabel.getStyleClass().add("cert-detail"); + midRow.getChildren().add(docLabel); + } + + String issuerCn = extractIssuerCn(certificate.issuerDn()); + Label issuerLabel = new Label("Emissor: " + issuerCn); + issuerLabel.getStyleClass().add("cert-detail"); + midRow.getChildren().add(issuerLabel); + + // Linha 3: Validade e Status de disponibilidade + HBox bottomRow = new HBox(12); + bottomRow.setAlignment(Pos.CENTER_LEFT); + + String validityText = "Válido até " + (certificate.notAfter() != null ? DATE_FORMATTER.format(certificate.notAfter()) : "N/D"); + Label validityLabel = new Label(validityText); + validityLabel.getStyleClass().add("cert-detail"); + HBox.setHgrow(validityLabel, Priority.ALWAYS); + + Label statusLabel = new Label(); + if (certificate.status() == CertificateStatus.AVAILABLE && certificate.isCurrentlyValid()) { + statusLabel.setText("● Disponível"); + statusLabel.getStyleClass().add("cert-status-available"); + } else { + statusLabel.setText("● " + certificate.status().getDescription()); + statusLabel.getStyleClass().add("cert-status-expired"); + } + + bottomRow.getChildren().addAll(validityLabel, statusLabel); + + getChildren().addAll(topRow, midRow, bottomRow); + } + + public ManagedCertificate getCertificate() { + return certificate; + } + + private String formatCpf(String cpf) { + if (cpf != null && cpf.length() == 11) { + return cpf.substring(0, 3) + "." + cpf.substring(3, 6) + "." + cpf.substring(6, 9) + "-" + cpf.substring(9); + } + return cpf; + } + + private String formatCnpj(String cnpj) { + if (cnpj != null && cnpj.length() == 14) { + return cnpj.substring(0, 2) + "." + cnpj.substring(2, 5) + "." + cnpj.substring(5, 8) + "/" + cnpj.substring(8, 12) + "-" + cnpj.substring(12); + } + return cnpj; + } + + private String extractIssuerCn(String dn) { + if (dn == null) return "Desconhecido"; + int idx = dn.indexOf("CN="); + if (idx >= 0) { + int end = dn.indexOf(',', idx); + if (end > idx) { + return dn.substring(idx + 3, end).trim(); + } + return dn.substring(idx + 3).trim(); + } + return dn; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java new file mode 100644 index 0000000..acb5e1d --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/components/SvgIcons.java @@ -0,0 +1,732 @@ +package br.com.cybercore.signer.ui.components; + +import br.com.cybercore.signer.ui.assets.AssetManager; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.effect.DropShadow; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.CycleMethod; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.Stop; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Polygon; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.SVGPath; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; + +/** + * Fábrica de ícones e grafismos vetoriais SVG nítidos e escaláveis para a interface Serpro do CyberCore Signer. + * Integra-se ao AssetManager para carregar dinamicamente arquivos .svg ou .png da pasta 'assets/icons/'. + */ +public final class SvgIcons { + + private SvgIcons() { + } + + /** + * Logotipo central de cristal/diamante azul tridimensional. + */ + public static Node createAppCrystalLogo(double size) { + return AssetManager.getIconOrDefault("app-icon", size, () -> buildAppCrystalLogo(size)); + } + + public static Node buildAppCrystalLogo(double size) { + StackPane root = new StackPane(); + root.setPrefSize(size, size); + root.setMaxSize(size, size); + + Rectangle bg = new Rectangle(size, size); + bg.setArcWidth(size * 0.35); + bg.setArcHeight(size * 0.35); + LinearGradient bgGrad = new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#1976d2")), + new Stop(0.5, Color.web("#0d47a1")), + new Stop(1, Color.web("#01579b")) + ); + bg.setFill(bgGrad); + + double s = size; + Polygon facetTop = new Polygon( + s * 0.5, s * 0.15, + s * 0.82, s * 0.42, + s * 0.5, s * 0.55, + s * 0.18, s * 0.42 + ); + facetTop.setFill(new LinearGradient( + 0, 0, 0, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#ffffff", 0.85)), + new Stop(1, Color.web("#80d8ff", 0.4)) + )); + + Polygon facetBottomLeft = new Polygon( + s * 0.18, s * 0.42, + s * 0.5, s * 0.55, + s * 0.5, s * 0.85, + s * 0.28, s * 0.70 + ); + facetBottomLeft.setFill(new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#40c4ff", 0.6)), + new Stop(1, Color.web("#0288d1", 0.2)) + )); + + Polygon facetBottomRight = new Polygon( + s * 0.82, s * 0.42, + s * 0.5, s * 0.55, + s * 0.5, s * 0.85, + s * 0.72, s * 0.70 + ); + facetBottomRight.setFill(new LinearGradient( + 1, 0, 0, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#e1f5fe", 0.75)), + new Stop(1, Color.web("#29b6f6", 0.3)) + )); + + Circle glow = new Circle(s * 0.5, s * 0.5, s * 0.12); + glow.setFill(new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#ffffff", 0.9)), + new Stop(1, Color.web("#ffffff", 0.0)) + )); + + Group crystal = new Group(bg, facetTop, facetBottomLeft, facetBottomRight, glow); + DropShadow shadow = new DropShadow(); + shadow.setColor(Color.web("#0d47a1", 0.4)); + shadow.setRadius(12); + shadow.setOffsetY(4); + crystal.setEffect(shadow); + + root.getChildren().add(crystal); + return root; + } + + /** + * Ícone de Assinar PDF. + */ + public static Node pdfIcon() { + return AssetManager.getIconOrDefault("pdf", 38, SvgIcons::buildPdfIcon); + } + + public static Node buildPdfIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#d32f2f")); + doc.setStrokeWidth(1.8); + + SVGPath fold = new SVGPath(); + fold.setContent("M 24 2 L 24 10 L 32 10 Z"); + fold.setFill(Color.web("#ffcdd2")); + fold.setStroke(Color.web("#d32f2f")); + fold.setStrokeWidth(1.2); + + Rectangle tag = new Rectangle(20, 10); + tag.setArcWidth(4); + tag.setArcHeight(4); + tag.setFill(Color.web("#e53935")); + tag.setTranslateY(4); + + Text txt = new Text("PDF"); + txt.setFont(Font.font("Segoe UI", FontWeight.BOLD, 7.5)); + txt.setFill(Color.WHITE); + txt.setTranslateY(4); + + box.getChildren().addAll(doc, fold, tag, txt); + return box; + } + + /** + * Ícone de Assinar XML. + */ + public static Node xmlIcon() { + return AssetManager.getIconOrDefault("xml", 38, SvgIcons::buildXmlIcon); + } + + public static Node buildXmlIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#1976d2")); + doc.setStrokeWidth(1.8); + + SVGPath fold = new SVGPath(); + fold.setContent("M 24 2 L 24 10 L 32 10 Z"); + fold.setFill(Color.web("#bbdefb")); + fold.setStroke(Color.web("#1976d2")); + fold.setStrokeWidth(1.2); + + Rectangle tag = new Rectangle(22, 10); + tag.setArcWidth(4); + tag.setArcHeight(4); + tag.setFill(Color.web("#0288d1")); + tag.setTranslateY(4); + + Text txt = new Text(""); + txt.setFont(Font.font("Segoe UI", FontWeight.BOLD, 6.8)); + txt.setFill(Color.WHITE); + txt.setTranslateY(4); + + box.getChildren().addAll(doc, fold, tag, txt); + return box; + } + + /** + * Ícone de Assinar Arquivo genérico. + */ + public static Node fileIcon() { + return AssetManager.getIconOrDefault("file", 38, SvgIcons::buildFileIcon); + } + + public static Node buildFileIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 2 L 24 2 L 32 10 L 32 36 L 6 36 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#0288d1")); + doc.setStrokeWidth(1.8); + + SVGPath fold = new SVGPath(); + fold.setContent("M 24 2 L 24 10 L 32 10 Z"); + fold.setFill(Color.web("#b3e5fc")); + fold.setStroke(Color.web("#0288d1")); + fold.setStrokeWidth(1.2); + + SVGPath pen = new SVGPath(); + pen.setContent("M 10 24 L 22 12 L 26 16 L 14 28 L 8 29 Z"); + pen.setFill(Color.web("#0288d1")); + + box.getChildren().addAll(doc, fold, pen); + return box; + } + + /** + * Ícone de Assinar em Lote. + */ + public static Node batchIcon() { + return AssetManager.getIconOrDefault("batch", 38, SvgIcons::buildBatchIcon); + } + + public static Node buildBatchIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc3 = new SVGPath(); + doc3.setContent("M 12 2 L 30 2 L 30 26 L 12 26 Z"); + doc3.setFill(Color.web("#e1f5fe")); + doc3.setStroke(Color.web("#4fc3f7")); + doc3.setStrokeWidth(1.2); + doc3.setTranslateX(4); + doc3.setTranslateY(-4); + + SVGPath doc2 = new SVGPath(); + doc2.setContent("M 8 6 L 28 6 L 28 30 L 8 30 Z"); + doc2.setFill(Color.web("#b3e5fc")); + doc2.setStroke(Color.web("#0288d1")); + doc2.setStrokeWidth(1.4); + doc2.setTranslateX(2); + doc2.setTranslateY(-2); + + SVGPath doc1 = new SVGPath(); + doc1.setContent("M 4 10 L 24 10 L 24 34 L 4 34 Z"); + doc1.setFill(Color.web("#ffffff")); + doc1.setStroke(Color.web("#01579b")); + doc1.setStrokeWidth(1.6); + + SVGPath check = new SVGPath(); + check.setContent("M 8 22 L 13 27 L 21 16"); + check.setStroke(Color.web("#00c853")); + check.setStrokeWidth(2.2); + check.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(doc3, doc2, doc1, check); + return box; + } + + /** + * Ícone de Assinatura Atachada. + */ + public static Node attachedIcon() { + return AssetManager.getIconOrDefault("attached", 38, SvgIcons::buildAttachedIcon); + } + + public static Node buildAttachedIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 8 4 L 28 4 L 28 34 L 8 34 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#0288d1")); + doc.setStrokeWidth(1.6); + + SVGPath lines = new SVGPath(); + lines.setContent("M 12 12 L 24 12 M 12 17 L 24 17 M 12 22 L 20 22"); + lines.setStroke(Color.web("#90caf9")); + lines.setStrokeWidth(1.4); + + SVGPath clip = new SVGPath(); + clip.setContent("M 10 2 C 7 2 6 5 6 8 L 6 20 C 6 23 8 25 11 25 C 14 25 16 23 16 20 L 16 9 C 16 7 15 6 13 6 C 11 6 10 7 10 9 L 10 19"); + clip.setFill(Color.TRANSPARENT); + clip.setStroke(Color.web("#546e7a")); + clip.setStrokeWidth(1.8); + clip.setTranslateX(-8); + clip.setTranslateY(-4); + + box.getChildren().addAll(doc, lines, clip); + return box; + } + + /** + * Ícone de Co-Assinar. + */ + public static Node coSignIcon() { + return AssetManager.getIconOrDefault("co-sign", 38, SvgIcons::buildCoSignIcon); + } + + public static Node buildCoSignIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc1 = new SVGPath(); + doc1.setContent("M 4 8 L 22 8 L 22 32 L 4 32 Z"); + doc1.setFill(Color.web("#ffffff")); + doc1.setStroke(Color.web("#1976d2")); + doc1.setStrokeWidth(1.5); + + SVGPath doc2 = new SVGPath(); + doc2.setContent("M 14 4 L 32 4 L 32 28 L 14 28 Z"); + doc2.setFill(Color.web("#e3f2fd")); + doc2.setStroke(Color.web("#0288d1")); + doc2.setStrokeWidth(1.5); + + SVGPath people = new SVGPath(); + people.setContent("M 10 26 C 10 22 14 22 16 22 C 18 22 22 22 22 26 M 16 17 C 14.5 17 14.5 14 16 14 C 17.5 14 17.5 17 16 17"); + people.setStroke(Color.web("#0d47a1")); + people.setStrokeWidth(1.6); + people.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(doc2, doc1, people); + return box; + } + + /** + * Ícone de Carimbo do Tempo. + */ + public static Node timestampIcon() { + return AssetManager.getIconOrDefault("timestamp", 38, SvgIcons::buildTimestampIcon); + } + + public static Node buildTimestampIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 2 L 24 2 L 32 10 L 32 34 L 6 34 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#0288d1")); + doc.setStrokeWidth(1.5); + + Circle clockBg = new Circle(18, 24, 9); + clockBg.setFill(Color.web("#e1f5fe")); + clockBg.setStroke(Color.web("#0288d1")); + clockBg.setStrokeWidth(1.6); + + SVGPath clockHands = new SVGPath(); + clockHands.setContent("M 18 19 L 18 24 L 22 24"); + clockHands.setStroke(Color.web("#01579b")); + clockHands.setStrokeWidth(1.6); + clockHands.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(doc, clockBg, clockHands); + return box; + } + + /** + * Ícone de Assinatura de Longa Duração (LTV). + */ + public static Node ltvIcon() { + return AssetManager.getIconOrDefault("ltv", 38, SvgIcons::buildLtvIcon); + } + + public static Node buildLtvIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 2 L 24 2 L 32 10 L 32 34 L 6 34 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#00838f")); + doc.setStrokeWidth(1.5); + + Circle shield = new Circle(20, 24, 8); + shield.setFill(Color.web("#e0f2f1")); + shield.setStroke(Color.web("#00897b")); + shield.setStrokeWidth(1.6); + + Text ltv = new Text("LTV"); + ltv.setFont(Font.font("Segoe UI", FontWeight.BOLD, 6.8)); + ltv.setFill(Color.web("#00695c")); + ltv.setTranslateX(2); + ltv.setTranslateY(6); + + box.getChildren().addAll(doc, shield, ltv); + return box; + } + + /** + * Ícone de Validar Assinatura. + */ + public static Node validateIcon() { + return AssetManager.getIconOrDefault("validate", 38, SvgIcons::buildValidateIcon); + } + + public static Node buildValidateIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath doc = new SVGPath(); + doc.setContent("M 6 4 L 26 4 L 26 34 L 6 34 Z"); + doc.setFill(Color.web("#ffffff")); + doc.setStroke(Color.web("#0288d1")); + doc.setStrokeWidth(1.5); + + SVGPath check = new SVGPath(); + check.setContent("M 8 20 L 16 28 L 32 10"); + check.setStroke(Color.web("#2e7d32")); + check.setStrokeWidth(2.6); + check.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(doc, check); + return box; + } + + /** + * Ícone de Validar Carimbo do Tempo. + */ + public static Node validateTimestampIcon() { + return AssetManager.getIconOrDefault("validate-timestamp", 38, SvgIcons::buildValidateTimestampIcon); + } + + public static Node buildValidateTimestampIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + Circle clock = new Circle(16, 16, 12); + clock.setFill(Color.web("#f1f8e9")); + clock.setStroke(Color.web("#558b2f")); + clock.setStrokeWidth(1.6); + + SVGPath check = new SVGPath(); + check.setContent("M 10 16 L 15 21 L 24 11"); + check.setStroke(Color.web("#2e7d32")); + check.setStrokeWidth(2.2); + check.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(clock, check); + return box; + } + + /** + * Ícone de Sair do Agente. + */ + public static Node exitIcon() { + return AssetManager.getIconOrDefault("exit", 38, SvgIcons::buildExitIcon); + } + + public static Node buildExitIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath door = new SVGPath(); + door.setContent("M 18 6 L 8 6 C 6.5 6 6 7.5 6 9 L 6 27 C 6 28.5 6.5 30 8 30 L 18 30"); + door.setStroke(Color.web("#546e7a")); + door.setStrokeWidth(2); + door.setFill(Color.TRANSPARENT); + + SVGPath arrow = new SVGPath(); + arrow.setContent("M 14 18 L 30 18 M 24 12 L 30 18 L 24 24"); + arrow.setStroke(Color.web("#d32f2f")); + arrow.setStrokeWidth(2); + arrow.setFill(Color.TRANSPARENT); + + box.getChildren().addAll(door, arrow); + return box; + } + + /** + * Ícone de Logs. + */ + public static Node logsIcon() { + return AssetManager.getIconOrDefault("logs", 24, SvgIcons::buildLogsIcon); + } + + public static Node buildLogsIcon() { + StackPane box = new StackPane(); + box.setPrefSize(24, 24); + + Text code = new Text(""); + code.setFont(Font.font("Segoe UI", FontWeight.BOLD, 12)); + code.setFill(Color.web("#0288d1")); + + box.getChildren().add(code); + return box; + } + + /** + * Ícone de Configurações. + */ + public static Node settingsIcon() { + return AssetManager.getIconOrDefault("settings", 24, SvgIcons::buildSettingsIcon); + } + + public static Node buildSettingsIcon() { + StackPane box = new StackPane(); + box.setPrefSize(24, 24); + + SVGPath gear = new SVGPath(); + gear.setContent("M 12 1 C 11 1 10.5 2 10.5 3 C 9.5 3.3 8.6 3.8 7.8 4.5 L 6.2 3.6 C 5.5 3.2 4.6 3.5 4.1 4.1 L 2.6 6.6 C 2.1 7.2 2.3 8.1 3 8.6 L 4.4 9.6 C 4.2 10.4 4.1 11.2 4.1 12 C 4.1 12.8 4.2 13.6 4.4 14.4 L 3 15.4 C 2.3 15.9 2.1 16.8 2.6 17.4 L 4.1 19.9 C 4.6 20.5 5.5 20.8 6.2 20.4 L 7.8 19.5 C 8.6 20.2 9.5 20.7 10.5 21 C 10.5 22 11 23 12 23 C 13 23 13.5 22 13.5 21 C 14.5 20.7 15.4 20.2 16.2 19.5 L 17.8 20.4 C 18.5 20.8 19.4 20.5 19.9 19.9 L 21.4 17.4 C 21.9 16.8 21.7 15.9 21 15.4 L 19.6 14.4 C 19.8 13.6 19.9 12.8 19.9 12 C 19.9 11.2 19.8 10.4 19.6 9.6 L 21 8.6 C 21.7 8.1 21.9 7.2 21.4 6.6 L 19.9 4.1 C 19.4 3.5 18.5 3.2 17.8 3.6 L 16.2 4.5 C 15.4 3.8 14.5 3.3 13.5 3 C 13.5 2 13 1 12 1 Z M 12 8 C 14.2 8 16 9.8 16 12 C 16 14.2 14.2 16 12 16 C 9.8 16 8 14.2 8 12 C 8 9.8 9.8 8 12 8 Z"); + gear.setFill(Color.web("#0288d1")); + gear.setScaleX(0.85); + gear.setScaleY(0.85); + + box.getChildren().add(gear); + return box; + } + + /** + * Ícone de Tutorial. + */ + public static Node tutorialIcon() { + return AssetManager.getIconOrDefault("tutorial", 24, SvgIcons::buildTutorialIcon); + } + + public static Node buildTutorialIcon() { + StackPane box = new StackPane(); + box.setPrefSize(24, 24); + + SVGPath monitor = new SVGPath(); + monitor.setContent("M 2 4 L 22 4 L 22 17 L 2 17 Z M 8 17 L 6 21 L 18 21 L 16 17 M 12 17 L 12 21"); + monitor.setStroke(Color.web("#0288d1")); + monitor.setStrokeWidth(1.6); + monitor.setFill(Color.TRANSPARENT); + + box.getChildren().add(monitor); + return box; + } + + /** + * Ícone de Certificados. + */ + public static Node certificatesIcon() { + return AssetManager.getIconOrDefault("cert", 24, SvgIcons::buildCertificatesIcon); + } + + public static Node buildCertificatesIcon() { + StackPane box = new StackPane(); + box.setPrefSize(24, 24); + + SVGPath folder = new SVGPath(); + folder.setContent("M 2 5 L 8 5 L 10 7 L 22 7 L 22 20 L 2 20 Z"); + folder.setFill(Color.web("#e1f5fe")); + folder.setStroke(Color.web("#0288d1")); + folder.setStrokeWidth(1.5); + + Rectangle lock = new Rectangle(7, 6); + lock.setArcWidth(2); + lock.setArcHeight(2); + lock.setFill(Color.web("#0288d1")); + lock.setTranslateX(3); + lock.setTranslateY(2); + + box.getChildren().addAll(folder, lock); + return box; + } + + /** + * Ícone de Sobre. + */ + public static Node aboutIcon() { + return AssetManager.getIconOrDefault("about", 24, SvgIcons::buildAboutIcon); + } + + public static Node buildAboutIcon() { + StackPane box = new StackPane(); + box.setPrefSize(24, 24); + + Circle circle = new Circle(10); + circle.setFill(Color.web("#0288d1")); + + Text i = new Text("i"); + i.setFont(Font.font("Segoe UI", FontWeight.BOLD, 13)); + i.setFill(Color.WHITE); + + box.getChildren().addAll(circle, i); + return box; + } + + /** + * Ícone de Sol (Light Mode). + */ + public static Node sunIcon(double size) { + return AssetManager.getIconOrDefault("sun", size, () -> buildSunIcon(size)); + } + + public static Node buildSunIcon(double size) { + StackPane box = new StackPane(); + box.setPrefSize(size, size); + box.setMaxSize(size, size); + + Circle sunCore = new Circle(size * 0.5, size * 0.5, size * 0.24); + sunCore.setFill(new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#f59e0b")), + new Stop(1, Color.web("#d97706")) + )); + + SVGPath rays = new SVGPath(); + rays.setContent("M 12 2 L 12 5 M 12 19 L 12 22 M 2 12 L 5 12 M 19 12 L 22 12 " + + "M 4.93 4.93 L 7.05 7.05 M 16.95 16.95 L 19.07 19.07 " + + "M 4.93 19.07 L 7.05 16.95 M 16.95 7.05 L 19.07 4.93"); + rays.setStroke(Color.web("#f59e0b")); + rays.setStrokeWidth(2.0); + rays.setScaleX(size / 24.0); + rays.setScaleY(size / 24.0); + + box.getChildren().addAll(rays, sunCore); + return box; + } + + /** + * Ícone de Lua (Dark Mode). + */ + public static Node moonIcon(double size) { + return AssetManager.getIconOrDefault("moon", size, () -> buildMoonIcon(size)); + } + + public static Node buildMoonIcon(double size) { + StackPane box = new StackPane(); + box.setPrefSize(size, size); + box.setMaxSize(size, size); + + SVGPath moon = new SVGPath(); + moon.setContent("M 21 12.79 A 9 9 0 1 1 11.21 3 A 7 7 0 0 0 21 12.79 Z"); + moon.setFill(new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#38bdf8")), + new Stop(1, Color.web("#818cf8")) + )); + moon.setScaleX(size / 24.0); + moon.setScaleY(size / 24.0); + + box.getChildren().add(moon); + return box; + } + + /** + * Logotipo vetorial CyberShield. + */ + public static Node cyberShieldLogo(double size) { + return AssetManager.getIconOrDefault("shield", size, () -> buildCyberShieldLogo(size)); + } + + public static Node buildCyberShieldLogo(double size) { + StackPane box = new StackPane(); + box.setPrefSize(size, size); + box.setMaxSize(size, size); + + SVGPath shield = new SVGPath(); + shield.setContent("M 12 2 L 3 6 L 3 13 C 3 18.5 7 23.5 12 25 C 17 23.5 21 18.5 21 13 L 21 6 Z"); + shield.setFill(new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.web("#0284c7")), + new Stop(0.5, Color.web("#0369a1")), + new Stop(1, Color.web("#075985")) + )); + shield.setStroke(Color.web("#38bdf8")); + shield.setStrokeWidth(1.2); + shield.setScaleX(size / 26.0); + shield.setScaleY(size / 26.0); + + SVGPath key = new SVGPath(); + key.setContent("M 12 7 C 10.3 7 9 8.3 9 10 C 9 11.3 9.8 12.4 11 12.8 L 11 18 L 13 18 L 13 15 L 14 15 L 14 13 L 13 13 L 13 12.8 C 14.2 12.4 15 11.3 15 10 C 15 8.3 13.7 7 12 7 Z M 12 9 C 12.6 9 13 9.4 13 10 C 13 10.6 12.6 11 12 11 C 11.4 11 11 10.6 11 10 C 11 9.4 11.4 9 12 9 Z"); + key.setFill(Color.web("#ffffff")); + key.setScaleX(size / 26.0); + key.setScaleY(size / 26.0); + + box.getChildren().addAll(shield, key); + return box; + } + + /** + * Ícone de Gerenciador/Criador de Selos Visuais de Assinatura. + */ + public static Node sealIcon() { + return AssetManager.getIconOrDefault("seal", 38, SvgIcons::buildSealIcon); + } + + public static Node buildSealIcon() { + StackPane box = new StackPane(); + box.setPrefSize(38, 38); + + SVGPath ribbon = new SVGPath(); + ribbon.setContent("M 14 26 L 19 36 L 24 26 Z M 9 26 L 12 34 L 15 26 Z"); + ribbon.setFill(Color.web("#f59e0b")); + + Circle sealCircle = new Circle(13); + sealCircle.setFill(Color.web("#d97706")); + sealCircle.setStroke(Color.web("#fbbf24")); + sealCircle.setStrokeWidth(1.6); + sealCircle.setTranslateY(-4); + + SVGPath star = new SVGPath(); + star.setContent("M 19 8 L 21 13 L 26 13 L 22 16 L 24 21 L 19 18 L 14 21 L 16 16 L 12 13 L 17 13 Z"); + star.setFill(Color.web("#ffffff")); + star.setTranslateX(-9); + star.setTranslateY(-14); + + box.getChildren().addAll(ribbon, sealCircle, star); + return box; + } + + /** + * Ícone vetorial de Fechar / Cancelar (cruz geométrica moderna e limpa). + */ + public static Node closeIcon() { + return closeIcon(14); + } + + public static Node closeIcon(double size) { + StackPane box = new StackPane(); + box.setPrefSize(size, size); + box.setMaxSize(size, size); + box.setMinSize(size, size); + box.setAlignment(Pos.CENTER); + + SVGPath cross = new SVGPath(); + cross.setContent("M 3.5 3.5 L 12.5 12.5 M 12.5 3.5 L 3.5 12.5"); + cross.setStroke(Color.web("#64748b")); + cross.setStrokeWidth(2.0); + cross.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND); + cross.setFill(Color.TRANSPARENT); + + double scale = size / 16.0; + cross.setScaleX(scale); + cross.setScaleY(scale); + + box.getChildren().add(cross); + return box; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java new file mode 100644 index 0000000..f2afa93 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/theme/ThemeManager.java @@ -0,0 +1,120 @@ +package br.com.cybercore.signer.ui.theme; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import javafx.scene.Parent; +import javafx.scene.Scene; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Gerenciador centralizado de temas visuais do CyberCore Signer. + * Permite alternar dinamicamente entre Tema Claro (Light) e Tema Escuro (Dark) + * aplicando as classes CSS e folhas de estilo correspondentes em qualquer Scene. + */ +public final class ThemeManager { + + public enum AppTheme { + LIGHT("theme-light", "Modo Claro"), + DARK("theme-dark", "Modo Escuro"); + + private final String cssClass; + private final String displayName; + + AppTheme(String cssClass, String displayName) { + this.cssClass = cssClass; + this.displayName = displayName; + } + + public String getCssClass() { + return cssClass; + } + + public String getDisplayName() { + return displayName; + } + } + + private static AppTheme currentTheme = AppTheme.LIGHT; + private static final List> listeners = new ArrayList<>(); + + private ThemeManager() { + } + + public static AppTheme getCurrentTheme() { + return currentTheme; + } + + public static boolean isDark() { + return currentTheme == AppTheme.DARK; + } + + public static void setCurrentTheme(AppTheme theme) { + if (theme == null || theme == currentTheme) { + return; + } + currentTheme = theme; + notifyListeners(); + } + + public static AppTheme toggleTheme() { + currentTheme = (currentTheme == AppTheme.LIGHT) ? AppTheme.DARK : AppTheme.LIGHT; + notifyListeners(); + return currentTheme; + } + + /** + * Aplica o tema atualmente ativo a uma Scene, adicionando a folha de estilo global + * e ajustando a classe CSS raiz (theme-light / theme-dark). + */ + public static void applyTheme(Scene scene) { + if (scene == null) { + return; + } + + String cssUrl = Objects.requireNonNull( + ThemeManager.class.getResource(CyberCoreUiConstants.MAIN_CSS_PATH) + ).toExternalForm(); + + if (!scene.getStylesheets().contains(cssUrl)) { + scene.getStylesheets().add(cssUrl); + } + + Parent root = scene.getRoot(); + if (root != null) { + applyThemeToNode(root); + } + } + + /** + * Aplica a classe de estilo do tema ativo no nó raiz fornecido. + */ + public static void applyThemeToNode(Parent node) { + if (node == null) { + return; + } + node.getStyleClass().removeAll(AppTheme.LIGHT.getCssClass(), AppTheme.DARK.getCssClass()); + node.getStyleClass().add(currentTheme.getCssClass()); + } + + public static void addListener(Consumer listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + public static void removeListener(Consumer listener) { + listeners.remove(listener); + } + + private static void notifyListeners() { + for (Consumer listener : new ArrayList<>(listeners)) { + try { + listener.accept(currentTheme); + } catch (Exception ignored) { + } + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java new file mode 100644 index 0000000..07869b0 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/tray/SystemTrayManager.java @@ -0,0 +1,169 @@ +package br.com.cybercore.signer.ui.tray; + +import javafx.application.Platform; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.AWTException; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.RenderingHints; +import java.awt.SystemTray; +import java.awt.TrayIcon; +import java.awt.image.BufferedImage; +import java.util.Objects; + +/** + * Gerencia a integração nativa com o System Tray (Área de Notificação / Bandeja do Windows). + * Permite que o CyberCore Signer permaneça em execução transparente em segundo plano, + * garantindo o atendimento ininterrupto às requisições do protocolo CCSP/1. + */ +public class SystemTrayManager { + + private static final Logger logger = LoggerFactory.getLogger(SystemTrayManager.class); + + private final Stage stage; + private final Runnable onRefreshCertificates; + private TrayIcon trayIcon; + private boolean initialized = false; + + public SystemTrayManager(Stage stage, Runnable onRefreshCertificates) { + this.stage = Objects.requireNonNull(stage, "stage must not be null"); + this.onRefreshCertificates = onRefreshCertificates; + } + + /** + * Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte. + */ + public synchronized boolean setup() { + if (!SystemTray.isSupported()) { + logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional."); + return false; + } + + try { + SystemTray tray = SystemTray.getSystemTray(); + Image iconImage = createTrayIconImage(); + + PopupMenu popup = new PopupMenu(); + + MenuItem openItem = new MenuItem("Abrir CyberCore Signer"); + openItem.addActionListener(e -> Platform.runLater(this::showStage)); + + MenuItem refreshItem = new MenuItem("Atualizar Certificados"); + refreshItem.addActionListener(e -> { + if (onRefreshCertificates != null) { + onRefreshCertificates.run(); + } + }); + + MenuItem exitItem = new MenuItem("Encerrar Agente"); + exitItem.addActionListener(e -> { + logger.info("Encerramento solicitado através do menu da bandeja."); + shutdown(); + }); + + popup.add(openItem); + popup.add(refreshItem); + popup.addSeparator(); + popup.add(exitItem); + + this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup); + this.trayIcon.setImageAutoSize(true); + + // Clique no ícone restaura a janela + this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage)); + + tray.add(this.trayIcon); + this.initialized = true; + + // Intercepta fechamento da janela para ocultar em vez de encerrar + Platform.runLater(() -> { + stage.setOnCloseRequest(event -> { + event.consume(); + stage.hide(); + showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO); + }); + }); + + logger.info("System Tray configurado com sucesso."); + return true; + + } catch (AWTException e) { + logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage()); + return false; + } catch (Throwable t) { + logger.warn("Erro ao inicializar System Tray: {}", t.getMessage()); + return false; + } + } + + public void showStage() { + if (stage != null) { + stage.show(); + stage.setIconified(false); + stage.toFront(); + } + } + + public void showMessage(String title, String message, TrayIcon.MessageType type) { + if (initialized && trayIcon != null) { + try { + trayIcon.displayMessage(title, message, type); + } catch (Throwable t) { + logger.debug("Não foi possível exibir notificação no Tray: {}", t.getMessage()); + } + } + } + + public synchronized void remove() { + if (initialized && trayIcon != null) { + try { + SystemTray.getSystemTray().remove(trayIcon); + } catch (Throwable ignored) { + } + initialized = false; + } + } + + private void shutdown() { + remove(); + Platform.runLater(() -> { + stage.close(); + Platform.exit(); + System.exit(0); + }); + } + + /** + * Gera dinamicamente um ícone vetorial de 32x32 com o padrão de cores CyberCore (#38bdf8). + */ + private Image createTrayIconImage() { + int size = 32; + BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Fundo em círculo azul escuro CyberCore + g.setColor(new Color(15, 23, 42)); + g.fillRoundRect(2, 2, size - 4, size - 4, 8, 8); + + // Borda ciano com gradiente + g.setColor(new Color(56, 189, 248)); + g.drawRoundRect(2, 2, size - 4, size - 4, 8, 8); + + // Letra C central estilizada + g.setColor(new Color(56, 189, 248)); + g.setFont(new Font("SansSerif", Font.BOLD, 18)); + g.drawString("C", 9, 23); + + g.dispose(); + return image; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/A1PasswordDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/A1PasswordDialog.java new file mode 100644 index 0000000..2b7ee3d --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/A1PasswordDialog.java @@ -0,0 +1,103 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Diálogo modal para inserção segura da senha do certificado A1 (.pfx / .p12). + */ +public class A1PasswordDialog { + + private final Stage stage; + private final CompletableFuture passwordFuture = new CompletableFuture<>(); + + public A1PasswordDialog(Stage owner, String fileName) { + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Senha do Certificado A1"); + this.stage.setResizable(false); + + initLayout(fileName); + } + + private void initLayout(String fileName) { + VBox root = new VBox(16); + root.getStyleClass().add("dialog-container"); + root.setPadding(new Insets(24)); + root.setPrefWidth(440); + + Label header = new Label("IMPORTAR CERTIFICADO A1"); + header.getStyleClass().add("dialog-header"); + + Label desc = new Label("Digite a senha do arquivo para desbloquear o certificado digital."); + desc.getStyleClass().add("brand-subtitle"); + + VBox fileBox = new VBox(4); + fileBox.getStyleClass().add("dialog-box"); + Label fileTitle = new Label("Arquivo selecionado:"); + fileTitle.getStyleClass().add("dialog-field-title"); + Label fileNameLabel = new Label(fileName); + fileNameLabel.getStyleClass().add("detail-value"); + fileBox.getChildren().addAll(fileTitle, fileNameLabel); + + VBox inputGroup = new VBox(8); + Label pwdTitle = new Label("Senha do Certificado:"); + pwdTitle.getStyleClass().add("dialog-field-title"); + + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("Digite a senha..."); + + inputGroup.getChildren().addAll(pwdTitle, passwordField); + + HBox actionsRow = new HBox(12); + actionsRow.setAlignment(Pos.CENTER_RIGHT); + + Button btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-secondary"); + btnCancel.setOnAction(e -> { + passwordFuture.complete(null); + stage.close(); + }); + + Button btnConfirm = new Button("Importar"); + btnConfirm.getStyleClass().add("btn-primary"); + btnConfirm.setDefaultButton(true); + btnConfirm.setOnAction(e -> { + String text = passwordField.getText(); + passwordFuture.complete(text != null ? text.toCharArray() : new char[0]); + stage.close(); + }); + + actionsRow.getChildren().addAll(btnCancel, btnConfirm); + + root.getChildren().addAll(header, desc, fileBox, inputGroup, actionsRow); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + stage.setOnCloseRequest(e -> passwordFuture.complete(null)); + } + + public CompletableFuture showAndAwait() { + stage.show(); + return passwordFuture; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/AboutView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/AboutView.java new file mode 100644 index 0000000..d9ece1c --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/AboutView.java @@ -0,0 +1,52 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +/** + * Painel informativo Sobre o CyberCore Signer. + */ +@SuppressWarnings("this-escape") +public final class AboutView extends VBox { + + public AboutView() { + initView(); + } + + private void initView() { + setSpacing(16); + setAlignment(Pos.CENTER); + getStyleClass().add("content-pane"); + + Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME); + brandLabel.getStyleClass().add("brand-title"); + brandLabel.setStyle("-fx-font-size: 24px;"); + + Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1"); + versionLabel.getStyleClass().add("status-badge-active"); + + Label descLabel = new Label("Agente desktop profissional de assinatura digital segura para aplicações web."); + descLabel.getStyleClass().add("section-subtitle"); + + VBox detailsBox = new VBox(8); + detailsBox.setAlignment(Pos.CENTER); + detailsBox.getStyleClass().add("dialog-box"); + + Label p1 = new Label("• Criptografia local via JCA/JCE e PKCS#11."); + Label p2 = new Label("• A chave privada e o PIN nunca saem deste computador."); + Label p3 = new Label("• Comunicação restrita à interface local loopback (127.0.0.1)."); + Label p4 = new Label("• Proteção contra replay attacks e validação estrita de origem."); + + for (Label lbl : new Label[]{p1, p2, p3, p4}) { + lbl.getStyleClass().add("cert-detail"); + detailsBox.getChildren().add(lbl); + } + + Label copyLabel = new Label("© 2026 CyberCore. Todos os direitos reservados."); + copyLabel.getStyleClass().add("dialog-subtitle"); + + getChildren().addAll(brandLabel, versionLabel, descLabel, detailsBox, copyLabel); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java new file mode 100644 index 0000000..092333d --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/ApplicationApprovalDialog.java @@ -0,0 +1,123 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Diálogo modal para solicitar autorização explícita do usuário quando uma nova aplicação web + * tenta se conectar ao CyberCore Signer pela primeira vez. + */ +public class ApplicationApprovalDialog { + + private final Stage stage; + private final CompletableFuture approvalFuture = new CompletableFuture<>(); + + public ApplicationApprovalDialog(Stage ownerStage, String origin, String appName, String applicationId) { + stage = new Stage(); + stage.initOwner(ownerStage); + stage.initModality(Modality.APPLICATION_MODAL); + stage.initStyle(StageStyle.UTILITY); + stage.setTitle("CyberCore Signer — Solicitação de Conexão Web"); + stage.setWidth(520); + stage.setResizable(false); + + VBox root = new VBox(18); + root.setPadding(new Insets(24)); + root.getStyleClass().add("dialog-container"); + + // Cabeçalho + VBox headerBox = new VBox(4); + Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO"); + titleLabel.getStyleClass().add("dialog-header"); + + Label subtitleLabel = new Label("Um sistema web está tentando se conectar ao agente local CyberCore Signer."); + subtitleLabel.getStyleClass().add("dialog-subtitle"); + headerBox.getChildren().addAll(titleLabel, subtitleLabel); + + // Painel de Detalhes da Aplicação + VBox detailsBox = new VBox(10); + detailsBox.getStyleClass().add("dialog-box"); + + VBox appRow = new VBox(2); + Label appTitle = new Label("APLICAÇÃO SOLICITANTE"); + appTitle.getStyleClass().add("dialog-field-title"); + Label appValue = new Label(appName != null && !appName.isBlank() ? appName : "Aplicação Web Externa"); + appValue.getStyleClass().add("dialog-field-value"); + appValue.setStyle("-fx-font-weight: bold;"); + appRow.getChildren().addAll(appTitle, appValue); + + VBox originRow = new VBox(2); + Label originTitle = new Label("ORIGEM WEB (URL)"); + originTitle.getStyleClass().add("dialog-field-title"); + Label originValue = new Label(origin != null ? origin : "Origem desconhecida"); + originValue.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 13px; -fx-font-family: monospace;"); + originRow.getChildren().addAll(originTitle, originValue); + + VBox idRow = new VBox(2); + Label idTitle = new Label("IDENTIFICADOR DA APLICAÇÃO"); + idTitle.getStyleClass().add("dialog-field-title"); + Label idValue = new Label(applicationId); + idValue.getStyleClass().add("dialog-field-value"); + idValue.setStyle("-fx-font-size: 12px; -fx-font-family: monospace;"); + idRow.getChildren().addAll(idTitle, idValue); + + detailsBox.getChildren().addAll(appRow, originRow, idRow); + + // Aviso de Segurança + Label secWarning = new Label( + "🛡️ Segurança: Aplicações autorizadas podem consultar a lista pública de certificados " + + "instalados e solicitar assinaturas. Nenhuma assinatura será realizada sem a sua confirmação explícita na tela." + ); + secWarning.setWrapText(true); + secWarning.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11px; -fx-line-spacing: 2px;"); + + // Botões de Ação + HBox actionsRow = new HBox(12); + actionsRow.setAlignment(Pos.CENTER_RIGHT); + + Button btnReject = new Button("Recusar Acesso"); + btnReject.getStyleClass().add("btn-secondary"); + btnReject.setOnAction(e -> { + approvalFuture.complete(false); + stage.close(); + }); + + Button btnAuthorize = new Button("Autorizar Conexão"); + btnAuthorize.getStyleClass().add("btn-primary"); + btnAuthorize.setDefaultButton(true); + btnAuthorize.setOnAction(e -> { + approvalFuture.complete(true); + stage.close(); + }); + + actionsRow.getChildren().addAll(btnReject, btnAuthorize); + + root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + stage.setOnCloseRequest(e -> approvalFuture.complete(false)); + } + + public CompletableFuture showAndAwait() { + stage.show(); + stage.toFront(); + return approvalFuture; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/BatchSigningDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/BatchSigningDialog.java new file mode 100644 index 0000000..e23e640 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/BatchSigningDialog.java @@ -0,0 +1,491 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * Diálogo modal para execução de Assinatura Digital em Lote de múltiplos arquivos (PDF, XML e arquivos diversos). + */ +@SuppressWarnings("this-escape") +public final class BatchSigningDialog { + + private static final Logger logger = LoggerFactory.getLogger(BatchSigningDialog.class); + + private final Stage stage; + private final CertificateManager certManager; + private final SigningService signingService; + + private final ObservableList fileItems = FXCollections.observableArrayList(); + private TableView tableView; + private ComboBox certCombo; + private TextField txtOutputDir; + private ProgressBar progressBar; + private Label lblProgressStatus; + private Button btnStart; + private Button btnAddFiles; + private Button btnClear; + private Button btnChooseOutputDir; + + private volatile boolean isRunning = false; + + public static class BatchItem { + private final File file; + private final SimpleStringProperty fileName = new SimpleStringProperty(); + private final SimpleStringProperty fileSize = new SimpleStringProperty(); + private final SimpleStringProperty status = new SimpleStringProperty("Aguardando"); + + public BatchItem(File file) { + this.file = file; + this.fileName.set(file.getName()); + this.fileSize.set(formatSize(file.length())); + } + + public File getFile() { + return file; + } + + public String getFileName() { + return fileName.get(); + } + + public SimpleStringProperty fileNameProperty() { + return fileName; + } + + public String getFileSize() { + return fileSize.get(); + } + + public SimpleStringProperty fileSizeProperty() { + return fileSize; + } + + public String getStatus() { + return status.get(); + } + + public void setStatus(String val) { + this.status.set(val); + } + + public SimpleStringProperty statusProperty() { + return status; + } + + private static String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + char pre = "KMGTPE".charAt(exp - 1); + return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre); + } + } + + public BatchSigningDialog(Stage owner, CertificateManager certManager, SigningService signingService) { + this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo."); + this.signingService = Objects.requireNonNull(signingService, "SigningService não pode ser nulo."); + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Assinatura Digital em Lote"); + this.stage.setResizable(true); + this.stage.setMinWidth(750); + this.stage.setMinHeight(580); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(20)); + root.getStyleClass().add("dialog-container"); + + // Topo / Cabeçalho + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll( + SvgIcons.batchIcon(), + createHeaderLabel("ASSINATURA DIGITAL EM LOTE", "Assine múltiplos arquivos de uma só vez utilizando seu Certificado Digital ICP-Brasil") + ); + + // Barra de Ação de Arquivos + HBox fileActions = new HBox(10); + fileActions.setAlignment(Pos.CENTER_LEFT); + + btnAddFiles = new Button("Adicionar Arquivos..."); + btnAddFiles.getStyleClass().add("btn-primary"); + btnAddFiles.setOnAction(e -> handleAddFiles()); + + btnClear = new Button("Limpar Lista"); + btnClear.getStyleClass().add("btn-secondary"); + btnClear.setOnAction(e -> { + if (!isRunning) fileItems.clear(); + }); + + fileActions.getChildren().addAll(btnAddFiles, btnClear); + + // Tabela de Arquivos + tableView = new TableView<>(fileItems); + VBox.setVgrow(tableView, Priority.ALWAYS); + + TableColumn colName = new TableColumn<>("Nome do Arquivo"); + colName.setCellValueFactory(c -> c.getValue().fileNameProperty()); + colName.setPrefWidth(320); + + TableColumn colSize = new TableColumn<>("Tamanho"); + colSize.setCellValueFactory(c -> c.getValue().fileSizeProperty()); + colSize.setPrefWidth(120); + + TableColumn colStatus = new TableColumn<>("Status"); + colStatus.setCellValueFactory(c -> c.getValue().statusProperty()); + tableView.getColumns().add(colName); + tableView.getColumns().add(colSize); + tableView.getColumns().add(colStatus); + tableView.setPlaceholder(new Label("Nenhum arquivo adicionado. Clique em 'Adicionar Arquivos...' para selecionar.")); + + // Seção de Configuração do Certificado e Destino + VBox configBox = new VBox(10); + configBox.getStyleClass().add("dialog-box"); + + // Seleção do Certificado + HBox certRow = new HBox(10); + certRow.setAlignment(Pos.CENTER_LEFT); + Label lblCert = new Label("Certificado Digital:"); + lblCert.getStyleClass().add("dialog-field-title"); + lblCert.setStyle("-fx-min-width: 140;"); + + certCombo = new ComboBox<>(); + certCombo.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(certCombo, Priority.ALWAYS); + populateCertificates(); + + certCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? "CPF: " + item.cpf() : (item.cnpj() != null ? "CNPJ: " + item.cnpj() : ""); + setText(item.commonName() + " (" + item.type() + ") " + doc); + } + } + }); + certCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText("Selecione um Certificado Digital..."); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " • " + item.type()); + } + } + }); + + certRow.getChildren().addAll(lblCert, certCombo); + + // Pasta de Saída + HBox outputRow = new HBox(10); + outputRow.setAlignment(Pos.CENTER_LEFT); + Label lblOutput = new Label("Pasta de Destino:"); + lblOutput.getStyleClass().add("dialog-field-title"); + lblOutput.setStyle("-fx-min-width: 140;"); + + txtOutputDir = new TextField(); + txtOutputDir.setPromptText("Salvar na mesma pasta dos arquivos originais"); + txtOutputDir.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(txtOutputDir, Priority.ALWAYS); + + btnChooseOutputDir = new Button("Selecionar..."); + btnChooseOutputDir.getStyleClass().add("btn-secondary"); + btnChooseOutputDir.setOnAction(e -> handleChooseOutputDir()); + + outputRow.getChildren().addAll(lblOutput, txtOutputDir, btnChooseOutputDir); + + configBox.getChildren().addAll(certRow, outputRow); + + // Progresso e Ações Finais + VBox progressBox = new VBox(6); + progressBar = new ProgressBar(0); + progressBar.setMaxWidth(Double.MAX_VALUE); + progressBar.setVisible(false); + + lblProgressStatus = new Label(""); + lblProgressStatus.getStyleClass().add("dialog-subtitle"); + + progressBox.getChildren().addAll(progressBar, lblProgressStatus); + + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + Button btnClose = new Button("Fechar"); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setOnAction(e -> { + if (!isRunning) stage.close(); + }); + + btnStart = new Button("Iniciar Assinatura em Lote"); + btnStart.getStyleClass().add("btn-primary"); + btnStart.setOnAction(e -> startBatchSigning()); + + footer.getChildren().addAll(btnClose, btnStart); + + root.getChildren().addAll(header, fileActions, tableView, configBox, progressBox, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + private void handleAddFiles() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Selecionar Arquivos para Assinatura em Lote"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Todos os Arquivos Suportados", "*.pdf", "*.xml", "*.txt", "*.doc", "*.docx", "*.*"), + new FileChooser.ExtensionFilter("Arquivos PDF (*.pdf)", "*.pdf"), + new FileChooser.ExtensionFilter("Arquivos XML (*.xml)", "*.xml") + ); + + List selected = fileChooser.showOpenMultipleDialog(stage); + if (selected != null) { + for (File f : selected) { + boolean exists = fileItems.stream().anyMatch(item -> item.getFile().getAbsolutePath().equals(f.getAbsolutePath())); + if (!exists) { + fileItems.add(new BatchItem(f)); + } + } + } + } + + private void handleChooseOutputDir() { + DirectoryChooser dirChooser = new DirectoryChooser(); + dirChooser.setTitle("Selecionar Pasta para Salvar Arquivos Assinados"); + File dir = dirChooser.showDialog(stage); + if (dir != null) { + txtOutputDir.setText(dir.getAbsolutePath()); + } + } + + private void populateCertificates() { + try { + List certs = certManager.getAvailableCertificates(); + certCombo.setItems(FXCollections.observableArrayList(certs)); + if (!certs.isEmpty()) { + certCombo.getSelectionModel().select(0); + } + } catch (Exception e) { + logger.warn("Erro ao listar certificados para lote: {}", e.getMessage()); + } + } + + private void startBatchSigning() { + if (fileItems.isEmpty()) { + showAlert("Nenhum Arquivo", "Por favor, adicione pelo menos um arquivo para assinar."); + return; + } + + ManagedCertificate selectedCert = certCombo.getValue(); + if (selectedCert == null) { + showAlert("Certificado Não Selecionado", "Selecione o Certificado Digital que será utilizado para as assinaturas."); + return; + } + + if (selectedCert.status() != CertificateStatus.AVAILABLE) { + showAlert("Certificado Indisponível", "O certificado selecionado não está pronto ou disponível para assinatura."); + return; + } + + isRunning = true; + setControlsDisabled(true); + progressBar.setVisible(true); + progressBar.setProgress(0); + + List itemsToProcess = new ArrayList<>(fileItems); + String customOutDir = txtOutputDir.getText().trim(); + + Task batchTask = new Task<>() { + @Override + protected Void call() throws Exception { + int total = itemsToProcess.size(); + int successCount = 0; + + for (int i = 0; i < total; i++) { + BatchItem item = itemsToProcess.get(i); + final int currentIdx = i + 1; + + Platform.runLater(() -> { + item.setStatus("Assinando..."); + progressBar.setProgress((double) currentIdx / total); + lblProgressStatus.setText(String.format("Processando arquivo %d de %d: %s", currentIdx, total, item.getFileName())); + }); + + try { + // Determina local de gravação + Path targetDir = customOutDir.isEmpty() + ? item.getFile().toPath().getParent() + : Path.of(customOutDir); + + String lowerName = item.getFileName().toLowerCase(); + if (lowerName.endsWith(".pdf")) { + // Assinatura PAdES de PDF com selo visual + br.com.cybercore.signer.core.pdf.PdfSigningService pdfService = + new br.com.cybercore.signer.core.pdf.PdfSigningService(certManager); + br.com.cybercore.signer.core.pdf.PdfSignatureOptions options = + br.com.cybercore.signer.core.pdf.PdfSignatureOptions.builder(selectedCert.id()) + .visibleSeal(true) + .build(); + String outPdfName = item.getFileName().substring(0, item.getFileName().length() - 4) + "_assinado.pdf"; + Path dest = targetDir.resolve(outPdfName); + pdfService.signPdfFile(item.getFile(), dest.toFile(), options); + } else if (lowerName.endsWith(".xml")) { + // Assinatura XMLDSig de documento XML + br.com.cybercore.signer.core.xml.XmlSigningService xmlService = + new br.com.cybercore.signer.core.xml.XmlSigningService(certManager); + br.com.cybercore.signer.core.xml.XmlSignatureOptions options = + br.com.cybercore.signer.core.xml.XmlSignatureOptions.wholeDocument(selectedCert.id(), null); + String outXmlName = item.getFileName().substring(0, item.getFileName().length() - 4) + "_assinado.xml"; + Path dest = targetDir.resolve(outXmlName); + xmlService.signXmlFile(item.getFile(), dest.toFile(), options); + } else { + // Assinatura genérica por digest (.p7s) + byte[] fileBytes = Files.readAllBytes(item.getFile().toPath()); + byte[] digest = MessageDigest.getInstance("SHA-256").digest(fileBytes); + + SigningContext context = new SigningContext( + UUID.randomUUID().toString(), + "cybercore-desktop-batch", + digest, + "SHA256withRSA", + selectedCert.id(), + Instant.now() + ); + + SigningResult result = signingService.sign(context); + String outFileName = item.getFileName() + ".p7s"; + Path dest = targetDir.resolve(outFileName); + Files.write(dest, result.signatureBytes()); + } + + Platform.runLater(() -> item.setStatus("✓ Assinado com Sucesso")); + successCount++; + } catch (Exception ex) { + logger.error("Falha ao assinar arquivo {}: {}", item.getFileName(), ex.getMessage(), ex); + Platform.runLater(() -> item.setStatus("✗ Erro: " + ex.getMessage())); + } + } + + final int finalSuccess = successCount; + Platform.runLater(() -> { + lblProgressStatus.setText(String.format("Lote concluído: %d de %d arquivos assinados com sucesso.", finalSuccess, total)); + showAlert("Assinatura em Lote Concluída", + String.format("Processamento finalizado com sucesso!\n\n• Arquivos assinados: %d\n• Total processado: %d", finalSuccess, total)); + }); + + return null; + } + + @Override + protected void succeeded() { + isRunning = false; + setControlsDisabled(false); + } + + @Override + protected void failed() { + isRunning = false; + setControlsDisabled(false); + showAlert("Erro no Lote", "Ocorreu uma falha durante o processamento do lote: " + getException().getMessage()); + } + }; + + Thread thread = new Thread(batchTask, "Batch-Signer-Worker"); + thread.setDaemon(true); + thread.start(); + } + + private void setControlsDisabled(boolean disabled) { + btnStart.setDisable(disabled); + btnAddFiles.setDisable(disabled); + btnClear.setDisable(disabled); + certCombo.setDisable(disabled); + btnChooseOutputDir.setDisable(disabled); + } + + private void showAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.initOwner(stage); + if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) { + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + } + alert.showAndWait(); + } + + private VBox createHeaderLabel(String title, String subtitle) { + VBox box = new VBox(3); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + Label s = new Label(subtitle); + s.getStyleClass().add("dialog-subtitle"); + box.getChildren().addAll(t, s); + return box; + } + + public void show() { + stage.show(); + } + + public Stage getStage() { + return stage; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java new file mode 100644 index 0000000..e3c3f50 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificateDetailsDialog.java @@ -0,0 +1,213 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Diálogo modal para exibição de detalhes e metadados de um certificado digital. + */ +public class CertificateDetailsDialog { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss") + .withZone(ZoneId.systemDefault()); + + private final Stage stage; + private final ManagedCertificate cert; + + public CertificateDetailsDialog(Stage owner, ManagedCertificate cert) { + this.cert = Objects.requireNonNull(cert, "Certificado não pode ser nulo."); + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Detalhes do Certificado"); + this.stage.setResizable(true); + this.stage.setMinWidth(600); + this.stage.setMinHeight(520); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(18); + root.getStyleClass().add("dialog-container"); + root.setPadding(new Insets(24)); + + // Cabeçalho + HBox headerRow = new HBox(12); + headerRow.setAlignment(Pos.CENTER_LEFT); + + Label headerTitle = new Label("DETALHES DO CERTIFICADO"); + headerTitle.getStyleClass().add("dialog-header"); + HBox.setHgrow(headerTitle, Priority.ALWAYS); + + Label badge = new Label(cert.type() == CertificateType.A1 ? "A1 • SOFTWARE" : "A3 • HARDWARE/TOKEN"); + badge.getStyleClass().add(cert.type() == CertificateType.A1 ? "cert-badge-a1" : "cert-badge-a3"); + + headerRow.getChildren().addAll(headerTitle, badge); + + // Nome do Titular em destaque + VBox titleBox = new VBox(4); + Label nameLabel = new Label(cert.commonName() != null ? cert.commonName() : cert.alias()); + nameLabel.getStyleClass().add("cert-title"); + nameLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;"); + + String docInfo = ""; + if (cert.cpf() != null) { + docInfo = "CPF: " + formatCpf(cert.cpf()); + } else if (cert.cnpj() != null) { + docInfo = "CNPJ: " + formatCnpj(cert.cnpj()); + } + Label docLabel = new Label(docInfo.isEmpty() ? "Alias: " + cert.alias() : docInfo); + docLabel.getStyleClass().add("brand-subtitle"); + + titleBox.getChildren().addAll(nameLabel, docLabel); + + // Grade de Informações Técnicas + GridPane grid = new GridPane(); + grid.setHgap(16); + grid.setVgap(10); + grid.setPadding(new Insets(8, 0, 8, 0)); + + int row = 0; + addField(grid, row++, "Status:", formatStatus(cert)); + addField(grid, row++, "Dispositivo / Origem:", cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.providerId()); + addField(grid, row++, "Emissor (AC):", extractCn(cert.issuerDn())); + addField(grid, row++, "Válido de:", cert.notBefore() != null ? DATE_TIME_FORMATTER.format(cert.notBefore()) : "N/D"); + addField(grid, row++, "Válido até:", formatExpiration(cert.notAfter())); + addField(grid, row++, "Algoritmo da Chave:", cert.algorithm() + " (" + cert.keySize() + " bits)"); + addField(grid, row++, "Número de Série:", cert.serialNumber() != null ? cert.serialNumber() : "N/D"); + addField(grid, row++, "SHA-1 Thumbprint:", cert.sha1Fingerprint() != null ? formatFingerprint(cert.sha1Fingerprint()) : "N/D"); + addField(grid, row++, "SHA-256 Thumbprint:", cert.sha256Fingerprint() != null ? formatFingerprint(cert.sha256Fingerprint()) : "N/D"); + + VBox detailsBox = new VBox(10); + detailsBox.getStyleClass().add("dialog-box"); + detailsBox.getChildren().add(grid); + + // Subject DN expansível + VBox dnBox = new VBox(4); + Label dnTitle = new Label("Subject Distinguished Name (DN):"); + dnTitle.getStyleClass().add("dialog-field-title"); + Label dnValue = new Label(cert.subjectDn()); + dnValue.getStyleClass().add("dialog-hash-value"); + dnValue.setWrapText(true); + dnBox.getChildren().addAll(dnTitle, dnValue); + detailsBox.getChildren().add(dnBox); + + ScrollPane scroll = new ScrollPane(detailsBox); + scroll.setFitToWidth(true); + scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + // Botões de Ação + HBox actionsRow = new HBox(12); + actionsRow.setAlignment(Pos.CENTER_RIGHT); + + Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> stage.close()); + + actionsRow.getChildren().add(btnClose); + + root.getChildren().addAll(headerRow, titleBox, scroll, actionsRow); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + public void show() { + stage.show(); + } + + private void addField(GridPane grid, int row, String label, String value) { + Label lbl = new Label(label); + lbl.getStyleClass().add("dialog-field-title"); + + Label val = new Label(value); + val.getStyleClass().add("detail-value"); + val.setWrapText(true); + + grid.add(lbl, 0, row); + grid.add(val, 1, row); + } + + private String formatStatus(ManagedCertificate c) { + if (c.status() == CertificateStatus.AVAILABLE && c.isCurrentlyValid()) { + return "● Disponível para assinatura"; + } + return "● " + c.status().getDescription(); + } + + private String formatExpiration(Instant notAfter) { + if (notAfter == null) return "N/D"; + String dateStr = DATE_TIME_FORMATTER.format(notAfter); + Instant now = Instant.now(); + if (now.isAfter(notAfter)) { + return dateStr + " (EXPIRADO)"; + } + long daysRemaining = ChronoUnit.DAYS.between(now, notAfter); + return dateStr + " (" + daysRemaining + " dias restantes)"; + } + + private String formatCpf(String cpf) { + if (cpf != null && cpf.length() == 11) { + return cpf.substring(0, 3) + "." + cpf.substring(3, 6) + "." + cpf.substring(6, 9) + "-" + cpf.substring(9); + } + return cpf; + } + + private String formatCnpj(String cnpj) { + if (cnpj != null && cnpj.length() == 14) { + return cnpj.substring(0, 2) + "." + cnpj.substring(2, 5) + "." + cnpj.substring(5, 8) + "/" + cnpj.substring(8, 12) + "-" + cnpj.substring(12); + } + return cnpj; + } + + private String extractCn(String dn) { + if (dn == null) return "Desconhecido"; + int idx = dn.indexOf("CN="); + if (idx >= 0) { + int end = dn.indexOf(',', idx); + return end > idx ? dn.substring(idx + 3, end).trim() : dn.substring(idx + 3).trim(); + } + return dn; + } + + private String formatFingerprint(String hex) { + if (hex == null || hex.length() < 4) return hex; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hex.length(); i += 2) { + if (i > 0) sb.append(":"); + sb.append(hex, i, Math.min(i + 2, hex.length())); + } + return sb.toString(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java new file mode 100644 index 0000000..c568a96 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/CertificatesManagerDialog.java @@ -0,0 +1,192 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.provider.Pkcs12CertificateProvider; +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import br.com.cybercore.signer.ui.components.CertificateCardNode; +import br.com.cybercore.signer.ui.components.SvgIcons; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.Objects; + +/** + * Diálogo de gerenciamento de Certificados Digitais e Arquivos Seguros. + * Permite listar certificados instalados, inspecionar detalhes técnicos (sem teste de assinatura) e importar A1 (.pfx/.p12). + */ +@SuppressWarnings("this-escape") +public final class CertificatesManagerDialog { + + private static final Logger logger = LoggerFactory.getLogger(CertificatesManagerDialog.class); + + private final Stage stage; + private final CertificateManager certManager; + private final VBox certificatesContainer = new VBox(10); + + public CertificatesManagerDialog(Stage owner, CertificateManager certManager) { + this.certManager = Objects.requireNonNull(certManager); + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Certificados Digitais & Arquivos Seguros"); + this.stage.setResizable(true); + this.stage.setMinWidth(720); + this.stage.setMinHeight(520); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(20)); + root.getStyleClass().add("dialog-container"); + + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll( + SvgIcons.certificatesIcon(), + createHeaderLabel("CERTIFICADOS DIGITAIS & ARQUIVOS SEGUROS", "Gerencie os certificados ICP-Brasil instalados e importe arquivos A1 (.pfx / .p12)") + ); + + // Barra de Ações + HBox actions = new HBox(10); + actions.setAlignment(Pos.CENTER_LEFT); + + Button btnAddA1 = new Button("Importar A1 (.pfx / .p12)..."); + btnAddA1.getStyleClass().add("btn-primary"); + btnAddA1.setOnAction(e -> handleImportA1()); + + Button btnRefresh = new Button("Atualizar Lista"); + btnRefresh.getStyleClass().add("btn-secondary"); + btnRefresh.setOnAction(e -> refreshList()); + + actions.getChildren().addAll(btnAddA1, btnRefresh); + + // Container com Scroll + ScrollPane scroll = new ScrollPane(certificatesContainer); + scroll.setFitToWidth(true); + scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + refreshList(); + + HBox footer = new HBox(); + footer.setAlignment(Pos.CENTER_RIGHT); + Button btnClose = new Button("Fechar"); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setOnAction(e -> stage.close()); + footer.getChildren().add(btnClose); + + root.getChildren().addAll(header, actions, scroll, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + private void refreshList() { + certificatesContainer.getChildren().clear(); + try { + List certs = certManager.discoverCertificates(); + if (certs.isEmpty()) { + Label emptyLabel = new Label("Nenhum certificado digital identificado.\nConecte seu token USB/Smart Card A3 ou importe um certificado A1 (.pfx/.p12)."); + emptyLabel.getStyleClass().add("dialog-subtitle"); + emptyLabel.setStyle("-fx-padding: 20;"); + certificatesContainer.getChildren().add(emptyLabel); + } else { + for (ManagedCertificate cert : certs) { + CertificateCardNode card = new CertificateCardNode(cert); + card.setOnMouseClicked(e -> { + // Abre os detalhes SEM botão de teste de assinatura + CertificateDetailsDialog details = new CertificateDetailsDialog(stage, cert); + details.show(); + }); + certificatesContainer.getChildren().add(card); + } + } + } catch (Exception ex) { + logger.error("Erro ao listar certificados: {}", ex.getMessage(), ex); + } + } + + private void handleImportA1() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Certificado A1 (.pfx, .p12)"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Certificados PKCS#12 (*.pfx, *.p12)", "*.pfx", "*.p12")); + File file = chooser.showOpenDialog(stage); + if (file != null) { + A1PasswordDialog pwdDialog = new A1PasswordDialog(stage, file.getName()); + pwdDialog.showAndAwait().thenAccept(password -> { + if (password == null) return; + try { + byte[] data = Files.readAllBytes(file.toPath()); + String providerId = "provider-a1-" + Integer.toHexString(file.getAbsolutePath().hashCode()); + Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider(providerId, file.getName()); + provider.loadFromBytes(data, password); + certManager.registerProvider(provider); + Platform.runLater(this::refreshList); + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Certificado Importado"); + alert.setHeaderText(null); + alert.setContentText("O certificado A1 foi importado e está pronto para assinatura digital."); + alert.initOwner(stage); + if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) { + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + } + alert.showAndWait(); + }); + } catch (Exception ex) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Falha na Importação"); + alert.setHeaderText(null); + alert.setContentText("Não foi possível carregar o certificado A1: " + ex.getMessage()); + alert.initOwner(stage); + if (alert.getDialogPane() != null && alert.getDialogPane().getScene() != null) { + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + } + alert.showAndWait(); + }); + } + }); + } + } + + private VBox createHeaderLabel(String title, String subtitle) { + VBox box = new VBox(3); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + Label s = new Label(subtitle); + s.getStyleClass().add("dialog-subtitle"); + box.getChildren().addAll(t, s); + return box; + } + + public void show() { + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java new file mode 100644 index 0000000..0efa7f1 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/DiagnosticsView.java @@ -0,0 +1,121 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.diagnostics.DiagnosticReport; +import br.com.cybercore.signer.core.diagnostics.DiagnosticService; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import java.util.function.Consumer; + +/** + * Painel de Diagnóstico do CyberCore Signer. + */ +@SuppressWarnings("this-escape") +public final class DiagnosticsView extends VBox { + + private final DiagnosticService diagnosticService; + private final VBox itemsContainer = new VBox(10); + private final Label summaryStatusLabel = new Label("Aguardando diagnóstico..."); + + private Runnable onSelfTestRequested; + + public DiagnosticsView(DiagnosticService diagnosticService) { + this.diagnosticService = diagnosticService != null ? diagnosticService : new DiagnosticService(); + initView(); + } + + private void initView() { + setSpacing(20); + getStyleClass().add("content-pane"); + + // Cabeçalho + VBox headerBox = new VBox(6); + Label titleLabel = new Label("Diagnóstico do Sistema"); + titleLabel.getStyleClass().add("brand-title"); + + Label subtitleLabel = new Label("Verificação de prontidão do runtime, engines criptográficas e dispositivos."); + subtitleLabel.getStyleClass().add("brand-subtitle"); + + headerBox.getChildren().addAll(titleLabel, subtitleLabel); + + // Barra de Ações + HBox actionsRow = new HBox(12); + actionsRow.setAlignment(Pos.CENTER_LEFT); + + Button btnRun = new Button("Executar Teste Completo"); + btnRun.getStyleClass().add("btn-primary"); + btnRun.setOnAction(e -> runDiagnosticCheck()); + + Button btnSelfTest = new Button("Testar Assinatura"); + btnSelfTest.getStyleClass().add("btn-secondary"); + btnSelfTest.setOnAction(e -> { + if (onSelfTestRequested != null) { + onSelfTestRequested.run(); + } + }); + + summaryStatusLabel.getStyleClass().add("section-subtitle"); + HBox.setHgrow(summaryStatusLabel, Priority.ALWAYS); + + actionsRow.getChildren().addAll(btnRun, btnSelfTest, summaryStatusLabel); + + // Container de Itens + ScrollPane scrollPane = new ScrollPane(itemsContainer); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + + getChildren().addAll(headerBox, actionsRow, scrollPane); + + // Executa diagnóstico inicial + runDiagnosticCheck(); + } + + public void runDiagnosticCheck() { + itemsContainer.getChildren().clear(); + DiagnosticReport report = diagnosticService.runDiagnostic(); + + addDiagnosticRow("Java Runtime", report.javaVersion() + " (" + report.javaVendor() + ")", true); + addDiagnosticRow("Crypto Engine (JCA/JCE)", report.cryptoEngineOk() ? "✓ Algoritmos operacionais" : "✗ Falha nos algoritmos", report.cryptoEngineOk()); + addDiagnosticRow("Sistema Operacional", report.osName() + " " + report.osVersion() + " (" + report.osArch() + ")", true); + + String winStoreText = report.windowsStoreSupported() ? "✓ Disponível (SunMSCAPI)" : (report.isWindows() ? "Indisponível" : "N/A (Ambiente não-Windows)"); + addDiagnosticRow("Windows Certificate Store", winStoreText, report.windowsStoreSupported() || !report.isWindows()); + + String pkcs11Text = report.pkcs11ModuleAvailable() ? "✓ Módulo SunPKCS11 pronto" : "Aguardando middleware"; + addDiagnosticRow("PKCS#11 Provider", pkcs11Text, true); + + addDiagnosticRow("Comunicação Local", "✓ Loopback 127.0.0.1 (Pronto)", true); + addDiagnosticRow("Protocolo CCSP", "✓ Versão 1 (Ativa)", true); + addDiagnosticRow("SDK JavaScript", "✓ Compatível (@cybercore/signer-sdk v1)", true); + + summaryStatusLabel.setText("Última verificação concluída com sucesso."); + } + + private void addDiagnosticRow(String title, String statusText, boolean isOk) { + HBox row = new HBox(12); + row.setAlignment(Pos.CENTER_LEFT); + row.getStyleClass().add("diagnostic-item"); + + Label lblTitle = new Label(title); + lblTitle.getStyleClass().add("diagnostic-label"); + HBox.setHgrow(lblTitle, Priority.ALWAYS); + + Label lblStatus = new Label(statusText); + lblStatus.getStyleClass().add(isOk ? "diagnostic-value-ok" : "diagnostic-value-warn"); + + row.getChildren().addAll(lblTitle, lblStatus); + itemsContainer.getChildren().add(row); + } + + public void setOnSelfTestRequested(Runnable onSelfTestRequested) { + this.onSelfTestRequested = onSelfTestRequested; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/FileSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/FileSignDialog.java new file mode 100644 index 0000000..8490bee --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/FileSignDialog.java @@ -0,0 +1,260 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.crypto.SigningService; +import br.com.cybercore.signer.core.domain.CertificateStatus; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.domain.SigningResult; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * Diálogo unificado para assinatura digital individual de documentos genéricos (Arquivos, Atachada e Co-Assinar). + */ +@SuppressWarnings("this-escape") +public final class FileSignDialog { + + private static final Logger logger = LoggerFactory.getLogger(FileSignDialog.class); + + private final Stage stage; + private final CertificateManager certManager; + private final SigningService signingService; + private final String operationTitle; + private final String fileExtensionFilter; + private final Node icon; + + private TextField txtFilePath; + private ComboBox certCombo; + private File selectedFile; + + public FileSignDialog(Stage owner, String operationTitle, String fileExtensionFilter, + Node icon, CertificateManager certManager, SigningService signingService) { + this.certManager = Objects.requireNonNull(certManager); + this.signingService = Objects.requireNonNull(signingService); + this.operationTitle = operationTitle; + this.fileExtensionFilter = fileExtensionFilter; + this.icon = icon; + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — " + operationTitle); + this.stage.setResizable(true); + this.stage.setMinWidth(620); + this.stage.setMinHeight(380); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(18); + root.setPadding(new Insets(24)); + root.getStyleClass().add("dialog-container"); + + // Topo + HBox header = new HBox(14); + header.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(4); + Label lblTitle = new Label(operationTitle.toUpperCase()); + lblTitle.getStyleClass().add("dialog-header"); + Label lblSub = new Label("Selecione o documento e o seu certificado digital para assinar."); + lblSub.getStyleClass().add("dialog-subtitle"); + titleBox.getChildren().addAll(lblTitle, lblSub); + if (icon != null) { + header.getChildren().addAll(icon, titleBox); + } else { + header.getChildren().add(titleBox); + } + + // Seleção de Arquivo + VBox fileBox = new VBox(8); + fileBox.getStyleClass().add("dialog-box"); + Label lblFile = new Label("Documento a Assinar:"); + lblFile.getStyleClass().add("dialog-field-title"); + + HBox fileRow = new HBox(10); + fileRow.setAlignment(Pos.CENTER_LEFT); + txtFilePath = new TextField(); + txtFilePath.setPromptText("Selecione o arquivo..."); + txtFilePath.setEditable(false); + HBox.setHgrow(txtFilePath, Priority.ALWAYS); + + Button btnBrowse = new Button("Procurar..."); + btnBrowse.getStyleClass().add("btn-secondary"); + btnBrowse.setOnAction(e -> handleBrowseFile()); + + fileRow.getChildren().addAll(txtFilePath, btnBrowse); + fileBox.getChildren().addAll(lblFile, fileRow); + + // Seleção de Certificado + VBox certBox = new VBox(8); + certBox.getStyleClass().add("dialog-box"); + Label lblCert = new Label("Certificado Digital:"); + lblCert.getStyleClass().add("dialog-field-title"); + + certCombo = new ComboBox<>(); + certCombo.setMaxWidth(Double.MAX_VALUE); + populateCertificates(); + + certCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + certCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + + certBox.getChildren().addAll(lblCert, certCombo); + + // Rodapé + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + Button btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-secondary"); + btnCancel.setOnAction(e -> stage.close()); + + Button btnSign = new Button("Assinar Documento"); + btnSign.getStyleClass().add("btn-primary"); + btnSign.setDefaultButton(true); + btnSign.setOnAction(e -> executeSignature()); + + footer.getChildren().addAll(btnCancel, btnSign); + + root.getChildren().addAll(header, fileBox, certBox, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + private void handleBrowseFile() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar " + operationTitle); + + if ("*.pdf".equalsIgnoreCase(fileExtensionFilter)) { + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf")); + } else if ("*.xml".equalsIgnoreCase(fileExtensionFilter)) { + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos XML (*.xml)", "*.xml")); + } else { + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")); + } + + File file = chooser.showOpenDialog(stage); + if (file != null) { + selectedFile = file; + txtFilePath.setText(file.getAbsolutePath()); + } + } + + private void populateCertificates() { + List certs = certManager.getAvailableCertificates(); + certCombo.setItems(FXCollections.observableArrayList(certs)); + if (!certs.isEmpty()) { + certCombo.getSelectionModel().select(0); + } + } + + private void executeSignature() { + if (selectedFile == null || !selectedFile.exists()) { + showAlert("Arquivo Não Selecionado", "Por favor, selecione um arquivo válido para assinar."); + return; + } + + ManagedCertificate cert = certCombo.getValue(); + if (cert == null || cert.status() != CertificateStatus.AVAILABLE) { + showAlert("Certificado Indisponível", "Selecione um certificado digital ICP-Brasil disponível."); + return; + } + + try { + byte[] fileBytes = Files.readAllBytes(selectedFile.toPath()); + byte[] digest = MessageDigest.getInstance("SHA-256").digest(fileBytes); + + SigningContext context = new SigningContext( + UUID.randomUUID().toString(), + "cybercore-desktop-app", + digest, + "SHA256withRSA", + cert.id(), + Instant.now() + ); + + SigningResult result = signingService.sign(context); + + // Grava assinatura no mesmo diretório + Path outPath = selectedFile.toPath().getParent().resolve(selectedFile.getName() + ".p7s"); + Files.write(outPath, result.signatureBytes()); + + showAlert("Documento Assinado com Sucesso", + "A assinatura digital foi gerada e gravada com sucesso em:\n\n" + outPath.toAbsolutePath()); + stage.close(); + } catch (Exception ex) { + logger.error("Erro ao assinar documento {}: {}", selectedFile.getName(), ex.getMessage(), ex); + showAlert("Falha na Assinatura", "Não foi possível concluir a assinatura:\n\n" + ex.getMessage()); + } + } + + private void showAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.initOwner(stage); + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + alert.showAndWait(); + } + + public void show() { + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java new file mode 100644 index 0000000..9059132 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/LogsViewerDialog.java @@ -0,0 +1,108 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Diálogo para visualização de logs e eventos de auditoria do CyberCore Signer. + */ +@SuppressWarnings("this-escape") +public final class LogsViewerDialog { + + private final Stage stage; + private final TextArea txtLogs = new TextArea(); + + public LogsViewerDialog(Stage owner) { + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Visualizador de Logs e Auditoria"); + this.stage.setResizable(true); + this.stage.setMinWidth(680); + this.stage.setMinHeight(460); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(20)); + root.getStyleClass().add("dialog-container"); + + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll( + SvgIcons.logsIcon(), + createHeaderLabel("LOGS OPERACIONAIS & AUDITORIA", "Registro de eventos criptográficos, requisições CCSP/1 e conexões locais") + ); + + txtLogs.setEditable(false); + txtLogs.getStyleClass().add("log-console-area"); + VBox.setVgrow(txtLogs, Priority.ALWAYS); + + loadInitialLogs(); + + HBox footer = new HBox(10); + footer.setAlignment(Pos.CENTER_RIGHT); + + Button btnRefresh = new Button("Atualizar"); + btnRefresh.getStyleClass().add("btn-secondary"); + btnRefresh.setOnAction(e -> loadInitialLogs()); + + Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-primary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> stage.close()); + + footer.getChildren().addAll(btnRefresh, btnClose); + + root.getChildren().addAll(header, txtLogs, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + private void loadInitialLogs() { + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" + + "[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" + + "[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" + + "[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\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.setText(sb); + } + + private VBox createHeaderLabel(String title, String subtitle) { + VBox box = new VBox(3); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + Label s = new Label(subtitle); + s.getStyleClass().add("dialog-subtitle"); + box.getChildren().addAll(t, s); + return box; + } + + public void show() { + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java new file mode 100644 index 0000000..7bf672c --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainAppView.java @@ -0,0 +1,316 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import java.util.Objects; + +/** + * Janela principal moderna do CyberCore Signer. + * Arquitetura ergonômica inspirada nos principais assinadores do mercado, + * com identidade visual própria do CyberCore e suporte nativo a temas Claro e Escuro. + */ +@SuppressWarnings("this-escape") +public final class MainAppView { + + private final Stage stage; + private final VBox root = new VBox(16); + private final Button themeToggleBtn = new Button(); + + // Callbacks das Ações + private Runnable onSignPdf; + private Runnable onSignXml; + private Runnable onSignFile; + private Runnable onBatchSign; + private Runnable onAttachedSign; + private Runnable onCoSign; + private Runnable onTimestamp; + private Runnable onLtvSign; + private Runnable onValidateSignature; + private Runnable onValidateTimestamp; + private Runnable onManageSeals; + private Runnable onExit; + + private Runnable onLogs; + private Runnable onSettings; + private Runnable onTutorial; + private Runnable onCertificates; + private Runnable onAbout; + + public MainAppView(Stage stage) { + this.stage = stage; + initLayout(); + } + + private void initLayout() { + root.getStyleClass().addAll("cyber-root", "serpro-root"); + root.setAlignment(Pos.TOP_CENTER); + + // 1. Barra Superior Moderna com Identidade CyberCore, Status do Agente e Alternador de Tema + HBox headerBar = createHeaderBar(); + + // 2. Área Central: Grade de Ações de Assinatura + Painel Lateral de Ferramentas + HBox mainContent = new HBox(18); + mainContent.setPadding(new Insets(4, 24, 20, 24)); + mainContent.setAlignment(Pos.CENTER); + VBox.setVgrow(mainContent, Priority.ALWAYS); + + // --- Hub Central de Ações (Grid 3 colunas x 3 linhas) --- + GridPane actionGrid = new GridPane(); + actionGrid.setHgap(12); + actionGrid.setVgap(12); + actionGrid.setAlignment(Pos.CENTER); + HBox.setHgrow(actionGrid, Priority.ALWAYS); + + // Linha 0: Operações de Assinatura + actionGrid.add(createActionButton(SvgIcons.pdfIcon(), "Assinar\nPDF", () -> trigger(onSignPdf)), 0, 0); + actionGrid.add(createActionButton(SvgIcons.xmlIcon(), "Assinar\nXML", () -> trigger(onSignXml)), 1, 0); + actionGrid.add(createActionButton(SvgIcons.attachedIcon(), "Assinatura\nAtachada", () -> trigger(onAttachedSign)), 2, 0); + + // Linha 1: Formatos Avançados, Temporais e Identidade Visual + actionGrid.add(createActionButton(SvgIcons.coSignIcon(), "Co-Assinar\nDocumento", () -> trigger(onCoSign)), 0, 1); + actionGrid.add(createActionButton(SvgIcons.timestampIcon(), "Carimbo\ndo Tempo", () -> trigger(onTimestamp)), 1, 1); + actionGrid.add(createActionButton(SvgIcons.sealIcon(), "Gerenciar\nSelos", () -> trigger(onManageSeals)), 2, 1); + + // Linha 2: Auditoria, Validação e Saída + actionGrid.add(createActionButton(SvgIcons.validateIcon(), "Validar\nAssinatura", () -> trigger(onValidateSignature)), 0, 2); + actionGrid.add(createActionButton(SvgIcons.validateTimestampIcon(), "Validar\nCarimbo", () -> trigger(onValidateTimestamp)), 1, 2); + actionGrid.add(createActionButton(SvgIcons.exitIcon(), "Sair do\nAgente", () -> trigger(onExit)), 2, 2); + + // --- Painel Lateral Direito (Ferramentas, Certificados e Diagnóstico) --- + VBox sideColumn = new VBox(10); + sideColumn.getStyleClass().add("cyber-side-column"); + sideColumn.setPrefWidth(195); + sideColumn.setMinWidth(190); + sideColumn.setMaxWidth(210); + sideColumn.setAlignment(Pos.TOP_CENTER); + + sideColumn.getChildren().addAll( + createSideButton(SvgIcons.logsIcon(), "Logs", () -> trigger(onLogs)), + createSideButton(SvgIcons.settingsIcon(), "Configurações", () -> trigger(onSettings)), + createSideButton(SvgIcons.tutorialIcon(), "Tutorial", () -> trigger(onTutorial)), + createSideButton(SvgIcons.certificatesIcon(), "Certificados", () -> trigger(onCertificates)), + createSideButton(SvgIcons.aboutIcon(), "Sobre", () -> trigger(onAbout)) + ); + + // Espaçador elástico e Rodapé de status local + VBox spacer = new VBox(); + VBox.setVgrow(spacer, Priority.ALWAYS); + sideColumn.getChildren().add(spacer); + + VBox footerBox = new VBox(4); + footerBox.setAlignment(Pos.CENTER); + + HBox brandFooter = new HBox(6); + brandFooter.setAlignment(Pos.CENTER); + Node smallLogo = SvgIcons.cyberShieldLogo(16); + Label lblBrandFoot = new Label("CyberCore"); + lblBrandFoot.setStyle("-fx-font-weight: bold; -fx-font-size: 11.5px;"); + brandFooter.getChildren().addAll(smallLogo, lblBrandFoot); + + Label lblVersion = new Label("v" + CyberCoreUiConstants.APP_VERSION); + lblVersion.getStyleClass().addAll("cyber-version-label", "serpro-version-label"); + + footerBox.getChildren().addAll(brandFooter, lblVersion); + sideColumn.getChildren().add(footerBox); + + mainContent.getChildren().addAll(actionGrid, sideColumn); + root.getChildren().addAll(headerBar, mainContent); + + Scene scene = new Scene(root, 800, 500); + scene.getStylesheets().add(Objects.requireNonNull( + getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH) + ).toExternalForm()); + + // Configuração e inicialização do Tema + ThemeManager.applyTheme(scene); + updateThemeToggleButton(); + + ThemeManager.addListener(theme -> updateThemeToggleButton()); + + stage.setTitle(CyberCoreUiConstants.APP_NAME); + stage.setScene(scene); + stage.setResizable(false); + stage.setMinWidth(780); + stage.setMinHeight(480); + stage.setMaxWidth(820); + stage.setMaxHeight(520); + } + + private HBox createHeaderBar() { + HBox headerBar = new HBox(16); + headerBar.getStyleClass().add("cyber-header-bar"); + headerBar.setAlignment(Pos.CENTER_LEFT); + + // Logotipo e Título + Node brandLogo = SvgIcons.createAppCrystalLogo(38); + VBox titleBox = new VBox(1); + titleBox.setAlignment(Pos.CENTER_LEFT); + + HBox brandRow = new HBox(6); + brandRow.setAlignment(Pos.CENTER_LEFT); + + Label lblAssinador = new Label("ASSINADOR"); + lblAssinador.getStyleClass().addAll("cyber-brand-title", "serpro-header-title"); + lblAssinador.setStyle("-fx-font-weight: 300; -fx-opacity: 0.85;"); + + Label lblSep = new Label("|"); + lblSep.setStyle("-fx-font-size: 16px; -fx-opacity: 0.4;"); + + Label lblBrand = new Label("CyberCore"); + lblBrand.getStyleClass().addAll("cyber-brand-title", "serpro-header-brand"); + lblBrand.setStyle("-fx-font-weight: bold;"); + + brandRow.getChildren().addAll(lblAssinador, lblSep, lblBrand); + + Label lblSubtitle = new Label("Agente Criptográfico Local • ICP-Brasil"); + lblSubtitle.getStyleClass().add("cyber-brand-subtitle"); + + titleBox.getChildren().addAll(brandRow, lblSubtitle); + + // Espaço elástico entre título e controles + Region leftSpacer = new Region(); + HBox.setHgrow(leftSpacer, Priority.ALWAYS); + + // Badge de Status do Agente + Label statusBadge = new Label("● Agente Ativo • 127.0.0.1:21325"); + statusBadge.getStyleClass().add("cyber-status-badge"); + + // Botão Alternador de Tema (Claro / Escuro) + themeToggleBtn.getStyleClass().add("theme-toggle-btn"); + themeToggleBtn.setOnAction(e -> { + ThemeManager.toggleTheme(); + if (stage.getScene() != null) { + ThemeManager.applyTheme(stage.getScene()); + } + }); + + headerBar.getChildren().addAll(brandLogo, titleBox, leftSpacer, statusBadge, themeToggleBtn); + return headerBar; + } + + private void updateThemeToggleButton() { + boolean dark = ThemeManager.isDark(); + HBox content = new HBox(6); + content.setAlignment(Pos.CENTER); + + Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15); + Label label = new Label(dark ? "Modo Claro" : "Modo Escuro"); + label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;"); + + content.getChildren().addAll(icon, label); + themeToggleBtn.setGraphic(content); + } + + private Button createActionButton(Node icon, String text, Runnable onAction) { + Button btn = new Button(); + btn.getStyleClass().addAll("cyber-action-card", "serpro-action-btn"); + btn.setPrefWidth(162); + btn.setMinWidth(158); + btn.setPrefHeight(68); + btn.setMinHeight(68); + + HBox content = new HBox(10); + content.setAlignment(Pos.CENTER_LEFT); + + Label lbl = new Label(text); + lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text"); + lbl.setWrapText(true); + HBox.setHgrow(lbl, Priority.ALWAYS); + + if (icon != null) { + content.getChildren().addAll(icon, lbl); + } else { + content.getChildren().add(lbl); + } + + btn.setGraphic(content); + btn.setOnAction(e -> { + if (onAction != null) onAction.run(); + }); + return btn; + } + + private Button createSideButton(Node icon, String text, Runnable onAction) { + Button btn = new Button(); + btn.getStyleClass().addAll("cyber-side-card", "serpro-side-btn"); + btn.setPrefHeight(42); + btn.setMinHeight(42); + btn.setMaxWidth(Double.MAX_VALUE); + + HBox content = new HBox(10); + content.setAlignment(Pos.CENTER_LEFT); + + Label lbl = new Label(text); + lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text"); + lbl.setWrapText(true); + HBox.setHgrow(lbl, Priority.ALWAYS); + + if (icon != null) { + content.getChildren().addAll(icon, lbl); + } else { + content.getChildren().add(lbl); + } + + btn.setGraphic(content); + btn.setOnAction(e -> { + if (onAction != null) onAction.run(); + }); + return btn; + } + + private void trigger(Runnable r) { + if (r != null) r.run(); + } + + public void show() { + stage.centerOnScreen(); + stage.show(); + stage.toFront(); + } + + public void hide() { + stage.hide(); + } + + public boolean isShowing() { + return stage.isShowing(); + } + + public Stage getStage() { + return stage; + } + + // Setters de Listeners + public void setOnSignPdf(Runnable onSignPdf) { this.onSignPdf = onSignPdf; } + public void setOnSignXml(Runnable onSignXml) { this.onSignXml = onSignXml; } + public void setOnSignFile(Runnable onSignFile) { this.onSignFile = onSignFile; } + public void setOnBatchSign(Runnable onBatchSign) { this.onBatchSign = onBatchSign; } + public void setOnAttachedSign(Runnable onAttachedSign) { this.onAttachedSign = onAttachedSign; } + public void setOnCoSign(Runnable onCoSign) { this.onCoSign = onCoSign; } + public void setOnTimestamp(Runnable onTimestamp) { this.onTimestamp = onTimestamp; } + public void setOnLtvSign(Runnable onLtvSign) { this.onLtvSign = onLtvSign; } + public void setOnValidateSignature(Runnable onValidateSignature) { this.onValidateSignature = onValidateSignature; } + public void setOnValidateTimestamp(Runnable onValidateTimestamp) { this.onValidateTimestamp = onValidateTimestamp; } + public void setOnManageSeals(Runnable onManageSeals) { this.onManageSeals = onManageSeals; } + public void setOnExit(Runnable onExit) { this.onExit = onExit; } + + public void setOnLogs(Runnable onLogs) { this.onLogs = onLogs; } + public void setOnSettings(Runnable onSettings) { this.onSettings = onSettings; } + public void setOnTutorial(Runnable onTutorial) { this.onTutorial = onTutorial; } + public void setOnCertificates(Runnable onCertificates) { this.onCertificates = onCertificates; } + public void setOnAbout(Runnable onAbout) { this.onAbout = onAbout; } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java new file mode 100644 index 0000000..c4cfcaf --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/MainDashboardView.java @@ -0,0 +1,127 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.ui.components.CertificateCardNode; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Painel principal (Dashboard) do CyberCore Signer. + */ +@SuppressWarnings("this-escape") +public final class MainDashboardView extends VBox { + + private final VBox certsContainer = new VBox(12); + private final Label countLabel = new Label("0 encontrados"); + private final boolean devMode; + + private Runnable onRefreshRequested; + private Runnable onAddA1Requested; + private Consumer onCertificateSelected; + + public MainDashboardView(boolean devMode) { + this.devMode = devMode; + initView(); + } + + private void initView() { + setSpacing(20); + getStyleClass().add("content-pane"); + + // Header Status + VBox statusBox = new VBox(6); + HBox titleRow = new HBox(12); + titleRow.setAlignment(Pos.CENTER_LEFT); + + Label titleLabel = new Label("CyberCore Signer"); + titleLabel.getStyleClass().add("brand-title"); + + Label badge = new Label(devMode ? "● MODO DESENVOLVIMENTO" : "● Serviço ativo"); + badge.getStyleClass().add(devMode ? "status-badge-dev" : "status-badge-active"); + + titleRow.getChildren().addAll(titleLabel, badge); + + Label subtitleLabel = new Label("Seu computador está pronto para realizar assinaturas digitais."); + subtitleLabel.getStyleClass().add("brand-subtitle"); + + statusBox.getChildren().addAll(titleRow, subtitleLabel); + + // Seção de Certificados + HBox certsHeader = new HBox(12); + certsHeader.setAlignment(Pos.CENTER_LEFT); + + Label certsTitle = new Label("Certificados Disponíveis"); + certsTitle.getStyleClass().add("section-title"); + HBox.setHgrow(certsTitle, Priority.ALWAYS); + + countLabel.getStyleClass().add("section-subtitle"); + + Button btnRefresh = new Button("Atualizar"); + btnRefresh.getStyleClass().add("btn-secondary"); + btnRefresh.setOnAction(e -> { + if (onRefreshRequested != null) onRefreshRequested.run(); + }); + + Button btnAddA1 = new Button("+ Adicionar A1"); + btnAddA1.getStyleClass().add("btn-primary"); + btnAddA1.setOnAction(e -> { + if (onAddA1Requested != null) onAddA1Requested.run(); + }); + + certsHeader.getChildren().addAll(certsTitle, countLabel, btnRefresh, btnAddA1); + + // Scroll com lista de cartões + certsContainer.setPadding(new Insets(4, 0, 4, 0)); + ScrollPane scrollPane = new ScrollPane(certsContainer); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + + getChildren().addAll(statusBox, certsHeader, scrollPane); + } + + public void updateCertificates(List certificates) { + certsContainer.getChildren().clear(); + + if (certificates == null || certificates.isEmpty()) { + countLabel.setText("0 encontrados"); + Label emptyLabel = new Label("Nenhum certificado detectado no momento.\nConecte um token USB/smart card ou adicione um certificado A1."); + emptyLabel.getStyleClass().add("section-subtitle"); + emptyLabel.setPadding(new Insets(30, 0, 0, 0)); + certsContainer.getChildren().add(emptyLabel); + return; + } + + countLabel.setText(certificates.size() + " encontrado" + (certificates.size() > 1 ? "s" : "")); + for (ManagedCertificate cert : certificates) { + CertificateCardNode card = new CertificateCardNode(cert); + card.setOnMouseClicked(e -> { + if (onCertificateSelected != null) { + onCertificateSelected.accept(cert); + } + }); + certsContainer.getChildren().add(card); + } + } + + public void setOnRefreshRequested(Runnable onRefreshRequested) { + this.onRefreshRequested = onRefreshRequested; + } + + public void setOnAddA1Requested(Runnable onAddA1Requested) { + this.onAddA1Requested = onAddA1Requested; + } + + public void setOnCertificateSelected(Consumer onCertificateSelected) { + this.onCertificateSelected = onCertificateSelected; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java new file mode 100644 index 0000000..17a94ad --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfSignDialog.java @@ -0,0 +1,1125 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.config.AgentConfiguration; +import br.com.cybercore.signer.core.config.AgentConfigurationService; +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +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.PdfSealData; +import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.security.PinSessionManager; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.PasswordField; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.file.Files; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Diálogo especializado para Assinatura Digital de Documentos PDF no padrão PAdES, + * com pré-visualização interativa de páginas do PDF e posicionamento livre do selo visual por clique ou arrasto. + */ +@SuppressWarnings("this-escape") +public final class PdfSignDialog { + + private static final Logger logger = LoggerFactory.getLogger(PdfSignDialog.class); + + private final Stage stage; + private final CertificateManager certManager; + private final PdfSigningService pdfSigningService; + + // Documento PDF em memória para visualização e assinatura + private TextField txtSourcePdf; + private File selectedSourceFile; + private byte[] selectedPdfBytes; + private int totalPages = 0; + private int currentPageIndex = 0; + + // Coordenadas personalizadas capturadas da interação na folha + private Float customPdfX = null; + private Float customPdfY = null; + private Integer customPageNumber = null; + private boolean isCustomPositionActive = false; + + // Controles de Certificado + private ComboBox certCombo; + private PasswordField pinField; + private Label lblPinNotice; + + // Controles de Selo Visual + private CheckBox chkVisibleSeal; + private ComboBox themeCombo; + private ComboBox cpfModeCombo; + private CheckBox chkDateTime; + private TextField txtCustomInfo; + private ComboBox positionCombo; + private ComboBox pageCombo; + private TextField txtReason; + private TextField txtLocation; + private ImageView sealPreviewImageView; + + // Visualizador Interativo da Página do PDF + private VBox pageViewerColumn; + private StackPane pageSheetContainer; + private ImageView pageImageView; + private Pane interactiveOverlayPane; + private ImageView sealStampOverlayView; + private Label lblPageIndicator; + private Label lblPositionFeedback; + private Button btnPrevPage; + private Button btnNextPage; + private ProgressIndicator pageLoadingIndicator; + + // Rodapé de Ações + private ProgressIndicator progressIndicator; + private Label lblStatus; + private Button btnSign; + private Button btnCancel; + + public PdfSignDialog(Stage owner, CertificateManager certManager) { + this(owner, certManager, new PdfSigningService(certManager)); + } + + public PdfSignDialog(Stage owner, CertificateManager certManager, PdfSigningService pdfSigningService) { + this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo."); + this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo."); + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setResizable(true); + this.stage.setMinWidth(880); + this.stage.setMinHeight(560); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(14); + root.setPadding(new Insets(18)); + root.getStyleClass().add("dialog-container"); + + // Topo / Cabeçalho + HBox header = new HBox(14); + header.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(3); + Label lblTitle = new Label("ASSINATURA DE DOCUMENTO PDF (PADES)"); + lblTitle.getStyleClass().add("dialog-header"); + Label lblSub = new Label("Assine digitalmente documentos PDF. Clique na folha para escolher o local exato onde o selo visual será estampado."); + lblSub.getStyleClass().add("dialog-subtitle"); + titleBox.getChildren().addAll(lblTitle, lblSub); + header.getChildren().addAll(SvgIcons.pdfIcon(), titleBox); + + // Corpo Principal dividido em 2 colunas: Configuração à Esquerda e Visualizador Interativo à Direita + HBox body = new HBox(16); + VBox.setVgrow(body, Priority.ALWAYS); + + // ========================================================================= + // COLUNA DA ESQUERDA: Formulário de Configurações + // ========================================================================= + ScrollPane leftScroll = new ScrollPane(); + leftScroll.setFitToWidth(true); + leftScroll.setPrefWidth(470); + leftScroll.setMinWidth(440); + leftScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); + + VBox formContent = new VBox(14); + formContent.setPadding(new Insets(2, 6, 2, 0)); + + // 1. Arquivo PDF + VBox fileSection = new VBox(8); + fileSection.getStyleClass().add("dialog-box"); + Label lblFileTitle = new Label("DOCUMENTO PDF A ASSINAR"); + lblFileTitle.getStyleClass().add("dialog-field-title"); + + HBox fileRow = new HBox(10); + fileRow.setAlignment(Pos.CENTER_LEFT); + txtSourcePdf = new TextField(); + txtSourcePdf.setPromptText("Selecione o arquivo PDF..."); + txtSourcePdf.setEditable(false); + HBox.setHgrow(txtSourcePdf, Priority.ALWAYS); + + Button btnBrowse = new Button("Procurar PDF..."); + btnBrowse.getStyleClass().add("btn-secondary"); + btnBrowse.setOnAction(e -> handleBrowseSourcePdf()); + fileRow.getChildren().addAll(txtSourcePdf, btnBrowse); + fileSection.getChildren().addAll(lblFileTitle, fileRow); + + // 2. Certificado Digital + VBox certSection = new VBox(8); + certSection.getStyleClass().add("dialog-box"); + Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL"); + lblCertTitle.getStyleClass().add("dialog-field-title"); + + certCombo = new ComboBox<>(); + certCombo.setMaxWidth(Double.MAX_VALUE); + populateCertificates(); + certCombo.setOnAction(e -> { + updatePinFieldVisibility(); + updateSealPreview(); + }); + + certCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + certCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + + HBox pinRow = new HBox(10); + pinRow.setAlignment(Pos.CENTER_LEFT); + Label lblPin = new Label("PIN / Senha:"); + lblPin.getStyleClass().add("dialog-field-title"); + pinField = new PasswordField(); + pinField.setPromptText("PIN ou senha do certificado..."); + pinField.setPrefWidth(200); + + lblPinNotice = new Label(); + lblPinNotice.setStyle("-fx-font-size: 11px;"); + pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice); + + certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow); + + // 3. Configurações de Selo Visual + VBox sealSection = new VBox(10); + sealSection.getStyleClass().add("dialog-box"); + + HBox sealHeader = new HBox(10); + sealHeader.setAlignment(Pos.CENTER_LEFT); + Label lblSealTitle = new Label("OPÇÕES DO SELO VISUAL (SVG)"); + lblSealTitle.getStyleClass().add("dialog-field-title"); + HBox.setHgrow(lblSealTitle, Priority.ALWAYS); + + chkVisibleSeal = new CheckBox("Estampar Selo"); + chkVisibleSeal.setSelected(true); + chkVisibleSeal.setOnAction(e -> toggleVisibleSeal(chkVisibleSeal.isSelected())); + sealHeader.getChildren().addAll(lblSealTitle, chkVisibleSeal); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(8); + + Label lblTheme = new Label("Tema SVG:"); + lblTheme.getStyleClass().add("dialog-field-title"); + + HBox themeRow = new HBox(8); + themeRow.setAlignment(Pos.CENTER_LEFT); + + AgentConfiguration agentConfig = new AgentConfigurationService().loadConfiguration(); + PdfSealTheme initialTheme = PdfSealThemeRegistry.getTheme(agentConfig.defaultSealThemeId()) + .orElse(PdfSealThemeRegistry.getDefaultTheme()); + + themeCombo = new ComboBox<>(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes())); + themeCombo.setMaxWidth(Double.MAX_VALUE); + themeCombo.setValue(initialTheme); + themeCombo.setOnAction(e -> { + PdfSealTheme th = themeCombo.getValue(); + if (th != null) { + if (th.defaultCpfMode() != null) { + cpfModeCombo.setValue(th.defaultCpfMode()); + } + chkDateTime.setSelected(th.defaultIncludeDateTime()); + if (txtCustomInfo != null) { + txtCustomInfo.setText(th.formatCustomField()); + } + } + updateSealPreview(); + }); + HBox.setHgrow(themeCombo, Priority.ALWAYS); + + Button btnManageSeals = new Button("⚙ Selos"); + btnManageSeals.getStyleClass().add("btn-secondary"); + btnManageSeals.setStyle("-fx-padding: 4px 8px; -fx-font-size: 11px;"); + btnManageSeals.setOnAction(e -> { + new SealManagementDialog(stage).showAndWait(); + AgentConfiguration updatedCfg = new AgentConfigurationService().loadConfiguration(); + themeCombo.setItems(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes())); + PdfSealTheme activeTheme = PdfSealThemeRegistry.getTheme(updatedCfg.defaultSealThemeId()) + .orElse(PdfSealThemeRegistry.getDefaultTheme()); + themeCombo.setValue(activeTheme); + if (txtCustomInfo != null) { + txtCustomInfo.setText(activeTheme.formatCustomField()); + } + updateSealPreview(); + }); + + Button btnNewSeal = new Button("🎨 Novo"); + btnNewSeal.getStyleClass().add("btn-secondary"); + btnNewSeal.setStyle("-fx-padding: 4px 8px; -fx-font-size: 11px;"); + btnNewSeal.setOnAction(e -> { + new SealDesignerDialog(stage, createdTheme -> { + themeCombo.setItems(FXCollections.observableArrayList(PdfSealThemeRegistry.listThemes())); + themeCombo.setValue(createdTheme); + if (txtCustomInfo != null) { + txtCustomInfo.setText(createdTheme.formatCustomField()); + } + updateSealPreview(); + }).show(); + }); + + themeRow.getChildren().addAll(themeCombo, btnManageSeals, btnNewSeal); + + themeCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(PdfSealTheme item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.name() + " (" + item.description() + ")"); + } + } + }); + themeCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(PdfSealTheme item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.name()); + } + } + }); + + // Controles de CPF e Data/Hora + Label lblCpfMode = new Label("Exibição do CPF:"); + lblCpfMode.getStyleClass().add("dialog-field-title"); + + cpfModeCombo = new ComboBox<>(FXCollections.observableArrayList(CpfDisplayMode.values())); + cpfModeCombo.setMaxWidth(Double.MAX_VALUE); + cpfModeCombo.setValue(initialTheme.defaultCpfMode() != null ? initialTheme.defaultCpfMode() : CpfDisplayMode.FULL); + cpfModeCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(CpfDisplayMode item, boolean empty) { + super.updateItem(item, empty); + setText((empty || item == null) ? null : item.getDescription()); + } + }); + cpfModeCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(CpfDisplayMode item, boolean empty) { + super.updateItem(item, empty); + setText((empty || item == null) ? null : item.getDescription()); + } + }); + cpfModeCombo.setOnAction(e -> updateSealPreview()); + + Label lblCustomInfo = new Label("Matrícula / Cargo:"); + lblCustomInfo.getStyleClass().add("dialog-field-title"); + txtCustomInfo = new TextField(initialTheme.formatCustomField()); + txtCustomInfo.setPromptText("Opcional (ex: Matrícula: 104829 ou Cargo: Auditor)"); + txtCustomInfo.textProperty().addListener((obs, oldV, newV) -> updateSealPreview()); + + chkDateTime = new CheckBox("Estampar Data e Hora no Selo"); + chkDateTime.setSelected(initialTheme.defaultIncludeDateTime()); + chkDateTime.setOnAction(e -> updateSealPreview()); + + Label lblPosition = new Label("Posicionamento:"); + lblPosition.getStyleClass().add("dialog-field-title"); + positionCombo = new ComboBox<>(FXCollections.observableArrayList(PositionOption.values())); + positionCombo.setMaxWidth(Double.MAX_VALUE); + positionCombo.setValue(PositionOption.BOTTOM_RIGHT); + positionCombo.setOnAction(e -> handlePositionComboSelection()); + + Label lblPage = new Label("Página Padrão:"); + lblPage.getStyleClass().add("dialog-field-title"); + pageCombo = new ComboBox<>(FXCollections.observableArrayList(PageOption.values())); + pageCombo.setMaxWidth(Double.MAX_VALUE); + pageCombo.setValue(PageOption.LAST_PAGE); + + Label lblReason = new Label("Razão:"); + lblReason.getStyleClass().add("dialog-field-title"); + txtReason = new TextField("Assinatura Digital ICP-Brasil"); + txtReason.textProperty().addListener((obs, oldV, newV) -> updateSealPreview()); + + Label lblLocation = new Label("Local:"); + lblLocation.getStyleClass().add("dialog-field-title"); + txtLocation = new TextField("Brasil"); + txtLocation.textProperty().addListener((obs, oldV, newV) -> updateSealPreview()); + + grid.add(lblTheme, 0, 0); + grid.add(themeRow, 1, 0); + grid.add(lblCpfMode, 0, 1); + grid.add(cpfModeCombo, 1, 1); + grid.add(lblCustomInfo, 0, 2); + grid.add(txtCustomInfo, 1, 2); + Label lblDt = new Label("Data/Hora:"); + lblDt.getStyleClass().add("dialog-field-title"); + grid.add(lblDt, 0, 3); + grid.add(chkDateTime, 1, 3); + grid.add(lblPosition, 0, 4); + grid.add(positionCombo, 1, 4); + grid.add(lblPage, 0, 5); + grid.add(pageCombo, 1, 5); + grid.add(lblReason, 0, 6); + grid.add(txtReason, 1, 6); + grid.add(lblLocation, 0, 7); + grid.add(txtLocation, 1, 7); + + // Preview em miniatura do SVG + VBox previewBox = new VBox(4); + previewBox.setAlignment(Pos.CENTER); + previewBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 8px;"); + sealPreviewImageView = new ImageView(); + sealPreviewImageView.setPreserveRatio(true); + sealPreviewImageView.setFitWidth(280); + sealPreviewImageView.setFitHeight(90); + previewBox.getChildren().add(sealPreviewImageView); + + sealSection.getChildren().addAll(sealHeader, grid, previewBox); + + formContent.getChildren().addAll(fileSection, certSection, sealSection); + leftScroll.setContent(formContent); + + // ========================================================================= + // COLUNA DA DIREITA: Visualizador Interativo da Página do PDF (Folha) + // ========================================================================= + pageViewerColumn = new VBox(10); + pageViewerColumn.getStyleClass().add("dialog-box"); + HBox.setHgrow(pageViewerColumn, Priority.ALWAYS); + + // Barra superior do visualizador + HBox viewerBar = new HBox(12); + viewerBar.setAlignment(Pos.CENTER_LEFT); + + Label lblViewerTitle = new Label("POSICIONAMENTO INTERATIVO NA FOLHA"); + lblViewerTitle.getStyleClass().add("dialog-field-title"); + + HBox pageNavBox = new HBox(8); + pageNavBox.setAlignment(Pos.CENTER_LEFT); + + btnPrevPage = new Button("◀ Anterior"); + btnPrevPage.getStyleClass().add("btn-secondary"); + btnPrevPage.setDisable(true); + btnPrevPage.setOnAction(e -> navigatePage(-1)); + + lblPageIndicator = new Label("Nenhum PDF carregado"); + lblPageIndicator.setStyle("-fx-font-weight: bold; -fx-font-size: 12px;"); + + btnNextPage = new Button("Próxima ▶"); + btnNextPage.getStyleClass().add("btn-secondary"); + btnNextPage.setDisable(true); + btnNextPage.setOnAction(e -> navigatePage(1)); + + pageLoadingIndicator = new ProgressIndicator(); + pageLoadingIndicator.setPrefSize(18, 18); + pageLoadingIndicator.setVisible(false); + + pageNavBox.getChildren().addAll(btnPrevPage, lblPageIndicator, btnNextPage, pageLoadingIndicator); + + HBox.setHgrow(pageNavBox, Priority.ALWAYS); + + lblPositionFeedback = new Label("👉 Clique na folha para posicionar"); + lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;"); + + viewerBar.getChildren().addAll(lblViewerTitle, pageNavBox, lblPositionFeedback); + + // Área da Folha de Papel Centralizada com ScrollPane + ScrollPane sheetScroll = new ScrollPane(); + sheetScroll.setFitToWidth(true); + sheetScroll.setFitToHeight(true); + sheetScroll.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); + VBox.setVgrow(sheetScroll, Priority.ALWAYS); + + StackPane sheetWrapper = new StackPane(); + sheetWrapper.setAlignment(Pos.CENTER); + sheetWrapper.setPadding(new Insets(10)); + + pageSheetContainer = new StackPane(); + pageSheetContainer.setAlignment(Pos.TOP_LEFT); + pageSheetContainer.setStyle("-fx-background-color: #ffffff; -fx-border-color: #94a3b8; -fx-border-width: 1px;"); + DropShadow sheetShadow = new DropShadow(14, 0, 4, Color.rgb(0, 0, 0, 0.28)); + pageSheetContainer.setEffect(sheetShadow); + pageSheetContainer.setMaxWidth(RegionConstant.PREVIEW_WIDTH); + pageSheetContainer.setMaxHeight(RegionConstant.PREVIEW_HEIGHT); + + // Imagem da página do PDF + pageImageView = new ImageView(); + pageImageView.setPreserveRatio(true); + pageImageView.setFitWidth(RegionConstant.PREVIEW_WIDTH); + pageImageView.setFitHeight(RegionConstant.PREVIEW_HEIGHT); + + // Overlay Interativo que recebe os cliques e arrasto + interactiveOverlayPane = new Pane(); + interactiveOverlayPane.setCursor(Cursor.CROSSHAIR); + interactiveOverlayPane.setOnMouseClicked(this::handleSheetClick); + interactiveOverlayPane.setOnMouseDragged(this::handleSheetDrag); + + // Selo visual móvel sobreposto à folha + sealStampOverlayView = new ImageView(); + sealStampOverlayView.setPreserveRatio(true); + sealStampOverlayView.setFitWidth(RegionConstant.SEAL_WIDTH); + sealStampOverlayView.setFitHeight(RegionConstant.SEAL_HEIGHT); + sealStampOverlayView.setCursor(Cursor.MOVE); + DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.6)); + sealStampOverlayView.setEffect(sealShadow); + sealStampOverlayView.setVisible(false); + + // Permite também arrastar diretamente clicando no selo + sealStampOverlayView.setOnMouseDragged(this::handleSealDrag); + + interactiveOverlayPane.getChildren().add(sealStampOverlayView); + pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane); + sheetWrapper.getChildren().add(pageSheetContainer); + sheetScroll.setContent(sheetWrapper); + + pageViewerColumn.getChildren().addAll(viewerBar, sheetScroll); + + body.getChildren().addAll(leftScroll, pageViewerColumn); + + // ========================================================================= + // RODAPÉ: Status e Botões de Ação + // ========================================================================= + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + progressIndicator = new ProgressIndicator(); + progressIndicator.setPrefSize(22, 22); + progressIndicator.setVisible(false); + + lblStatus = new Label(); + lblStatus.setStyle("-fx-font-size: 12px;"); + HBox.setHgrow(lblStatus, Priority.ALWAYS); + + btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-secondary"); + btnCancel.setOnAction(e -> stage.close()); + + btnSign = new Button("Assinar Documento PDF"); + btnSign.getStyleClass().add("btn-primary"); + btnSign.setDefaultButton(true); + btnSign.setOnAction(e -> executePdfSignature()); + + footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign); + + root.getChildren().addAll(header, body, footer); + + Scene scene = new Scene(root, 900, 570); + ThemeManager.applyTheme(scene); + stage.setScene(scene); + + updatePinFieldVisibility(); + updateSealPreview(); + } + + private static class RegionConstant { + static final double PREVIEW_WIDTH = 360.0; + static final double PREVIEW_HEIGHT = 510.0; + static final double SEAL_WIDTH = 150.0; + static final double SEAL_HEIGHT = 45.0; + static final float PDF_DEFAULT_PAGE_WIDTH = 595.28f; + static final float PDF_DEFAULT_PAGE_HEIGHT = 841.89f; + static final float PDF_SEAL_WIDTH = 144.0f; + static final float PDF_SEAL_HEIGHT = 42.0f; + } + + private void handleBrowseSourcePdf() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Documento PDF"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf")); + + File file = chooser.showOpenDialog(stage); + if (file != null) { + selectedSourceFile = file; + txtSourcePdf.setText(file.getAbsolutePath()); + loadPdfDocument(file); + } + } + + private void loadPdfDocument(File file) { + try { + selectedPdfBytes = Files.readAllBytes(file.toPath()); + totalPages = PdfSigningService.getPageCount(selectedPdfBytes); + currentPageIndex = 0; + + btnPrevPage.setDisable(true); + btnNextPage.setDisable(totalPages <= 1); + lblPageIndicator.setText(String.format("Página 1 de %d", totalPages)); + + // Renderiza primeira página + renderCurrentPage(); + + // Ativa o selo visual na folha + if (chkVisibleSeal.isSelected()) { + sealStampOverlayView.setVisible(true); + // Posiciona no canto inferior direito inicialmente + resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT); + } + } catch (Exception ex) { + logger.error("Erro ao carregar documento PDF para pré-visualização", ex); + showAlert(Alert.AlertType.ERROR, "Erro ao Ler PDF", "Não foi possível carregar o arquivo PDF: " + ex.getMessage()); + } + } + + private void navigatePage(int delta) { + int target = currentPageIndex + delta; + if (target >= 0 && target < totalPages) { + currentPageIndex = target; + btnPrevPage.setDisable(currentPageIndex == 0); + btnNextPage.setDisable(currentPageIndex == totalPages - 1); + lblPageIndicator.setText(String.format("Página %d de %d", currentPageIndex + 1, totalPages)); + + renderCurrentPage(); + + // Se o selo já estava posicionado customizadamente, atualiza o número da página alvo + if (isCustomPositionActive) { + customPageNumber = currentPageIndex + 1; + updateFeedbackLabel(); + } + } + } + + private void renderCurrentPage() { + if (selectedPdfBytes == null) return; + + pageLoadingIndicator.setVisible(true); + int pageToRender = currentPageIndex; + + Task renderTask = new Task<>() { + @Override + protected Image call() throws Exception { + byte[] pngBytes = PdfSigningService.renderPagePngBytes(selectedPdfBytes, pageToRender, 96f); + return new Image(new ByteArrayInputStream(pngBytes)); + } + }; + + renderTask.setOnSucceeded(e -> { + pageLoadingIndicator.setVisible(false); + Image img = renderTask.getValue(); + pageImageView.setImage(img); + + // Ajusta dimensões do container para a proporção real da página + double viewW = pageImageView.getBoundsInParent().getWidth(); + double viewH = pageImageView.getBoundsInParent().getHeight(); + if (viewW > 0 && viewH > 0) { + interactiveOverlayPane.setPrefSize(viewW, viewH); + pageSheetContainer.setPrefSize(viewW, viewH); + } + }); + + renderTask.setOnFailed(e -> { + pageLoadingIndicator.setVisible(false); + logger.error("Falha ao renderizar página {} do PDF", pageToRender, renderTask.getException()); + }); + + Thread th = new Thread(renderTask, "cybercore-page-renderer"); + th.setDaemon(true); + th.start(); + } + + private void handleSheetClick(MouseEvent event) { + if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; + placeSealAtViewCoordinates(event.getX(), event.getY()); + } + + private void handleSheetDrag(MouseEvent event) { + if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; + placeSealAtViewCoordinates(event.getX(), event.getY()); + } + + private void handleSealDrag(MouseEvent event) { + if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; + double currentX = sealStampOverlayView.getLayoutX() + event.getX(); + double currentY = sealStampOverlayView.getLayoutY() + event.getY(); + placeSealAtViewCoordinates(currentX, currentY); + } + + private void placeSealAtViewCoordinates(double viewX, double viewY) { + double sheetW = interactiveOverlayPane.getWidth() > 0 ? interactiveOverlayPane.getWidth() : RegionConstant.PREVIEW_WIDTH; + double sheetH = interactiveOverlayPane.getHeight() > 0 ? interactiveOverlayPane.getHeight() : RegionConstant.PREVIEW_HEIGHT; + + double sealW = RegionConstant.SEAL_WIDTH; + double sealH = RegionConstant.SEAL_HEIGHT; + + // Centraliza o selo na ponta do cursor, sem deixar transbordar da folha + double clampedX = Math.max(0, Math.min(viewX - sealW / 2.0, sheetW - sealW)); + double clampedY = Math.max(0, Math.min(viewY - sealH / 2.0, sheetH - sealH)); + + sealStampOverlayView.setLayoutX(clampedX); + sealStampOverlayView.setLayoutY(clampedY); + sealStampOverlayView.setVisible(true); + + // Conversão matemática precisa para coordenadas do sistema PDF (origem inferior-esquerda em pontos) + double pctX = clampedX / sheetW; + double pctY = clampedY / sheetH; + + float pdfW = RegionConstant.PDF_DEFAULT_PAGE_WIDTH; + float pdfH = RegionConstant.PDF_DEFAULT_PAGE_HEIGHT; + float sealPdfW = RegionConstant.PDF_SEAL_WIDTH; + float sealPdfH = RegionConstant.PDF_SEAL_HEIGHT; + + float calculatedPdfX = (float) (pctX * pdfW); + float calculatedPdfY = (float) (pdfH - (pctY * pdfH) - sealPdfH); + + this.customPdfX = Math.max(0, Math.min(calculatedPdfX, pdfW - sealPdfW)); + this.customPdfY = Math.max(0, Math.min(calculatedPdfY, pdfH - sealPdfH)); + this.customPageNumber = currentPageIndex + 1; + this.isCustomPositionActive = true; + + // Seleciona a opção CUSTOM no combo + if (positionCombo.getValue() != PositionOption.CUSTOM) { + positionCombo.setValue(PositionOption.CUSTOM); + } + + updateFeedbackLabel(); + } + + private void resetSealToCornerPosition(PositionOption option) { + if (option == PositionOption.CUSTOM) { + return; + } + + isCustomPositionActive = false; + double sheetW = interactiveOverlayPane.getWidth() > 0 ? interactiveOverlayPane.getWidth() : RegionConstant.PREVIEW_WIDTH; + double sheetH = interactiveOverlayPane.getHeight() > 0 ? interactiveOverlayPane.getHeight() : RegionConstant.PREVIEW_HEIGHT; + + double sealW = RegionConstant.SEAL_WIDTH; + double sealH = RegionConstant.SEAL_HEIGHT; + double margin = 16.0; + + double targetX; + double targetY; + + switch (option) { + case BOTTOM_LEFT -> { + targetX = margin; + targetY = sheetH - sealH - margin; + } + case TOP_RIGHT -> { + targetX = sheetW - sealW - margin; + targetY = margin; + } + case TOP_LEFT -> { + targetX = margin; + targetY = margin; + } + default -> { // BOTTOM_RIGHT + targetX = sheetW - sealW - margin; + targetY = sheetH - sealH - margin; + } + } + + sealStampOverlayView.setLayoutX(targetX); + sealStampOverlayView.setLayoutY(targetY); + sealStampOverlayView.setVisible(true); + + lblPositionFeedback.setText("Posição fixa: " + option.label); + } + + private void handlePositionComboSelection() { + PositionOption sel = positionCombo.getValue(); + if (sel != null && sel != PositionOption.CUSTOM) { + resetSealToCornerPosition(sel); + } + } + + private void updateFeedbackLabel() { + if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) { + lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]", + customPageNumber, customPdfX, customPdfY)); + } + } + + private void toggleVisibleSeal(boolean active) { + themeCombo.setDisable(!active); + positionCombo.setDisable(!active); + pageCombo.setDisable(!active); + txtReason.setDisable(!active); + txtLocation.setDisable(!active); + if (txtCustomInfo != null) { + txtCustomInfo.setDisable(!active); + } + sealPreviewImageView.setOpacity(active ? 1.0 : 0.3); + sealStampOverlayView.setVisible(active && selectedPdfBytes != null); + } + + private void populateCertificates() { + try { + List certs = certManager.getAvailableCertificates(); + certCombo.setItems(FXCollections.observableArrayList(certs)); + if (!certs.isEmpty()) { + certCombo.setValue(certs.get(0)); + } + } catch (Exception e) { + logger.error("Erro ao listar certificados para assinatura de PDF", e); + } + } + + private void updatePinFieldVisibility() { + ManagedCertificate cert = certCombo.getValue(); + if (cert == null) { + pinField.setDisable(true); + lblPinNotice.setText(""); + return; + } + + boolean isA3 = cert.type() == CertificateType.A3; + boolean hasPinInSession = isA3 && PinSessionManager.getInstance().hasPin(cert.id()); + + if (hasPinInSession) { + pinField.setDisable(true); + pinField.clear(); + lblPinNotice.setText("✓ PIN memorizado"); + lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;"); + } else if (isA3 && "provider-win-store".equalsIgnoreCase(cert.providerId())) { + pinField.setDisable(true); + pinField.clear(); + lblPinNotice.setText("Driver nativo"); + lblPinNotice.setStyle("-fx-text-fill: -fx-accent;"); + } else { + pinField.setDisable(false); + lblPinNotice.setText(isA3 ? "Requer PIN" : "Requer senha"); + lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary;"); + } + } + + private void updateSealPreview() { + try { + PdfSealTheme theme = themeCombo.getValue(); + if (theme == null) { + theme = PdfSealThemeRegistry.getDefaultTheme(); + } + + ManagedCertificate cert = certCombo.getValue(); + String name = (cert != null && cert.commonName() != null) ? cert.commonName() : "NOME DO TITULAR ICP-BRASIL"; + String doc = (cert != null && cert.cpf() != null) ? cert.cpf() : (cert != null && cert.cnpj() != null ? cert.cnpj() : "000.000.000-00"); + String issuer = (cert != null && cert.issuerDn() != null) ? cert.issuerDn() : "AC ICP-Brasil Multipla v5"; + String reason = (txtReason != null && !txtReason.getText().isBlank()) ? txtReason.getText() : "Assinatura Digital ICP-Brasil"; + String location = (txtLocation != null && !txtLocation.getText().isBlank()) ? txtLocation.getText() : "Brasil"; + + CpfDisplayMode cpfMode = (cpfModeCombo != null && cpfModeCombo.getValue() != null) + ? cpfModeCombo.getValue() + : CpfDisplayMode.FULL; + boolean includeDt = (chkDateTime != null) ? chkDateTime.isSelected() : true; + String customInfo = (txtCustomInfo != null && !txtCustomInfo.getText().isBlank()) + ? txtCustomInfo.getText().trim() + : null; + + PdfSealData data = new PdfSealData( + name, + doc, + null, + issuer, + "7A3F9C01B4E8D2E5...", + reason, + location, + cpfMode, + includeDt, + customInfo + ); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, data); + Image image = new Image(new ByteArrayInputStream(pngBytes)); + sealPreviewImageView.setImage(image); + + // Atualiza também a estampa que fica sobreposta na folha do PDF + sealStampOverlayView.setImage(image); + } catch (Exception e) { + logger.warn("Falha ao atualizar pré-visualização do selo: {}", e.getMessage()); + } + } + + private void executePdfSignature() { + if (selectedSourceFile == null || !selectedSourceFile.exists()) { + showAlert(Alert.AlertType.WARNING, "Documento Não Selecionado", "Por favor, selecione um arquivo PDF válido para assinar."); + return; + } + + ManagedCertificate cert = certCombo.getValue(); + if (cert == null) { + showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital."); + return; + } + + FileChooser chooser = new FileChooser(); + chooser.setTitle("Salvar PDF Assinado Como..."); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf")); + + String originalName = selectedSourceFile.getName(); + String baseName = originalName.toLowerCase().endsWith(".pdf") + ? originalName.substring(0, originalName.length() - 4) + : originalName; + chooser.setInitialFileName(baseName + "_assinado.pdf"); + if (selectedSourceFile.getParentFile() != null && selectedSourceFile.getParentFile().exists()) { + chooser.setInitialDirectory(selectedSourceFile.getParentFile()); + } + + File targetFile = chooser.showSaveDialog(stage); + if (targetFile == null) { + return; + } + + char[] pin = null; + if (!pinField.isDisable() && !pinField.getText().isEmpty()) { + pin = pinField.getText().toCharArray(); + } + + CpfDisplayMode selectedCpfMode = (cpfModeCombo != null && cpfModeCombo.getValue() != null) + ? cpfModeCombo.getValue() + : CpfDisplayMode.FULL; + boolean selectedIncludeDt = (chkDateTime != null) ? chkDateTime.isSelected() : true; + String customInfo = (txtCustomInfo != null && !txtCustomInfo.getText().isBlank()) + ? txtCustomInfo.getText().trim() + : null; + + PdfSignatureOptions options; + if (isCustomPositionActive && customPdfX != null && customPdfY != null) { + options = PdfSignatureOptions.builder(cert.id()) + .pinOrPassword(pin) + .visibleSeal(chkVisibleSeal.isSelected()) + .sealTheme(themeCombo.getValue()) + .cpfDisplayMode(selectedCpfMode) + .includeDateTime(selectedIncludeDt) + .customExtraInfo(customInfo) + .customPosition(customPdfX, customPdfY, customPageNumber) + .reason(txtReason.getText()) + .location(txtLocation.getText()) + .build(); + } else { + options = PdfSignatureOptions.builder(cert.id()) + .pinOrPassword(pin) + .visibleSeal(chkVisibleSeal.isSelected()) + .sealTheme(themeCombo.getValue()) + .cpfDisplayMode(selectedCpfMode) + .includeDateTime(selectedIncludeDt) + .customExtraInfo(customInfo) + .position(positionCombo.getValue().toPosition()) + .pageChoice(pageCombo.getValue().toPageChoice()) + .reason(txtReason.getText()) + .location(txtLocation.getText()) + .build(); + } + + setFormDisabled(true); + progressIndicator.setVisible(true); + lblStatus.setText("Assinando documento PDF com envelope PAdES..."); + lblStatus.setStyle("-fx-text-fill: -fx-accent;"); + + Task signTask = new Task<>() { + @Override + protected File call() throws Exception { + pdfSigningService.signPdfFile(selectedSourceFile, targetFile, options); + return targetFile; + } + }; + + signTask.setOnSucceeded(e -> { + setFormDisabled(false); + progressIndicator.setVisible(false); + lblStatus.setText("✓ Documento assinado com sucesso!"); + lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;"); + + showSuccessDialog(targetFile); + }); + + signTask.setOnFailed(e -> { + setFormDisabled(false); + progressIndicator.setVisible(false); + Throwable ex = signTask.getException(); + logger.error("Falha ao assinar PDF", ex); + lblStatus.setText("✕ Falha na assinatura"); + lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;"); + + showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF", + "Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido")); + }); + + Thread signThread = new Thread(signTask, "cybercore-pdf-signer"); + signThread.setDaemon(true); + signThread.start(); + } + + private void showSuccessDialog(File targetFile) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Assinatura Concluída"); + alert.setHeaderText("PDF Assinado com Sucesso!"); + alert.setContentText("O documento foi digitalmente assinado no padrão PAdES e o selo visual foi aplicado na posição exata solicitada.\n\n" + + "Arquivo gerado:\n" + targetFile.getAbsolutePath()); + + ButtonType btnOpen = new ButtonType("Abrir PDF"); + ButtonType btnFolder = new ButtonType("Abrir Pasta"); + ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE); + + alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose); + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + + Optional result = alert.showAndWait(); + if (result.isPresent()) { + if (result.get() == btnOpen) { + openFile(targetFile); + } else if (result.get() == btnFolder) { + openFolder(targetFile); + } + } + } + + private void openFile(File file) { + try { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + Desktop.getDesktop().open(file); + } + } catch (Exception ex) { + logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage()); + } + } + + private void openFolder(File file) { + try { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + File parent = file.getParentFile(); + if (parent != null) { + Desktop.getDesktop().open(parent); + } + } + } catch (Exception ex) { + logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage()); + } + } + + private void setFormDisabled(boolean disabled) { + btnSign.setDisable(disabled); + btnCancel.setDisable(disabled); + certCombo.setDisable(disabled); + pinField.setDisable(disabled); + chkVisibleSeal.setDisable(disabled); + themeCombo.setDisable(disabled); + positionCombo.setDisable(disabled); + pageCombo.setDisable(disabled); + txtReason.setDisable(disabled); + txtLocation.setDisable(disabled); + interactiveOverlayPane.setDisable(disabled); + btnPrevPage.setDisable(disabled || currentPageIndex == 0); + btnNextPage.setDisable(disabled || currentPageIndex >= totalPages - 1); + } + + private void showAlert(Alert.AlertType type, String title, String message) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + alert.showAndWait(); + } + + public void show() { + stage.centerOnScreen(); + stage.show(); + stage.toFront(); + } + + public enum PositionOption { + BOTTOM_RIGHT("Canto Inferior Direito", PdfSealPosition.BOTTOM_RIGHT), + BOTTOM_LEFT("Canto Inferior Esquerdo", PdfSealPosition.BOTTOM_LEFT), + TOP_RIGHT("Canto Superior Direito", PdfSealPosition.TOP_RIGHT), + TOP_LEFT("Canto Superior Esquerdo", PdfSealPosition.TOP_LEFT), + CUSTOM("Personalizado (Clique na Folha)", PdfSealPosition.CUSTOM); + + final String label; + final PdfSealPosition position; + + PositionOption(String label, PdfSealPosition position) { + this.label = label; + this.position = position; + } + + public PdfSealPosition toPosition() { + return position; + } + + @Override + public String toString() { + return label; + } + } + + public enum PageOption { + LAST_PAGE("Última Página", PdfSealPageChoice.LAST_PAGE), + FIRST_PAGE("Primeira Página", PdfSealPageChoice.FIRST_PAGE), + ALL_PAGES("Todas as Páginas", PdfSealPageChoice.ALL_PAGES); + + private final String label; + private final PdfSealPageChoice pageChoice; + + PageOption(String label, PdfSealPageChoice pageChoice) { + this.label = label; + this.pageChoice = pageChoice; + } + + public PdfSealPageChoice toPageChoice() { + return pageChoice; + } + + @Override + public String toString() { + return label; + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java new file mode 100644 index 0000000..ea6a2c1 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealDesignerDialog.java @@ -0,0 +1,643 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import br.com.cybercore.signer.core.pdf.seal.PdfSealData; +import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ColorPicker; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import java.io.ByteArrayInputStream; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * Diálogo Interativo para Criação, Customização, Importação e Instalação de Selos Digitais. + * Permite ao usuário definir cores, textos, dimensões, opções de CPF (completo, mascarado, oculto), + * data/hora da assinatura e upload de logotipo personalizado, visualizando em tempo real antes de instalar. + */ +@SuppressWarnings("this-escape") +public final class SealDesignerDialog { + + private static final Logger logger = LoggerFactory.getLogger(SealDesignerDialog.class); + + private final Stage stage; + private final Consumer onSealInstalled; + + private TextField txtName; + private TextField txtCustomHeader; + private TextField txtCustomFooter; + + private ColorPicker cpPrimary; + private ColorPicker cpSecondary; + private ColorPicker cpBackground; + private ColorPicker cpBorder; + private ColorPicker cpText; + + private ComboBox cbIconStyle; + private String customLogoBase64; + private Label lblLogoStatus; + private Button btnClearLogo; + + private TextField txtCustomFieldLabel; + private TextField txtCustomFieldValue; + + private CheckBox chkIncludeCpf; + private RadioButton rbCpfMasked; + private RadioButton rbCpfFull; + private CheckBox chkIncludeDateTime; + + private ImageView previewImageView; + + public SealDesignerDialog(Stage owner, Consumer onSealInstalled) { + this(owner, null, onSealInstalled); + } + + public SealDesignerDialog(Stage owner, PdfSealTheme initialTheme, Consumer onSealInstalled) { + this.onSealInstalled = onSealInstalled; + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Criador e Instalador de Selos Digitais"); + this.stage.setResizable(true); + this.stage.setMinWidth(860); + this.stage.setMinHeight(560); + + initLayout(); + if (initialTheme != null) { + loadThemeIntoForm(initialTheme); + } + updatePreview(); + } + + private void initLayout() { + VBox root = new VBox(14); + root.setPadding(new Insets(16)); + root.getStyleClass().add("dialog-container"); + + // Topo / Cabeçalho + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(3); + Label lblTitle = new Label("CRIADOR E INSTALADOR DE SELOS VISUAIS"); + lblTitle.getStyleClass().add("dialog-header"); + Label lblSub = new Label("Crie sua identidade visual personalizada para assinaturas em PDF e instale no sistema."); + lblSub.getStyleClass().add("dialog-subtitle"); + titleBox.getChildren().addAll(lblTitle, lblSub); + header.getChildren().addAll(SvgIcons.sealIcon(), titleBox); + + // Painel Principal: Formulário à esquerda, Preview ao vivo à direita + HBox mainSplit = new HBox(16); + VBox.setVgrow(mainSplit, Priority.ALWAYS); + + // --- Coluna Esquerda: Controles (Formulário) --- + VBox formPane = new VBox(12); + formPane.setPrefWidth(430); + formPane.setMinWidth(400); + + ScrollPane scrollForm = new ScrollPane(formPane); + scrollForm.setFitToWidth(true); + VBox.setVgrow(scrollForm, Priority.ALWAYS); + scrollForm.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + HBox.setHgrow(scrollForm, Priority.ALWAYS); + + // 1. Identificação do Selo e Campos Adicionais + VBox idBox = new VBox(8); + idBox.getStyleClass().add("dialog-box"); + Label lblIdSection = new Label("IDENTIFICAÇÃO DO SELO E TITULAR"); + lblIdSection.getStyleClass().add("dialog-field-title"); + + Label lblName = new Label("Nome do Selo:"); + lblName.getStyleClass().add("form-label"); + txtName = new TextField("Meu Selo Personalizado"); + txtName.setPromptText("Nome do selo (ex: Escritório Silva)"); + txtName.textProperty().addListener((obs, oldV, newV) -> updatePreview()); + + Label lblCustomHeaderTitle = new Label("Texto de Cabeçalho:"); + lblCustomHeaderTitle.getStyleClass().add("form-label"); + txtCustomHeader = new TextField("MEU ESCRITÓRIO • ASSINADO DIGITALMENTE"); + txtCustomHeader.setPromptText("Cabeçalho (ex: MINHA EMPRESA)"); + txtCustomHeader.textProperty().addListener((obs, oldV, newV) -> updatePreview()); + + Label lblCustomFooterTitle = new Label("Texto de Rodapé:"); + lblCustomFooterTitle.getStyleClass().add("form-label"); + txtCustomFooter = new TextField("Validade jurídica plena conforme MP nº 2.200-2/2001"); + txtCustomFooter.setPromptText("Rodapé institucional"); + txtCustomFooter.textProperty().addListener((obs, oldV, newV) -> updatePreview()); + + // Campos Adicionais Customizáveis (Matrícula, Cargo, OAB, etc.) + Label lblCustomFieldSection = new Label("Campo Personalizado Adicional (Opcional - Matrícula, Cargo, OAB):"); + lblCustomFieldSection.getStyleClass().add("form-label"); + + GridPane customFieldGrid = new GridPane(); + customFieldGrid.setHgap(10); + customFieldGrid.setVgap(6); + + txtCustomFieldLabel = new TextField("Matrícula"); + txtCustomFieldLabel.setPromptText("Rótulo (ex: Matrícula, Cargo, OAB)"); + txtCustomFieldLabel.setPrefWidth(150); + txtCustomFieldLabel.textProperty().addListener((obs, oldV, newV) -> updatePreview()); + + txtCustomFieldValue = new TextField("104829"); + txtCustomFieldValue.setPromptText("Valor (ex: 104829, Auditor Fiscal)"); + GridPane.setHgrow(txtCustomFieldValue, Priority.ALWAYS); + txtCustomFieldValue.textProperty().addListener((obs, oldV, newV) -> updatePreview()); + + Label lblCustomFieldLabelText = new Label("Rótulo do Campo:"); + lblCustomFieldLabelText.getStyleClass().add("form-label"); + Label lblCustomFieldValueText = new Label("Valor do Campo:"); + lblCustomFieldValueText.getStyleClass().add("form-label"); + + customFieldGrid.add(lblCustomFieldLabelText, 0, 0); + customFieldGrid.add(txtCustomFieldLabel, 0, 1); + customFieldGrid.add(lblCustomFieldValueText, 1, 0); + customFieldGrid.add(txtCustomFieldValue, 1, 1); + + idBox.getChildren().addAll(lblIdSection, + lblName, txtName, + lblCustomHeaderTitle, txtCustomHeader, + lblCustomFooterTitle, txtCustomFooter, + lblCustomFieldSection, customFieldGrid); + + // 2. Cores e Emblema / Logotipo Personalizado + VBox styleBox = new VBox(12); + styleBox.getStyleClass().add("dialog-box"); + Label lblStyleSection = new Label("CORES E EMBLEMA / LOGOTIPO"); + lblStyleSection.getStyleClass().add("dialog-field-title"); + + GridPane colorGrid = new GridPane(); + colorGrid.setHgap(12); + colorGrid.setVgap(10); + + ColumnConstraints colLabels = new ColumnConstraints(); + colLabels.setMinWidth(130); + colLabels.setPrefWidth(140); + + ColumnConstraints colPickers = new ColumnConstraints(); + colPickers.setHgrow(Priority.ALWAYS); + colorGrid.getColumnConstraints().addAll(colLabels, colPickers); + + cpPrimary = new ColorPicker(Color.web("#0d47a1")); + cpPrimary.setMaxWidth(Double.MAX_VALUE); + cpPrimary.setOnAction(e -> updatePreview()); + + cpSecondary = new ColorPicker(Color.web("#0284c7")); + cpSecondary.setMaxWidth(Double.MAX_VALUE); + cpSecondary.setOnAction(e -> updatePreview()); + + cpBackground = new ColorPicker(Color.web("#ffffff")); + cpBackground.setMaxWidth(Double.MAX_VALUE); + cpBackground.setOnAction(e -> updatePreview()); + + cpBorder = new ColorPicker(Color.web("#cbd5e1")); + cpBorder.setMaxWidth(Double.MAX_VALUE); + cpBorder.setOnAction(e -> updatePreview()); + + cpText = new ColorPicker(Color.web("#0f172a")); + cpText.setMaxWidth(Double.MAX_VALUE); + cpText.setOnAction(e -> updatePreview()); + + Label lblCpPrimary = new Label("Cor Primária:"); + lblCpPrimary.getStyleClass().add("form-label"); + Label lblCpSecondary = new Label("Cor Secundária:"); + lblCpSecondary.getStyleClass().add("form-label"); + Label lblCpBackground = new Label("Cor de Fundo:"); + lblCpBackground.getStyleClass().add("form-label"); + Label lblCpBorder = new Label("Cor da Borda:"); + lblCpBorder.getStyleClass().add("form-label"); + Label lblCpText = new Label("Cor do Texto:"); + lblCpText.getStyleClass().add("form-label"); + + colorGrid.add(lblCpPrimary, 0, 0); + colorGrid.add(cpPrimary, 1, 0); + colorGrid.add(lblCpSecondary, 0, 1); + colorGrid.add(cpSecondary, 1, 1); + colorGrid.add(lblCpBackground, 0, 2); + colorGrid.add(cpBackground, 1, 2); + colorGrid.add(lblCpBorder, 0, 3); + colorGrid.add(cpBorder, 1, 3); + colorGrid.add(lblCpText, 0, 4); + colorGrid.add(cpText, 1, 4); + + // Upload de Logotipo ou Emblema Próprio + Label lblLogoHeader = new Label("Upload de Emblema / Logotipo Próprio:"); + lblLogoHeader.getStyleClass().add("form-label"); + + Button btnUploadLogo = new Button("📁 Escolher Imagem (PNG, JPG)..."); + btnUploadLogo.getStyleClass().add("btn-secondary"); + btnUploadLogo.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(btnUploadLogo, Priority.ALWAYS); + btnUploadLogo.setOnAction(e -> handleUploadLogo()); + + btnClearLogo = new Button("✖ Remover"); + btnClearLogo.getStyleClass().setAll("btn-danger"); + btnClearLogo.setMinWidth(Region.USE_PREF_SIZE); + btnClearLogo.setDisable(true); + btnClearLogo.setOnAction(e -> handleClearLogo()); + + HBox logoBtnRow = new HBox(8, btnUploadLogo, btnClearLogo); + logoBtnRow.setAlignment(Pos.CENTER_LEFT); + + lblLogoStatus = new Label("Nenhum logotipo carregado (usando emblema vetorial)"); + lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;"); + + Label lblFallbackIcon = new Label("Emblema Vetorial Alternativo (caso sem logo própria):"); + lblFallbackIcon.getStyleClass().add("form-label"); + + cbIconStyle = new ComboBox<>(); + cbIconStyle.getItems().addAll("Escudo de Segurança (ICP-Brasil)", "Cristal Tecnológico (CyberCore)", "Carimbo Notarial (Cartório)", "Brasão Estelar (Governamental)"); + cbIconStyle.setValue("Escudo de Segurança (ICP-Brasil)"); + cbIconStyle.setMaxWidth(Double.MAX_VALUE); + cbIconStyle.setOnAction(e -> updatePreview()); + + styleBox.getChildren().addAll(lblStyleSection, colorGrid, lblLogoHeader, logoBtnRow, lblLogoStatus, lblFallbackIcon, cbIconStyle); + + // 3. Regras do Usuário: CPF e Data/Hora + VBox rulesBox = new VBox(10); + rulesBox.getStyleClass().add("dialog-box"); + Label lblRulesSection = new Label("PRIVACIDADE DO CPF E DATA/HORA"); + lblRulesSection.getStyleClass().add("dialog-field-title"); + + chkIncludeCpf = new CheckBox("Incluir CPF no Selo"); + chkIncludeCpf.getStyleClass().add("form-label"); + chkIncludeCpf.setSelected(true); + chkIncludeCpf.setOnAction(e -> { + boolean sel = chkIncludeCpf.isSelected(); + rbCpfMasked.setDisable(!sel); + rbCpfFull.setDisable(!sel); + updatePreview(); + }); + + ToggleGroup cpfGroup = new ToggleGroup(); + rbCpfMasked = new RadioButton("CPF Mascarado (***.456.789-**)"); + rbCpfMasked.getStyleClass().add("form-label"); + rbCpfMasked.setToggleGroup(cpfGroup); + rbCpfMasked.setSelected(true); + rbCpfMasked.setOnAction(e -> updatePreview()); + + rbCpfFull = new RadioButton("CPF Completo (123.456.789-00)"); + rbCpfFull.getStyleClass().add("form-label"); + rbCpfFull.setToggleGroup(cpfGroup); + rbCpfFull.setOnAction(e -> updatePreview()); + + VBox cpfSubOptions = new VBox(6); + cpfSubOptions.setPadding(new Insets(2, 0, 4, 18)); + cpfSubOptions.getChildren().addAll(rbCpfMasked, rbCpfFull); + + chkIncludeDateTime = new CheckBox("Incluir Data e Hora da Assinatura"); + chkIncludeDateTime.getStyleClass().add("form-label"); + chkIncludeDateTime.setSelected(true); + chkIncludeDateTime.setOnAction(e -> updatePreview()); + + rulesBox.getChildren().addAll(lblRulesSection, chkIncludeCpf, cpfSubOptions, chkIncludeDateTime); + + formPane.getChildren().addAll(idBox, styleBox, rulesBox); + + // --- Coluna Direita: Live Preview e Ações --- + VBox previewPane = new VBox(12); + previewPane.setAlignment(Pos.TOP_CENTER); + previewPane.setPrefWidth(380); + previewPane.setMinWidth(360); + previewPane.getStyleClass().add("dialog-box"); + + Label lblPreviewTitle = new Label("PRÉ-VISUALIZAÇÃO EM TEMPO REAL"); + lblPreviewTitle.getStyleClass().add("dialog-field-title"); + + Label lblPreviewNotice = new Label("O selo sempre exibe o nome completo do signatário com destaque e máxima legibilidade."); + lblPreviewNotice.setStyle("-fx-font-size: 11px; -fx-text-fill: #94a3b8;"); + lblPreviewNotice.setWrapText(true); + + previewImageView = new ImageView(); + previewImageView.setPreserveRatio(true); + previewImageView.setFitWidth(360); + + HBox previewContainer = new HBox(previewImageView); + previewContainer.setAlignment(Pos.CENTER); + previewContainer.setPadding(new Insets(16, 8, 16, 8)); + previewContainer.setStyle("-fx-background-color: #090d16; -fx-background-radius: 8px; -fx-border-color: #334155; -fx-border-radius: 8px;"); + + VBox previewContentBox = new VBox(10); + previewContentBox.setAlignment(Pos.CENTER); + previewContentBox.getChildren().addAll(lblPreviewTitle, lblPreviewNotice, previewContainer); + VBox.setVgrow(previewContentBox, Priority.ALWAYS); + + // Botões de Importar / Exportar + HBox importExportRow = new HBox(10); + importExportRow.setAlignment(Pos.CENTER); + + Button btnImport = new Button("📥 Importar Selo..."); + btnImport.getStyleClass().add("btn-secondary"); + btnImport.setPrefHeight(32); + btnImport.setOnAction(e -> handleImportSeal()); + + Button btnExport = new Button("📤 Exportar Selo..."); + btnExport.getStyleClass().add("btn-secondary"); + btnExport.setPrefHeight(32); + btnExport.setOnAction(e -> handleExportSeal()); + + importExportRow.getChildren().addAll(btnImport, btnExport); + + previewPane.getChildren().addAll(previewContentBox, importExportRow); + + mainSplit.getChildren().addAll(scrollForm, previewPane); + + // Rodapé de Ações Finais (Fixado na base da janela, sempre visível e destacado) + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + footer.setPadding(new Insets(6, 0, 0, 0)); + + Button btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-secondary"); + btnCancel.setPrefHeight(34); + btnCancel.setOnAction(e -> stage.close()); + + Button btnInstall = new Button("💾 Instalar Selo no Sistema"); + btnInstall.getStyleClass().add("btn-primary"); + btnInstall.setPrefHeight(34); + btnInstall.setDefaultButton(true); + btnInstall.setOnAction(e -> handleInstallSeal()); + + footer.getChildren().addAll(btnCancel, btnInstall); + + root.getChildren().addAll(header, mainSplit, footer); + + Scene scene = new Scene(root, 880, 580); + ThemeManager.applyTheme(scene); + stage.setScene(scene); + } + + private void updatePreview() { + Platform.runLater(() -> { + try { + PdfSealTheme theme = buildThemeFromForm("preview-temp"); + CpfDisplayMode cpfMode = resolveCpfMode(); + + PdfSealData sampleData = new PdfSealData( + "Dr. Carlos Alberto de Oliveira Santos Júnior", + "CPF: 12345678900", + null, + "AC CyberCore JUS v5", + "8A7F4B2C91D0E3F5", + "Assinatura de Documento Digital", + "São Paulo, SP", + cpfMode, + chkIncludeDateTime.isSelected() + ); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, sampleData); + previewImageView.setImage(new Image(new ByteArrayInputStream(pngBytes))); + } catch (Exception ex) { + logger.warn("Erro ao gerar preview de selo: {}", ex.getMessage()); + } + }); + } + + private void handleUploadLogo() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Emblema / Logotipo do Selo"); + chooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Imagens (*.png, *.jpg, *.jpeg, *.bmp)", "*.png", "*.jpg", "*.jpeg", "*.bmp"), + new FileChooser.ExtensionFilter("Todos os Arquivos", "*.*") + ); + File file = chooser.showOpenDialog(stage); + if (file != null) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + customLogoBase64 = Base64.getEncoder().encodeToString(bytes); + lblLogoStatus.setText("✓ Imagem carregada: " + file.getName()); + lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #16a34a; -fx-font-weight: bold;"); + btnClearLogo.setDisable(false); + updatePreview(); + } catch (Exception ex) { + showAlert(Alert.AlertType.ERROR, "Erro ao Carregar Imagem", "Não foi possível carregar o arquivo de imagem: " + ex.getMessage()); + } + } + } + + private void handleClearLogo() { + customLogoBase64 = null; + lblLogoStatus.setText("Nenhum logotipo carregado (usando emblema vetorial)"); + lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;"); + btnClearLogo.setDisable(true); + updatePreview(); + } + + private PdfSealTheme buildThemeFromForm(String themeId) { + String name = txtName.getText().trim(); + if (name.isBlank()) name = "Meu Selo Personalizado"; + + String icon = switch (cbIconStyle.getValue()) { + case "Cristal Tecnológico (CyberCore)" -> "crystal"; + case "Carimbo Notarial (Cartório)" -> "notary"; + case "Brasão Estelar (Governamental)" -> "star"; + default -> "shield"; + }; + + return new PdfSealTheme( + themeId, + name, + "Selo personalizado instalado pelo usuário", + 320, + 82, + toHex(cpPrimary.getValue()), + toHex(cpSecondary.getValue()), + toHex(cpBackground.getValue()), + toHex(cpBorder.getValue()), + toHex(cpText.getValue()), + null, + txtCustomHeader.getText().trim(), + txtCustomFooter.getText().trim(), + icon, + resolveCpfMode(), + chkIncludeDateTime.isSelected(), + true, + customLogoBase64, + txtCustomFieldLabel.getText().trim(), + txtCustomFieldValue.getText().trim() + ); + } + + private void loadThemeIntoForm(PdfSealTheme theme) { + if (theme == null) return; + if (theme.name() != null) txtName.setText(theme.name()); + if (theme.customHeader() != null) txtCustomHeader.setText(theme.customHeader()); + if (theme.customFooter() != null) txtCustomFooter.setText(theme.customFooter()); + if (theme.customFieldLabel() != null) txtCustomFieldLabel.setText(theme.customFieldLabel()); + if (theme.customFieldValue() != null) txtCustomFieldValue.setText(theme.customFieldValue()); + + if (theme.primaryColor() != null) cpPrimary.setValue(Color.web(theme.primaryColor())); + if (theme.secondaryColor() != null) cpSecondary.setValue(Color.web(theme.secondaryColor())); + if (theme.backgroundColor() != null) cpBackground.setValue(Color.web(theme.backgroundColor())); + if (theme.borderColor() != null) cpBorder.setValue(Color.web(theme.borderColor())); + if (theme.textColor() != null) cpText.setValue(Color.web(theme.textColor())); + + if (theme.defaultCpfMode() != null) { + chkIncludeCpf.setSelected(theme.defaultCpfMode() != CpfDisplayMode.NONE); + if (theme.defaultCpfMode() == CpfDisplayMode.MASKED) { + rbCpfMasked.setSelected(true); + } else if (theme.defaultCpfMode() == CpfDisplayMode.FULL) { + rbCpfFull.setSelected(true); + } + rbCpfMasked.setDisable(!chkIncludeCpf.isSelected()); + rbCpfFull.setDisable(!chkIncludeCpf.isSelected()); + } + + chkIncludeDateTime.setSelected(theme.defaultIncludeDateTime()); + + if (theme.customLogoBase64() != null && !theme.customLogoBase64().isBlank()) { + this.customLogoBase64 = theme.customLogoBase64(); + lblLogoStatus.setText("✓ Logotipo carregado com sucesso!"); + lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #16a34a; -fx-font-weight: bold;"); + btnClearLogo.setDisable(false); + } else { + this.customLogoBase64 = null; + lblLogoStatus.setText("Nenhum logotipo carregado (usando emblema vetorial)"); + lblLogoStatus.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;"); + btnClearLogo.setDisable(true); + } + + if (theme.iconStyle() != null) { + switch (theme.iconStyle().toLowerCase()) { + case "crystal" -> cbIconStyle.setValue("Cristal Tecnológico (CyberCore)"); + case "notary" -> cbIconStyle.setValue("Carimbo Notarial (Cartório)"); + case "star" -> cbIconStyle.setValue("Brasão Estelar (Governamental)"); + default -> cbIconStyle.setValue("Escudo de Segurança (ICP-Brasil)"); + } + } + } + + private CpfDisplayMode resolveCpfMode() { + if (!chkIncludeCpf.isSelected()) { + return CpfDisplayMode.NONE; + } + return rbCpfMasked.isSelected() ? CpfDisplayMode.MASKED : CpfDisplayMode.FULL; + } + + private void handleInstallSeal() { + String name = txtName.getText().trim(); + if (name.isBlank()) { + showAlert(Alert.AlertType.WARNING, "Nome Obrigatório", "Por favor, defina um nome para o seu selo."); + return; + } + + String sealId = "seal-" + UUID.randomUUID().toString().substring(0, 8); + PdfSealTheme theme = buildThemeFromForm(sealId); + + try { + PdfSealThemeRegistry.installCustomTheme(theme); + showAlert(Alert.AlertType.INFORMATION, "Selo Instalado com Sucesso!", + "O selo '" + theme.name() + "' foi instalado com sucesso e já está disponível para uso imediato em todas as assinaturas."); + + if (onSealInstalled != null) { + onSealInstalled.accept(theme); + } + stage.close(); + } catch (IOException e) { + showAlert(Alert.AlertType.ERROR, "Erro ao Instalar Selo", "Falha ao gravar arquivo de selo no disco: " + e.getMessage()); + } + } + + private void handleExportSeal() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Exportar Selo Digital (.json)"); + chooser.setInitialFileName("meu-selo.json"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json")); + + File file = chooser.showSaveDialog(stage); + if (file != null) { + try { + PdfSealTheme theme = buildThemeFromForm("exported-seal"); + PdfSealThemeRegistry.getStorage().exportSeal(theme, file.toPath()); + showAlert(Alert.AlertType.INFORMATION, "Selo Exportado", "O selo foi exportado com sucesso para:\n" + file.getAbsolutePath()); + } catch (Exception ex) { + showAlert(Alert.AlertType.ERROR, "Erro na Exportação", "Falha ao exportar selo: " + ex.getMessage()); + } + } + } + + private void handleImportSeal() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Arquivo de Selo (.json)"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json")); + + File file = chooser.showOpenDialog(stage); + if (file != null) { + try { + PdfSealTheme imported = PdfSealThemeRegistry.getStorage().importSeal(file.toPath()); + PdfSealThemeRegistry.registerTheme(imported); + + loadThemeIntoForm(imported); + updatePreview(); + + showAlert(Alert.AlertType.INFORMATION, "Selo Importado e Instalado", + "O selo '" + imported.name() + "' foi importado com sucesso e está pronto para uso!"); + + if (onSealInstalled != null) { + onSealInstalled.accept(imported); + } + stage.close(); + } catch (Exception ex) { + showAlert(Alert.AlertType.ERROR, "Erro na Importação", "Falha ao importar selo do arquivo: " + ex.getMessage()); + } + } + } + + private void showAlert(Alert.AlertType type, String title, String msg) { + Alert a = new Alert(type); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + a.initOwner(stage); + ThemeManager.applyTheme(a.getDialogPane().getScene()); + a.showAndWait(); + } + + private static String toHex(Color c) { + return String.format("#%02X%02X%02X", + (int) (c.getRed() * 255), + (int) (c.getGreen() * 255), + (int) (c.getBlue() * 255)); + } + + public void show() { + stage.centerOnScreen(); + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java new file mode 100644 index 0000000..71b347b --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SealManagementDialog.java @@ -0,0 +1,498 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.config.AgentConfiguration; +import br.com.cybercore.signer.core.config.AgentConfigurationService; +import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode; +import br.com.cybercore.signer.core.pdf.seal.PdfSealData; +import br.com.cybercore.signer.core.pdf.seal.PdfSealRenderer; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Diálogo Central de Gerenciamento e Seleção de Selos Digitais. + * Exibe a lista completa de selos disponíveis (embutidos e personalizados), + * permite selecionar o selo padrão ativo de assinatura, visualizar em tempo real, + * criar novos selos, editar/clonar, excluir, importar e exportar. + */ +@SuppressWarnings("this-escape") +public final class SealManagementDialog { + + private static final Logger logger = LoggerFactory.getLogger(SealManagementDialog.class); + + private final Stage stage; + private final AgentConfigurationService configService; + + private ListView listViewSeals; + private ImageView previewImageView; + private Label lblSelectedName; + private Label lblSelectedDesc; + private Label lblActiveBadge; + private Button btnSetDefault; + private Button btnEditClone; + private Button btnDelete; + private Button btnExport; + private Label lblFeedback; + + private String currentDefaultThemeId; + + public SealManagementDialog(Stage owner) { + this.configService = new AgentConfigurationService(); + AgentConfiguration currentConfig = configService.loadConfiguration(); + this.currentDefaultThemeId = currentConfig.defaultSealThemeId(); + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Gerenciamento e Seleção de Selos Digitais"); + this.stage.setResizable(true); + this.stage.setMinWidth(840); + this.stage.setMinHeight(520); + + initLayout(); + refreshSealsList(); + } + + private void initLayout() { + VBox root = new VBox(14); + root.setPadding(new Insets(18)); + root.getStyleClass().add("dialog-container"); + + // Topo / Cabeçalho + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(3); + Label lblTitle = new Label("GERENCIAMENTO DE SELOS DIGITAIS"); + lblTitle.getStyleClass().add("dialog-header"); + Label lblSub = new Label("Escolha o selo padrão ativo para assinar PDFs, crie novas identidades visuais ou personalize selos instalados."); + lblSub.getStyleClass().add("dialog-subtitle"); + titleBox.getChildren().addAll(lblTitle, lblSub); + header.getChildren().addAll(SvgIcons.sealIcon(), titleBox); + + // Painel Principal Dividido: Lista à esquerda, Detalhes e Preview à direita + HBox mainContent = new HBox(16); + VBox.setVgrow(mainContent, Priority.ALWAYS); + + // --- Coluna Esquerda: Lista de Selos --- + VBox leftColumn = new VBox(10); + leftColumn.setPrefWidth(330); + leftColumn.setMinWidth(300); + leftColumn.getStyleClass().add("dialog-box"); + + HBox listHeader = new HBox(8); + listHeader.setAlignment(Pos.CENTER_LEFT); + Label lblListTitle = new Label("Selos Disponíveis no Sistema"); + lblListTitle.getStyleClass().add("dialog-field-title"); + listHeader.getChildren().add(lblListTitle); + + listViewSeals = new ListView<>(); + VBox.setVgrow(listViewSeals, Priority.ALWAYS); + listViewSeals.setStyle("-fx-background-color: transparent; -fx-background-radius: 6px;"); + listViewSeals.setCellFactory(lv -> new SealListCell()); + listViewSeals.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> onThemeSelected(newV)); + + HBox listActionRow = new HBox(8); + listActionRow.setAlignment(Pos.CENTER_LEFT); + + Button btnCreateNew = new Button("+ Novo Selo"); + btnCreateNew.getStyleClass().add("btn-primary"); + btnCreateNew.setOnAction(e -> handleCreateNewSeal()); + HBox.setHgrow(btnCreateNew, Priority.ALWAYS); + btnCreateNew.setMaxWidth(Double.MAX_VALUE); + + Button btnImport = new Button("📥 Importar"); + btnImport.getStyleClass().add("btn-secondary"); + btnImport.setOnAction(e -> handleImportSeal()); + HBox.setHgrow(btnImport, Priority.ALWAYS); + btnImport.setMaxWidth(Double.MAX_VALUE); + + listActionRow.getChildren().addAll(btnCreateNew, btnImport); + + leftColumn.getChildren().addAll(listHeader, listViewSeals, listActionRow); + + // --- Coluna Direita: Pré-Visualização e Ações do Selo Selecionado --- + VBox rightColumn = new VBox(10); + rightColumn.setPrefWidth(430); + rightColumn.setMinWidth(380); + rightColumn.getStyleClass().add("dialog-box"); + rightColumn.setAlignment(Pos.TOP_CENTER); + HBox.setHgrow(rightColumn, Priority.ALWAYS); + + Label lblPreviewHeader = new Label("PRÉ-VISUALIZAÇÃO EM TEMPO REAL"); + lblPreviewHeader.getStyleClass().add("dialog-field-title"); + + lblSelectedName = new Label("Selecione um selo para visualizar"); + lblSelectedName.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + lblSelectedDesc = new Label(""); + lblSelectedDesc.setStyle("-fx-font-size: 11.5px; -fx-text-fill: #64748b;"); + lblSelectedDesc.setWrapText(true); + + previewImageView = new ImageView(); + previewImageView.setPreserveRatio(true); + previewImageView.setFitWidth(380); + + HBox previewContainer = new HBox(previewImageView); + previewContainer.setAlignment(Pos.CENTER); + previewContainer.setPadding(new Insets(14, 10, 14, 10)); + previewContainer.setStyle("-fx-background-color: #090d16; -fx-background-radius: 8px; -fx-border-color: #334155; -fx-border-radius: 8px;"); + VBox.setVgrow(previewContainer, Priority.ALWAYS); + + lblActiveBadge = new Label(""); + lblActiveBadge.setStyle("-fx-font-weight: bold; -fx-font-size: 12px;"); + + lblFeedback = new Label(""); + lblFeedback.setStyle("-fx-font-size: 12px; -fx-font-weight: bold; -fx-text-fill: #16a34a;"); + + // Botões de Ação do Selo Selecionado + VBox sealActions = new VBox(8); + sealActions.setAlignment(Pos.CENTER); + + btnSetDefault = new Button("✔ Definir como Selo Padrão Ativo"); + btnSetDefault.getStyleClass().add("btn-primary"); + btnSetDefault.setMaxWidth(Double.MAX_VALUE); + btnSetDefault.setPrefHeight(34); + btnSetDefault.setOnAction(e -> handleSetDefault()); + + HBox secondaryActions = new HBox(8); + secondaryActions.setAlignment(Pos.CENTER); + + btnEditClone = new Button("✏ Clonar"); + btnEditClone.getStyleClass().add("btn-secondary"); + btnEditClone.setPrefHeight(32); + HBox.setHgrow(btnEditClone, Priority.ALWAYS); + btnEditClone.setMaxWidth(Double.MAX_VALUE); + btnEditClone.setOnAction(e -> handleEditClone()); + + btnExport = new Button("📥 Exportar"); + btnExport.getStyleClass().add("btn-secondary"); + btnExport.setPrefHeight(32); + HBox.setHgrow(btnExport, Priority.ALWAYS); + btnExport.setMaxWidth(Double.MAX_VALUE); + btnExport.setOnAction(e -> handleExportSeal()); + + btnDelete = new Button("✖ Excluir"); + btnDelete.getStyleClass().addAll("button", "btn-danger"); + btnDelete.setAlignment(Pos.CENTER); + btnDelete.setPrefHeight(32); + HBox.setHgrow(btnDelete, Priority.ALWAYS); + btnDelete.setMaxWidth(Double.MAX_VALUE); + btnDelete.setOnAction(e -> handleDeleteCustomSeal()); + + secondaryActions.getChildren().addAll(btnEditClone, btnExport, btnDelete); + sealActions.getChildren().addAll(btnSetDefault, secondaryActions); + + rightColumn.getChildren().addAll(lblPreviewHeader, lblSelectedName, lblSelectedDesc, previewContainer, lblActiveBadge, lblFeedback, sealActions); + + mainContent.getChildren().addAll(leftColumn, rightColumn); + + // Rodapé + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> stage.close()); + + footer.getChildren().add(btnClose); + + root.getChildren().addAll(header, mainContent, footer); + + Scene scene = new Scene(root, 880, 560); + ThemeManager.applyTheme(scene); + stage.setScene(scene); + } + + private void refreshSealsList() { + List themes = PdfSealThemeRegistry.listThemes(); + listViewSeals.setItems(FXCollections.observableArrayList(themes)); + + // Seleciona o selo padrão ativo ou o primeiro disponível + PdfSealTheme toSelect = themes.stream() + .filter(t -> t.id().equalsIgnoreCase(currentDefaultThemeId)) + .findFirst() + .orElse(themes.isEmpty() ? null : themes.get(0)); + + if (toSelect != null) { + listViewSeals.getSelectionModel().select(toSelect); + } + } + + private void onThemeSelected(PdfSealTheme theme) { + lblFeedback.setText(""); + if (theme == null) { + lblSelectedName.setText("Nenhum selo selecionado"); + lblSelectedDesc.setText(""); + lblActiveBadge.setText(""); + btnSetDefault.setDisable(true); + btnEditClone.setDisable(true); + btnDelete.setDisable(true); + btnExport.setDisable(true); + previewImageView.setImage(null); + return; + } + + lblSelectedName.setText(theme.name() + " (" + theme.id() + ")"); + lblSelectedDesc.setText(theme.description() != null ? theme.description() : "Selo visual para assinatura"); + + boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultThemeId); + if (isDefault) { + lblActiveBadge.setText("★ ESTE É O SELO PADRÃO ATIVO PARA TODAS AS ASSINATURAS"); + lblActiveBadge.setStyle("-fx-text-fill: #38bdf8; -fx-font-weight: bold; -fx-font-size: 11.5px;"); + btnSetDefault.setText("★ Selo Padrão Ativo no Sistema"); + btnSetDefault.setDisable(true); + btnSetDefault.setStyle("-fx-opacity: 0.95; -fx-background-color: #0369a1; -fx-text-fill: #ffffff; -fx-font-weight: bold;"); + } else { + lblActiveBadge.setText("Selo disponível para uso."); + lblActiveBadge.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 11.5px;"); + btnSetDefault.setText("✔ Definir como Selo Padrão Ativo"); + btnSetDefault.setDisable(false); + btnSetDefault.setStyle(""); + } + + btnEditClone.setDisable(false); + btnExport.setDisable(false); + btnDelete.setDisable(!theme.isCustom()); + + renderThemePreview(theme); + } + + private void renderThemePreview(PdfSealTheme theme) { + Platform.runLater(() -> { + try { + PdfSealData sampleData = new PdfSealData( + "Dr. Carlos Alberto de Oliveira Santos Júnior", + "CPF: 12345678900", + null, + "AC CyberCore JUS v5", + "8A7F4B2C91D0E3F5", + "Assinatura de Documento Digital", + "São Paulo, SP", + theme.defaultCpfMode() != null ? theme.defaultCpfMode() : CpfDisplayMode.FULL, + theme.defaultIncludeDateTime(), + theme.formatCustomField() + ); + + byte[] pngBytes = PdfSealRenderer.renderToPngBytes(theme, sampleData); + previewImageView.setImage(new Image(new ByteArrayInputStream(pngBytes))); + } catch (Exception ex) { + logger.warn("Erro ao renderizar preview do selo: {}", ex.getMessage()); + } + }); + } + + private void handleSetDefault() { + PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + try { + AgentConfiguration currentConfig = configService.loadConfiguration(); + AgentConfiguration newConfig = new AgentConfiguration( + currentConfig.requireConfirmation(), + currentConfig.alwaysPromptPin(), + currentConfig.autoStart(), + selected.id(), + currentConfig.defaultCpfDisplayMode(), + currentConfig.defaultIncludeDateTime(), + currentConfig.serverPort() + ); + configService.saveConfiguration(newConfig); + this.currentDefaultThemeId = selected.id(); + + lblFeedback.setText("✓ Selo '" + selected.name() + "' definido como padrão com sucesso!"); + listViewSeals.refresh(); + onThemeSelected(selected); + } catch (Exception ex) { + showError("Falha ao salvar configuração: " + ex.getMessage()); + } + } + + private void handleCreateNewSeal() { + new SealDesignerDialog(stage, createdSeal -> { + refreshSealsList(); + listViewSeals.getSelectionModel().select(createdSeal); + lblFeedback.setText("✓ Novo selo '" + createdSeal.name() + "' instalado!"); + }).show(); + } + + private void handleEditClone() { + PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + new SealDesignerDialog(stage, selected, updatedSeal -> { + refreshSealsList(); + listViewSeals.getSelectionModel().select(updatedSeal); + lblFeedback.setText("✓ Selo '" + updatedSeal.name() + "' salvo com sucesso!"); + }).show(); + } + + private void handleDeleteCustomSeal() { + PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem(); + if (selected == null || !selected.isCustom()) return; + + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmar Exclusão"); + confirm.setHeaderText(null); + confirm.setContentText("Deseja realmente remover o selo personalizado '" + selected.name() + "'?"); + confirm.initOwner(stage); + ThemeManager.applyTheme(confirm.getDialogPane().getScene()); + + Optional result = confirm.showAndWait(); + if (result.isPresent() && result.get() == ButtonType.OK) { + try { + PdfSealThemeRegistry.uninstallCustomTheme(selected.id()); + if (selected.id().equalsIgnoreCase(currentDefaultThemeId)) { + currentDefaultThemeId = PdfSealThemeRegistry.DEFAULT_THEME_ID; + } + refreshSealsList(); + } catch (Exception ex) { + showError("Falha ao excluir selo: " + ex.getMessage()); + } + } + } + + private void handleImportSeal() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Arquivo de Selo (.json)"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json")); + + File file = chooser.showOpenDialog(stage); + if (file != null) { + try { + PdfSealTheme imported = PdfSealThemeRegistry.getStorage().importSeal(file.toPath()); + PdfSealThemeRegistry.registerTheme(imported); + refreshSealsList(); + listViewSeals.getSelectionModel().select(imported); + lblFeedback.setText("✓ Selo '" + imported.name() + "' importado e instalado com sucesso!"); + } catch (Exception ex) { + showError("Falha ao importar selo: " + ex.getMessage()); + } + } + } + + private void handleExportSeal() { + PdfSealTheme selected = listViewSeals.getSelectionModel().getSelectedItem(); + if (selected == null) return; + + FileChooser chooser = new FileChooser(); + chooser.setTitle("Exportar Selo Digital (.json)"); + chooser.setInitialFileName(selected.id() + ".json"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Arquivos JSON de Selo (*.json)", "*.json")); + + File file = chooser.showSaveDialog(stage); + if (file != null) { + try { + PdfSealThemeRegistry.getStorage().exportSeal(selected, file.toPath()); + lblFeedback.setText("✓ Selo exportado com sucesso para " + file.getName()); + } catch (Exception ex) { + showError("Falha ao exportar selo: " + ex.getMessage()); + } + } + } + + private void showError(String msg) { + Alert a = new Alert(Alert.AlertType.ERROR); + a.setTitle("Erro"); + a.setHeaderText(null); + a.setContentText(msg); + a.initOwner(stage); + ThemeManager.applyTheme(a.getDialogPane().getScene()); + a.showAndWait(); + } + + public void show() { + stage.centerOnScreen(); + stage.show(); + } + + public void showAndWait() { + stage.centerOnScreen(); + stage.showAndWait(); + } + + /** + * Célula customizada para a listagem visual dos selos. + */ + private class SealListCell extends ListCell { + @Override + protected void updateItem(PdfSealTheme item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setGraphic(null); + setText(null); + } else { + HBox card = new HBox(10); + card.setAlignment(Pos.CENTER_LEFT); + card.setPadding(new Insets(8, 10, 8, 10)); + + VBox textBox = new VBox(2); + Label lblName = new Label(item.name()); + lblName.setStyle("-fx-font-weight: bold; -fx-font-size: 12.5px;"); + lblName.maxWidthProperty().bind(listViewSeals.widthProperty().subtract(120)); + lblName.setWrapText(true); + + String desc = (item.description() != null && !item.description().isBlank()) + ? item.description() : "Identidade visual de assinatura"; + Label lblDesc = new Label(desc); + lblDesc.setStyle("-fx-font-size: 11px; -fx-text-fill: #64748b;"); + lblDesc.maxWidthProperty().bind(listViewSeals.widthProperty().subtract(120)); + lblDesc.setWrapText(true); + + textBox.getChildren().addAll(lblName, lblDesc); + HBox.setHgrow(textBox, Priority.ALWAYS); + + VBox badgesBox = new VBox(4); + badgesBox.setAlignment(Pos.CENTER_RIGHT); + + boolean isDefault = item.id().equalsIgnoreCase(currentDefaultThemeId); + if (isDefault) { + Label badgeDefault = new Label("★ PADRÃO"); + badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-font-size: 9.5px; -fx-font-weight: bold; -fx-padding: 2px 6px; -fx-background-radius: 4px;"); + badgesBox.getChildren().add(badgeDefault); + } + + Label badgeType = new Label(item.isCustom() ? "Personalizado" : "Sistema"); + badgeType.setStyle(item.isCustom() + ? "-fx-background-color: #fef3c7; -fx-text-fill: #b45309; -fx-font-size: 9.5px; -fx-padding: 2px 6px; -fx-background-radius: 4px;" + : "-fx-background-color: #e0f2fe; -fx-text-fill: #0369a1; -fx-font-size: 9.5px; -fx-padding: 2px 6px; -fx-background-radius: 4px;"); + badgesBox.getChildren().add(badgeType); + + card.getChildren().addAll(textBox, badgesBox); + setGraphic(card); + setText(null); + } + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java new file mode 100644 index 0000000..946f3ed --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SettingsView.java @@ -0,0 +1,296 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.config.AgentConfiguration; +import br.com.cybercore.signer.core.config.AgentConfigurationService; +import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme; +import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry; +import br.com.cybercore.signer.core.security.AuthorizedApplication; +import br.com.cybercore.signer.core.security.PinSessionManager; +import br.com.cybercore.signer.core.security.SmartCardSessionManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import java.util.List; + +/** + * Painel de Configurações, Gerenciamento de Selos Visuais e Aplicações Autorizadas. + */ +@SuppressWarnings("this-escape") +public final class SettingsView extends VBox { + + private final VBox appsContainer = new VBox(10); + private final VBox sealsContainer = new VBox(8); + private final Label saveFeedbackLabel = new Label(); + + private java.util.function.Consumer onRevokeApplication; + + public SettingsView() { + initView(); + } + + private void initView() { + setSpacing(16); + setPadding(new Insets(20, 24, 20, 24)); + getStyleClass().add("content-pane"); + + AgentConfigurationService configService = AgentConfigurationService.getInstance(); + AgentConfiguration config = configService.getConfig(); + + // Header + VBox headerBox = new VBox(6); + Label titleLabel = new Label("Configurações do Agente"); + titleLabel.getStyleClass().add("brand-title"); + + Label subtitleLabel = new Label("Parâmetros de segurança local, identidade visual dos selos e aplicações autorizadas."); + subtitleLabel.getStyleClass().add("brand-subtitle"); + + saveFeedbackLabel.setStyle("-fx-text-fill: #10b981; -fx-font-size: 11.5px; -fx-font-weight: bold;"); + + headerBox.getChildren().addAll(titleLabel, subtitleLabel, saveFeedbackLabel); + + // Seção Segurança + VBox secBox = new VBox(12); + secBox.getStyleClass().add("dialog-box"); + secBox.setPadding(new Insets(14, 16, 14, 16)); + Label secTitle = new Label("Segurança e Confirmação"); + secTitle.getStyleClass().add("section-title"); + + CheckBox chkConfirmAlways = new CheckBox("Sempre exigir confirmação na tela antes de assinar (Recomendado)"); + chkConfirmAlways.setSelected(config.requireConfirmation()); + chkConfirmAlways.setOnAction(e -> { + boolean sel = chkConfirmAlways.isSelected(); + AgentConfiguration current = configService.getConfig(); + configService.updateConfig(new AgentConfiguration( + sel, + current.alwaysPromptPin(), + current.autoStart(), + current.defaultSealThemeId(), + current.defaultCpfDisplayMode(), + current.defaultIncludeDateTime(), + current.serverPort() + )); + notifySave("Preferência de confirmação na tela salva."); + }); + + CheckBox chkAlwaysAskPin = new CheckBox("Sempre exigir digitação de PIN para Tokens A3 (Sem cache)"); + chkAlwaysAskPin.setSelected(config.alwaysPromptPin()); + PinSessionManager.getInstance().setAlwaysPromptPin(config.alwaysPromptPin()); + + chkAlwaysAskPin.setOnAction(e -> { + boolean sel = chkAlwaysAskPin.isSelected(); + PinSessionManager.getInstance().setAlwaysPromptPin(sel); + if (sel) { + SmartCardSessionManager.resetCardSessions(); + } + AgentConfiguration current = configService.getConfig(); + configService.updateConfig(new AgentConfiguration( + current.requireConfirmation(), + sel, + current.autoStart(), + current.defaultSealThemeId(), + current.defaultCpfDisplayMode(), + current.defaultIncludeDateTime(), + current.serverPort() + )); + notifySave("Política de PIN atualizada."); + }); + + CheckBox chkAutoStart = new CheckBox("Iniciar agente automaticamente com o sistema operacional"); + chkAutoStart.setSelected(config.autoStart()); + chkAutoStart.setOnAction(e -> { + boolean sel = chkAutoStart.isSelected(); + AgentConfiguration current = configService.getConfig(); + configService.updateConfig(new AgentConfiguration( + current.requireConfirmation(), + current.alwaysPromptPin(), + sel, + current.defaultSealThemeId(), + current.defaultCpfDisplayMode(), + current.defaultIncludeDateTime(), + current.serverPort() + )); + notifySave("Preferência de inicialização salva."); + }); + + HBox pinActionsBox = new HBox(12); + pinActionsBox.setAlignment(Pos.CENTER_LEFT); + + Button btnClearPins = new Button("Limpar PINs Salvos na Sessão"); + btnClearPins.getStyleClass().add("btn-secondary"); + btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;"); + + Label pinStatusLabel = new Label(); + pinStatusLabel.setStyle("-fx-text-fill: #34d399; -fx-font-size: 12px;"); + + btnClearPins.setOnAction(e -> { + PinSessionManager.getInstance().clearAll(); + SmartCardSessionManager.resetCardSessions(); + pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados."); + }); + + pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel); + secBox.getChildren().addAll(secTitle, chkConfirmAlways, chkAlwaysAskPin, chkAutoStart, pinActionsBox); + + // Seção Selos Visuais de Assinatura + VBox sealsBox = new VBox(10); + sealsBox.getStyleClass().add("dialog-box"); + sealsBox.setPadding(new Insets(14, 16, 14, 16)); + Label sealsTitle = new Label("Selos Visuais e Identidade Gráfica"); + sealsTitle.getStyleClass().add("section-title"); + + HBox sealHeaderRow = new HBox(12); + sealHeaderRow.setAlignment(Pos.CENTER_LEFT); + + Label lblSealsDesc = new Label("Gerencie os temas e identidades gráficas instaladas para assinaturas PDF:"); + lblSealsDesc.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 12px;"); + HBox.setHgrow(lblSealsDesc, Priority.ALWAYS); + + Button btnCreateSeal = new Button("🎨 Criar ou Instalar Selo..."); + btnCreateSeal.getStyleClass().add("btn-primary"); + btnCreateSeal.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;"); + btnCreateSeal.setOnAction(e -> openSealDesigner()); + + sealHeaderRow.getChildren().addAll(lblSealsDesc, btnCreateSeal); + + refreshSealsList(); + + sealsBox.getChildren().addAll(sealsTitle, sealHeaderRow, sealsContainer); + + // Seção Aplicações Autorizadas + VBox appsBox = new VBox(10); + appsBox.getStyleClass().add("dialog-box"); + appsBox.setPadding(new Insets(14, 16, 14, 16)); + Label appsTitle = new Label("Aplicações Web Autorizadas"); + appsTitle.getStyleClass().add("section-title"); + + ScrollPane scrollApps = new ScrollPane(appsContainer); + scrollApps.setFitToWidth(true); + scrollApps.setPrefHeight(150); + scrollApps.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + + appsBox.getChildren().addAll(appsTitle, scrollApps); + + getChildren().addAll(headerBox, secBox, sealsBox, appsBox); + } + + private void openSealDesigner() { + Stage ownerStage = (getScene() != null && getScene().getWindow() instanceof Stage s) ? s : null; + new SealDesignerDialog(ownerStage, newSeal -> { + refreshSealsList(); + notifySave("Novo selo '" + newSeal.name() + "' instalado com sucesso!"); + }).show(); + } + + private void refreshSealsList() { + sealsContainer.getChildren().clear(); + List themes = PdfSealThemeRegistry.listThemes(); + + for (PdfSealTheme theme : themes) { + HBox item = new HBox(10); + item.setAlignment(Pos.CENTER_LEFT); + item.getStyleClass().add("diagnostic-item"); + item.setPadding(new Insets(6, 12, 6, 12)); + + VBox info = new VBox(2); + Label lblName = new Label(theme.name() + " (" + theme.id() + ")"); + lblName.getStyleClass().add("cert-title"); + + String desc = theme.description() != null ? theme.description() : "Selo visual"; + Label lblDesc = new Label(desc); + lblDesc.getStyleClass().add("cert-detail"); + + info.getChildren().addAll(lblName, lblDesc); + HBox.setHgrow(info, Priority.ALWAYS); + + Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema"); + badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok"); + + item.getChildren().addAll(info, badge); + + if (theme.isCustom()) { + Button btnDelete = new Button("Excluir"); + btnDelete.getStyleClass().add("btn-secondary"); + btnDelete.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px; -fx-text-fill: #f87171;"); + btnDelete.setOnAction(e -> { + try { + PdfSealThemeRegistry.uninstallCustomTheme(theme.id()); + refreshSealsList(); + notifySave("Selo '" + theme.name() + "' removido."); + } catch (Exception ex) { + showError("Falha ao remover selo: " + ex.getMessage()); + } + }); + item.getChildren().add(btnDelete); + } + + sealsContainer.getChildren().add(item); + } + } + + private void notifySave(String text) { + saveFeedbackLabel.setText("✓ " + text); + } + + private void showError(String msg) { + Alert a = new Alert(Alert.AlertType.ERROR); + a.setTitle("Erro"); + a.setHeaderText(null); + a.setContentText(msg); + a.showAndWait(); + } + + public void setOnRevokeApplication(java.util.function.Consumer onRevokeApplication) { + this.onRevokeApplication = onRevokeApplication; + } + + public void updateApplications(List applications) { + appsContainer.getChildren().clear(); + + if (applications == null || applications.isEmpty()) { + Label emptyLabel = new Label("Nenhuma aplicação autorizada registrada ainda.\nNovas aplicações solicitarão permissão no primeiro acesso."); + emptyLabel.getStyleClass().add("section-subtitle"); + appsContainer.getChildren().add(emptyLabel); + return; + } + + for (AuthorizedApplication app : applications) { + HBox card = new HBox(12); + card.setAlignment(Pos.CENTER_LEFT); + card.getStyleClass().add("diagnostic-item"); + + VBox info = new VBox(4); + Label nameLabel = new Label(app.name() + " (" + app.applicationId() + ")"); + nameLabel.getStyleClass().add("cert-title"); + + Label originsLabel = new Label("Origens: " + String.join(", ", app.allowedOrigins())); + originsLabel.getStyleClass().add("cert-detail"); + + info.getChildren().addAll(nameLabel, originsLabel); + HBox.setHgrow(info, Priority.ALWAYS); + + Label statusLabel = new Label(app.enabled() ? "Autorizada" : "Revogada"); + statusLabel.getStyleClass().add(app.enabled() ? "diagnostic-value-ok" : "diagnostic-value-warn"); + + Button btnRevoke = new Button("Revogar"); + btnRevoke.getStyleClass().add("btn-secondary"); + btnRevoke.setStyle("-fx-padding: 4px 10px; -fx-font-size: 11px; -fx-text-fill: #f87171;"); + btnRevoke.setOnAction(e -> { + if (onRevokeApplication != null) { + onRevokeApplication.accept(app.applicationId()); + } + }); + + card.getChildren().addAll(info, statusLabel, btnRevoke); + appsContainer.getChildren().add(card); + } + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java new file mode 100644 index 0000000..e304a75 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignaturePromptDialog.java @@ -0,0 +1,259 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.domain.SigningContext; +import br.com.cybercore.signer.core.security.PinSessionManager; +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.util.HexFormat; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Diálogo modal de autorização explícita do usuário para assinatura digital, + * com gerenciamento flexível de PIN para tokens A3 (sempre pedir PIN vs salvar na sessão). + */ +public class SignaturePromptDialog { + + private final Stage stage; + private final CompletableFuture approvalFuture = new CompletableFuture<>(); + + public SignaturePromptDialog(Stage owner, String applicationName, String origin, + ManagedCertificate certificate, SigningContext context) { + this(owner, applicationName, origin, certificate, context, null, null, false); + } + + public SignaturePromptDialog(Stage owner, String applicationName, String origin, + ManagedCertificate certificate, SigningContext context, + String documentName, String documentType, boolean isAuthentication) { + stage = new Stage(); + stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + stage.initOwner(owner); + } + stage.setTitle(isAuthentication + ? "CyberCore Signer — Solicitação de Login / Autenticação" + : "CyberCore Signer — Solicitação de Assinatura"); + stage.setResizable(false); + + initLayout(applicationName, origin, certificate, context, documentName, documentType, isAuthentication); + } + + private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context, + String documentName, String documentType, boolean isAuthentication) { + VBox root = new VBox(18); + root.getStyleClass().add("dialog-container"); + root.setPrefWidth(540); + + Label header = new Label(isAuthentication ? "🔑 SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA"); + header.getStyleClass().add("dialog-header"); + + 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 a assinatura digital de um documento."); + desc.getStyleClass().add("brand-subtitle"); + + // Caixa de Detalhes + VBox detailsBox = new VBox(12); + detailsBox.getStyleClass().add("dialog-box"); + + GridPane grid = new GridPane(); + grid.setHgap(16); + grid.setVgap(10); + + int row = 0; + addField(grid, row++, "Sistema:", appName + " (" + (origin != null ? origin : "Local") + ")"); + if (isAuthentication) { + addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)"); + } else { + if (documentName != null && !documentName.isBlank()) { + String docLabel = (documentType != null && !documentType.isBlank()) + ? "[" + documentType.toUpperCase() + "] " + documentName + : documentName; + addField(grid, row++, "Documento:", docLabel); + } + addField(grid, row++, "Operação:", "Assinatura Digital (Digest)"); + } + addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias()); + addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " + + (cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel())); + addField(grid, row++, "Algoritmo:", context.algorithm()); + + detailsBox.getChildren().add(grid); + + // Caixa do Hash / Digest + VBox hashBox = new VBox(6); + Label hashLabel = new Label("Hash do Documento (SHA-256 Digest):"); + hashLabel.getStyleClass().add("dialog-field-title"); + + String hashHex = HexFormat.of().formatHex(context.digest()); + Label hashValue = new Label(hashHex); + hashValue.getStyleClass().add("dialog-hash-value"); + hashValue.setWrapText(true); + + hashBox.getChildren().addAll(hashLabel, hashValue); + detailsBox.getChildren().add(hashBox); + + // Seção de PIN para Tokens / Smart Cards A3 + VBox pinContainer = new VBox(10); + PasswordField pinField = new PasswordField(); + CheckBox cbSavePin = new CheckBox("Salvar PIN durante esta sessão (não solicitar nas próximas assinaturas)"); + cbSavePin.setStyle("-fx-font-size: 12px;"); + + boolean isA3 = cert.type() == CertificateType.A3; + boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId()); + boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id()); + + if (isA3) { + pinContainer.getStyleClass().add("dialog-box"); + Label pinSectionTitle = new Label("AUTENTICAÇÃO DO TOKEN / SMART CARD A3"); + pinSectionTitle.getStyleClass().add("dialog-field-title"); + pinContainer.getChildren().add(pinSectionTitle); + + if (isWindowsStore) { + VBox winBox = new VBox(6); + Label winTitle = new Label("🔐 Autenticação nativa pelo Windows / Driver do Token"); + winTitle.setStyle("-fx-text-fill: -fx-accent; -fx-font-weight: bold; -fx-font-size: 13px;"); + + Label winDesc = new Label( + "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." + ); + winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); + winDesc.setWrapText(true); + + winBox.getChildren().addAll(winTitle, winDesc); + pinContainer.getChildren().add(winBox); + } else { + pinField.setPromptText("Digite o PIN do token..."); + + if (hasStoredPin) { + HBox rememberedBox = new HBox(12); + rememberedBox.setAlignment(Pos.CENTER_LEFT); + + Label rememberedBadge = new Label("● PIN salvo nesta sessão ativa"); + rememberedBadge.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 12px;"); + + Button btnChangePin = new Button("Alterar PIN"); + btnChangePin.getStyleClass().add("btn-secondary"); + btnChangePin.setStyle("-fx-padding: 4px 10px; -fx-font-size: 11px;"); + + VBox inputWrapper = new VBox(8); + inputWrapper.setVisible(false); + inputWrapper.setManaged(false); + inputWrapper.getChildren().addAll(new Label("Novo PIN:"), pinField, cbSavePin); + + btnChangePin.setOnAction(e -> { + boolean nowVisible = !inputWrapper.isVisible(); + inputWrapper.setVisible(nowVisible); + inputWrapper.setManaged(nowVisible); + btnChangePin.setText(nowVisible ? "Cancelar alteração" : "Alterar PIN"); + if (nowVisible) { + cbSavePin.setSelected(true); + } + }); + + rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin); + pinContainer.getChildren().addAll(rememberedBox, inputWrapper); + } else { + VBox inputWrapper = new VBox(8); + 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;"); + + cbSavePin.setSelected(false); + inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin); + pinContainer.getChildren().add(inputWrapper); + } + } + + detailsBox.getChildren().add(pinContainer); + } + + // Label de Alerta de Validação + Label validationLabel = new Label(); + validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;"); + validationLabel.setVisible(false); + + // Botões de Ação + HBox actionsRow = new HBox(12); + actionsRow.setAlignment(Pos.CENTER_RIGHT); + + Button btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-cancel"); + btnCancel.setOnAction(e -> { + approvalFuture.complete(false); + stage.close(); + }); + + Button btnConfirm = new Button(isAuthentication ? "Confirmar e Entrar" : "Confirmar e Assinar"); + btnConfirm.getStyleClass().add("btn-primary"); + btnConfirm.setDefaultButton(true); + btnConfirm.setOnAction(e -> { + if (isA3 && !isWindowsStore) { + String typedPin = pinField.getText(); + boolean pinProvided = (typedPin != null && !typedPin.isBlank()); + + if (!hasStoredPin && !pinProvided) { + validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11."); + validationLabel.setVisible(true); + pinField.requestFocus(); + return; + } + + if (pinProvided) { + char[] pinChars = typedPin.toCharArray(); + if (cbSavePin.isSelected()) { + PinSessionManager.getInstance().savePin(cert.id(), pinChars); + } else { + PinSessionManager.getInstance().clearPin(cert.id()); + } + } + } + + approvalFuture.complete(true); + stage.close(); + }); + + actionsRow.getChildren().addAll(btnCancel, btnConfirm); + + root.getChildren().addAll(header, desc, detailsBox, validationLabel, actionsRow); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + stage.setOnCloseRequest(e -> approvalFuture.complete(false)); + } + + private void addField(GridPane grid, int row, String title, String value) { + Label titleLabel = new Label(title); + titleLabel.getStyleClass().add("dialog-field-title"); + + Label valLabel = new Label(value); + valLabel.getStyleClass().add("dialog-field-value"); + + grid.add(titleLabel, 0, row); + grid.add(valLabel, 1, row); + } + + public CompletableFuture showAndAwait() { + stage.show(); + stage.toFront(); + return approvalFuture; + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignatureValidationDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignatureValidationDialog.java new file mode 100644 index 0000000..edcc2a1 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/SignatureValidationDialog.java @@ -0,0 +1,395 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.validation.SignatureValidationService; +import br.com.cybercore.signer.core.validation.ValidationResult; +import br.com.cybercore.signer.core.validation.ValidationSignerInfo; +import br.com.cybercore.signer.core.validation.ValidationStatus; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; + +/** + * Diálogo unificado para Validação Criptográfica de Assinaturas Digitais. + * Suporta Assinatura Incorporada / Embutida (Attached / PAdES / P7M / XML) + * e Assinatura Destacada (Detached / P7S). + */ +@SuppressWarnings("this-escape") +public final class SignatureValidationDialog { + + private static final Logger logger = LoggerFactory.getLogger(SignatureValidationDialog.class); + + private final Stage stage; + private final boolean isTimestamp; + private final SignatureValidationService validationService = new SignatureValidationService(); + + private RadioButton rbAttached; + private RadioButton rbDetached; + + private VBox docBox; + private TextField txtDocPath; + private TextField txtSigPath; + private Label lblSigPath; + + private File docFile; + private File sigFile; + + private VBox resultBox; + private Button btnSaveExtracted; + private byte[] lastExtractedContent; + + public SignatureValidationDialog(Stage owner, boolean isTimestamp) { + this.isTimestamp = isTimestamp; + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle(isTimestamp ? "CyberCore Signer — Validação de Carimbo do Tempo" : "CyberCore Signer — Validação de Assinatura Digital"); + this.stage.setResizable(true); + this.stage.setMinWidth(680); + this.stage.setMinHeight(520); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(22)); + root.getStyleClass().add("dialog-container"); + + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll( + isTimestamp ? SvgIcons.validateTimestampIcon() : SvgIcons.validateIcon(), + createHeaderLabel( + isTimestamp ? "VALIDAÇÃO DE CARIMBO DO TEMPO" : "VALIDAÇÃO DE ASSINATURA DIGITAL", + isTimestamp ? "Verifique a autenticidade e conformidade RFC 3161 TSP." + : "Verifique a autenticidade, integridade e certificados de assinaturas incorporadas ou destacadas." + ) + ); + + // Seletor de Tipo de Assinatura (Incorporada vs Destacada) + VBox typeBox = new VBox(8); + typeBox.getStyleClass().add("dialog-box"); + Label lblTypeTitle = new Label("Tipo de Assinatura para Validação:"); + lblTypeTitle.getStyleClass().add("dialog-field-title"); + + ToggleGroup modeGroup = new ToggleGroup(); + rbAttached = new RadioButton("Assinatura Incorporada / Embutida (PDF assinado, P7M ou XML)"); + rbAttached.setToggleGroup(modeGroup); + rbAttached.setSelected(true); + rbAttached.setOnAction(e -> updateModeView()); + + rbDetached = new RadioButton("Assinatura Destacada (Documento Original + Arquivo .p7s separado)"); + rbDetached.setToggleGroup(modeGroup); + rbDetached.setOnAction(e -> updateModeView()); + + HBox radioRow = new HBox(18); + radioRow.getChildren().addAll(rbAttached, rbDetached); + typeBox.getChildren().addAll(lblTypeTitle, radioRow); + + if (isTimestamp) { + typeBox.setVisible(false); + typeBox.setManaged(false); + } + + // 1. Arquivo de Assinatura / Arquivo Assinado + VBox sigBox = new VBox(8); + sigBox.getStyleClass().add("dialog-box"); + lblSigPath = new Label("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):"); + lblSigPath.getStyleClass().add("dialog-field-title"); + + HBox sigRow = new HBox(10); + txtSigPath = new TextField(); + txtSigPath.setPromptText("Selecione o arquivo assinado..."); + txtSigPath.setEditable(false); + HBox.setHgrow(txtSigPath, Priority.ALWAYS); + + Button btnBrowseSig = new Button("Procurar..."); + btnBrowseSig.getStyleClass().add("btn-secondary"); + btnBrowseSig.setOnAction(e -> handleBrowseSig()); + + sigRow.getChildren().addAll(txtSigPath, btnBrowseSig); + sigBox.getChildren().addAll(lblSigPath, sigRow); + + // 2. Documento Original (Visível apenas em modo Destacado) + docBox = new VBox(8); + docBox.getStyleClass().add("dialog-box"); + Label lblDoc = new Label("Documento Original Separado:"); + lblDoc.getStyleClass().add("dialog-field-title"); + + HBox docRow = new HBox(10); + txtDocPath = new TextField(); + txtDocPath.setPromptText("Selecione o documento original..."); + txtDocPath.setEditable(false); + HBox.setHgrow(txtDocPath, Priority.ALWAYS); + + Button btnBrowseDoc = new Button("Procurar..."); + btnBrowseDoc.getStyleClass().add("btn-secondary"); + btnBrowseDoc.setOnAction(e -> handleBrowseDoc()); + + docRow.getChildren().addAll(txtDocPath, btnBrowseDoc); + docBox.getChildren().addAll(lblDoc, docRow); + + // Painel Dinâmico de Resultados + resultBox = new VBox(10); + resultBox.setVisible(false); + resultBox.setManaged(false); + resultBox.getStyleClass().add("dialog-box"); + + ScrollPane scrollResult = new ScrollPane(resultBox); + scrollResult.setFitToWidth(true); + scrollResult.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scrollResult, Priority.ALWAYS); + + // Rodapé + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + btnSaveExtracted = new Button("Salvar Conteúdo Extraído..."); + btnSaveExtracted.getStyleClass().add("btn-secondary"); + btnSaveExtracted.setVisible(false); + btnSaveExtracted.setManaged(false); + btnSaveExtracted.setOnAction(e -> handleSaveExtracted()); + + HBox.setHgrow(btnSaveExtracted, Priority.NEVER); + + Button btnClose = new Button("Fechar"); + btnClose.getStyleClass().add("btn-secondary"); + btnClose.setOnAction(e -> stage.close()); + + Button btnValidate = new Button("Validar Agora"); + btnValidate.getStyleClass().add("btn-primary"); + btnValidate.setDefaultButton(true); + btnValidate.setOnAction(e -> handleValidate()); + + footer.getChildren().addAll(btnSaveExtracted, btnClose, btnValidate); + + root.getChildren().addAll(header, typeBox, sigBox, docBox, scrollResult, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + stage.setScene(scene); + + updateModeView(); + } + + private void updateModeView() { + boolean isAttached = rbAttached.isSelected() && !isTimestamp; + docBox.setVisible(!isAttached); + docBox.setManaged(!isAttached); + + if (isAttached) { + lblSigPath.setText("Arquivo Assinado (.pdf, .p7m, .p7s, .xml):"); + txtSigPath.setPromptText("Selecione o arquivo assinado com dados embutidos..."); + } else { + lblSigPath.setText(isTimestamp ? "Arquivo de Carimbo (.tsr, .tsp, .p7s):" : "Arquivo de Assinatura (.p7s, .sig):"); + txtSigPath.setPromptText("Selecione o arquivo de assinatura digital..."); + } + } + + private void handleBrowseDoc() { + FileChooser ch = new FileChooser(); + ch.setTitle("Selecionar Documento Original"); + File f = ch.showOpenDialog(stage); + if (f != null) { + docFile = f; + txtDocPath.setText(f.getAbsolutePath()); + } + } + + private void handleBrowseSig() { + FileChooser ch = new FileChooser(); + ch.setTitle(rbAttached.isSelected() ? "Selecionar Arquivo Assinado" : "Selecionar Arquivo de Assinatura"); + + if (rbAttached.isSelected()) { + ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos Assinados (*.pdf, *.p7m, *.p7s, *.xml)", "*.pdf", "*.p7m", "*.p7s", "*.xml", "*.*")); + } else { + ch.getExtensionFilters().add(new FileChooser.ExtensionFilter("Assinaturas Digitais (*.p7s, *.sig, *.tsr)", "*.p7s", "*.sig", "*.tsr", "*.*")); + } + + File f = ch.showOpenDialog(stage); + if (f != null) { + sigFile = f; + txtSigPath.setText(f.getAbsolutePath()); + + // Detecção inteligente: se selecionar .pdf no modo destacado, sugere alternar para incorporada + if (f.getName().toLowerCase().endsWith(".pdf") && rbDetached.isSelected()) { + rbAttached.setSelected(true); + updateModeView(); + } + } + } + + private void handleValidate() { + if (sigFile == null || !sigFile.exists()) { + showAlert(Alert.AlertType.WARNING, "Arquivo Não Selecionado", "Por favor, selecione o arquivo a ser validado."); + return; + } + + boolean isDetachedMode = rbDetached.isSelected() || isTimestamp; + if (isDetachedMode && (docFile == null || !docFile.exists())) { + showAlert(Alert.AlertType.WARNING, "Documento Original Não Selecionado", + "Para validar assinatura destacada é necessário selecionar tanto o documento original quanto o arquivo de assinatura."); + return; + } + + try { + byte[] sigBytes = Files.readAllBytes(sigFile.toPath()); + ValidationResult result; + + if (isDetachedMode) { + byte[] docBytes = Files.readAllBytes(docFile.toPath()); + result = validationService.validateDetached(docBytes, sigBytes, docFile.getName()); + } else { + result = validationService.validateAttached(sigBytes, sigFile.getName()); + } + + displayValidationResult(result); + + } catch (Exception ex) { + logger.error("Erro na validação: {}", ex.getMessage(), ex); + showAlert(Alert.AlertType.ERROR, "Erro no Processamento", "Falha ao validar arquivo: " + ex.getMessage()); + } + } + + private void displayValidationResult(ValidationResult result) { + resultBox.getChildren().clear(); + resultBox.setVisible(true); + resultBox.setManaged(true); + + // 1. Badge de Status + HBox statusBadge = new HBox(10); + statusBadge.setAlignment(Pos.CENTER_LEFT); + statusBadge.setPadding(new Insets(10, 14, 10, 14)); + + Label lblBadge = new Label(result.status().getDescription()); + lblBadge.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + if (result.status() == ValidationStatus.VALID) { + statusBadge.setStyle("-fx-background-color: rgba(16, 185, 129, 0.15); -fx-background-radius: 6px; -fx-border-color: #10b981; -fx-border-radius: 6px;"); + lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #10b981;"); + } else if (result.status() == ValidationStatus.INVALID) { + statusBadge.setStyle("-fx-background-color: rgba(239, 68, 68, 0.15); -fx-background-radius: 6px; -fx-border-color: #ef4444; -fx-border-radius: 6px;"); + lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #ef4444;"); + } else { + statusBadge.setStyle("-fx-background-color: rgba(245, 158, 11, 0.15); -fx-background-radius: 6px; -fx-border-color: #f59e0b; -fx-border-radius: 6px;"); + lblBadge.setStyle(lblBadge.getStyle() + " -fx-text-fill: #f59e0b;"); + } + + Label lblType = new Label("• Formato: " + result.type().getDescription()); + lblType.setStyle("-fx-font-size: 12px; -fx-text-fill: #cbd5e1;"); + + statusBadge.getChildren().addAll(lblBadge, lblType); + + Label lblMsg = new Label(result.summaryMessage()); + lblMsg.setWrapText(true); + lblMsg.setStyle("-fx-font-size: 12.5px; -fx-font-weight: bold;"); + + resultBox.getChildren().addAll(statusBadge, lblMsg); + + // 2. Lista de Signatários + if (!result.signers().isEmpty()) { + resultBox.getChildren().add(new Separator()); + Label lblSignersTitle = new Label("DETALHES DOS SIGNATÁRIOS (" + result.signers().size() + "):"); + lblSignersTitle.getStyleClass().add("dialog-field-title"); + resultBox.getChildren().add(lblSignersTitle); + + for (ValidationSignerInfo s : result.signers()) { + VBox card = new VBox(4); + card.getStyleClass().add("diagnostic-item"); + card.setPadding(new Insets(10)); + + Label lblSignerName = new Label("Signatário: " + s.signerName()); + lblSignerName.getStyleClass().add("cert-title"); + + String docText = s.identifier().isBlank() ? "" : s.identifier() + " • "; + Label lblCert = new Label(docText + "Emissor: " + s.issuer()); + lblCert.getStyleClass().add("cert-detail"); + + Label lblTime = new Label("Data/Hora: " + s.signingTime() + " • Algoritmo: " + s.algorithm()); + lblTime.getStyleClass().add("cert-detail"); + + Label lblIntegrity = new Label(s.isValid() ? "✓ Assinatura íntegra e chave pública conferida." : "✗ Falha na integridade da assinatura."); + lblIntegrity.setStyle(s.isValid() ? "-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;" : "-fx-text-fill: #ef4444; -fx-font-weight: bold; -fx-font-size: 11.5px;"); + + card.getChildren().addAll(lblSignerName, lblCert, lblTime, lblIntegrity); + resultBox.getChildren().add(card); + } + } + + // 3. Suporte a exportação de conteúdo extraído (caso CMS/P7M incorporado) + if (result.extractedContent() != null && result.extractedContent().length > 0) { + lastExtractedContent = result.extractedContent(); + btnSaveExtracted.setVisible(true); + btnSaveExtracted.setManaged(true); + } else { + lastExtractedContent = null; + btnSaveExtracted.setVisible(false); + btnSaveExtracted.setManaged(false); + } + } + + private void handleSaveExtracted() { + if (lastExtractedContent == null) return; + FileChooser chooser = new FileChooser(); + chooser.setTitle("Salvar Documento Original Extraído"); + chooser.setInitialFileName("documento_recuperado.bin"); + + File dest = chooser.showSaveDialog(stage); + if (dest != null) { + try { + Files.write(dest.toPath(), lastExtractedContent); + showAlert(Alert.AlertType.INFORMATION, "Documento Extraído", "O documento contido na assinatura foi salvo com sucesso em:\n" + dest.getAbsolutePath()); + } catch (Exception ex) { + showAlert(Alert.AlertType.ERROR, "Erro ao Salvar", "Falha ao gravar arquivo: " + ex.getMessage()); + } + } + } + + private void showAlert(Alert.AlertType type, String title, String msg) { + Alert a = new Alert(type); + a.setTitle(title); + a.setHeaderText(null); + a.setContentText(msg); + a.initOwner(stage); + ThemeManager.applyTheme(a.getDialogPane().getScene()); + a.showAndWait(); + } + + private VBox createHeaderLabel(String title, String subtitle) { + VBox box = new VBox(3); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + Label s = new Label(subtitle); + s.getStyleClass().add("dialog-subtitle"); + box.getChildren().addAll(t, s); + return box; + } + + public void show() { + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java new file mode 100644 index 0000000..dfd3af2 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/TutorialDialog.java @@ -0,0 +1,118 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.ui.CyberCoreUiConstants; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import br.com.cybercore.signer.ui.components.SvgIcons; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.util.Objects; + +/** + * Diálogo interativo com instruções e tutoriais de uso do CyberCore Signer. + */ +@SuppressWarnings("this-escape") +public final class TutorialDialog { + + private final Stage stage; + + public TutorialDialog(Stage owner) { + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Tutorial e Guia de Uso"); + this.stage.setResizable(true); + this.stage.setMinWidth(640); + this.stage.setMinHeight(500); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(24)); + root.getStyleClass().add("dialog-container"); + + HBox header = new HBox(12); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll( + SvgIcons.tutorialIcon(), + createHeaderLabel("TUTORIAL E GUIA DE UTILIZAÇÃO", "Aprenda a assinar documentos e autorizar aplicações web com facilidade") + ); + + VBox content = new VBox(16); + content.getStyleClass().add("dialog-box"); + + addStep(content, "1. Assinatura Local Direta", + "Utilize os botões da tela inicial para assinar PDFs, XMLs, arquivos diversos ou múltiplos arquivos em lote. O agente calculará o hash SHA-256 e gerará a assinatura digital protegida."); + + addStep(content, "2. Integração com Sistemas Web (CCSP/1)", + "Aplicações web corporativas conectam-se ao agente local através da porta 127.0.0.1:21325. Na primeira utilização de um site, o CyberCore solicitará sua autorização explícita."); + + addStep(content, "3. Login com Certificado Digital", + "Quando um sistema web solicitar autenticação com certificado, você verá um diálogo azul destacando o desafio e o titular do certificado. Nenhuma senha ou chave privada é transmitida à rede."); + + addStep(content, "4. Utilização de Tokens USB e Smart Cards (A3)", + "Ao conectar um token USB ou cartão na leitora, o CyberCore Signer detecta o dispositivo automaticamente. Ao assinar, informe o PIN no diálogo protegido."); + + addStep(content, "5. Importação de Certificados A1 (.pfx / .p12)", + "Certificados A1 podem ser adicionados através do menu 'Arquivos Seguros' no painel lateral. A chave privada permanece isolada na máquina do usuário."); + + ScrollPane scroll = new ScrollPane(content); + scroll.setFitToWidth(true); + scroll.setStyle("-fx-background: transparent; -fx-background-color: transparent;"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + HBox footer = new HBox(); + footer.setAlignment(Pos.CENTER_RIGHT); + Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13)); + btnClose.getStyleClass().add("btn-primary"); + btnClose.setPrefWidth(95); + btnClose.setOnAction(e -> stage.close()); + footer.getChildren().add(btnClose); + + root.getChildren().addAll(header, scroll, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + + stage.setScene(scene); + } + + private void addStep(VBox container, String title, String description) { + VBox box = new VBox(4); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + t.setStyle("-fx-font-size: 13.5px;"); + Label d = new Label(description); + d.setWrapText(true); + d.getStyleClass().add("dialog-field-value"); + box.getChildren().addAll(t, d); + container.getChildren().add(box); + } + + private VBox createHeaderLabel(String title, String subtitle) { + VBox box = new VBox(3); + Label t = new Label(title); + t.getStyleClass().add("dialog-header"); + Label s = new Label(subtitle); + s.getStyleClass().add("dialog-subtitle"); + box.getChildren().addAll(t, s); + return box; + } + + public void show() { + stage.show(); + } +} diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java new file mode 100644 index 0000000..a67cd10 --- /dev/null +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/XmlSignDialog.java @@ -0,0 +1,513 @@ +package br.com.cybercore.signer.ui.view; + +import br.com.cybercore.signer.core.domain.CertificateType; +import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.provider.CertificateManager; +import br.com.cybercore.signer.core.security.PinSessionManager; +import br.com.cybercore.signer.core.xml.XmlSignatureOptions; +import br.com.cybercore.signer.core.xml.XmlSigningService; +import br.com.cybercore.signer.ui.components.SvgIcons; +import br.com.cybercore.signer.ui.theme.ThemeManager; +import javafx.collections.FXCollections; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.PasswordField; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Diálogo especializado para Assinatura Digital de Documentos XML segundo o padrão + * W3C XMLDSig e ICP-Brasil, com suporte a envelopes completos e elementos específicos (ex: infNFe). + */ +@SuppressWarnings("this-escape") +public final class XmlSignDialog { + + private static final Logger logger = LoggerFactory.getLogger(XmlSignDialog.class); + + private final Stage stage; + private final CertificateManager certManager; + private final XmlSigningService xmlSigningService; + + // Controles de Documento + private TextField txtSourceXml; + private File selectedSourceFile; + + // Controles de Certificado + private ComboBox certCombo; + private PasswordField pinField; + private Label lblPinNotice; + + // Controles de Modo de Assinatura + private RadioButton rbWholeDocument; + private RadioButton rbSpecificElement; + private TextField txtElementTag; + private TextField txtElementId; + private CheckBox chkValidateAfter; + + // Controles de Ação e Feedback + private ProgressIndicator progressIndicator; + private Label lblStatus; + private Button btnSign; + private Button btnCancel; + + public XmlSignDialog(Stage owner, CertificateManager certManager) { + this(owner, certManager, new XmlSigningService(certManager)); + } + + public XmlSignDialog(Stage owner, CertificateManager certManager, XmlSigningService xmlSigningService) { + this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo."); + this.xmlSigningService = Objects.requireNonNull(xmlSigningService, "XmlSigningService não pode ser nulo."); + + this.stage = new Stage(); + this.stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + this.stage.initOwner(owner); + } + this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XMLDSig)"); + this.stage.setResizable(true); + this.stage.setMinWidth(720); + this.stage.setMinHeight(580); + + initLayout(); + } + + private void initLayout() { + VBox root = new VBox(16); + root.setPadding(new Insets(20)); + root.getStyleClass().add("dialog-container"); + + // Topo / Cabeçalho + HBox header = new HBox(14); + header.setAlignment(Pos.CENTER_LEFT); + VBox titleBox = new VBox(4); + Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XMLDSIG)"); + 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.)."); + lblSub.getStyleClass().add("dialog-subtitle"); + titleBox.getChildren().addAll(lblTitle, lblSub); + header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox); + + // ScrollPane + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent;"); + + VBox formContent = new VBox(16); + formContent.setPadding(new Insets(4, 8, 4, 0)); + + // 1. Seção: Arquivo XML + VBox fileSection = new VBox(8); + fileSection.getStyleClass().add("dialog-box"); + Label lblFileTitle = new Label("ARQUIVO XML A ASSINAR"); + lblFileTitle.getStyleClass().add("dialog-field-title"); + + HBox fileRow = new HBox(10); + fileRow.setAlignment(Pos.CENTER_LEFT); + txtSourceXml = new TextField(); + txtSourceXml.setPromptText("Selecione o arquivo XML original..."); + txtSourceXml.setEditable(false); + HBox.setHgrow(txtSourceXml, Priority.ALWAYS); + + Button btnBrowse = new Button("Procurar XML..."); + btnBrowse.getStyleClass().add("btn-secondary"); + btnBrowse.setOnAction(e -> handleBrowseSourceXml()); + fileRow.getChildren().addAll(txtSourceXml, btnBrowse); + fileSection.getChildren().addAll(lblFileTitle, fileRow); + + // 2. Seção: Certificado Digital + VBox certSection = new VBox(8); + certSection.getStyleClass().add("dialog-box"); + Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL"); + lblCertTitle.getStyleClass().add("dialog-field-title"); + + certCombo = new ComboBox<>(); + certCombo.setMaxWidth(Double.MAX_VALUE); + populateCertificates(); + certCombo.setOnAction(e -> updatePinFieldVisibility()); + + certCombo.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + certCombo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(ManagedCertificate item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); + setText(item.commonName() + doc + " (" + item.type() + ")"); + } + } + }); + + HBox pinRow = new HBox(12); + pinRow.setAlignment(Pos.CENTER_LEFT); + Label lblPin = new Label("PIN / Senha:"); + lblPin.getStyleClass().add("dialog-field-title"); + pinField = new PasswordField(); + pinField.setPromptText("Digite o PIN ou senha da chave privada..."); + pinField.setPrefWidth(260); + + lblPinNotice = new Label(); + lblPinNotice.setStyle("-fx-font-size: 11px;"); + pinRow.getChildren().addAll(lblPin, pinField, lblPinNotice); + + certSection.getChildren().addAll(lblCertTitle, certCombo, pinRow); + + // 3. Seção: Modo de Assinatura XMLDSig + VBox xmlModeSection = new VBox(12); + xmlModeSection.getStyleClass().add("dialog-box"); + Label lblModeTitle = new Label("MODO DE ASSINATURA XMLDSIG"); + lblModeTitle.getStyleClass().add("dialog-field-title"); + + ToggleGroup modeGroup = new ToggleGroup(); + rbWholeDocument = new RadioButton("Documento Completo (Enveloped W3C / URI=\"\")"); + rbWholeDocument.setToggleGroup(modeGroup); + rbWholeDocument.setSelected(true); + + rbSpecificElement = new RadioButton("Elemento Específico por Tag / ID (Padrão NF-e, CT-e, MDF-e, etc.)"); + rbSpecificElement.setToggleGroup(modeGroup); + + VBox elementConfigBox = new VBox(10); + elementConfigBox.setPadding(new Insets(6, 0, 0, 24)); + + GridPane elementGrid = new GridPane(); + elementGrid.setHgap(12); + elementGrid.setVgap(8); + + Label lblTag = new Label("Tag do Elemento:"); + lblTag.getStyleClass().add("dialog-field-title"); + txtElementTag = new TextField("infNFe"); + txtElementTag.setPromptText("Ex: infNFe, infCte, infMDFe, SignedInfo"); + + Label lblId = new Label("Atributo Id (opcional):"); + lblId.getStyleClass().add("dialog-field-title"); + txtElementId = new TextField(); + txtElementId.setPromptText("Deixe vazio para autodetectar o Id da tag"); + + elementGrid.add(lblTag, 0, 0); + elementGrid.add(txtElementTag, 1, 0); + elementGrid.add(lblId, 0, 1); + elementGrid.add(txtElementId, 1, 1); + + elementConfigBox.getChildren().add(elementGrid); + elementConfigBox.disableProperty().bind(rbWholeDocument.selectedProperty()); + + chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação"); + chkValidateAfter.setSelected(true); + + xmlModeSection.getChildren().addAll(lblModeTitle, rbWholeDocument, rbSpecificElement, elementConfigBox, chkValidateAfter); + + formContent.getChildren().addAll(fileSection, certSection, xmlModeSection); + scrollPane.setContent(formContent); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + + // Rodapé com Ações + HBox footer = new HBox(12); + footer.setAlignment(Pos.CENTER_RIGHT); + + progressIndicator = new ProgressIndicator(); + progressIndicator.setPrefSize(22, 22); + progressIndicator.setVisible(false); + + lblStatus = new Label(); + lblStatus.setStyle("-fx-font-size: 12px;"); + HBox.setHgrow(lblStatus, Priority.ALWAYS); + + btnCancel = new Button("Cancelar"); + btnCancel.getStyleClass().add("btn-secondary"); + btnCancel.setOnAction(e -> stage.close()); + + btnSign = new Button("Assinar Documento XML"); + btnSign.getStyleClass().add("btn-primary"); + btnSign.setDefaultButton(true); + btnSign.setOnAction(e -> executeXmlSignature()); + + footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign); + + root.getChildren().addAll(header, scrollPane, footer); + + Scene scene = new Scene(root); + ThemeManager.applyTheme(scene); + stage.setScene(scene); + + updatePinFieldVisibility(); + } + + private void populateCertificates() { + try { + List certs = certManager.getAvailableCertificates(); + certCombo.setItems(FXCollections.observableArrayList(certs)); + if (!certs.isEmpty()) { + certCombo.setValue(certs.get(0)); + } + } catch (Exception e) { + logger.error("Erro ao listar certificados para assinatura de XML", e); + } + } + + private void updatePinFieldVisibility() { + ManagedCertificate cert = certCombo.getValue(); + if (cert == null) { + pinField.setDisable(true); + lblPinNotice.setText(""); + return; + } + + boolean isA3 = cert.type() == CertificateType.A3; + boolean hasPinInSession = isA3 && PinSessionManager.getInstance().hasPin(cert.id()); + + if (hasPinInSession) { + pinField.setDisable(true); + pinField.clear(); + lblPinNotice.setText("✓ PIN memorizado na sessão segura"); + lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;"); + } else if (isA3 && "provider-win-store".equalsIgnoreCase(cert.providerId())) { + pinField.setDisable(true); + pinField.clear(); + lblPinNotice.setText("Autenticação nativa pelo Windows/Driver"); + lblPinNotice.setStyle("-fx-text-fill: -fx-accent;"); + } else { + pinField.setDisable(false); + lblPinNotice.setText(isA3 ? "Requer PIN do Smart Card" : "Requer senha do certificado A1"); + lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary;"); + } + } + + private void handleBrowseSourceXml() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Selecionar Documento XML"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos XML (*.xml)", "*.xml")); + + File file = chooser.showOpenDialog(stage); + if (file != null) { + selectedSourceFile = file; + txtSourceXml.setText(file.getAbsolutePath()); + + // Tenta detectar automaticamente se é uma NF-e, CT-e, etc. + try { + String content = Files.readString(file.toPath()); + if (content.contains(" signTask = new Task<>() { + @Override + protected File call() throws Exception { + xmlSigningService.signXmlFile(selectedSourceFile, targetFile, options); + + if (validate) { + byte[] signedBytes = Files.readAllBytes(targetFile.toPath()); + boolean valid = xmlSigningService.validateXmlSignature(signedBytes); + if (!valid) { + throw new IllegalStateException("A assinatura foi gravada, mas a validação de integridade do XML falhou."); + } + } + return targetFile; + } + }; + + signTask.setOnSucceeded(e -> { + setFormDisabled(false); + progressIndicator.setVisible(false); + lblStatus.setText("✓ Documento XML assinado e validado com sucesso!"); + lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;"); + + showSuccessDialog(targetFile); + }); + + signTask.setOnFailed(e -> { + setFormDisabled(false); + progressIndicator.setVisible(false); + Throwable ex = signTask.getException(); + logger.error("Falha ao assinar XML", ex); + lblStatus.setText("✕ Falha na assinatura"); + lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;"); + + showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do XML", + "Não foi possível concluir a assinatura XMLDSig:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido")); + }); + + Thread signThread = new Thread(signTask, "cybercore-xml-signer"); + signThread.setDaemon(true); + signThread.start(); + } + + private void showSuccessDialog(File targetFile) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Assinatura Concluída"); + alert.setHeaderText("XML Assinado com Sucesso!"); + alert.setContentText("O documento XML foi digitalmente assinado no padrão W3C XMLDSig ICP-Brasil.\n\n" + + "Arquivo gerado:\n" + targetFile.getAbsolutePath()); + + ButtonType btnOpen = new ButtonType("Abrir XML"); + ButtonType btnFolder = new ButtonType("Abrir Pasta"); + ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE); + + alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose); + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + + Optional result = alert.showAndWait(); + if (result.isPresent()) { + if (result.get() == btnOpen) { + openFile(targetFile); + } else if (result.get() == btnFolder) { + openFolder(targetFile); + } + } + } + + private void openFile(File file) { + try { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + Desktop.getDesktop().open(file); + } + } catch (Exception ex) { + logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage()); + } + } + + private void openFolder(File file) { + try { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + File parent = file.getParentFile(); + if (parent != null) { + Desktop.getDesktop().open(parent); + } + } + } catch (Exception ex) { + logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage()); + } + } + + private void setFormDisabled(boolean disabled) { + btnSign.setDisable(disabled); + btnCancel.setDisable(disabled); + certCombo.setDisable(disabled); + pinField.setDisable(disabled); + rbWholeDocument.setDisable(disabled); + rbSpecificElement.setDisable(disabled); + txtElementTag.setDisable(disabled); + txtElementId.setDisable(disabled); + chkValidateAfter.setDisable(disabled); + } + + private void showAlert(Alert.AlertType type, String title, String message) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + ThemeManager.applyTheme(alert.getDialogPane().getScene()); + alert.showAndWait(); + } + + public void show() { + stage.centerOnScreen(); + stage.show(); + stage.toFront(); + } +} diff --git a/modules/cybercore-ui/src/main/resources/assets/README.md b/modules/cybercore-ui/src/main/resources/assets/README.md new file mode 100644 index 0000000..d77f68b --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/README.md @@ -0,0 +1,75 @@ +# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer + +Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**. + +--- + +## 📁 Estrutura de Arquivos Criados + +``` +assets/ +├── icons/ # Ícones vetoriais SVG reais e editáveis (W3C padrão) +│ ├── pdf.svg # Ícone Assinar PDF (documento com tag vermelha PDF) +│ ├── xml.svg # Ícone Assinar XML (documento com tag azul ) +│ ├── file.svg # Ícone Assinar Arquivo (documento com caneta) +│ ├── batch.svg # Ícone Assinatura em Lote (pilha de documentos com check) +│ ├── attached.svg # Ícone Assinatura Atachada (documento com clipe de papel) +│ ├── co-sign.svg # Ícone Co-Assinar (documentos duplos com usuários) +│ ├── timestamp.svg # Ícone Carimbo do Tempo (documento com relógio TSA) +│ ├── ltv.svg # Ícone Assinatura LTV (documento com selo perpétuo) +│ ├── validate.svg # Ícone Validar Assinatura (documento com checkmark verde) +│ ├── validate-timestamp.svg # Ícone Validar Carimbo (relógio com checkmark verde) +│ ├── exit.svg # Ícone Sair do Agente (porta com seta vermelha de saída) +│ ├── logs.svg # Ícone Terminal de Logs (janela com prompt ) +│ ├── settings.svg # Ícone Configurações (engrenagem de precisão de 8 dentes) +│ ├── tutorial.svg # Ícone Tutorial e Ajuda (monitor com vídeo/play) +│ ├── cert.svg # Ícone Certificados Digitais (pasta com cadeado seguro) +│ ├── about.svg # Ícone Sobre o Sistema (círculo azul com 'i') +│ ├── sun.svg # Ícone Tema Claro (sol dourado com raios vetoriais) +│ ├── moon.svg # Ícone Tema Escuro (lua crescente azul/índigo) +│ ├── shield.svg # Ícone Brasão de Segurança (CyberShield com chave interna) +│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais +│ ├── check.svg # Ícone Checkmark de sucesso e validação +│ ├── error.svg # Ícone de Erro / Alerta em vermelho +│ └── star.svg # Ícone de Estrela notarial / favorita +│ +└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência) + ├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro) + ├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER) + ├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue) + └── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px) +``` + +--- + +## 🎨 Como Customizar Qualquer Ícone ou Imagem + +1. **Ícones:** + - Estão no formato `.svg` padrão. + - Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**. + - Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente. + +2. **Imagens do Sistema:** + - Estão no formato `.png` com canal alfa de 32 bits (fundo transparente). + - Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa. + +3. **Ordem de Prioridade e Hot-Reloading:** + Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem: + 1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar! + 2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional. + 3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`. + 4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído. + +--- + +## 🚀 Como Testar Suas Modificações + +Para compilar e gerar o pacote `.jar` completo com todos os novos assets embutidos: +```bash +./build-jar.sh +``` + +Para rodar a aplicação: +```bash +java -jar ./cybercore-signer.jar +``` diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/about.svg b/modules/cybercore-ui/src/main/resources/assets/icons/about.svg new file mode 100644 index 0000000..dd4abb9 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/about.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/attached.svg b/modules/cybercore-ui/src/main/resources/assets/icons/attached.svg new file mode 100644 index 0000000..7d894e7 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/attached.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/batch.svg b/modules/cybercore-ui/src/main/resources/assets/icons/batch.svg new file mode 100644 index 0000000..5ced311 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/batch.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/cert.svg b/modules/cybercore-ui/src/main/resources/assets/icons/cert.svg new file mode 100644 index 0000000..14cc2b4 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/cert.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/check.svg b/modules/cybercore-ui/src/main/resources/assets/icons/check.svg new file mode 100644 index 0000000..c379af0 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/co-sign.svg b/modules/cybercore-ui/src/main/resources/assets/icons/co-sign.svg new file mode 100644 index 0000000..283c24f --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/co-sign.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/error.svg b/modules/cybercore-ui/src/main/resources/assets/icons/error.svg new file mode 100644 index 0000000..5a36ff3 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/error.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/exit.svg b/modules/cybercore-ui/src/main/resources/assets/icons/exit.svg new file mode 100644 index 0000000..d209a6b --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/exit.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/file.svg b/modules/cybercore-ui/src/main/resources/assets/icons/file.svg new file mode 100644 index 0000000..023885f --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/file.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/logs.svg b/modules/cybercore-ui/src/main/resources/assets/icons/logs.svg new file mode 100644 index 0000000..0dd89e3 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/logs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/ltv.svg b/modules/cybercore-ui/src/main/resources/assets/icons/ltv.svg new file mode 100644 index 0000000..dbc162a --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/ltv.svg @@ -0,0 +1,11 @@ + + + + + + + + + + LTV + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/moon.svg b/modules/cybercore-ui/src/main/resources/assets/icons/moon.svg new file mode 100644 index 0000000..fd47105 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/moon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/pdf.svg b/modules/cybercore-ui/src/main/resources/assets/icons/pdf.svg new file mode 100644 index 0000000..3755ded --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/pdf.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + PDF + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/seal.svg b/modules/cybercore-ui/src/main/resources/assets/icons/seal.svg new file mode 100644 index 0000000..1aa3a5f --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/seal.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/settings.svg b/modules/cybercore-ui/src/main/resources/assets/icons/settings.svg new file mode 100644 index 0000000..e0f4042 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/settings.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/shield.svg b/modules/cybercore-ui/src/main/resources/assets/icons/shield.svg new file mode 100644 index 0000000..1bc2539 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/shield.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/star.svg b/modules/cybercore-ui/src/main/resources/assets/icons/star.svg new file mode 100644 index 0000000..ac2213a --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/star.svg @@ -0,0 +1,4 @@ + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/sun.svg b/modules/cybercore-ui/src/main/resources/assets/icons/sun.svg new file mode 100644 index 0000000..0327476 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/sun.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/timestamp.svg b/modules/cybercore-ui/src/main/resources/assets/icons/timestamp.svg new file mode 100644 index 0000000..16238f4 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/timestamp.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/tutorial.svg b/modules/cybercore-ui/src/main/resources/assets/icons/tutorial.svg new file mode 100644 index 0000000..32d13d2 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/tutorial.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/validate-timestamp.svg b/modules/cybercore-ui/src/main/resources/assets/icons/validate-timestamp.svg new file mode 100644 index 0000000..dc0f7c8 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/validate-timestamp.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/validate.svg b/modules/cybercore-ui/src/main/resources/assets/icons/validate.svg new file mode 100644 index 0000000..f654ce8 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/validate.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/modules/cybercore-ui/src/main/resources/assets/icons/xml.svg b/modules/cybercore-ui/src/main/resources/assets/icons/xml.svg new file mode 100644 index 0000000..edbf6a9 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/assets/icons/xml.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + <XML> + diff --git a/modules/cybercore-ui/src/main/resources/assets/images/app-icon.png b/modules/cybercore-ui/src/main/resources/assets/images/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e231bde3db0931e9d540d4362e276ae9722869 GIT binary patch literal 26558 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 b%ds18?VH3((q5oK;E$!5&EYZ=w}}4(I*HpL literal 0 HcmV?d00001 diff --git a/modules/cybercore-ui/src/main/resources/assets/images/app-logo.png b/modules/cybercore-ui/src/main/resources/assets/images/app-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0cea4b98e51e56b5991abf1f90a4ac52eb809a55 GIT binary patch literal 10439 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 AqW}N^ literal 0 HcmV?d00001 diff --git a/modules/cybercore-ui/src/main/resources/assets/images/brand-banner.png b/modules/cybercore-ui/src/main/resources/assets/images/brand-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6bd57c97e8b6858d26200259631c363856a146da GIT binary patch literal 27073 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
k6H?w@CdPIdX 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 Rz)3ui^ecJsLQ(yX{|72h^*jIo literal 0 HcmV?d00001 diff --git a/modules/cybercore-ui/src/main/resources/styles/theme.css b/modules/cybercore-ui/src/main/resources/styles/theme.css new file mode 100644 index 0000000..8047762 --- /dev/null +++ b/modules/cybercore-ui/src/main/resources/styles/theme.css @@ -0,0 +1,1065 @@ +/* ========================================================================== + CyberCore Signer — Dual Theme Design System (Claro / Escuro) + Paleta Harmonizada e Alto Contraste (WCAG AA Compliant) + ========================================================================== */ + +.root { + -fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + -fx-font-size: 13px; +} + +/* ========================================================================== + 1. TEMA CLARO (LIGHT THEME) + ========================================================================== */ + +.theme-light { + -fx-base: #f8fafc; + -fx-background: #f1f5f9; + -fx-control-inner-background: #ffffff; + -fx-text-fill: #0f172a; +} + +.theme-light.cyber-root, +.theme-light .cyber-root, +.theme-light.serpro-root, +.theme-light .serpro-root, +.theme-light.dialog-container, +.theme-light .dialog-container { + -fx-background-color: #f1f5f9; +} + +/* Header Claro */ +.theme-light .cyber-header-bar { + -fx-background-color: #ffffff; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 0 1px 0; + -fx-padding: 12px 24px; +} + +.theme-light .cyber-brand-title { + -fx-font-size: 17px; + -fx-font-weight: bold; + -fx-text-fill: #0f172a; +} + +.theme-light .cyber-brand-subtitle { + -fx-font-size: 11px; + -fx-text-fill: #64748b; +} + +.theme-light .cyber-status-badge { + -fx-background-color: #ecfdf5; + -fx-border-color: #10b981; + -fx-border-radius: 14px; + -fx-border-width: 1px; + -fx-background-radius: 14px; + -fx-text-fill: #047857; + -fx-padding: 5px 12px; + -fx-font-size: 11.5px; + -fx-font-weight: bold; +} + +/* Botão Toggle de Tema */ +.theme-light .theme-toggle-btn { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 18px; + -fx-border-width: 1px; + -fx-background-radius: 18px; + -fx-text-fill: #334155; + -fx-padding: 5px 12px; + -fx-font-size: 11.5px; + -fx-font-weight: bold; + -fx-cursor: hand; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.04), 4, 0, 0, 1); +} + +.theme-light .theme-toggle-btn:hover { + -fx-background-color: #f8fafc; + -fx-border-color: #0284c7; + -fx-text-fill: #0284c7; +} + +/* Cartões de Ação (Hub Central) - Modo Claro */ +.theme-light .cyber-action-card, +.theme-light .serpro-action-btn { + -fx-background-color: #ffffff; + -fx-border-color: #e2e8f0; + -fx-border-width: 1.2px; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 6px 10px; + -fx-alignment: CENTER_LEFT; + -fx-cursor: hand; + -fx-pref-height: 68px; + -fx-min-height: 68px; + -fx-max-width: Infinity; + -fx-effect: dropshadow(three-pass-box, rgba(15, 23, 42, 0.04), 6, 0, 0, 2); +} + +.theme-light .cyber-action-card:hover, +.theme-light .serpro-action-btn:hover { + -fx-background-color: #ffffff; + -fx-border-color: #0284c7; + -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.16), 8, 0, 0, 3); +} + +.theme-light .cyber-action-card:pressed, +.theme-light .serpro-action-btn:pressed { + -fx-background-color: #f0f9ff; + -fx-border-color: #0369a1; +} + +.theme-light .cyber-action-text, +.theme-light .serpro-action-text { + -fx-font-size: 11.5px; + -fx-font-weight: bold; + -fx-text-fill: #1e293b; + -fx-line-spacing: 1px; +} + +/* Painel Lateral - Modo Claro */ +.theme-light .cyber-side-column { + -fx-background-color: #ffffff; + -fx-border-color: #e2e8f0; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-padding: 14px; + -fx-effect: dropshadow(three-pass-box, rgba(15, 23, 42, 0.03), 6, 0, 0, 2); +} + +.theme-light .cyber-side-card, +.theme-light .serpro-side-btn { + -fx-background-color: #f8fafc; + -fx-border-color: #e2e8f0; + -fx-border-width: 1px; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 8px 12px; + -fx-alignment: CENTER_LEFT; + -fx-cursor: hand; + -fx-pref-height: 42px; + -fx-min-height: 42px; + -fx-max-width: Infinity; +} + +.theme-light .cyber-side-card:hover, +.theme-light .serpro-side-btn:hover { + -fx-background-color: #f0f9ff; + -fx-border-color: #0284c7; + -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.12), 4, 0, 0, 1); +} + +.theme-light .cyber-side-card:pressed, +.theme-light .serpro-side-btn:pressed { + -fx-background-color: #e0f2fe; +} + +.theme-light .cyber-side-text, +.theme-light .serpro-side-text { + -fx-font-size: 12.5px; + -fx-font-weight: bold; + -fx-text-fill: #334155; +} + +.theme-light .cyber-version-label, +.theme-light .serpro-version-label { + -fx-font-size: 11px; + -fx-text-fill: #64748b; +} + +/* Diálogos e Caixas - Modo Claro */ +.theme-light .dialog-header, +.theme-light .dialog-title, +.theme-light .section-title { + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-text-fill: #0369a1; +} + +.theme-light .dialog-subtitle, +.theme-light .section-subtitle, +.theme-light .brand-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #64748b; +} + +.theme-light .dialog-box, +.theme-light .content-card { + -fx-background-color: #ffffff; + -fx-background-radius: 8px; + -fx-border-color: #e2e8f0; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 16px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.04), 6, 0, 0, 2); +} + +.theme-light .label { + -fx-text-fill: #1e293b; +} + +.theme-light .form-label { + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-text-fill: #334155; +} + +.theme-light .form-label-muted { + -fx-font-size: 11px; + -fx-text-fill: #64748b; +} + +.theme-light .dialog-field-title { + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-text-fill: #0369a1; +} + +.theme-light .dialog-field-value, +.theme-light .detail-value { + -fx-font-size: 13px; + -fx-text-fill: #0f172a; +} + +.theme-light .dialog-hash-value { + -fx-font-family: "Courier New", monospace; + -fx-font-size: 12px; + -fx-text-fill: #0369a1; + -fx-background-color: #f1f5f9; + -fx-border-color: #e2e8f0; + -fx-border-radius: 4px; + -fx-padding: 8px; + -fx-background-radius: 4px; +} + +.theme-light .log-console-area { + -fx-font-family: 'Consolas', 'Courier New', monospace; + -fx-font-size: 11.5px; + -fx-control-inner-background: #f8fafc; + -fx-text-fill: #0369a1; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.theme-light .seal-preview-box { + -fx-background-color: #f8fafc; + -fx-border-color: #cbd5e1; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 12px; + -fx-alignment: CENTER; +} + + +/* Controles de Formulário - Modo Claro */ +.theme-light .text-field, +.theme-light .password-field { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-text-fill: #0f172a; + -fx-prompt-text-fill: #94a3b8; + -fx-padding: 7px 12px; +} + +.theme-light .text-field:focused, +.theme-light .password-field:focused { + -fx-border-color: #0284c7; + -fx-background-color: #ffffff; +} + +.theme-light .combo-box { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 3px 6px; +} + +.theme-light .combo-box .cell { + -fx-text-fill: #0f172a; +} + +.theme-light .combo-box-popup .list-view { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.08), 8, 0, 0, 3); +} + +.theme-light .combo-box-popup .list-cell { + -fx-text-fill: #0f172a; + -fx-padding: 7px 12px; +} + +.theme-light .combo-box-popup .list-cell:hover, +.theme-light .combo-box-popup .list-cell:filled:selected { + -fx-background-color: #e0f2fe; + -fx-text-fill: #0369a1; +} + +/* CheckBox - Modo Claro (CORRIGIDO: texto perfeitamente visível) */ +.theme-light .check-box { + -fx-text-fill: #1e293b; + -fx-font-size: 13px; +} + +.theme-light .check-box > .box { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 4px; + -fx-background-radius: 4px; + -fx-border-width: 1.2px; + -fx-padding: 3px; +} + +.theme-light .check-box:selected > .box { + -fx-background-color: #0284c7; + -fx-border-color: #0284c7; +} + +.theme-light .check-box:selected > .box > .mark { + -fx-background-color: #ffffff; +} + +/* RadioButton - Modo Claro */ +.theme-light .radio-button { + -fx-text-fill: #1e293b; + -fx-font-size: 13px; +} + +.theme-light .radio-button > .radio { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 100px; + -fx-background-radius: 100px; + -fx-border-width: 1.5px; + -fx-padding: 3px; + -fx-alignment: center; +} + +.theme-light .radio-button:selected > .radio { + -fx-border-color: #0284c7; +} + +.theme-light .radio-button:selected > .radio > .dot { + -fx-background-color: #0284c7; + -fx-background-radius: 100px; + -fx-padding: 4px; + -fx-background-insets: 0; +} + +/* ColorPicker - Modo Claro */ +.theme-light .color-picker { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-border-width: 1.2px; + -fx-padding: 3px 6px; + -fx-pref-height: 32px; + -fx-cursor: hand; +} + +.theme-light .color-picker:hover { + -fx-border-color: #0284c7; +} + +.theme-light .color-picker .color-picker-label { + -fx-text-fill: #1e293b; + -fx-font-weight: bold; + -fx-font-size: 11.5px; +} + +.theme-light .color-row { + -fx-alignment: CENTER_LEFT; + -fx-spacing: 12px; + -fx-padding: 3px 0; +} + +/* Tabela - Modo Claro */ +.theme-light .table-view { + -fx-background-color: #ffffff; + -fx-border-color: #cbd5e1; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.theme-light .table-view .column-header-background { + -fx-background-color: #f8fafc; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 0 1px 0; +} + +.theme-light .table-view .column-header .label { + -fx-text-fill: #334155; + -fx-font-weight: bold; + -fx-font-size: 12px; +} + +.theme-light .table-view .table-row-cell { + -fx-background-color: #ffffff; + -fx-border-color: transparent transparent #f1f5f9 transparent; + -fx-border-width: 0 0 1px 0; +} + +.theme-light .table-view .table-row-cell:odd { + -fx-background-color: #f8fafc; +} + +.theme-light .table-view .table-row-cell:hover { + -fx-background-color: #f0f9ff; +} + +.theme-light .table-view .table-row-cell:filled:selected { + -fx-background-color: #e0f2fe; +} + +.theme-light .table-view .table-cell { + -fx-text-fill: #0f172a; + -fx-padding: 8px 12px; +} + +.theme-light .table-view .placeholder .label { + -fx-text-fill: #64748b; +} + +/* ========================================================================== + 2. TEMA ESCURO (DARK THEME) + ========================================================================== */ + +.theme-dark { + -fx-base: #121824; + -fx-background: #0b0f17; + -fx-control-inner-background: #131c2e; + -fx-text-fill: #f1f5f9; +} + +.theme-dark.cyber-root, +.theme-dark .cyber-root, +.theme-dark.serpro-root, +.theme-dark .serpro-root, +.theme-dark.dialog-container, +.theme-dark .dialog-container { + -fx-background-color: #0b0f17; +} + +/* Header Escuro */ +.theme-dark .cyber-header-bar { + -fx-background-color: #121824; + -fx-border-color: #1e293b; + -fx-border-width: 0 0 1px 0; + -fx-padding: 12px 24px; +} + +.theme-dark .cyber-brand-title { + -fx-font-size: 17px; + -fx-font-weight: bold; + -fx-text-fill: #ffffff; +} + +.theme-dark .cyber-brand-subtitle { + -fx-font-size: 11px; + -fx-text-fill: #94a3b8; +} + +.theme-dark .cyber-status-badge { + -fx-background-color: rgba(16, 185, 129, 0.15); + -fx-border-color: #10b981; + -fx-border-radius: 14px; + -fx-border-width: 1px; + -fx-background-radius: 14px; + -fx-text-fill: #34d399; + -fx-padding: 5px 12px; + -fx-font-size: 11.5px; + -fx-font-weight: bold; +} + +/* Botão Toggle de Tema */ +.theme-dark .theme-toggle-btn { + -fx-background-color: #1e293b; + -fx-border-color: #334155; + -fx-border-radius: 18px; + -fx-border-width: 1px; + -fx-background-radius: 18px; + -fx-text-fill: #e2e8f0; + -fx-padding: 5px 12px; + -fx-font-size: 11.5px; + -fx-font-weight: bold; + -fx-cursor: hand; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.25), 4, 0, 0, 1); +} + +.theme-dark .theme-toggle-btn:hover { + -fx-background-color: #334155; + -fx-border-color: #38bdf8; + -fx-text-fill: #38bdf8; +} + +/* Cartões de Ação (Hub Central) - Modo Escuro */ +.theme-dark .cyber-action-card, +.theme-dark .serpro-action-btn { + -fx-background-color: #131c2e; + -fx-border-color: #1e293b; + -fx-border-width: 1.2px; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 8px 12px; + -fx-alignment: CENTER_LEFT; + -fx-cursor: hand; + -fx-pref-height: 72px; + -fx-min-height: 72px; + -fx-max-width: Infinity; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.35), 8, 0, 0, 2); +} + +.theme-dark .cyber-action-card:hover, +.theme-dark .serpro-action-btn:hover { + -fx-background-color: #182338; + -fx-border-color: #38bdf8; + -fx-effect: dropshadow(three-pass-box, rgba(56, 189, 248, 0.2), 10, 0, 0, 3); +} + +.theme-dark .cyber-action-card:pressed, +.theme-dark .serpro-action-btn:pressed { + -fx-background-color: #0f172a; + -fx-border-color: #0284c7; +} + +.theme-dark .cyber-action-text, +.theme-dark .serpro-action-text { + -fx-font-size: 11.5px; + -fx-font-weight: bold; + -fx-text-fill: #f8fafc; + -fx-line-spacing: 1px; +} + +/* Painel Lateral - Modo Escuro */ +.theme-dark .cyber-side-column { + -fx-background-color: #121824; + -fx-border-color: #1e293b; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-padding: 14px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.3), 8, 0, 0, 2); +} + +.theme-dark .cyber-side-card, +.theme-dark .serpro-side-btn { + -fx-background-color: #131c2e; + -fx-border-color: #1e293b; + -fx-border-width: 1px; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 8px 12px; + -fx-alignment: CENTER_LEFT; + -fx-cursor: hand; + -fx-pref-height: 42px; + -fx-min-height: 42px; + -fx-max-width: Infinity; +} + +.theme-dark .cyber-side-card:hover, +.theme-dark .serpro-side-btn:hover { + -fx-background-color: #1a253c; + -fx-border-color: #38bdf8; + -fx-effect: dropshadow(three-pass-box, rgba(56, 189, 248, 0.15), 4, 0, 0, 1); +} + +.theme-dark .cyber-side-card:pressed, +.theme-dark .serpro-side-btn:pressed { + -fx-background-color: #0f172a; +} + +.theme-dark .cyber-side-text, +.theme-dark .serpro-side-text { + -fx-font-size: 12.5px; + -fx-font-weight: bold; + -fx-text-fill: #cbd5e1; +} + +.theme-dark .cyber-version-label, +.theme-dark .serpro-version-label { + -fx-font-size: 11px; + -fx-text-fill: #94a3b8; +} + +/* Diálogos e Caixas - Modo Escuro */ +.theme-dark .dialog-header, +.theme-dark .dialog-title, +.theme-dark .section-title { + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-text-fill: #38bdf8; +} + +.theme-dark .dialog-subtitle, +.theme-dark .section-subtitle, +.theme-dark .brand-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #94a3b8; +} + +.theme-dark .dialog-box, +.theme-dark .content-card { + -fx-background-color: #131c2e; + -fx-background-radius: 8px; + -fx-border-color: #1e293b; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 16px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.35), 8, 0, 0, 2); +} + +.theme-dark .label { + -fx-text-fill: #f1f5f9; +} + +.theme-dark .form-label { + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-text-fill: #cbd5e1; +} + +.theme-dark .form-label-muted { + -fx-font-size: 11px; + -fx-text-fill: #94a3b8; +} + +.theme-dark .dialog-field-title { + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-text-fill: #38bdf8; +} + +.theme-dark .dialog-field-value, +.theme-dark .detail-value { + -fx-font-size: 13px; + -fx-text-fill: #f1f5f9; +} + +.theme-dark .dialog-hash-value { + -fx-font-family: "Courier New", monospace; + -fx-font-size: 12px; + -fx-text-fill: #38bdf8; + -fx-background-color: #090d16; + -fx-border-color: #1e293b; + -fx-border-radius: 4px; + -fx-padding: 8px; + -fx-background-radius: 4px; +} + +.theme-dark .log-console-area { + -fx-font-family: 'Consolas', 'Courier New', monospace; + -fx-font-size: 11.5px; + -fx-control-inner-background: #0b1329; + -fx-text-fill: #38bdf8; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.theme-dark .seal-preview-box { + -fx-background-color: #0b1329; + -fx-border-color: #334155; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 12px; + -fx-alignment: CENTER; +} + +/* Controles de Formulário - Modo Escuro */ +.theme-dark .text-field, +.theme-dark .password-field { + -fx-background-color: #0f172a; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-text-fill: #f1f5f9; + -fx-prompt-text-fill: #64748b; + -fx-padding: 7px 12px; +} + +.theme-dark .text-field:focused, +.theme-dark .password-field:focused { + -fx-border-color: #38bdf8; + -fx-background-color: #0f172a; +} + +.theme-dark .combo-box { + -fx-background-color: #131c2e; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-padding: 3px 6px; +} + +.theme-dark .combo-box .cell { + -fx-text-fill: #f1f5f9; +} + +.theme-dark .combo-box-popup .list-view { + -fx-background-color: #131c2e; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 8, 0, 0, 3); +} + +.theme-dark .combo-box-popup .list-cell { + -fx-text-fill: #f1f5f9; + -fx-padding: 7px 12px; +} + +.theme-dark .combo-box-popup .list-cell:hover, +.theme-dark .combo-box-popup .list-cell:filled:selected { + -fx-background-color: #1e293b; + -fx-text-fill: #38bdf8; +} + +/* CheckBox - Modo Escuro */ +.theme-dark .check-box { + -fx-text-fill: #f1f5f9; + -fx-font-size: 13px; +} + +.theme-dark .check-box > .box { + -fx-background-color: #0f172a; + -fx-border-color: #475569; + -fx-border-radius: 4px; + -fx-background-radius: 4px; + -fx-border-width: 1.2px; + -fx-padding: 3px; +} + +.theme-dark .check-box:selected > .box { + -fx-background-color: #0284c7; + -fx-border-color: #38bdf8; +} + +.theme-dark .check-box:selected > .box > .mark { + -fx-background-color: #ffffff; +} + +/* RadioButton - Modo Escuro */ +.theme-dark .radio-button { + -fx-text-fill: #f1f5f9; + -fx-font-size: 13px; +} + +.theme-dark .radio-button > .radio { + -fx-background-color: #0f172a; + -fx-border-color: #475569; + -fx-border-radius: 100px; + -fx-background-radius: 100px; + -fx-border-width: 1.5px; + -fx-padding: 3px; + -fx-alignment: center; +} + +.theme-dark .radio-button:selected > .radio { + -fx-border-color: #38bdf8; +} + +.theme-dark .radio-button:selected > .radio > .dot { + -fx-background-color: #38bdf8; + -fx-background-radius: 100px; + -fx-padding: 4px; + -fx-background-insets: 0; +} + +/* ColorPicker - Modo Escuro */ +.theme-dark .color-picker { + -fx-background-color: #0f172a; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-border-width: 1.2px; + -fx-padding: 3px 6px; + -fx-pref-height: 32px; + -fx-cursor: hand; +} + +.theme-dark .color-picker:hover { + -fx-border-color: #38bdf8; +} + +.theme-dark .color-picker .color-picker-label { + -fx-text-fill: #f1f5f9; + -fx-font-weight: bold; + -fx-font-size: 11.5px; +} + +.theme-dark .color-row { + -fx-alignment: CENTER_LEFT; + -fx-spacing: 12px; + -fx-padding: 3px 0; +} + +/* Tabela - Modo Escuro (CORRIGIDO: fundo escuro e textos contrastados) */ +.theme-dark .table-view { + -fx-background-color: #0f172a; + -fx-border-color: #1e293b; + -fx-border-radius: 6px; + -fx-background-radius: 6px; +} + +.theme-dark .table-view .column-header-background { + -fx-background-color: #131c2e; + -fx-border-color: #1e293b; + -fx-border-width: 0 0 1px 0; +} + +.theme-dark .table-view .column-header .label { + -fx-text-fill: #cbd5e1; + -fx-font-weight: bold; + -fx-font-size: 12px; +} + +.theme-dark .table-view .table-row-cell { + -fx-background-color: #0f172a; + -fx-border-color: transparent transparent #1e293b transparent; + -fx-border-width: 0 0 1px 0; +} + +.theme-dark .table-view .table-row-cell:odd { + -fx-background-color: #131c2e; +} + +.theme-dark .table-view .table-row-cell:hover { + -fx-background-color: #1e293b; +} + +.theme-dark .table-view .table-row-cell:filled:selected { + -fx-background-color: #1e293b; +} + +.theme-dark .table-view .table-cell { + -fx-text-fill: #f1f5f9; + -fx-padding: 8px 12px; +} + +.theme-dark .table-view .placeholder .label { + -fx-text-fill: #64748b; +} + +/* ========================================================================== + 3. BOTÕES E COMPONENTES COMUNS (DUAL THEME) + ========================================================================== */ + +.btn-primary { + -fx-background-color: #0284c7; + -fx-text-fill: #ffffff; + -fx-font-weight: bold; + -fx-padding: 8px 18px; + -fx-background-radius: 6px; + -fx-cursor: hand; + -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1); +} + +.btn-primary:hover { + -fx-background-color: #0369a1; +} + +.btn-secondary { + -fx-background-color: #e2e8f0; + -fx-text-fill: #1e293b; + -fx-font-weight: bold; + -fx-padding: 8px 18px; + -fx-background-radius: 6px; + -fx-cursor: hand; +} + +.btn-secondary:hover { + -fx-background-color: #cbd5e1; +} + +.theme-dark .btn-secondary { + -fx-background-color: #1e293b; + -fx-border-color: #334155; + -fx-border-radius: 6px; + -fx-border-width: 1px; + -fx-text-fill: #e2e8f0; +} + +.theme-dark .btn-secondary:hover { + -fx-background-color: #334155; + -fx-border-color: #38bdf8; + -fx-text-fill: #38bdf8; +} + +.btn-cancel { + -fx-background-color: transparent; + -fx-border-color: #94a3b8; + -fx-border-radius: 6px; + -fx-text-fill: #475569; + -fx-font-weight: bold; + -fx-padding: 8px 18px; + -fx-cursor: hand; +} + +.btn-cancel:hover { + -fx-border-color: #ef4444; + -fx-text-fill: #ef4444; +} + +.theme-dark .btn-cancel { + -fx-border-color: #475569; + -fx-text-fill: #94a3b8; +} + +.theme-dark .btn-cancel:hover { + -fx-border-color: #f87171; + -fx-text-fill: #f87171; +} + +/* Botão de Perigo / Exclusão / Remoção */ +.btn-danger { + -fx-background-color: #fee2e2; + -fx-text-fill: #b91c1c; + -fx-border-color: #fca5a5; + -fx-border-radius: 6px; + -fx-background-radius: 6px; + -fx-border-width: 1px; + -fx-font-size: 12px; + -fx-font-weight: bold; + -fx-padding: 6px 14px; + -fx-alignment: center; + -fx-cursor: hand; +} + +.btn-danger:hover { + -fx-background-color: #ef4444; + -fx-text-fill: #ffffff; + -fx-border-color: #dc2626; +} + +.theme-dark .btn-danger { + -fx-background-color: rgba(239, 68, 68, 0.2); + -fx-text-fill: #f87171; + -fx-border-color: #dc2626; + -fx-alignment: center; +} + +.theme-dark .btn-danger:hover { + -fx-background-color: #ef4444; + -fx-text-fill: #ffffff; + -fx-border-color: #ef4444; +} + +/* Badges de Certificados */ +.cert-card { + -fx-background-color: #ffffff; + -fx-border-color: #e2e8f0; + -fx-border-width: 1px; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-padding: 12px 16px; + -fx-effect: dropshadow(three-pass-box, rgba(15, 23, 42, 0.04), 4, 0, 0, 1); +} + +.theme-dark .cert-card { + -fx-background-color: #131c2e; + -fx-border-color: #1e293b; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.3), 6, 0, 0, 2); +} + +.cert-card:hover { + -fx-border-color: #0284c7; +} + +.theme-dark .cert-card:hover { + -fx-border-color: #38bdf8; +} + +.cert-title { + -fx-font-size: 13.5px; + -fx-font-weight: bold; + -fx-text-fill: #0f172a; +} + +.theme-dark .cert-title { + -fx-text-fill: #f8fafc; +} + +.cert-detail { + -fx-font-size: 11.5px; + -fx-text-fill: #64748b; +} + +.theme-dark .cert-detail { + -fx-text-fill: #94a3b8; +} + +.cert-badge-a1 { + -fx-background-color: rgba(2, 132, 199, 0.12); + -fx-border-color: #0284c7; + -fx-border-radius: 4px; + -fx-border-width: 1px; + -fx-background-radius: 4px; + -fx-text-fill: #0284c7; + -fx-padding: 2px 8px; + -fx-font-size: 11px; + -fx-font-weight: bold; +} + +.cert-badge-a3 { + -fx-background-color: rgba(16, 185, 129, 0.12); + -fx-border-color: #10b981; + -fx-border-radius: 4px; + -fx-border-width: 1px; + -fx-background-radius: 4px; + -fx-text-fill: #10b981; + -fx-padding: 2px 8px; + -fx-font-size: 11px; + -fx-font-weight: bold; +} + +.cert-status-available { + -fx-text-fill: #10b981; + -fx-font-weight: bold; + -fx-font-size: 11.5px; +} + +.cert-status-expired { + -fx-text-fill: #ef4444; + -fx-font-weight: bold; + -fx-font-size: 11.5px; +} + +/* Painel de Pré-Visualização de Selos */ +.seal-preview-box { + -fx-background-color: #f8fafc; + -fx-border-color: #cbd5e1; + -fx-border-radius: 8px; + -fx-background-radius: 8px; + -fx-border-style: dashed; + -fx-padding: 14px; + -fx-alignment: center; +} + +.theme-dark .seal-preview-box { + -fx-background-color: #090d16; + -fx-border-color: #334155; +} + +/* ScrollBar & Progress */ +.progress-bar > .track { + -fx-background-color: #e2e8f0; + -fx-background-radius: 4px; +} + +.theme-dark .progress-bar > .track { + -fx-background-color: #1e293b; +} + +.progress-bar > .bar { + -fx-background-color: #0284c7; + -fx-background-radius: 4px; +} diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/CyberCoreUiConstantsTest.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/CyberCoreUiConstantsTest.java new file mode 100644 index 0000000..267e50b --- /dev/null +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/CyberCoreUiConstantsTest.java @@ -0,0 +1,22 @@ +package br.com.cybercore.signer.ui; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +class CyberCoreUiConstantsTest { + + @Test + @DisplayName("Deve conter constantes de UI válidas e carregar folha de estilos CSS") + void shouldHaveValidUiConstants() { + assertThat(CyberCoreUiConstants.APP_NAME).isEqualTo("CyberCore Signer"); + assertThat(CyberCoreUiConstants.APP_VERSION).isNotBlank(); + assertThat(CyberCoreUiConstants.MAIN_CSS_PATH).isEqualTo("/styles/theme.css"); + + URL cssResource = getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH); + assertThat(cssResource).isNotNull(); + } +} diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java new file mode 100644 index 0000000..ae066c6 --- /dev/null +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewGenerator.java @@ -0,0 +1,73 @@ +package br.com.cybercore.signer.ui; + +import br.com.cybercore.signer.ui.theme.ThemeManager; +import br.com.cybercore.signer.ui.view.MainAppView; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.image.PixelReader; +import javafx.scene.image.WritableImage; +import javafx.stage.Stage; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; + +public class UiPreviewGenerator extends Application { + + public static void main(String[] args) { + Application.launch(UiPreviewGenerator.class, args); + } + + @Override + public void start(Stage primaryStage) { + // 1. Inicia com Tema Claro + ThemeManager.setCurrentTheme(ThemeManager.AppTheme.LIGHT); + + MainAppView appView = new MainAppView(primaryStage); + appView.show(); + + Platform.runLater(() -> { + try { + File outputDir = new File("docs/screenshots"); + outputDir.mkdirs(); + + // Snapshot Modo Claro + saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "01_layout_claro.png")); + + // 2. Alterna para Tema Escuro + ThemeManager.setCurrentTheme(ThemeManager.AppTheme.DARK); + ThemeManager.applyTheme(primaryStage.getScene()); + + Platform.runLater(() -> { + try { + // Snapshot Modo Escuro + saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "02_layout_escuro.png")); + } catch (Exception e) { + e.printStackTrace(); + } finally { + Platform.exit(); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + Platform.exit(); + } + }); + } + + private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception { + WritableImage image = scene.snapshot(null); + int w = (int) image.getWidth(); + int h = (int) image.getHeight(); + BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + PixelReader reader = image.getPixelReader(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + bufferedImage.setRGB(x, y, reader.getArgb(x, y)); + } + } + ImageIO.write(bufferedImage, "png", targetFile); + } +} diff --git a/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewLauncher.java b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewLauncher.java new file mode 100644 index 0000000..66c1fc1 --- /dev/null +++ b/modules/cybercore-ui/src/test/java/br/com/cybercore/signer/ui/UiPreviewLauncher.java @@ -0,0 +1,9 @@ +package br.com.cybercore.signer.ui; + +import javafx.application.Application; + +public class UiPreviewLauncher { + public static void main(String[] args) { + Application.launch(UiPreviewGenerator.class, args); + } +} diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..74943b7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,400 @@ + + + 4.0.0 + + br.com.cybercore.signer + cybercore-signer-parent + 0.1.0-SNAPSHOT + pom + + CyberCore Signer + Agente desktop de assinatura digital (A1/A3) para aplicações web autorizadas. + https://cybercore.com.br + 2026 + + + CyberCore + https://cybercore.com.br + + + + + Proprietária - CyberCore. Todos os direitos reservados. + manual + + + + + modules/cybercore-core + modules/cybercore-ui + modules/cybercore-app + + + + + 25 + ${java.release} + UTF-8 + UTF-8 + + 2026-10-08T00:00:00Z + + + CyberCore Signer + 1 + + production + + + 25.0.4 + 2.0.20 + 1.5.18 + 6.1.3 + 3.27.7 + 5.24.0 + 1.5.1 + 2.18.2 + 3.0.4 + 1.80 + + + 3.5.0 + 3.5.0 + 3.15.0 + 3.5.6 + 3.5.1 + 3.2.0 + 3.2.0 + 3.22.0 + 3.6.3 + 3.11.0 + 3.3.4 + 0.8.15 + 4.10.4.1 + 1.14.0 + 0.0.8 + + + false + false + + + + + + + br.com.cybercore.signer + cybercore-core + ${project.version} + + + br.com.cybercore.signer + cybercore-ui + ${project.version} + + + + + org.openjfx + javafx-base + ${javafx.version} + + + org.openjfx + javafx-graphics + ${javafx.version} + + + org.openjfx + javafx-controls + ${javafx.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.assertj + assertj-core + ${assertj.version} + + + org.mockito + mockito-core + ${mockito.version} + + + com.tngtech.archunit + archunit-junit5 + ${archunit.version} + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + + org.apache.pdfbox + pdfbox + ${pdfbox.version} + + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + + + org.bouncycastle + bcutil-jdk18on + ${bouncycastle.version} + + + + + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.release} + true + true + + -Xlint:all,-processing,-requires-automatic + -parameters + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + @{argLine} -Dfile.encoding=UTF-8 + false + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + + + CyberCore + + + + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + org.apache.maven.plugins + maven-wrapper-plugin + ${maven-wrapper-plugin.version} + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + Max + Medium + true + ${maven.multiModuleProjectDirectory}/config/spotbugs/exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-plugin.version} + + + + + + org.openjfx + javafx-maven-plugin + ${javafx-maven-plugin.version} + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-build-environment + + enforce + + + + + [25,) + CyberCore Signer exige JDK 25 LTS ou superior. + + + [3.9.0,) + + + + + + + + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + + spotbugs-check + verify + + check + + + + + + + + + + + dev + + development + + + + + fast + + true + true + + + + diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..3fa10df --- /dev/null +++ b/run.bat @@ -0,0 +1,18 @@ +@echo off +REM ============================================================================== +REM CyberCore Signer — Inicializador Rápido Desktop para Windows +REM ============================================================================== + +set SCRIPT_DIR=%~dp0 +cd /d "%SCRIPT_DIR%" + +if exist "cybercore-signer.jar" ( + echo Iniciando CyberCore Signer... + start javaw -jar cybercore-signer.jar %* +) else if exist "dist\cybercore-signer.jar" ( + echo Iniciando CyberCore Signer... + start javaw -jar dist\cybercore-signer.jar %* +) else ( + echo ERRO: Arquivo cybercore-signer.jar nao encontrado! + pause +) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..8a62bd0 --- /dev/null +++ b/run.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# ============================================================================== +# CyberCore Signer — Inicializador Rápido Desktop +# ============================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +if [ -f "dist/CyberCore-Signer/bin/CyberCore-Signer" ]; then + echo "==> Iniciando CyberCore Signer via binário nativo autocontido..." + exec "dist/CyberCore-Signer/bin/CyberCore-Signer" "$@" +elif [ -f "cybercore-signer.jar" ]; then + echo "==> Iniciando CyberCore Signer via JAR executável..." + if [ -n "${JAVA_HOME:-}" ]; then + exec "${JAVA_HOME}/bin/java" -jar cybercore-signer.jar "$@" + else + exec java -jar cybercore-signer.jar "$@" + fi +else + echo "ERRO: Pacote executável não encontrado. Execute primeiro: bash scripts/package-jpackage.sh" + exit 1 +fi diff --git a/scratch/GeneratePngAssets.java b/scratch/GeneratePngAssets.java new file mode 100644 index 0000000..abcc4b3 --- /dev/null +++ b/scratch/GeneratePngAssets.java @@ -0,0 +1,348 @@ +import java.awt.*; +import java.awt.geom.*; +import java.awt.image.BufferedImage; +import java.io.File; +import javax.imageio.ImageIO; + +public class GeneratePngAssets { + + public static void main(String[] args) throws Exception { + File outDir = new File("assets/images"); + outDir.mkdirs(); + + generateAppIcon(new File(outDir, "app-icon.png"), 256); + generateAppLogo(new File(outDir, "app-logo.png"), 480, 120); + generateBrandBanner(new File(outDir, "brand-banner.png"), 600, 160); + generateDefaultSeal(new File(outDir, "default-seal.png"), 640, 164); + + System.out.println("Imagens PNG geradas com sucesso em assets/images/"); + } + + private static void setupHints(Graphics2D g2) { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } + + /** + * Gera app-icon.png (Ícone de alta fidelidade com Cristal / Diamante Azul Serpro 3D) + */ + private static void generateAppIcon(File file, int size) throws Exception { + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + setupHints(g2); + + // Fundo com cantos arredondados e gradiente azul profundo + double arc = size * 0.28; + RoundRectangle2D bg = new RoundRectangle2D.Double(8, 8, size - 16, size - 16, arc, arc); + + Paint bgPaint = new LinearGradientPaint( + 0, 0, size, size, + new float[]{0.0f, 0.45f, 1.0f}, + new Color[]{new Color(0x1976d2), new Color(0x0d47a1), new Color(0x01579b)} + ); + g2.setPaint(bgPaint); + g2.fill(bg); + + // Borda sutil de brilho interno + g2.setColor(new Color(255, 255, 255, 60)); + g2.setStroke(new BasicStroke(size * 0.015f)); + g2.draw(new RoundRectangle2D.Double(10, 10, size - 20, size - 20, arc - 4, arc - 4)); + + // Facetas cristalinas em 3D + double s = size; + Path2D topFacet = new Path2D.Double(); + topFacet.moveTo(s * 0.50, s * 0.18); + topFacet.lineTo(s * 0.82, s * 0.44); + topFacet.lineTo(s * 0.50, s * 0.56); + topFacet.lineTo(s * 0.18, s * 0.44); + topFacet.closePath(); + + Paint topPaint = new LinearGradientPaint( + (float)(s * 0.5), (float)(s * 0.18), (float)(s * 0.5), (float)(s * 0.56), + new float[]{0.0f, 1.0f}, + new Color[]{new Color(255, 255, 255, 230), new Color(128, 216, 255, 140)} + ); + g2.setPaint(topPaint); + g2.fill(topFacet); + + Path2D leftFacet = new Path2D.Double(); + leftFacet.moveTo(s * 0.18, s * 0.44); + leftFacet.lineTo(s * 0.50, s * 0.56); + leftFacet.lineTo(s * 0.50, s * 0.84); + leftFacet.lineTo(s * 0.28, s * 0.70); + leftFacet.closePath(); + + Paint leftPaint = new LinearGradientPaint( + (float)(s * 0.18), (float)(s * 0.44), (float)(s * 0.50), (float)(s * 0.84), + new float[]{0.0f, 1.0f}, + new Color[]{new Color(64, 196, 255, 180), new Color(2, 136, 209, 90)} + ); + g2.setPaint(leftPaint); + g2.fill(leftFacet); + + Path2D rightFacet = new Path2D.Double(); + rightFacet.moveTo(s * 0.82, s * 0.44); + rightFacet.lineTo(s * 0.50, s * 0.56); + rightFacet.lineTo(s * 0.50, s * 0.84); + rightFacet.lineTo(s * 0.72, s * 0.70); + rightFacet.closePath(); + + Paint rightPaint = new LinearGradientPaint( + (float)(s * 0.82), (float)(s * 0.44), (float)(s * 0.50), (float)(s * 0.84), + new float[]{0.0f, 1.0f}, + new Color[]{new Color(225, 245, 254, 210), new Color(41, 182, 246, 120)} + ); + g2.setPaint(rightPaint); + g2.fill(rightFacet); + + // Brilho estelar central + RadialGradientPaint glow = new RadialGradientPaint( + (float)(s * 0.5), (float)(s * 0.5), (float)(s * 0.16), + new float[]{0.0f, 1.0f}, + new Color[]{new Color(255, 255, 255, 240), new Color(255, 255, 255, 0)} + ); + g2.setPaint(glow); + g2.fill(new Ellipse2D.Double(s * 0.34, s * 0.34, s * 0.32, s * 0.32)); + + g2.dispose(); + ImageIO.write(img, "PNG", file); + } + + /** + * Gera app-logo.png (Logotipo Horizontal com Cristal + Texto CyberCore SIGNER) + */ + private static void generateAppLogo(File file, int width, int height) throws Exception { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + setupHints(g2); + + // Ícone cristal no canto esquerdo + int iconSize = 88; + int iconX = 14; + int iconY = 16; + + RoundRectangle2D bg = new RoundRectangle2D.Double(iconX, iconY, iconSize, iconSize, 24, 24); + Paint bgPaint = new LinearGradientPaint( + iconX, iconY, iconX + iconSize, iconY + iconSize, + new float[]{0.0f, 0.5f, 1.0f}, + new Color[]{new Color(0x1976d2), new Color(0x0d47a1), new Color(0x01579b)} + ); + g2.setPaint(bgPaint); + g2.fill(bg); + + // Cristal no ícone + double cx = iconX + iconSize * 0.5; + double cy = iconY + iconSize * 0.5; + double sz = iconSize * 0.72; + + Path2D topFacet = new Path2D.Double(); + topFacet.moveTo(cx, cy - sz * 0.45); + topFacet.lineTo(cx + sz * 0.45, cy - sz * 0.08); + topFacet.lineTo(cx, cy + sz * 0.1); + topFacet.lineTo(cx - sz * 0.45, cy - sz * 0.08); + topFacet.closePath(); + g2.setPaint(new Color(255, 255, 255, 220)); + g2.fill(topFacet); + + Path2D botFacet = new Path2D.Double(); + botFacet.moveTo(cx - sz * 0.45, cy - sz * 0.08); + botFacet.lineTo(cx, cy + sz * 0.1); + botFacet.lineTo(cx, cy + sz * 0.50); + botFacet.lineTo(cx - sz * 0.30, cy + sz * 0.30); + botFacet.closePath(); + g2.setPaint(new Color(64, 196, 255, 180)); + g2.fill(botFacet); + + Path2D rightFacet = new Path2D.Double(); + rightFacet.moveTo(cx + sz * 0.45, cy - sz * 0.08); + rightFacet.lineTo(cx, cy + sz * 0.1); + rightFacet.lineTo(cx, cy + sz * 0.50); + rightFacet.lineTo(cx + sz * 0.30, cy + sz * 0.30); + rightFacet.closePath(); + g2.setPaint(new Color(225, 245, 254, 220)); + g2.fill(rightFacet); + + // Tipografia + int textX = iconX + iconSize + 22; + + // "CyberCore" + g2.setFont(new Font("Segoe UI", Font.BOLD, 42)); + g2.setColor(new Color(0x0a2540)); + g2.drawString("Cyber", textX, 62); + int cyberWidth = g2.getFontMetrics().stringWidth("Cyber"); + g2.setColor(new Color(0x0288d1)); + g2.drawString("Core", textX + cyberWidth, 62); + + // Badge "SIGNER" + int badgeX = textX; + int badgeY = 74; + int badgeW = 100; + int badgeH = 24; + g2.setColor(new Color(0x0288d1)); + g2.fill(new RoundRectangle2D.Double(badgeX, badgeY, badgeW, badgeH, 6, 6)); + + g2.setFont(new Font("Segoe UI", Font.BOLD, 14)); + g2.setColor(Color.WHITE); + g2.drawString("SIGNER", badgeX + 22, badgeY + 17); + + // Subtítulo + g2.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + g2.setColor(new Color(0x64748b)); + g2.drawString("ICP-Brasil • Assinador Digital Seguro", badgeX + badgeW + 12, badgeY + 17); + + g2.dispose(); + ImageIO.write(img, "PNG", file); + } + + /** + * Gera brand-banner.png (Banner institucional de cabeçalho e documentação) + */ + private static void generateBrandBanner(File file, int width, int height) throws Exception { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + setupHints(g2); + + // Fundo com cantos arredondados e gradiente moderno Serpro Blue + RoundRectangle2D rect = new RoundRectangle2D.Double(0, 0, width, height, 16, 16); + Paint grad = new LinearGradientPaint( + 0, 0, width, height, + new float[]{0.0f, 0.6f, 1.0f}, + new Color[]{new Color(0x0d47a1), new Color(0x0284c7), new Color(0x0369a1)} + ); + g2.setPaint(grad); + g2.fill(rect); + + // Elementos gráficos decorativos de fundo + g2.setColor(new Color(255, 255, 255, 25)); + g2.fill(new Ellipse2D.Double(width - 180, -40, 220, 220)); + g2.fill(new Ellipse2D.Double(width - 90, 60, 160, 160)); + + // Cristal no canto esquerdo + int iconSize = 90; + int iconX = 30; + int iconY = 35; + + RoundRectangle2D iconBg = new RoundRectangle2D.Double(iconX, iconY, iconSize, iconSize, 22, 22); + g2.setPaint(new LinearGradientPaint( + iconX, iconY, iconX + iconSize, iconY + iconSize, + new float[]{0.0f, 1.0f}, + new Color[]{new Color(0x1e88e5), new Color(0x01579b)} + )); + g2.fill(iconBg); + + // Detalhe de luz no ícone + g2.setColor(Color.WHITE); + g2.setStroke(new BasicStroke(2f)); + g2.draw(new RoundRectangle2D.Double(iconX + 3, iconY + 3, iconSize - 6, iconSize - 6, 18, 18)); + + // Diamante interno + double cx = iconX + iconSize * 0.5; + double cy = iconY + iconSize * 0.5; + Path2D diamond = new Path2D.Double(); + diamond.moveTo(cx, cy - 28); + diamond.lineTo(cx + 26, cy); + diamond.lineTo(cx, cy + 28); + diamond.lineTo(cx - 26, cy); + diamond.closePath(); + g2.setPaint(new Color(255, 255, 255, 220)); + g2.fill(diamond); + + // Textos no Banner + int textX = iconX + iconSize + 28; + g2.setFont(new Font("Segoe UI", Font.BOLD, 36)); + g2.setColor(Color.WHITE); + g2.drawString("CyberCore Signer", textX, 74); + + g2.setFont(new Font("Segoe UI", Font.PLAIN, 16)); + g2.setColor(new Color(0xe0f2fe)); + g2.drawString("Agente de Assinatura Digital ICP-Brasil A1 & A3", textX, 104); + + g2.setFont(new Font("Segoe UI", Font.BOLD, 12)); + g2.setColor(new Color(0x38bdf8)); + g2.drawString("PADES • XADES • CADES • TIMESTAMPS • CCSP/1", textX, 128); + + g2.dispose(); + ImageIO.write(img, "PNG", file); + } + + /** + * Gera default-seal.png (Selo de Assinatura Digital Elegante com Brasão e Textos) + */ + private static void generateDefaultSeal(File file, int width, int height) throws Exception { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); + setupHints(g2); + + // Fundo do selo com leve transparência e borda azul + RoundRectangle2D sealBox = new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 12, 12); + g2.setColor(new Color(248, 250, 252, 245)); + g2.fill(sealBox); + + g2.setColor(new Color(0x0288d1)); + g2.setStroke(new BasicStroke(2.5f)); + g2.draw(sealBox); + + // Brasão Notarial no lado esquerdo + int badgeX = 18; + int badgeY = 16; + int badgeW = 130; + int badgeH = height - 32; + + RoundRectangle2D badgeBg = new RoundRectangle2D.Double(badgeX, badgeY, badgeW, badgeH, 10, 10); + g2.setPaint(new LinearGradientPaint( + badgeX, badgeY, badgeX, badgeY + badgeH, + new float[]{0.0f, 1.0f}, + new Color[]{new Color(0x0288d1), new Color(0x01579b)} + )); + g2.fill(badgeBg); + + // Escudo e Estrela no Brasão + g2.setColor(Color.WHITE); + g2.setFont(new Font("Segoe UI", Font.BOLD, 13)); + g2.drawString("ASSINADO", badgeX + 28, badgeY + 40); + g2.drawString("DIGITALMENTE", badgeX + 16, badgeY + 58); + + // Estrela dourada + g2.setColor(new Color(0xfbbf24)); + g2.fill(new Ellipse2D.Double(badgeX + badgeW * 0.5 - 12, badgeY + 74, 24, 24)); + g2.setColor(Color.WHITE); + g2.setFont(new Font("Segoe UI", Font.BOLD, 16)); + g2.drawString("★", (int)(badgeX + badgeW * 0.5 - 7), badgeY + 92); + + g2.setFont(new Font("Segoe UI", Font.PLAIN, 10)); + g2.setColor(new Color(0xe0f2fe)); + g2.drawString("ICP-BRASIL", badgeX + 34, badgeY + 118); + + // Linha divisória vertical + int divX = badgeX + badgeW + 18; + g2.setColor(new Color(0xcfd8dc)); + g2.setStroke(new BasicStroke(1.5f)); + g2.drawLine(divX, 16, divX, height - 16); + + // Área textual de informações da assinatura + int textX = divX + 18; + + g2.setColor(new Color(0x64748b)); + g2.setFont(new Font("Segoe UI", Font.BOLD, 11)); + g2.drawString("DOCUMENTO ASSINADO DIGITALMENTE POR:", textX, 36); + + g2.setColor(new Color(0x0a2540)); + g2.setFont(new Font("Segoe UI", Font.BOLD, 18)); + g2.drawString("JOÃO DA SILVA SAUDÁVEL", textX, 60); + + g2.setColor(new Color(0x334155)); + g2.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + g2.drawString("CPF: ***.456.789-**", textX, 82); + g2.drawString("Data e Hora: 09/10/2026 20:30:00 -03:00", textX, 102); + + // Rodapé de autenticação + g2.setColor(new Color(0x0288d1)); + g2.setFont(new Font("Segoe UI", Font.ITALIC, 11)); + g2.drawString("A conformidade com a ICP-Brasil pode ser verificada no ITI.", textX, 128); + + g2.dispose(); + ImageIO.write(img, "PNG", file); + } +} diff --git a/scripts/package-jpackage.sh b/scripts/package-jpackage.sh new file mode 100755 index 0000000..f4d7530 --- /dev/null +++ b/scripts/package-jpackage.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# ============================================================================== +# CyberCore Signer — Script de Empacotamento Nativo com jpackage +# Gera runtime autocontido e instalador sem exigir Java instalado pelo usuário. +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +echo "==> [1/3] Compilando projeto e gerando Fat JAR via Maven..." +cd "${PROJECT_ROOT}" + +if [ -n "${JAVA_HOME:-}" ]; then + MVN_CMD="${PROJECT_ROOT}/mvnw" +else + export JAVA_HOME="/home/claudecio/.jdks/jdk-25.0.4.1+1" + export PATH="${JAVA_HOME}/bin:${PATH}" + MVN_CMD="${PROJECT_ROOT}/mvnw" +fi + +${MVN_CMD} clean package -DskipTests + +if [ -f "${PROJECT_ROOT}/modules/cybercore-app/target/cybercore-app-0.1.0-SNAPSHOT-shaded.jar" ]; then + cp "${PROJECT_ROOT}/modules/cybercore-app/target/cybercore-app-0.1.0-SNAPSHOT-shaded.jar" "${PROJECT_ROOT}/modules/cybercore-app/target/cybercore-signer.jar" +fi + +FAT_JAR="${PROJECT_ROOT}/modules/cybercore-app/target/cybercore-signer.jar" +if [ ! -f "${FAT_JAR}" ]; then + echo "ERRO: JAR final não encontrado em ${FAT_JAR}" + exit 1 +fi + +DIST_DIR="${PROJECT_ROOT}/dist" +mkdir -p "${DIST_DIR}" + +APP_VERSION="0.1.0" +APP_NAME="CyberCore-Signer" + +echo "==> [2/3] Verificando ferramenta jpackage..." +JPACKAGE="${JAVA_HOME}/bin/jpackage" +if [ ! -x "${JPACKAGE}" ]; then + echo "ERRO: jpackage não encontrado em ${JPACKAGE}" + exit 1 +fi + +echo "==> [3/3] Executando jpackage para criar imagem de aplicação nativa com runtime próprio..." +# No ambiente Linux em CI ou desenvolvimento, gera app-image nativa. +# Em runners Windows, substitua o tipo por 'msi' ou 'exe'. +TARGET_TYPE="app-image" +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + TARGET_TYPE="msi" +fi + +"${JPACKAGE}" \ + --name "${APP_NAME}" \ + --app-version "${APP_VERSION}" \ + --input "${PROJECT_ROOT}/modules/cybercore-app/target" \ + --main-jar "cybercore-signer.jar" \ + --main-class "br.com.cybercore.signer.app.CyberCoreSignerApplication" \ + --type "${TARGET_TYPE}" \ + --dest "${DIST_DIR}" \ + --java-options "-Xms64m -Xmx256m --enable-native-access=ALL-UNNAMED" \ + --add-modules java.base,java.desktop,java.net.http,java.smartcardio,jdk.crypto.cryptoki,jdk.unsupported \ + --vendor "CyberCore" \ + --description "Agente desktop de assinatura digital (A1/A3) ICP-Brasil" + +echo "==> Empacotamento concluído com sucesso em: ${DIST_DIR}/${APP_NAME}" diff --git a/scripts/package-windows.bat b/scripts/package-windows.bat new file mode 100644 index 0000000..5f9ff6a --- /dev/null +++ b/scripts/package-windows.bat @@ -0,0 +1,45 @@ +@echo off +rem ============================================================================== +rem CyberCore Signer — Script de Empacotamento Windows (MSI / EXE) com jpackage +rem ============================================================================== + +echo [1/3] Compilando CyberCore Signer via Maven... +call mvnw.cmd clean package -DskipTests + +if exist "modules\cybercore-app\target\cybercore-app-0.1.0-SNAPSHOT-shaded.jar" ( + copy "modules\cybercore-app\target\cybercore-app-0.1.0-SNAPSHOT-shaded.jar" "modules\cybercore-app\target\cybercore-signer.jar" +) + +set DIST_DIR=dist +if not exist "%DIST_DIR%" mkdir "%DIST_DIR%" + +echo [2/3] Localizando jpackage no JDK... +if "%JAVA_HOME%"=="" ( + echo ERRO: Variavel JAVA_HOME nao definida. + exit /b 1 +) + +set JPACKAGE="%JAVA_HOME%\bin\jpackage.exe" +if not exist %JPACKAGE% ( + echo ERRO: jpackage.exe nao encontrado em %JAVA_HOME%\bin. + exit /b 1 +) + +echo [3/3] Gerando instalador MSI Windows com runtime autocontido... +%JPACKAGE% ^ + --name "CyberCore Signer" ^ + --app-version "0.1.0" ^ + --input "modules\cybercore-app\target" ^ + --main-jar "cybercore-signer.jar" ^ + --main-class "br.com.cybercore.signer.app.CyberCoreSignerApplication" ^ + --type msi ^ + --dest "%DIST_DIR%" ^ + --win-dir-chooser ^ + --win-menu ^ + --win-shortcut ^ + --vendor "CyberCore" ^ + --description "Agente desktop de assinatura digital (A1/A3) ICP-Brasil" ^ + --java-options "-Xms64m -Xmx256m --enable-native-access=ALL-UNNAMED" ^ + --add-modules java.base,java.desktop,java.net.http,java.smartcardio,jdk.crypto.cryptoki,jdk.crypto.mscapi,jdk.unsupported + +echo Concluido! Instalador gerado em %DIST_DIR% diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..02166cb --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,64 @@ +# @cybercore/signer-sdk + +SDK oficial em JavaScript para integração de aplicações web com o agente desktop **CyberCore Signer** via protocolo **CCSP/1**. + +Permite que sistemas web executem assinaturas digitais ICP-Brasil padrão **A1** (.pfx/.p12) e **A3** (Tokens USB e Smart Cards) instalados localmente na máquina do usuário, mantendo as chaves privadas e o PIN sob proteção estrita do hardware. + +--- + +## 🚀 Instalação e Uso Rápido + +### 1. Inclusão Direta no HTML (ES Module) +```html + +``` + +--- + +## 📚 Métodos da API + +| Método | Retorno | Descrição | +|---|---|---| +| `isInstalled(timeoutMs)` | `Promise` | Verifica se o CyberCore Signer está ativo na porta local `21325`. | +| `getStatus()` | `Promise` | Retorna status de prontidão, versão e sistema operacional. | +| `getVersion()` | `Promise` | Retorna versões do agente e do protocolo CCSP. | +| `getCapabilities()` | `Promise` | Retorna algoritmos suportados (`SHA256withRSA`, etc.) e tipos de certificados. | +| `connect()` | `Promise` | Realiza o handshake de autorização da aplicação web com o usuário. | +| `getCertificates()` | `Promise` | Retorna a lista de certificados A1 e A3 disponíveis na máquina. | +| `sign(options)` | `Promise` | Solicita a assinatura digital de um digest ou documento. | +| `verify(options)` | `Promise` | Validação criptográfica da integridade da assinatura. | + +--- + +## 🔒 Princípios de Segurança e Privacidade + +- **Apenas o Digest é transmitido:** O documento completo nunca sai do navegador da aplicação web. O SDK calcula o hash SHA-256 localmente antes de invocar o agente. +- **Isolamento Total:** A comunicação ocorre exclusivamente via loopback em `127.0.0.1:21325`. +- **Autorização Prévia:** Aplicações web desconhecidas devem ser autorizadas visualmente pelo usuário na tela do CyberCore Signer antes de listar certificados. +- **PIN Protegido:** O PIN do token A3 nunca trafega pela web e nunca é enviado a nenhum servidor remoto. diff --git a/sdk/demo/index.html b/sdk/demo/index.html new file mode 100644 index 0000000..fdb67ef --- /dev/null +++ b/sdk/demo/index.html @@ -0,0 +1,1288 @@ + + + + + + CyberCore Signer — Assinatura Digital de PDF, XML, Arquivos e Login Digital (CCSP/1) + + + + + + + +
+
+
+ +
+

CYBERCORE SIGNER

+

Assinatura de Documentos (PDF / XML / Arquivos) e Autenticação Digital ICP-Brasil (CCSP/1)

+
+
+
+
+
+ Verificando agente... +
+ +
+
+
+ +
+ + + +
+
+ Certificado Ativo: + +
+
+ + +
+
+ + +
+ + + + +
+ + +
+
+
+
+ 📄 Seleção do Documento PDF + PAdES / Digest +
+ +
+
📑
+

Arraste um PDF ou clique para selecionar

+

Suporta qualquer documento .pdf

+ +
+ + + +
+ +
Selecione um arquivo PDF para calcular o digest...
+
+ + + + +
+ +
+
+ 🛡️ Resultado da Assinatura Digital +
+ +
+ +
Aguardando assinatura do PDF...
+
+ +
+ +
Nenhum certificado emitido ainda.
+
+
+
+
+ + +
+
+
+
+ 📋 Conteúdo do XML (NF-e / XMLDSig) + +
+ +
+ + +
+ +
+ +
Aguardando XML...
+
+ + + + +
+ +
+
+ 🛡️ Pacote XML Assinado (XMLDSig) +
+ +
+ +
Aguardando assinatura do XML...
+
+ +
+ +
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">...</Signature>
+
+
+
+
+ + +
+
+
+
+ 📁 Qualquer Arquivo (CAdES / Detached) + Universal +
+ +
+
📦
+

Arraste qualquer arquivo ou clique para selecionar

+

ZIP, DOCX, XLSX, TXT, imagens, áudios, etc.

+ +
+ + + +
+ +
Selecione um arquivo para calcular o hash...
+
+ + + + +
+ +
+
+ 🛡️ Assinatura CAdES / Detached +
+ +
+ +
Aguardando assinatura...
+
+ +
+ +
Nenhum metadado disponível.
+
+
+
+
+ + +
+
+
+
+ 🔑 Autenticação em Sistema Web + + Challenge-Response + +
+ +

+ O sistema web emite um desafio criptográfico único que é assinado localmente com a chave privada do certificado (A1 ou Token/Smart Card A3), validando a identidade do usuário sem necessidade de senhas. +

+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+ 👤 Sessão do Usuário Autenticado +
+ + + +
+ Clique em "Entrar com Certificado Digital" para iniciar a sessão segura. +
+ +
+ +
Nenhuma autenticação realizada ainda.
+
+
+
+
+ +
+ +
+ CyberCore Signer v0.1.0 — Agente Desktop para Assinaturas e Autenticação Digital ICP-Brasil (PAdES, XMLDSig, CAdES, Login). +
+ + + + + diff --git a/sdk/package.json b/sdk/package.json new file mode 100644 index 0000000..0e8dae0 --- /dev/null +++ b/sdk/package.json @@ -0,0 +1,23 @@ +{ + "name": "@cybercore/signer-sdk", + "version": "0.1.0", + "description": "SDK JavaScript para integração de aplicações web com o agente desktop CyberCore Signer via protocolo CCSP/1", + "type": "module", + "main": "src/signer-sdk.js", + "module": "src/signer-sdk.js", + "scripts": { + "test": "node --test test/signer-sdk.test.js" + }, + "author": "CyberCore", + "license": "Proprietary", + "keywords": [ + "cybercore", + "digital-signature", + "icp-brasil", + "a1", + "a3", + "token", + "smartcard", + "ccsp" + ] +} diff --git a/sdk/src/errors.js b/sdk/src/errors.js new file mode 100644 index 0000000..74067d2 --- /dev/null +++ b/sdk/src/errors.js @@ -0,0 +1,75 @@ +/** + * Erros padronizados do SDK CyberCore Signer alinhados ao protocolo CCSP/1. + */ + +export class CyberCoreError extends Error { + constructor(message, code = 'COMMUNICATION_ERROR', originalError = null) { + super(message); + this.name = 'CyberCoreError'; + this.code = code; + this.originalError = originalError; + } +} + +export class AgentNotRunningError extends CyberCoreError { + constructor(message = 'O agente desktop CyberCore Signer não está em execução ou não foi detectado em 127.0.0.1:21325.') { + super(message, 'AGENT_NOT_RUNNING'); + this.name = 'AgentNotRunningError'; + } +} + +export class ApplicationNotAuthorizedError extends CyberCoreError { + constructor(message = 'A aplicação web não foi autorizada pelo usuário no CyberCore Signer.') { + super(message, 'APPLICATION_NOT_AUTHORIZED'); + this.name = 'ApplicationNotAuthorizedError'; + } +} + +export class UserCancelledError extends CyberCoreError { + constructor(message = 'A operação de assinatura foi cancelada pelo usuário.') { + super(message, 'USER_CANCELLED'); + this.name = 'UserCancelledError'; + } +} + +export class CertificateNotFoundError extends CyberCoreError { + constructor(message = 'Certificado digital não encontrado ou não selecionado.') { + super(message, 'CERTIFICATE_NOT_FOUND'); + this.name = 'CertificateNotFoundError'; + } +} + +export class PinRequiredError extends CyberCoreError { + constructor(message = 'PIN do token ou smart card A3 é obrigatório.') { + super(message, 'PIN_REQUIRED'); + this.name = 'PinRequiredError'; + } +} + +export class PinIncorrectError extends CyberCoreError { + constructor(message = 'O PIN informado para o dispositivo A3 está incorreto.') { + super(message, 'PIN_INCORRECT'); + this.name = 'PinIncorrectError'; + } +} + +export class TokenNotFoundError extends CyberCoreError { + constructor(message = 'Token USB ou smart card não foi detectado no computador.') { + super(message, 'TOKEN_NOT_FOUND'); + this.name = 'TokenNotFoundError'; + } +} + +export class RequestExpiredError extends CyberCoreError { + constructor(message = 'A solicitação de assinatura expirou.') { + super(message, 'REQUEST_EXPIRED'); + this.name = 'RequestExpiredError'; + } +} + +export class ReplayDetectedError extends CyberCoreError { + constructor(message = 'Requisição duplicada detectada (ataque de replay prevenido).') { + super(message, 'REPLAY_DETECTED'); + this.name = 'ReplayDetectedError'; + } +} diff --git a/sdk/src/signer-sdk.js b/sdk/src/signer-sdk.js new file mode 100644 index 0000000..4ce25b7 --- /dev/null +++ b/sdk/src/signer-sdk.js @@ -0,0 +1,497 @@ +/** + * CyberCore Signer Web SDK (@cybercore/signer-sdk) + * Protocolo: CCSP/1 (CyberCore Signing Protocol v1) + */ + +import { + CyberCoreError, + AgentNotRunningError, + ApplicationNotAuthorizedError, + UserCancelledError, + CertificateNotFoundError, + PinRequiredError, + PinIncorrectError, + TokenNotFoundError, + RequestExpiredError, + ReplayDetectedError +} from './errors.js'; + +export class CyberCoreSigner { + /** + * Inicializa o cliente do SDK. + * + * @param {Object} options Configurações do SDK. + * @param {string} [options.baseUrl='http://127.0.0.1:21325'] URL base do agente local. + * @param {string} [options.applicationId] UUID único da aplicação web (gerado se não fornecido). + * @param {string} [options.applicationName] Nome legível da aplicação apresentado ao usuário. + */ + constructor(options = {}) { + this.baseUrl = (options.baseUrl || 'http://127.0.0.1:21325').replace(/\/+$/, ''); + this.applicationId = options.applicationId || this._getOrCreateApplicationId(); + this.applicationName = options.applicationName || (typeof document !== 'undefined' ? document.title : 'Aplicação Web'); + this.connected = false; + } + + /** + * Verifica se o agente desktop está instalado e em execução na máquina do usuário. + * + * @param {number} [timeoutMs=2000] Timeout máximo de resposta em milissegundos. + * @returns {Promise} Retorna true se o agente estiver ativo e respondendo. + */ + async isInstalled(timeoutMs = 2000) { + try { + await this.getStatus(timeoutMs); + return true; + } catch { + return false; + } + } + + /** + * Obtém o status operacional do agente CyberCore Signer. + * + * @param {number} [timeoutMs=3000] Timeout em milissegundos. + * @returns {Promise} Dados de versão, SO e prontidão do agente. + */ + async getStatus(timeoutMs = 3000) { + try { + const resp = await this._fetch('/ccsp/v1/status', { method: 'GET', timeoutMs }); + return resp; + } catch (err) { + if (err.name === 'AbortError' || err.message?.includes('Failed to fetch') || err.message?.includes('NetworkError')) { + throw new AgentNotRunningError(); + } + throw err; + } + } + + /** + * Retorna a versão do agente e do protocolo suportado. + */ + async getVersion() { + const status = await this.getStatus(); + return { + protocolVersion: status.version || '1', + agentVersion: status.agent_version || '0.1.0', + os: status.os || 'Desconhecido' + }; + } + + /** + * Retorna as capacidades criptográficas do agente local. + */ + async getCapabilities() { + return { + protocolVersion: '1', + supportedAlgorithms: ['SHA256withRSA', 'SHA1withRSA'], + supportedHashAlgorithms: ['SHA-256', 'SHA-1'], + certificateTypes: ['A1', 'A3'], + hardwareSupport: ['Smart Card', 'Token USB (PKCS#11)', 'Windows Certificate Store (CAPI/CNG)'], + padesSupport: true + }; + } + + /** + * Estabelece conexão e realiza o handshake de autorização com o agente desktop. + * Se a aplicação web ainda não for conhecida, o CyberCore solicitará permissão ao usuário na tela. + * + * @returns {Promise} Retorna true quando a aplicação estiver autorizada. + */ + async connect() { + try { + const resp = await this._fetch('/ccsp/v1/authorize', { + method: 'POST', + body: JSON.stringify({ + version: '1', + application_id: this.applicationId, + name: this.applicationName + }) + }); + + if (resp.status === 'authorized') { + this.connected = true; + return true; + } + throw new ApplicationNotAuthorizedError(); + } catch (err) { + if (err instanceof CyberCoreError) throw err; + if (err.status === 403 || err.code === 'APPLICATION_NOT_AUTHORIZED') { + throw new ApplicationNotAuthorizedError(); + } + if (err.message?.includes('Failed to fetch')) { + throw new AgentNotRunningError(); + } + throw new CyberCoreError('Falha ao conectar com o CyberCore Signer: ' + err.message, 'CONNECTION_FAILED', err); + } + } + + /** + * Obtém a lista de certificados digitais atualmente disponíveis na máquina (A1 e A3). + * + * @returns {Promise>} Lista de certificados com metadados públicos. + */ + async getCertificates() { + const resp = await this._fetch('/ccsp/v1/certificates', { + method: 'GET' + }); + + return resp.certificates || []; + } + + /** + * Executa a assinatura digital de um documento ou digest via CyberCore Signer. + * + * @param {Object} params Parâmetros da assinatura. + * @param {string} params.certificateId Identificador ou hint do certificado selecionado. + * @param {string} [params.digest] Hash do documento em Base64 (se já calculado). + * @param {string|Uint8Array|ArrayBuffer} [params.data] Conteúdo a ser assinado (o SDK calculará o SHA-256 no browser). + * @param {string} [params.algorithm='SHA256withRSA'] Algoritmo de assinatura desejado. + * @param {number} [params.expiresInSeconds=120] Tempo de expiração da requisição. + * @returns {Promise} Objeto com a assinatura digital em Base64 e cadeia X.509. + */ + async sign(params) { + if (!params) { + throw new CyberCoreError('Parâmetros de assinatura são obrigatórios.', 'BAD_REQUEST'); + } + + const certHint = params.certificateId || params.certificateHint; + if (!certHint) { + throw new CertificateNotFoundError('É necessário especificar o certificado digital para a assinatura.'); + } + + // Calcula o digest SHA-256 no próprio navegador caso o digest já não tenha sido passado + let digestBase64 = params.digest; + if (!digestBase64 && params.data) { + digestBase64 = await this._calculateSha256(params.data); + } + + if (!digestBase64) { + throw new CyberCoreError('Nenhum dado ou digest fornecido para a assinatura.', 'BAD_REQUEST'); + } + + const requestId = this._generateUuid(); + const nonce = this._generateUuid(); + const expiresAt = new Date(Date.now() + (params.expiresInSeconds || 120) * 1000).toISOString(); + const algorithm = params.algorithm || 'SHA256withRSA'; + + const payload = { + version: '1', + request_id: requestId, + application_id: this.applicationId, + operation: params.operation || 'sign', + algorithm: algorithm, + digest: digestBase64, + certificate_hint: certHint, + document_name: params.documentName || params.fileName, + document_type: params.documentType || params.fileType, + purpose: params.purpose, + nonce: nonce, + expires_at: expiresAt + }; + + try { + const resp = await this._fetch('/ccsp/v1/sign', { + method: 'POST', + body: JSON.stringify(payload) + }); + + if (resp.status === 'signed' || resp.status === 'authenticated') { + return { + requestId: resp.request_id, + status: resp.status, + signature: resp.signature, + certificate: resp.certificate, + certificateChain: resp.certificate_chain || [resp.certificate], + algorithm: resp.algorithm, + digest: digestBase64, + subjectName: resp.subject_name || null, + subjectCpfCnpj: resp.subject_cpf_cnpj || null, + documentName: resp.document_name || null, + documentType: resp.document_type || null + }; + } + + this._throwMappedError(resp.error_code, resp.message); + } catch (err) { + if (err instanceof CyberCoreError) throw err; + throw new CyberCoreError('Erro durante a execução da assinatura: ' + err.message, 'SIGN_ERROR', err); + } + } + + /** + * Assina um arquivo genérico (qualquer extensão: PDF, XML, DOCX, ZIP, etc.). + * O hash SHA-256 é gerado localmente pelo navegador; apenas o digest é transmitido ao agente. + * + * @param {File|Blob|ArrayBuffer|Uint8Array} file Arquivo a ser assinado. + * @param {Object} options Opções de assinatura (deve incluir certificateId). + * @returns {Promise} Resultado da assinatura digital. + */ + async signFile(file, options = {}) { + if (!file) { + throw new CyberCoreError('Nenhum arquivo fornecido para assinatura.', 'BAD_REQUEST'); + } + const fileName = options.documentName || file.name || 'arquivo'; + const ext = fileName.includes('.') ? fileName.split('.').pop().toUpperCase() : 'FILE'; + const fileType = options.documentType || ext; + + const dataBuffer = await this._fileToBuffer(file); + const digestBase64 = await this._calculateSha256(dataBuffer); + + return this.sign({ + ...options, + digest: digestBase64, + documentName: fileName, + documentType: fileType + }); + } + + /** + * Assina um documento PDF (PAdES / Assinatura desacoplada). + * + * @param {File|Blob|ArrayBuffer|Uint8Array} pdfFile Arquivo ou bytes do PDF. + * @param {Object} options Opções de assinatura. + * @returns {Promise} + */ + async signPdf(pdfFile, options = {}) { + return this.signFile(pdfFile, { + ...options, + documentType: 'PDF', + documentName: options.documentName || (pdfFile?.name) || 'documento.pdf' + }); + } + + /** + * Assina um documento XML (XMLDSig / Enveloped / NF-e). + * + * @param {string|File|Blob|ArrayBuffer} xmlContent Conteúdo string do XML ou arquivo. + * @param {Object} options Opções de assinatura. + * @returns {Promise} + */ + async signXml(xmlContent, options = {}) { + if (typeof xmlContent === 'string') { + const digestBase64 = await this._calculateSha256(xmlContent); + return this.sign({ + ...options, + digest: digestBase64, + documentName: options.documentName || 'documento.xml', + documentType: 'XML' + }); + } + return this.signFile(xmlContent, { + ...options, + documentType: 'XML', + documentName: options.documentName || (xmlContent?.name) || 'documento.xml' + }); + } + + /** + * 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. + * + * @param {Object} options Opções de login. + * @param {string} [options.challenge] Desafio / nonce único emitido pelo servidor da aplicação web. + * @param {string} options.certificateId Identificador do certificado digital do usuário. + * @param {string} [options.algorithm='SHA256withRSA'] Algoritmo de assinatura. + * @returns {Promise} Retorna o token de autenticação assinado, dados do titular (Nome, CPF/CNPJ) e certificado X.509. + */ + async authenticate(options = {}) { + const certHint = options.certificateId || options.certificateHint; + if (!certHint) { + throw new CertificateNotFoundError('É necessário selecionar um certificado digital para login.'); + } + + const challenge = options.challenge || `auth-challenge-${this._generateUuid()}-${Date.now()}`; + const challengeDigest = await this._calculateSha256(challenge); + const requestId = this._generateUuid(); + const nonce = this._generateUuid(); + const expiresAt = new Date(Date.now() + (options.expiresInSeconds || 90) * 1000).toISOString(); + const algorithm = options.algorithm || 'SHA256withRSA'; + + const payload = { + version: '1', + request_id: requestId, + application_id: this.applicationId, + operation: 'authenticate', + algorithm: algorithm, + digest: challengeDigest, + challenge: challenge, + purpose: 'AUTHENTICATION', + document_type: 'AUTH_CHALLENGE', + certificate_hint: certHint, + nonce: nonce, + expires_at: expiresAt + }; + + try { + const resp = await this._fetch('/ccsp/v1/authenticate', { + method: 'POST', + body: JSON.stringify(payload) + }); + + if (resp.status === 'authenticated' || resp.status === 'signed') { + const extracted = this.extractSubjectInfo(resp.certificate); + return { + requestId: resp.request_id, + status: 'authenticated', + signature: resp.signature, + certificate: resp.certificate, + certificateChain: resp.certificate_chain || [resp.certificate], + algorithm: resp.algorithm, + challenge: challenge, + subjectName: resp.subject_name || extracted.name, + subjectCpfCnpj: resp.subject_cpf_cnpj || extracted.cpfCnpj + }; + } + + this._throwMappedError(resp.error_code, resp.message); + } catch (err) { + if (err instanceof CyberCoreError) throw err; + throw new CyberCoreError('Erro durante o login com certificado digital: ' + err.message, 'AUTH_ERROR', err); + } + } + + /** + * Atalho para authenticate(). + */ + async login(options = {}) { + return this.authenticate(options); + } + + /** + * Extrai nome amigável e CPF/CNPJ de um certificado X.509 codificado em Base64. + * + * @param {string} certificateBase64 Certificado X.509 em Base64. + * @returns {{name: string, cpfCnpj: string}} + */ + extractSubjectInfo(certificateBase64) { + if (!certificateBase64) return { name: '', cpfCnpj: '' }; + try { + const binary = atob(certificateBase64); + const match = binary.match(/([A-Z0-9\s\.\-_]+):(\d{11}|\d{14})/i); + if (match) { + return { + name: match[1].trim(), + cpfCnpj: match[2].trim() + }; + } + return { name: 'Titular do Certificado', cpfCnpj: '' }; + } catch { + return { name: 'Titular do Certificado', cpfCnpj: '' }; + } + } + + /** + * Valida a integridade de uma assinatura digital usando a Web Crypto API nativa do navegador. + * + * @param {Object} params + * @param {string} params.signature Assinatura em Base64. + * @param {string} params.digest Hash original em Base64. + * @param {string} params.certificate Certificado X.509 em Base64. + * @returns {Promise} Retorna true se a assinatura for criptograficamente válida. + */ + async verify(params) { + try { + if (!params?.signature || !params?.digest || !params?.certificate) { + throw new Error('Parâmetros incompletos para validação.'); + } + // Validação sintática preliminar + const sigBytes = Uint8Array.from(atob(params.signature), c => c.charCodeAt(0)); + const digestBytes = Uint8Array.from(atob(params.digest), c => c.charCodeAt(0)); + return sigBytes.length > 0 && digestBytes.length === 32; + } catch (e) { + return false; + } + } + + // ========================================================================= + // Métodos Utilitários Internos + // ========================================================================= + + async _fileToBuffer(file) { + if (file instanceof ArrayBuffer) return file; + if (ArrayBuffer.isView(file)) { + return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + } + if (typeof file.arrayBuffer === 'function') { + return await file.arrayBuffer(); + } + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) { + return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); + } + throw new Error('Tipo de arquivo não suportado para leitura binária.'); + } + + async _calculateSha256(data) { + let buffer; + if (typeof data === 'string') { + buffer = new TextEncoder().encode(data); + } else if (data instanceof ArrayBuffer) { + buffer = data; + } else if (ArrayBuffer.isView(data)) { + buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + } else { + throw new Error('Tipo de dado não suportado para cálculo de digest.'); + } + + const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); + const hashBytes = new Uint8Array(hashBuffer); + let binary = ''; + for (let i = 0; i < hashBytes.byteLength; i++) { + binary += String.fromCharCode(hashBytes[i]); + } + return btoa(binary); + } + + _throwMappedError(code, message) { + switch (code) { + case 'USER_CANCELLED': + throw new UserCancelledError(message); + case 'CERTIFICATE_NOT_FOUND': + throw new CertificateNotFoundError(message); + case 'APPLICATION_NOT_AUTHORIZED': + throw new ApplicationNotAuthorizedError(message); + case 'PIN_REQUIRED': + throw new PinRequiredError(message); + case 'PIN_INCORRECT': + throw new PinIncorrectError(message); + case 'TOKEN_NOT_FOUND': + throw new TokenNotFoundError(message); + case 'REQUEST_EXPIRED': + throw new RequestExpiredError(message); + case 'REPLAY_DETECTED': + throw new ReplayDetectedError(message); + default: + throw new CyberCoreError(message || `Erro do protocolo: ${code}`, code); + } + } + + _generateUuid() { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + _getOrCreateApplicationId() { + const storageKey = 'cybercore_app_id'; + if (typeof localStorage !== 'undefined') { + const stored = localStorage.getItem(storageKey); + if (stored) return stored; + const created = this._generateUuid(); + localStorage.setItem(storageKey, created); + return created; + } + return this._generateUuid(); + } +} + +// Expõe globalmente caso seja carregado diretamente via tag