From eeeecb0604bbfb1a23cb0f969b4e47590ea8445c Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Tue, 20 Oct 2026 14:50:34 -0300 Subject: [PATCH] feat(security): protect authorized-apps.json integrity with hmac and restricted permissions --- .../core/security/ApplicationRegistry.java | 68 +++++++- .../core/security/HmacIntegrityManager.java | 151 ++++++++++++++++++ .../security/ApplicationRegistryTest.java | 25 +++ .../security/HmacIntegrityManagerTest.java | 60 +++++++ 4 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/HmacIntegrityManager.java create mode 100644 modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/HmacIntegrityManagerTest.java 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 index 234239d..13749d9 100644 --- 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 @@ -25,10 +25,12 @@ 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 String HMAC_FILE_SUFFIX = ".hmac"; private static final ApplicationRegistry INSTANCE = new ApplicationRegistry(); private final Path storagePath; + private final HmacIntegrityManager integrityManager; private final Map applications = new ConcurrentHashMap<>(); private final ObjectMapper objectMapper; @@ -37,15 +39,27 @@ public final class ApplicationRegistry { } public ApplicationRegistry() { - this(resolveDefaultStoragePath()); + this(resolveDefaultStoragePath(), new HmacIntegrityManager()); } public ApplicationRegistry(Path storagePath) { + this(storagePath, new HmacIntegrityManager(resolveKeyPathForStorage(storagePath))); + } + + public ApplicationRegistry(Path storagePath, HmacIntegrityManager integrityManager) { this.storagePath = storagePath; + this.integrityManager = integrityManager != null ? integrityManager : new HmacIntegrityManager(); this.objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); load(); } + private static Path resolveKeyPathForStorage(Path storagePath) { + if (storagePath == null || storagePath.getParent() == null) { + return null; + } + return storagePath.getParent().resolve(".vault_key"); + } + private static Path resolveDefaultStoragePath() { String userHome = System.getProperty("user.home", "."); return Path.of(userHome, ".cybercore", DEFAULT_FILE_NAME); @@ -108,8 +122,15 @@ public final class ApplicationRegistry { return Collections.unmodifiableList(new ArrayList<>(applications.values())); } + private Path resolveHmacPath() { + if (storagePath == null) { + return null; + } + return Path.of(storagePath.toString() + HMAC_FILE_SUFFIX); + } + /** - * Carrega as aplicações persistidas em disco. + * Carrega as aplicações persistidas em disco verificando a integridade por HMAC-SHA256. */ public synchronized void load() { if (storagePath == null || !Files.exists(storagePath)) { @@ -118,22 +139,42 @@ public final class ApplicationRegistry { } try { + byte[] fileBytes = Files.readAllBytes(storagePath); + Path hmacPath = resolveHmacPath(); + + if (hmacPath != null && Files.exists(hmacPath)) { + String expectedHmac = Files.readString(hmacPath).trim(); + boolean valid = integrityManager.verifyHmac(fileBytes, expectedHmac); + if (!valid) { + logger.error("ALERTA DE AUDITORIA: Adulteração detectada no arquivo de aplicações autorizadas {}. O HMAC diverge do esperado. Todas as autorizações foram invalidadas.", storagePath); + applications.clear(); + return; + } + } else if (hmacPath != null) { + // Se existe o JSON mas não o HMAC correspondente (migração/inicialização segura) + logger.warn("ALERTA DE AUDITORIA: Arquivo {} sem assinatura HMAC de integridade. Calculando e gerando assinatura inicial.", storagePath); + String newHmac = integrityManager.computeHmacHex(fileBytes); + Files.writeString(hmacPath, newHmac); + HmacIntegrityManager.restrictPermissions(hmacPath); + } + List list = objectMapper.readValue( - storagePath.toFile(), + fileBytes, 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); + logger.info("Carregadas {} aplicação(ões) autorizada(s) íntegras de {}", applications.size(), storagePath); } catch (Exception e) { logger.warn("Falha ao ler aplicações autorizadas de {}: {}", storagePath, e.getMessage()); + applications.clear(); } } /** - * Salva as aplicações atuais em disco. + * Salva as aplicações atuais em disco com assinatura HMAC e permissões de acesso restritas. */ public synchronized void save() { if (storagePath == null) { @@ -144,12 +185,23 @@ public final class ApplicationRegistry { Path parent = storagePath.getParent(); if (parent != null && !Files.exists(parent)) { Files.createDirectories(parent); + HmacIntegrityManager.restrictPermissions(parent); } - objectMapper.writerWithDefaultPrettyPrinter().writeValue( - storagePath.toFile(), + + byte[] jsonBytes = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes( new ArrayList<>(applications.values()) ); - logger.debug("Aplicações autorizadas salvas com sucesso em {}", storagePath); + Files.write(storagePath, jsonBytes); + HmacIntegrityManager.restrictPermissions(storagePath); + + Path hmacPath = resolveHmacPath(); + if (hmacPath != null) { + String hmacHex = integrityManager.computeHmacHex(jsonBytes); + Files.writeString(hmacPath, hmacHex); + HmacIntegrityManager.restrictPermissions(hmacPath); + } + + logger.debug("Aplicações autorizadas e HMAC salvos com sucesso em {}", storagePath); } catch (Exception e) { logger.error("Erro ao salvar aplicações autorizadas em {}: {}", storagePath, e.getMessage()); } diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/HmacIntegrityManager.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/HmacIntegrityManager.java new file mode 100644 index 0000000..c9ccae1 --- /dev/null +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/security/HmacIntegrityManager.java @@ -0,0 +1,151 @@ +package br.com.cybercore.signer.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.EnumSet; +import java.util.HexFormat; +import java.util.Set; + +/** + * Gerenciador de integridade criptográfica por HMAC-SHA256 para arquivos de configuração e + * autorizações locais, com custódia de chave mestre isolada e permissões de arquivo restritas. + */ +public class HmacIntegrityManager { + + private static final Logger logger = LoggerFactory.getLogger(HmacIntegrityManager.class); + private static final String HMAC_ALGORITHM = "HmacSHA256"; + private static final int KEY_SIZE_BYTES = 32; + private static final String DEFAULT_KEY_FILE = ".vault_key"; + + private final Path keyPath; + private byte[] secretKey; + + public HmacIntegrityManager() { + this(resolveDefaultKeyPath()); + } + + public HmacIntegrityManager(Path keyPath) { + this.keyPath = keyPath; + initSecretKey(); + } + + private static Path resolveDefaultKeyPath() { + String userHome = System.getProperty("user.home", "."); + return Path.of(userHome, ".cybercore", DEFAULT_KEY_FILE); + } + + private synchronized void initSecretKey() { + if (keyPath == null) { + byte[] ephemeralKey = new byte[KEY_SIZE_BYTES]; + new SecureRandom().nextBytes(ephemeralKey); + this.secretKey = ephemeralKey; + return; + } + + try { + if (Files.exists(keyPath)) { + byte[] loaded = Files.readAllBytes(keyPath); + if (loaded.length >= KEY_SIZE_BYTES) { + this.secretKey = loaded; + restrictPermissions(keyPath); + return; + } + } + + // Gera nova chave criptográfica forte + byte[] newKey = new byte[KEY_SIZE_BYTES]; + new SecureRandom().nextBytes(newKey); + this.secretKey = newKey; + + Path parent = keyPath.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + restrictPermissions(parent); + } + + Files.write(keyPath, newKey); + restrictPermissions(keyPath); + logger.info("Chave mestre de integridade inicializada com permissões restritas em {}", keyPath); + } catch (Exception e) { + logger.warn("Falha ao persistir chave em {}. Usando chave em memória: {}", keyPath, e.getMessage()); + byte[] fallbackKey = new byte[KEY_SIZE_BYTES]; + new SecureRandom().nextBytes(fallbackKey); + this.secretKey = fallbackKey; + } + } + + /** + * Calcula o HMAC-SHA256 dos dados informados e devolve em hexadecimal. + */ + public String computeHmacHex(byte[] data) { + if (data == null) { + return ""; + } + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + SecretKeySpec keySpec = new SecretKeySpec(secretKey, HMAC_ALGORITHM); + mac.init(keySpec); + byte[] hmacBytes = mac.doFinal(data); + return HexFormat.of().formatHex(hmacBytes); + } catch (Exception e) { + throw new IllegalStateException("Erro ao calcular HMAC-SHA256: " + e.getMessage(), e); + } + } + + /** + * Verifica a integridade dos dados contra um HMAC esperado usando comparação em tempo constante + * para mitigar timing attacks. + */ + public boolean verifyHmac(byte[] data, String expectedHex) { + if (data == null || expectedHex == null || expectedHex.isBlank()) { + return false; + } + String calculatedHex = computeHmacHex(data); + byte[] expectedBytes = expectedHex.trim().getBytes(StandardCharsets.UTF_8); + byte[] calculatedBytes = calculatedHex.getBytes(StandardCharsets.UTF_8); + return MessageDigest.isEqual(expectedBytes, calculatedBytes); + } + + /** + * Restringe as permissões do arquivo/diretório para acesso exclusivo do usuário atual (0600 / 0700). + */ + public static void restrictPermissions(Path path) { + if (path == null || !Files.exists(path)) { + return; + } + + File file = path.toFile(); + // Proteção padrão Java SE (funciona em Windows e Unix) + file.setReadable(false, false); + file.setReadable(true, true); + file.setWritable(false, false); + file.setWritable(true, true); + file.setExecutable(false, false); + + // Permissões POSIX estritas (Linux / macOS) + if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + try { + boolean isDir = Files.isDirectory(path); + Set permissions = isDir + ? EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE) + : EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, permissions); + } catch (IOException e) { + logger.debug("Não foi possível aplicar atributos POSIX em {}: {}", path, e.getMessage()); + } + } + } +} 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 index 0c2cc9f..f0b3e83 100644 --- 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 @@ -44,4 +44,29 @@ class ApplicationRegistryTest { reloaded.revoke("app-123"); assertThat(reloaded.findById("app-123")).isEmpty(); } + + @Test + @DisplayName("Deve descartar aplicações e alertar se o arquivo json for adulterado sem HMAC correspondente") + void testTamperDetection(@TempDir Path tempDir) throws Exception { + Path jsonPath = tempDir.resolve("authorized-apps.json"); + ApplicationRegistry registry = new ApplicationRegistry(jsonPath); + + AuthorizedApplication app = new AuthorizedApplication( + "app-1", + "Legit App", + Set.of("https://legit.com"), + true, + Instant.now() + ); + registry.register(app); + + // Adultera o arquivo JSON diretamente no disco simulando malware local + java.nio.file.Files.writeString(jsonPath, "[{\"applicationId\":\"malicious\",\"name\":\"Malware\",\"allowedOrigins\":[\"*\"],\"active\":true}]"); + + // Novo registry tenta carregar + ApplicationRegistry tamperedRegistry = new ApplicationRegistry(jsonPath); + assertThat(tamperedRegistry.findById("malicious")).isEmpty(); + assertThat(tamperedRegistry.findById("app-1")).isEmpty(); + assertThat(tamperedRegistry.listAll()).isEmpty(); + } } diff --git a/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/HmacIntegrityManagerTest.java b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/HmacIntegrityManagerTest.java new file mode 100644 index 0000000..2bb95df --- /dev/null +++ b/modules/cybercore-core/src/test/java/br/com/cybercore/signer/core/security/HmacIntegrityManagerTest.java @@ -0,0 +1,60 @@ +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.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testes de Integridade Criptográfica (HmacIntegrityManager)") +class HmacIntegrityManagerTest { + + @Test + @DisplayName("Deve gerar HMAC-SHA256 válido e verificar correspondência em tempo constante") + void testComputeAndVerifyHmac(@TempDir Path tempDir) { + Path keyPath = tempDir.resolve(".vault_key"); + HmacIntegrityManager manager = new HmacIntegrityManager(keyPath); + + byte[] payload = "{\"test\":\"cybercore\"}".getBytes(StandardCharsets.UTF_8); + String hmac = manager.computeHmacHex(payload); + + assertThat(hmac).isNotEmpty().hasSize(64); // 256 bits = 64 hex chars + assertThat(manager.verifyHmac(payload, hmac)).isTrue(); + assertThat(manager.verifyHmac("tampered".getBytes(StandardCharsets.UTF_8), hmac)).isFalse(); + assertThat(manager.verifyHmac(payload, "invalidhmac")).isFalse(); + assertThat(manager.verifyHmac(null, hmac)).isFalse(); + assertThat(manager.verifyHmac(payload, null)).isFalse(); + } + + @Test + @DisplayName("Deve reutilizar chave persistida em disco") + void testKeyPersistence(@TempDir Path tempDir) { + Path keyPath = tempDir.resolve(".vault_key"); + HmacIntegrityManager manager1 = new HmacIntegrityManager(keyPath); + + byte[] payload = "data-to-protect".getBytes(StandardCharsets.UTF_8); + String hmac1 = manager1.computeHmacHex(payload); + + // Novo manager apontando para a mesma chave persistida + HmacIntegrityManager manager2 = new HmacIntegrityManager(keyPath); + String hmac2 = manager2.computeHmacHex(payload); + + assertThat(hmac1).isEqualTo(hmac2); + assertThat(manager2.verifyHmac(payload, hmac1)).isTrue(); + } + + @Test + @DisplayName("Deve restringir permissões de arquivo e diretório") + void testRestrictPermissions(@TempDir Path tempDir) throws Exception { + Path testFile = tempDir.resolve("protected.txt"); + Files.writeString(testFile, "sensitive-data"); + + HmacIntegrityManager.restrictPermissions(testFile); + assertThat(Files.isReadable(testFile)).isTrue(); + assertThat(Files.isWritable(testFile)).isTrue(); + } +}