diff --git a/docs/CCSP.md b/docs/CCSP.md index 7496d5c..44f393b 100644 --- a/docs/CCSP.md +++ b/docs/CCSP.md @@ -152,11 +152,20 @@ Solicita a assinatura digital local do digest informado com o certificado escolh "certificate_hint": "win-4340656a", "document_name": "contrato_prestacao_servicos.pdf", "document_type": "PDF", + "document_size": 1474560, + "origin": "https://meusistema.empresa.com.br", "nonce": "c4ca4238a0b923820dcc509a6f75849b", "expires_at": "2026-10-11T20:50:00Z" } ``` +> **Garantia WYSIWYS (What You See Is What You Sign):** +> O agente CyberCore Signer apresenta ao usuário, antes de qualquer operação criptográfica: +> - **Aplicação Solicitante e Origem Web:** Exibe a URL de origem da página web chamadora para prevenir ataques de personificação (Spoofing). +> - **Nome e Tamanho do Documento:** Se informados via `document_name` e `document_size` (em bytes), são apresentados de forma legível (ex: `1.4 MB`). +> - **Hash do Documento (SHA-256 Digest):** Formatado em blocos legíveis de 4 caracteres (ex: `A1B2 C3D4 ...`) em tipografia monoespaçada para fácil conferência humana. +> - **Certificado e Dispositivo:** Exibe titular, CPF/CNPJ, tipo A1/A3 e algoritmo de assinatura. + #### Resposta de Sucesso (`200 OK`) ```json { 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 index c0ffd94..ba966bf 100644 --- 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 @@ -116,7 +116,13 @@ public record CcspRequest( String targetElementTag, @JsonProperty("target_element_id") - String targetElementId + String targetElementId, + + @JsonProperty("document_size") + Long documentSize, + + @JsonProperty("origin") + String origin ) implements Serializable { public static final String PROTOCOL_VERSION = "1"; @@ -130,7 +136,8 @@ public record CcspRequest( certificateHint, expiresAt, timestamp, nonce, signatureType, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null, + null, null); } public boolean isVersionSupported() { 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 index 5185923..1ff0243 100644 --- 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 @@ -472,9 +472,27 @@ public class CyberCoreDesktopApplication extends Application { .map(AuthorizedApplication::name) .orElse(request.applicationId()); + Long docSize = request.documentSize(); + if (docSize == null && request.pdf() != null) { + try { + docSize = (long) Base64.getDecoder().decode(request.pdf()).length; + } catch (Exception ignored) {} + } else if (docSize == null && request.xmlBase64() != null) { + try { + docSize = (long) Base64.getDecoder().decode(request.xmlBase64()).length; + } catch (Exception ignored) {} + } + + String requestOrigin = request.origin(); + if (requestOrigin == null || requestOrigin.isBlank()) { + requestOrigin = ApplicationRegistry.getInstance().findById(request.applicationId()) + .map(a -> String.join(", ", a.allowedOrigins())) + .orElse("Requisição Web Local"); + } + SignaturePromptDialog dialog = new SignaturePromptDialog( - stage, appName, "Requisição Web CCSP", certificate, context, - request.documentName(), request.documentType(), request.isAuthentication() + stage, appName, requestOrigin, certificate, context, + request.documentName(), request.documentType(), docSize, request.isAuthentication() ); dialog.showAndAwait().whenComplete((approved, err) -> { if (err != null) { 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 index 5b3c893..4fafec3 100644 --- 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 @@ -36,12 +36,18 @@ public class SignaturePromptDialog { public SignaturePromptDialog(Stage owner, String applicationName, String origin, ManagedCertificate certificate, SigningContext context) { - this(owner, applicationName, origin, certificate, context, null, null, false); + this(owner, applicationName, origin, certificate, context, null, null, null, false); } public SignaturePromptDialog(Stage owner, String applicationName, String origin, ManagedCertificate certificate, SigningContext context, String documentName, String documentType, boolean isAuthentication) { + this(owner, applicationName, origin, certificate, context, documentName, documentType, null, isAuthentication); + } + + public SignaturePromptDialog(Stage owner, String applicationName, String origin, + ManagedCertificate certificate, SigningContext context, + String documentName, String documentType, Long documentSize, boolean isAuthentication) { stage = new Stage(); stage.initModality(Modality.APPLICATION_MODAL); if (owner != null) { @@ -52,11 +58,11 @@ public class SignaturePromptDialog { : "CyberCore Signer — Solicitação de Assinatura"); stage.setResizable(false); - initLayout(applicationName, origin, certificate, context, documentName, documentType, isAuthentication); + initLayout(applicationName, origin, certificate, context, documentName, documentType, documentSize, isAuthentication); } private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context, - String documentName, String documentType, boolean isAuthentication) { + String documentName, String documentType, Long documentSize, boolean isAuthentication) { VBox root = new VBox(16); root.getStyleClass().add("dialog-container"); root.setPadding(new Insets(20, 24, 20, 24)); @@ -94,7 +100,8 @@ public class SignaturePromptDialog { grid.setVgap(10); int row = 0; - addField(grid, row++, "Sistema:", appName + " (" + (origin != null ? origin : "Local") + ")"); + addField(grid, row++, "Aplicação Solicitante:", appName); + addField(grid, row++, "Origem Web:", (origin != null && !origin.isBlank() ? origin : "Local")); if (isAuthentication) { addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)"); } else { @@ -102,7 +109,12 @@ public class SignaturePromptDialog { String docLabel = (documentType != null && !documentType.isBlank()) ? "[" + documentType.toUpperCase() + "] " + documentName : documentName; + if (documentSize != null && documentSize > 0) { + docLabel += " (" + formatFileSize(documentSize) + ")"; + } addField(grid, row++, "Documento:", docLabel); + } else if (documentSize != null && documentSize > 0) { + addField(grid, row++, "Tamanho do Documento:", formatFileSize(documentSize)); } addField(grid, row++, "Operação:", "Assinatura Digital (Digest)"); } @@ -128,14 +140,16 @@ public class SignaturePromptDialog { detailsBox.getChildren().add(grid); - // Caixa do Hash / Digest + // Caixa do Hash / Digest formatado em blocos legíveis (WYSIWYS) VBox hashBox = new VBox(6); - Label hashLabel = new Label("Hash do Documento (SHA-256 Digest):"); + Label hashLabel = new Label("Hash do Documento (SHA-256 Digest formatado):"); hashLabel.getStyleClass().add("dialog-field-title"); String hashHex = HexFormat.of().formatHex(context.digest()); - Label hashValue = new Label(hashHex); + String formattedHash = formatHashBlocks(hashHex); + Label hashValue = new Label(formattedHash); hashValue.getStyleClass().add("dialog-hash-value"); + hashValue.setStyle("-fx-font-family: 'Liberation Mono', 'Courier New', monospace; -fx-font-size: 11px;"); hashValue.setWrapText(true); hashBox.getChildren().addAll(hashLabel, hashValue); @@ -301,4 +315,27 @@ public class SignaturePromptDialog { stage.toFront(); return approvalFuture; } + + private static String formatFileSize(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return String.format(java.util.Locale.ROOT, "%.1f KB", bytes / 1024.0); + } else { + return String.format(java.util.Locale.ROOT, "%.1f MB", bytes / (1024.0 * 1024.0)); + } + } + + private static String formatHashBlocks(String hex) { + if (hex == null || hex.isBlank()) return ""; + StringBuilder sb = new StringBuilder(); + String upper = hex.toUpperCase(java.util.Locale.ROOT); + for (int i = 0; i < upper.length(); i++) { + if (i > 0 && i % 4 == 0) { + sb.append(' '); + } + sb.append(upper.charAt(i)); + } + return sb.toString(); + } }