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 index 0ca13a4..073e2aa 100644 --- 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 @@ -2,6 +2,7 @@ package br.com.cybercore.signer.core.provider; import br.com.cybercore.signer.core.domain.CertificateStatus; import br.com.cybercore.signer.core.domain.ManagedCertificate; +import br.com.cybercore.signer.core.security.SmartCardWatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,16 +20,57 @@ 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. + * Implementa cache sob demanda com TTL e invalidação imediata em eventos de hardware (SmartCardWatcher). */ -public class CertificateManager { +public class CertificateManager implements SmartCardWatcher.CardEventListener { private static final Logger logger = LoggerFactory.getLogger(CertificateManager.class); + private static final long DEFAULT_CACHE_TTL_MS = 3_000L; + private final long cacheTtlMs; private final List providers = new CopyOnWriteArrayList<>(); private final Map certificateToProviderMap = new ConcurrentHashMap<>(); private final Map cachedCertificates = new ConcurrentHashMap<>(); + private volatile long lastDiscoveryTimestamp = 0L; + private volatile List cachedDiscoveredList = Collections.emptyList(); + public CertificateManager() { + this(DEFAULT_CACHE_TTL_MS); + } + + public CertificateManager(long cacheTtlMs) { + this.cacheTtlMs = Math.max(0, cacheTtlMs); + } + + /** + * Invalida explicitamente o cache em memória de certificados descobertos. + */ + public void invalidateCache() { + lastDiscoveryTimestamp = 0L; + } + + /** + * Vincula um SmartCardWatcher para invalidar o cache de certificados automaticamente + * assim que um token/cartão físico for inserido ou removido. + */ + public void attachSmartCardWatcher(SmartCardWatcher watcher) { + if (watcher != null) { + watcher.addListener(this); + logger.info("SmartCardWatcher vinculado com sucesso ao CertificateManager para invalidação automática de cache."); + } + } + + @Override + public void onCardInserted(String terminalName) { + logger.info("Notificação de hardware: Smart Card inserido no terminal '{}'. Invalidando cache de certificados.", terminalName); + invalidateCache(); + } + + @Override + public void onCardRemoved(String terminalName) { + logger.info("Notificação de hardware: Smart Card removido do terminal '{}'. Invalidando cache de certificados.", terminalName); + invalidateCache(); } /** @@ -65,6 +107,7 @@ public class CertificateManager { // Evita duplicidade por providerId providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(provider.getProviderId())); providers.add(provider); + invalidateCache(); logger.info("Provedor registrado no CertificateManager: {} [{}] (disponível={})", provider.getDisplayName(), provider.getProviderId(), provider.isAvailable()); } @@ -76,6 +119,7 @@ public class CertificateManager { if (providerId != null) { providers.removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId)); certificateToProviderMap.values().removeIf(p -> p.getProviderId().equalsIgnoreCase(providerId)); + invalidateCache(); } } @@ -85,9 +129,23 @@ public class CertificateManager { /** * Executa a descoberta de certificados em todos os provedores operacionais. - * Consolida os resultados, atualiza o mapeamento de provedores e ordena por relevância/disponibilidade. + * Utiliza cache sob demanda se a última descoberta ocorreu dentro da janela de TTL. */ public synchronized List discoverCertificates() { + return discoverCertificates(false); + } + + /** + * Executa a descoberta de certificados em todos os provedores operacionais, com opção + * de forçar o recarregamento ignorando o TTL do cache. + */ + public synchronized List discoverCertificates(boolean forceRefresh) { + long now = System.currentTimeMillis(); + if (!forceRefresh && (now - lastDiscoveryTimestamp < cacheTtlMs) && !cachedDiscoveredList.isEmpty()) { + logger.debug("Retornando lista de certificados a partir do cache sob demanda (TTL: {} ms).", cacheTtlMs); + return cachedDiscoveredList; + } + logger.info("Iniciando descoberta de certificados digitais nos provedores ativos..."); certificateToProviderMap.clear(); @@ -119,8 +177,11 @@ public class CertificateManager { .comparing((ManagedCertificate c) -> c.status() == CertificateStatus.AVAILABLE ? 0 : 1) .thenComparing(ManagedCertificate::commonName, String.CASE_INSENSITIVE_ORDER)); + cachedDiscoveredList = Collections.unmodifiableList(discovered); + lastDiscoveryTimestamp = System.currentTimeMillis(); + logger.info("Descoberta concluída: {} certificado(s) total no agente.", discovered.size()); - return Collections.unmodifiableList(discovered); + return cachedDiscoveredList; } /** @@ -197,6 +258,7 @@ public class CertificateManager { * Solicita recarregamento de todos os provedores cadastrados. */ public void refreshAll() { + invalidateCache(); for (CertificateProvider provider : providers) { try { provider.refresh(); @@ -204,6 +266,6 @@ public class CertificateManager { logger.warn("Erro ao recarregar provedor '{}': {}", provider.getProviderId(), e.getMessage()); } } - discoverCertificates(); + discoverCertificates(true); } } 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 index 71e70aa..12205b8 100644 --- 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 @@ -29,6 +29,10 @@ public class SmartCardWatcher { void onCardRemoved(String terminalName); } + private static final long FAST_POLL_TIMEOUT_MS = 1500L; + private static final long MAX_POLL_TIMEOUT_MS = 5000L; + private static final int IDLE_CYCLES_BEFORE_BACKOFF = 4; + private final List listeners = new CopyOnWriteArrayList<>(); private final Set knownCards = Collections.synchronizedSet(new HashSet<>()); @@ -109,14 +113,21 @@ public class SmartCardWatcher { return; } + long currentTimeout = FAST_POLL_TIMEOUT_MS; + int consecutiveIdleCycles = 0; + 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); + // Aguarda alteração de estado no hardware com timeout adaptativo para reduzir desperdício de CPU + Boolean changed = (Boolean) waitForChangeMethod.invoke(cardTerminals, currentTimeout); if (Boolean.TRUE.equals(changed) || !running) { if (!running) break; + // Restabelece polling rápido imediatamente ao registrar atividade no hardware + currentTimeout = FAST_POLL_TIMEOUT_MS; + consecutiveIdleCycles = 0; + Set current = getCurrentTerminalsWithCards(cardTerminals, listMethod, cardPresentState); // Dispositivos removidos @@ -140,6 +151,12 @@ public class SmartCardWatcher { knownCards.clear(); knownCards.addAll(current); + } else { + // Nenhuma alteração no ciclo: aumenta gradualmente o intervalo de espera para economizar CPU + consecutiveIdleCycles++; + if (consecutiveIdleCycles >= IDLE_CYCLES_BEFORE_BACKOFF) { + currentTimeout = Math.min(MAX_POLL_TIMEOUT_MS, currentTimeout + 1000L); + } } } catch (Throwable e) { if (e instanceof InterruptedException || (e.getCause() instanceof InterruptedException)) { 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 index b732443..2924ee9 100644 --- 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 @@ -93,4 +93,51 @@ class CertificateManagerTest { System.clearProperty("cybercore.diag.no-wincert"); } } + + @Test + @DisplayName("Deve reutilizar cache sob demanda dentro do TTL e invalidar sob forceRefresh ou hardware events") + void shouldReuseCacheWithinTtlAndInvalidateOnHardwareEvents() throws Exception { + CertificateManager manager = new CertificateManager(5_000L); // 5 segundos TTL + + CertificateProvider provider = mock(CertificateProvider.class); + when(provider.getProviderId()).thenReturn("mock-prov"); + when(provider.isAvailable()).thenReturn(true); + + ManagedCertificate cert = new ManagedCertificate( + "c1", "Mock Cert", CertificateType.A1, CertificateStatus.AVAILABLE, + "CN=Mock", "CN=AC", "1", "s1", "s2", + Instant.now().minus(1, ChronoUnit.DAYS), Instant.now().plus(30, ChronoUnit.DAYS), + "RSA", 2048, "Mock", null, null, "mock-prov", null + ); + when(provider.listCertificates()).thenReturn(List.of(cert)); + + manager.registerProvider(provider); + + // 1ª chamada: consulta o provider + List first = manager.discoverCertificates(); + assertThat(first).hasSize(1); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(1)).listCertificates(); + + // 2ª chamada: dentro do TTL de 5s, não deve chamar o provider novamente + List second = manager.discoverCertificates(); + assertThat(second).isSameAs(first); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(1)).listCertificates(); + + // 3ª chamada com forceRefresh=true: deve chamar o provider + List third = manager.discoverCertificates(true); + assertThat(third).hasSize(1); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(2)).listCertificates(); + + // 4ª chamada: simula evento de hardware (inserção de cartão) + manager.onCardInserted("Terminal-USB-1"); + List fourth = manager.discoverCertificates(); + assertThat(fourth).hasSize(1); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(3)).listCertificates(); + + // 5ª chamada: simula evento de hardware (remoção de cartão) + manager.onCardRemoved("Terminal-USB-1"); + List fifth = manager.discoverCertificates(); + assertThat(fifth).hasSize(1); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.times(4)).listCertificates(); + } }