feat(ui): enhance signature confirmation dialog with wysiwys details and update ccsp spec
This commit is contained in:
@@ -152,11 +152,20 @@ Solicita a assinatura digital local do digest informado com o certificado escolh
|
|||||||
"certificate_hint": "win-4340656a",
|
"certificate_hint": "win-4340656a",
|
||||||
"document_name": "contrato_prestacao_servicos.pdf",
|
"document_name": "contrato_prestacao_servicos.pdf",
|
||||||
"document_type": "PDF",
|
"document_type": "PDF",
|
||||||
|
"document_size": 1474560,
|
||||||
|
"origin": "https://meusistema.empresa.com.br",
|
||||||
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
"nonce": "c4ca4238a0b923820dcc509a6f75849b",
|
||||||
"expires_at": "2026-10-11T20:50:00Z"
|
"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`)
|
#### Resposta de Sucesso (`200 OK`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
+9
-2
@@ -116,7 +116,13 @@ public record CcspRequest(
|
|||||||
String targetElementTag,
|
String targetElementTag,
|
||||||
|
|
||||||
@JsonProperty("target_element_id")
|
@JsonProperty("target_element_id")
|
||||||
String targetElementId
|
String targetElementId,
|
||||||
|
|
||||||
|
@JsonProperty("document_size")
|
||||||
|
Long documentSize,
|
||||||
|
|
||||||
|
@JsonProperty("origin")
|
||||||
|
String origin
|
||||||
) implements Serializable {
|
) implements Serializable {
|
||||||
|
|
||||||
public static final String PROTOCOL_VERSION = "1";
|
public static final String PROTOCOL_VERSION = "1";
|
||||||
@@ -130,7 +136,8 @@ public record CcspRequest(
|
|||||||
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
certificateHint, expiresAt, timestamp, nonce, signatureType,
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
null, null, null, null, null, null, null, null, null, null,
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
null, null, null, null, null, null, null, null, null, null);
|
null, null, null, null, null, null, null, null, null, null,
|
||||||
|
null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVersionSupported() {
|
public boolean isVersionSupported() {
|
||||||
|
|||||||
+20
-2
@@ -472,9 +472,27 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
.map(AuthorizedApplication::name)
|
.map(AuthorizedApplication::name)
|
||||||
.orElse(request.applicationId());
|
.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(
|
SignaturePromptDialog dialog = new SignaturePromptDialog(
|
||||||
stage, appName, "Requisição Web CCSP", certificate, context,
|
stage, appName, requestOrigin, certificate, context,
|
||||||
request.documentName(), request.documentType(), request.isAuthentication()
|
request.documentName(), request.documentType(), docSize, request.isAuthentication()
|
||||||
);
|
);
|
||||||
dialog.showAndAwait().whenComplete((approved, err) -> {
|
dialog.showAndAwait().whenComplete((approved, err) -> {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
|
|||||||
+44
-7
@@ -36,12 +36,18 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
||||||
ManagedCertificate certificate, SigningContext context) {
|
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,
|
public SignaturePromptDialog(Stage owner, String applicationName, String origin,
|
||||||
ManagedCertificate certificate, SigningContext context,
|
ManagedCertificate certificate, SigningContext context,
|
||||||
String documentName, String documentType, boolean isAuthentication) {
|
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 = new Stage();
|
||||||
stage.initModality(Modality.APPLICATION_MODAL);
|
stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
@@ -52,11 +58,11 @@ public class SignaturePromptDialog {
|
|||||||
: "CyberCore Signer — Solicitação de Assinatura");
|
: "CyberCore Signer — Solicitação de Assinatura");
|
||||||
stage.setResizable(false);
|
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,
|
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);
|
VBox root = new VBox(16);
|
||||||
root.getStyleClass().add("dialog-container");
|
root.getStyleClass().add("dialog-container");
|
||||||
root.setPadding(new Insets(20, 24, 20, 24));
|
root.setPadding(new Insets(20, 24, 20, 24));
|
||||||
@@ -94,7 +100,8 @@ public class SignaturePromptDialog {
|
|||||||
grid.setVgap(10);
|
grid.setVgap(10);
|
||||||
|
|
||||||
int row = 0;
|
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) {
|
if (isAuthentication) {
|
||||||
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
|
addField(grid, row++, "Operação:", "Autenticação de Usuário (Challenge-Response)");
|
||||||
} else {
|
} else {
|
||||||
@@ -102,7 +109,12 @@ public class SignaturePromptDialog {
|
|||||||
String docLabel = (documentType != null && !documentType.isBlank())
|
String docLabel = (documentType != null && !documentType.isBlank())
|
||||||
? "[" + documentType.toUpperCase() + "] " + documentName
|
? "[" + documentType.toUpperCase() + "] " + documentName
|
||||||
: documentName;
|
: documentName;
|
||||||
|
if (documentSize != null && documentSize > 0) {
|
||||||
|
docLabel += " (" + formatFileSize(documentSize) + ")";
|
||||||
|
}
|
||||||
addField(grid, row++, "Documento:", docLabel);
|
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)");
|
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
|
||||||
}
|
}
|
||||||
@@ -128,14 +140,16 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
detailsBox.getChildren().add(grid);
|
detailsBox.getChildren().add(grid);
|
||||||
|
|
||||||
// Caixa do Hash / Digest
|
// Caixa do Hash / Digest formatado em blocos legíveis (WYSIWYS)
|
||||||
VBox hashBox = new VBox(6);
|
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");
|
hashLabel.getStyleClass().add("dialog-field-title");
|
||||||
|
|
||||||
String hashHex = HexFormat.of().formatHex(context.digest());
|
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.getStyleClass().add("dialog-hash-value");
|
||||||
|
hashValue.setStyle("-fx-font-family: 'Liberation Mono', 'Courier New', monospace; -fx-font-size: 11px;");
|
||||||
hashValue.setWrapText(true);
|
hashValue.setWrapText(true);
|
||||||
|
|
||||||
hashBox.getChildren().addAll(hashLabel, hashValue);
|
hashBox.getChildren().addAll(hashLabel, hashValue);
|
||||||
@@ -301,4 +315,27 @@ public class SignaturePromptDialog {
|
|||||||
stage.toFront();
|
stage.toFront();
|
||||||
return approvalFuture;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user