diff --git a/build-jar.sh b/build-jar.sh index f42ecd9..da845ab 100755 --- a/build-jar.sh +++ b/build-jar.sh @@ -55,7 +55,7 @@ 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" + "${HOME}/.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" 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 index 2a7b6d2..c0ed29b 100644 --- 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 @@ -218,16 +218,20 @@ public class CcspServer { 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; - } + // Exige obrigatoriamente a identificação da aplicação para listagem de certificados + if (appId == null || appId.isBlank()) { + sendErrorResponse(exchange, 401, CcspErrorCode.APPLICATION_NOT_AUTHORIZED.name(), + "application_id (via query param ou cabeçalho X-CyberCore-AppId) é obrigatório."); + return; + } + + 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; } } @@ -350,14 +354,17 @@ public class CcspServer { 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; - } + // 3. Validação anti-replay (Nonce obrigatório) + if (request.nonce() == null || request.nonce().isBlank()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "O parâmetro 'nonce' é obrigatório para proteção contra replay attack."); + return; + } + 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 @@ -520,13 +527,17 @@ public class CcspServer { return; } - if (request.nonce() != null && !request.nonce().isBlank()) { - boolean accepted = nonceManager.registerNonce(request.nonce()); - if (!accepted) { - sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, - "Requisição duplicada detectada (replay prevenido)."); - return; - } + // Validação anti-replay (Nonce obrigatório) + if (request.nonce() == null || request.nonce().isBlank()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "O parâmetro 'nonce' é obrigatório para proteção contra replay attack."); + return; + } + boolean accepted = nonceManager.registerNonce(request.nonce()); + if (!accepted) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "Requisição duplicada detectada (replay prevenido)."); + return; } String appId = request.applicationId(); @@ -735,13 +746,17 @@ public class CcspServer { return; } - if (request.nonce() != null && !request.nonce().isBlank()) { - boolean accepted = nonceManager.registerNonce(request.nonce()); - if (!accepted) { - sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, - "Requisição duplicada detectada (replay prevenido)."); - return; - } + // Validação anti-replay (Nonce obrigatório) + if (request.nonce() == null || request.nonce().isBlank()) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "O parâmetro 'nonce' é obrigatório para proteção contra replay attack."); + return; + } + boolean accepted = nonceManager.registerNonce(request.nonce()); + if (!accepted) { + sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED, + "Requisição duplicada detectada (replay prevenido)."); + return; } String appId = request.applicationId(); @@ -1032,25 +1047,53 @@ public class CcspServer { } private boolean handleCors(HttpExchange exchange) throws IOException { + String path = exchange.getRequestURI().getPath(); String origin = getOrigin(exchange); + if (origin == null || origin.isBlank()) { - origin = "*"; + if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + exchange.close(); + return true; + } + return false; } - 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, Accept"); - exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true"); - exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); + // Determina se a origem ou rota é elegível para CORS + boolean isHandshakePath = path.endsWith("/status") || path.endsWith("/authorize"); + boolean isAuthorizedOrigin = applicationRegistry.findByOrigin(origin).isPresent() || isLocalOrigin(origin); + + if (isHandshakePath || isAuthorizedOrigin) { + 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, Accept"); + exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true"); + exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); + } if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { - exchange.sendResponseHeaders(204, -1); + if (isHandshakePath || isAuthorizedOrigin) { + exchange.sendResponseHeaders(204, -1); + } else { + exchange.sendResponseHeaders(403, -1); + } exchange.close(); return true; } return false; } + private boolean isLocalOrigin(String origin) { + if (origin == null || origin.isBlank()) return false; + try { + URI uri = URI.create(origin); + String host = uri.getHost(); + return "localhost".equalsIgnoreCase(host) || "127.0.0.1".equals(host); + } catch (Exception e) { + return false; + } + } + private String getOrigin(HttpExchange exchange) { String origin = exchange.getRequestHeaders().getFirst("Origin"); if (origin == null || origin.isBlank()) { 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 index 9d83944..ed618dc 100644 --- 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 @@ -3,6 +3,8 @@ package br.com.cybercore.signer.core.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -12,15 +14,19 @@ 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". + * Os PINs são escopados por aplicação web (applicationId + certId) e possuem expiração por inatividade (TTL de 15 min). * 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 long SESSION_TTL_MINUTES = 15; private static final PinSessionManager INSTANCE = new PinSessionManager(); - private final Map sessionPins = new ConcurrentHashMap<>(); + private record PinEntry(char[] pin, Instant lastAccessedAt) {} + + private final Map sessionPins = new ConcurrentHashMap<>(); private volatile boolean alwaysPromptPin = false; public PinSessionManager() { @@ -30,63 +36,107 @@ public class PinSessionManager { return INSTANCE; } + private String buildKey(String applicationId, String certId) { + String appId = (applicationId != null && !applicationId.isBlank()) ? applicationId.trim() : "default"; + return appId + "::" + certId.trim(); + } + /** - * Salva o PIN de um certificado/token para a sessão atual em memória volátil. + * Salva o PIN associado a uma aplicação específica e certificado. */ - public synchronized void savePin(String certId, char[] pin) { + public synchronized void savePin(String applicationId, String certId, char[] pin) { Objects.requireNonNull(certId, "certId must not be null"); + String key = buildKey(applicationId, certId); if (pin == null || pin.length == 0) { - clearPin(certId); + clearPin(applicationId, 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); + clearPin(applicationId, certId); + sessionPins.put(key, new PinEntry(pin.clone(), Instant.now())); + logger.debug("PIN salvo em sessão para app '{}' e cert '{}'. Tamanho: {} dígitos", applicationId, certId, pin.length); } /** - * Recupera o PIN salvo em sessão para o certificado informado. + * Salva o PIN no escopo padrão (compatibilidade). */ - public synchronized char[] getPin(String certId) { + public synchronized void savePin(String certId, char[] pin) { + savePin("default", certId, pin); + } + + /** + * Recupera o PIN salvo em sessão para a aplicação e certificado especificados. + */ + public synchronized char[] getPin(String applicationId, String certId) { if (certId == null || alwaysPromptPin) { return null; } - char[] stored = sessionPins.get(certId); - return stored != null ? stored.clone() : null; + String key = buildKey(applicationId, certId); + PinEntry entry = sessionPins.get(key); + if (entry == null) { + return null; + } + + // Verifica expiração por inatividade (TTL) + if (Instant.now().isAfter(entry.lastAccessedAt().plus(SESSION_TTL_MINUTES, ChronoUnit.MINUTES))) { + clearPin(applicationId, certId); + logger.info("PIN da sessão expirou por inatividade (TTL {} min) para app '{}' e cert '{}'.", SESSION_TTL_MINUTES, applicationId, certId); + return null; + } + + // Atualiza timestamp de último acesso + sessionPins.put(key, new PinEntry(entry.pin(), Instant.now())); + return entry.pin().clone(); } /** - * Verifica se existe um PIN válido salvo nesta sessão para o certificado. + * Recupera o PIN no escopo padrão (compatibilidade). + */ + public synchronized char[] getPin(String certId) { + return getPin("default", certId); + } + + /** + * Verifica se existe um PIN válido salvo nesta sessão para a aplicação e certificado. + */ + public synchronized boolean hasPin(String applicationId, String certId) { + return getPin(applicationId, certId) != null; + } + + /** + * Verifica se existe um PIN no escopo padrão (compatibilidade). */ public synchronized boolean hasPin(String certId) { - if (certId == null || alwaysPromptPin) { - return false; - } - return sessionPins.containsKey(certId); + return hasPin("default", certId); } /** - * Remove o PIN salvo para um certificado específico, zerando a memória. + * Remove o PIN salvo para uma aplicação e certificado específicos, zerando a memória. */ - public synchronized void clearPin(String certId) { + public synchronized void clearPin(String applicationId, String certId) { if (certId != null) { - char[] old = sessionPins.remove(certId); - if (old != null) { - Arrays.fill(old, ' '); + String key = buildKey(applicationId, certId); + PinEntry old = sessionPins.remove(key); + if (old != null && old.pin() != null) { + Arrays.fill(old.pin(), ' '); } } } + /** + * Remove o PIN no escopo padrão (compatibilidade). + */ + public synchronized void clearPin(String certId) { + clearPin("default", certId); + } + /** * 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, ' '); + for (PinEntry entry : sessionPins.values()) { + if (entry != null && entry.pin() != null) { + Arrays.fill(entry.pin(), ' '); } } sessionPins.clear(); diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java index 12f1b0b..da0b43e 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/SecurePinStorage.java @@ -11,12 +11,12 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; import java.security.SecureRandom; import java.security.spec.KeySpec; import java.util.Arrays; @@ -24,13 +24,14 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * Armazenamento local seguro e criptografado de PINs/senhas de certificados. *

* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente - * a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256). + * a partir de semente aleatória protegida no disco local do usuário e atributos estáveis da máquina (PBKDF2WithHmacSHA256). * Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}. */ public final class SecurePinStorage { @@ -39,6 +40,7 @@ public final class SecurePinStorage { private static final String FILE_NAME = "secure-pins.dat"; private static final String SALT_FILE_NAME = "secure-pins.salt"; + private static final String SEED_FILE_NAME = "secure-pins.seed"; private static final int GCM_IV_LENGTH = 12; private static final int GCM_TAG_LENGTH = 128; private static final int PBKDF2_ITERATIONS = 65536; @@ -49,6 +51,7 @@ public final class SecurePinStorage { private final Path storageDirectory; private final Path dataFile; private final Path saltFile; + private final Path seedFile; private final ObjectMapper objectMapper; private final Map memoryCache = new ConcurrentHashMap<>(); @@ -64,6 +67,7 @@ public final class SecurePinStorage { this.storageDirectory = Objects.requireNonNull(storageDirectory); this.dataFile = storageDirectory.resolve(FILE_NAME); this.saltFile = storageDirectory.resolve(SALT_FILE_NAME); + this.seedFile = storageDirectory.resolve(SEED_FILE_NAME); this.objectMapper = new ObjectMapper(); loadFromDisk(); } @@ -200,6 +204,7 @@ public final class SecurePinStorage { fos.write(iv); fos.write(ciphertext); } + restrictFilePermissions(dataFile); } catch (Exception e) { logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e); } @@ -248,22 +253,56 @@ public final class SecurePinStorage { byte[] salt = new byte[32]; new SecureRandom().nextBytes(salt); Files.write(saltFile, salt); + restrictFilePermissions(saltFile); return salt; } + private byte[] getOrCreateSeed() throws Exception { + if (Files.exists(seedFile)) { + return Files.readAllBytes(seedFile); + } + byte[] seed = new byte[32]; + new SecureRandom().nextBytes(seed); + Files.write(seedFile, seed); + restrictFilePermissions(seedFile); + return seed; + } + + private void restrictFilePermissions(Path path) { + try { + PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class); + if (view != null) { + Files.setPosixFilePermissions(path, Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE + )); + } + } catch (Exception ignored) { + // Em sistemas não-POSIX como Windows, as permissões são herdadas da pasta de perfil do usuário (~/.cybercore) + } + } + private SecretKey deriveKey(byte[] salt) throws Exception { - // Combina identificadores estáveis exclusivos da máquina e usuário + byte[] seed = getOrCreateSeed(); String hostIdentity = String.join(":", System.getProperty("user.name", "unknown"), System.getProperty("user.home", "unknown"), System.getProperty("os.name", "unknown"), - System.getProperty("os.arch", "unknown"), - "cybercore-signer-hardware-pin-binding" + System.getProperty("os.arch", "unknown") ); + byte[] hostBytes = hostIdentity.getBytes(StandardCharsets.UTF_8); + byte[] combinedMaterial = new byte[seed.length + hostBytes.length]; + System.arraycopy(seed, 0, combinedMaterial, 0, seed.length); + System.arraycopy(hostBytes, 0, combinedMaterial, seed.length, hostBytes.length); - KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS); + char[] materialChars = Base64.getEncoder().encodeToString(combinedMaterial).toCharArray(); + KeySpec spec = new PBEKeySpec(materialChars, salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + + Arrays.fill(materialChars, ' '); + Arrays.fill(combinedMaterial, (byte) 0); + return new SecretKeySpec(keyBytes, "AES"); } } 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 index 05d10ba..f922f52 100644 --- 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 @@ -198,6 +198,63 @@ class CcspServerTest { assertThat(resp.body()).contains("Teste ICP-Brasil"); } + @Test + @DisplayName("Endpoint /certificates deve rejeitar com 401 requisições sem application_id") + void testGetCertificatesShouldRejectWhenAppIdMissing() throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/certificates")) + .header("Origin", "https://meusistema.com.br") + .GET() + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + assertThat(resp.statusCode()).isEqualTo(401); + assertThat(resp.body()).contains("APPLICATION_NOT_AUTHORIZED"); + } + + @Test + @DisplayName("Requisição OPTIONS com origem desconhecida deve ser rejeitada com 403") + void testCorsPreflightShouldRejectUnauthorizedOrigin() throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/ccsp/v1/sign")) + .method("OPTIONS", HttpRequest.BodyPublishers.noBody()) + .header("Origin", "https://malicious-attacker.com") + .build(); + + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.discarding()); + assertThat(resp.statusCode()).isEqualTo(403); + } + + @Test + @DisplayName("Endpoint /sign deve rejeitar requisição sem nonce") + void testSignShouldRejectWhenNonceIsMissing() throws Exception { + byte[] docHash = MessageDigest.getInstance("SHA-256").digest("Documento".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" + } + """.formatted(UUID.randomUUID(), hashBase64); + + 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(400); + assertThat(resp.body()).contains("REPLAY_DETECTED"); + } + @Test @DisplayName("Requisição OPTIONS deve retornar headers CORS válidos") void testCorsPreflight() throws Exception { @@ -398,7 +455,7 @@ class CcspServerTest { assertThat(resp.body()).contains("\"status\":\"success\""); assertThat(resp.body()).contains("\"seals\":"); assertThat(resp.body()).contains("icp-brasil"); - assertThat(resp.body()).contains("gov-br"); + assertThat(resp.body()).contains("ICP-Brasil Institucional"); } @Test 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 index 759750e..bd2369a 100644 --- 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 @@ -58,4 +58,17 @@ class PinSessionManagerTest { assertThat(manager.hasPin("cert-a")).isFalse(); assertThat(manager.hasPin("cert-b")).isFalse(); } + + @Test + @DisplayName("Deve isolar PINs entre diferentes aplicações para o mesmo certificado") + void shouldIsolatePinByApplicationId() { + manager.savePin("app-1", "cert-x", "pin-app-1".toCharArray()); + + assertThat(manager.hasPin("app-1", "cert-x")).isTrue(); + assertThat(manager.getPin("app-1", "cert-x")).isEqualTo("pin-app-1".toCharArray()); + + // Aplicação 2 não deve ter acesso ao PIN da Aplicação 1 + assertThat(manager.hasPin("app-2", "cert-x")).isFalse(); + assertThat(manager.getPin("app-2", "cert-x")).isNull(); + } } 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 83e8d2b..5b3c893 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 @@ -149,7 +149,7 @@ public class SignaturePromptDialog { boolean isA3 = cert.type() == CertificateType.A3; boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId()); - boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(cert.id()); + boolean hasStoredPin = isA3 && PinSessionManager.getInstance().hasPin(context != null ? context.applicationId() : null, cert.id()); if (isA3) { pinContainer.getStyleClass().add("dialog-box"); @@ -255,9 +255,9 @@ public class SignaturePromptDialog { if (pinProvided) { char[] pinChars = typedPin.toCharArray(); if (cbSavePin.isSelected()) { - PinSessionManager.getInstance().savePin(cert.id(), pinChars); + PinSessionManager.getInstance().savePin(context != null ? context.applicationId() : null, cert.id(), pinChars); } else { - PinSessionManager.getInstance().clearPin(cert.id()); + PinSessionManager.getInstance().clearPin(context != null ? context.applicationId() : null, cert.id()); } } } diff --git a/scripts/package-jpackage.sh b/scripts/package-jpackage.sh index f4d7530..c507814 100755 --- a/scripts/package-jpackage.sh +++ b/scripts/package-jpackage.sh @@ -15,7 +15,7 @@ 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 JAVA_HOME="${JAVA_HOME:-${HOME}/.jdks/jdk-25.0.4.1+1}" export PATH="${JAVA_HOME}/bin:${PATH}" MVN_CMD="${PROJECT_ROOT}/mvnw" fi diff --git a/sdk/src/pdf-seal-designer.js b/sdk/src/pdf-seal-designer.js index c74f0ec..a4aaa72 100644 --- a/sdk/src/pdf-seal-designer.js +++ b/sdk/src/pdf-seal-designer.js @@ -4,6 +4,25 @@ * de temas de selos de assinatura digital PAdES (ICP-Brasil). */ +function escapeSvgText(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function sanitizeLogoUrl(url) { + if (!url || typeof url !== 'string') return ''; + const trimmed = url.trim(); + if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) { + return trimmed; + } + return ''; +} + export class PdfSealDesigner { /** * @param {Object} options Configurações iniciais do designer. @@ -592,8 +611,9 @@ export class PdfSealDesigner { } let iconGraphic = ''; - if (t.customLogoBase64) { - iconGraphic = ``; + const safeLogo = sanitizeLogoUrl(t.customLogoBase64); + if (safeLogo) { + iconGraphic = ``; } else { switch (t.iconStyle) { case 'star': @@ -640,11 +660,11 @@ export class PdfSealDesigner { - ${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'} - MARIA DA SILVA - ${[docText, extraText].filter(Boolean).join(' • ')} - ${dateText ? `Data: ${dateText}` : ''} - ${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'} + ${escapeSvgText(t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE')} + MARIA DA SILVA + ${escapeSvgText([docText, extraText].filter(Boolean).join(' • '))} + ${escapeSvgText(dateText ? `Data: ${dateText}` : '')} + ${escapeSvgText(t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001')} `; diff --git a/sdk/src/pdf-stamp-picker.js b/sdk/src/pdf-stamp-picker.js index cda12da..61f79e8 100644 --- a/sdk/src/pdf-stamp-picker.js +++ b/sdk/src/pdf-stamp-picker.js @@ -4,6 +4,25 @@ * e geração do payload de assinatura em tempo real. */ +function escapeSvgText(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function sanitizeLogoUrl(url) { + if (!url || typeof url !== 'string') return ''; + const trimmed = url.trim(); + if (/^data:image\/(png|jpeg|jpg|svg\+xml);base64,[A-Za-z0-9+/=]+$/i.test(trimmed)) { + return trimmed; + } + return ''; +} + export class PdfStampPicker { /** * @param {Object} options Configurações do componente. @@ -949,8 +968,9 @@ export class PdfStampPicker { else if (name.length > 26) nameFontSize = 10.0; let iconGraphic = ''; - if (theme.customLogoBase64) { - iconGraphic = ``; + const safeLogo = sanitizeLogoUrl(theme.customLogoBase64); + if (safeLogo) { + iconGraphic = ``; } else { switch (theme.iconStyle || theme.id) { case 'star': @@ -1004,11 +1024,11 @@ export class PdfStampPicker { - ${header} - ${name} - ${doc} - Data: ${dateText} - ${footer} + ${escapeSvgText(header)} + ${escapeSvgText(name)} + ${escapeSvgText(doc)} + Data: ${escapeSvgText(dateText)} + ${escapeSvgText(footer)} `; diff --git a/sdk/src/signer-sdk.js b/sdk/src/signer-sdk.js index d7be5c2..1588c9c 100644 --- a/sdk/src/signer-sdk.js +++ b/sdk/src/signer-sdk.js @@ -136,7 +136,8 @@ export class CyberCoreSigner { * @returns {Promise>} Lista de certificados com metadados públicos. */ async getCertificates() { - const resp = await this._fetch('/ccsp/v1/certificates', { + const url = `/ccsp/v1/certificates?application_id=${encodeURIComponent(this.applicationId)}`; + const resp = await this._fetch(url, { method: 'GET' });